-
-
Notifications
You must be signed in to change notification settings - Fork 2k
WIP: Prototype for Quantity support in Models #3852
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…hem to interact correctly with quantities. This does nothing with regard to model evaluation yet, just adds basic support for setting parameters with units. Needs tests.
…ne minor issue in comparison operators on parameters.
…. In particular this can fix the output units to the units of one of the parameters (in the case of many of the current built-in models the output units simply come from the amplitude parameter, which makes sense--the rest of the model evaluates to a dimensionless quantity). This may seem mostly superfluous at the moment, because when you call the model's eval of course you'll just get outputs in the same units as the amplitude parameter. However, this will be more important once we actually implement converting the parameter values to a common base for fitting and the like. So this provides a nice way to specify (without too much magic) how the model should determine the units of its output. Will add a similar mechanism for inputs.
…ith how to handle inputs with unspecified units. Previously my approach was to require all inputs to have explicitly specified units if any parameters are using units. But for now let's be a little more flexible and see where that takes us.
…all inputs when evaluating the model. As this made output_unit_types mostly superfluous (no need to check the outputs if we can check that the inputs will produce the correct outputs), it was removed. input_units more or less has the same semantics that output_unit_types did before I removed it. I think this simplifies things a great deal. Also continued to improve support for arbitrary unit inputs/outputs.
astropy/modeling/core.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly going through to try to understand it, but since any short-circuits, this can just be:
self._using_quantities |= any(isinstance(input_, Quantity) for input_ in inputs)
(which I think is more readable).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I like that. I think originally there was more going on this loop, but after I made some simplifications it was nothing more than this.
|
I cannot judge this well enough to do a thorough review, but like where it is going very much. I'll think a bit more about the arbitrary unit -- probably more helpful! |
astropy/modeling/core.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: We may be able to soften this to just allow anything with a .unit as in some of the other parts of Astropy. But for now I want to be stricter about this so that I can limit the number of assumptions that need to be made.
…ultiple inputs or outputs. Previously multiple inputs (outputs) *required* the input/output_units to be a tuple with a rule for each input. I later realized it would be convenient to still allow a single rule, so that the same rule can be applied to *all* inputs. This is convenient, for example, to write a rule that all inputs have the equivalent units. Same for outputs.
…nate the explicit for loop at the beginning of _prepare_input_units, and eliminate the awkward (and broken) toggling of an internal instance attribute depending on whether or not some inputs had units when the model is called.
…ing a bit long, so moved the input/output validation to a subroutine. If astropy#3861 is implemented most of this code will be moved into individual Operator classes.
…city's sake--it also makes clearer that if a Parameter has a unit specified at all that unit is *fixed* for all time (up to equivalencies). Also make sure to always show the unit in reprs, even for unbound parameters, and to copy the unit when creating Parameter copies.
…for their parameters, and improved some of the overall logic surrounding handling of parameters with units. Still split on whether Parameter.value should return a *Quantity*, or whether Parameter should masquerade as a Quantity itself, such that .value returns the dimensionless value (while .unit returns the unit in either case). Likewise split on whether Parameter.default should return a Quantity or not. These details will be easy to update depending on feedback.
…3912/files#r33822647 , ensure that __array__ returns a Quantity for parameters with units, so that np.asanyarray() on a parameter returns a Quantity.
|
@embray - can we remove the 1.1 milestone? (to try and do the feature freeze this week) |
|
Don't think this can realistically go in 1.1, removing milestone. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe use QTable here, so that the parameters are actually stored as quantities? But that would mean changing the logic above a bit, since one cannot set a unit of a quantity directly (as is done below).
Perhaps easiest to leave it to a comment for now.
|
This comment ignores all the detailed discussion between @mhvk and @embray and just answers in general to the "big block of text" on the top. The concept looks workable to me. I'm not saying "good" because it's another layer of a fairly complex interface. I understand that in the most general case, defining a model is a complex undertaking that requires a complex interface. I assume that is the reason, why non of the other fitting packages that I know of has something like this. (Some domain specific packages like sherpa use some simpler internal tracking of units to distingiush counts/s/keV from counts/s/bin, but that's not really transparent for meaningful for the user.) That said, this interface covers all the use cases I can think of for now, most importantly, it is opt-in for all models so far. Even going forward, I think it's important to be able to offer an opt-out of units option for all models, even those where a default units makes total sense (blackbody). Astropy is still unique in it's use of units and interaction of astropy.modelling with other packages requires that is all works on plain floats or numpy arrays. I understand that even with a requires unit I could always write a wrapper function to add the required units before I call the astropy model and strip them off again after, but I see that as deterring people form an otherwise nice framework. |
|
@hamogu Thanks for the feedback. I forget I mention this anywhere else, but one thing I've been meaning to add to this is an option, defined at the class-level, for how strict it is about units. I think some models might absolutely require units, where others could be more flexible in what it allows. There were several possible settings for this. I could also be fine with a user-supplied argument though to completely ignore all units handling. |
|
@embray - I'd be interested in helping make this happen. Are you still going to have time to work on this once you have adequate feedback? I'm going to review the concepts here and provide some feedback in my next comment. If you don't have time to pursue this further, or not enough time to do it all, I'm happy to help implement some of the remaining things. |
|
@embray - with apologies for the delay, here is some detailed feedback on the high-level ideas here. I've tested out the PR but haven't looked at any code at this point. As mentioned in my previous comment, I'd be very interested in making sure this happens and can commit some time to working on it. If you no longer have time, let me know, and I can take over this branch and work from it - otherwise I can also do PRs to your branch if you want to share the work. In some cases below, I'm just summarizing some of the changes with examples for anyone else who wants to review this (and also will help figure out if I'm misunderstanding anything) Parameters with unitsI think your approach to setting parameters to Quantities is good. Essentially, the units get set at initialization time: In [1]: g = Gaussian1D(amplitude=1 * u.Jy)
In [2]: g
Out[2]: <Gaussian1D(amplitude=1.0 Jy, mean=0.0, stddev=1.0)>It's then not possible to change the units afterwards: In [3]: g.amplitude = 3 * u.joule
In [4]: g
Out[4]: <Gaussian1D(amplitude=3.0 Jy, mean=0.0, stddev=1.0)>However, this does illustrate that we need better validation of input units when we set the parameters (the above should have raised an error). Assuming we would raise an error here, this seems fine to me. In some cases, some units should be linked, so for example the units for In [5]: g = Gaussian1D(amplitude=1 * u.Jy, mean=2 * u.km, stddev=4 * u.s)
In [6]: g
Out[6]: <Gaussian1D(amplitude=1.0 Jy, mean=2.0 km, stddev=4.0 s)>I do realize that this is just a WIP, and not expected to all work yet, but just using it as an illustration of what we need to consider. And you did mention this as a to-do:
so +1 to this. Any active equivalency can be taken into account, but what about also allowing equivalencies to be attached to parameters? For example, one could imagine wanting to have a spectral parameter which always includes the spectral equivalency - for example for a blackbody model but you wouldn't want the equivalency to apply to other parameters. Evaluating modelsAs you mentioned, you can set In [7]: g.input_units
Out[7]: 'mean'
In [8]: g.output_units
Out[8]: 'amplitude'This makes sense to me, and I think we should also not accept multiple incompatible units as input since it wouldn't make sense (and equivalencies take care of the cases where different units could make sense). Evaluating a model using a value with the wrong units leads to an error: In [9]: g = Gaussian1D(amplitude=1 * u.Jy, mean=2 * u.micron, stddev=0.1 * u.micron)
In [10]: g(3)
...
UnitConversionError: Units of input 'x', (dimensionless), could not be converted to required input units of micron (length)whereas using the correct units or equivalent ones works: In [11]: g(3 * u.nm)
Out[11]: <Quantity 2.5204894036926934e-87 Jy>
In [12]: u.set_enabled_equivalencies(u.spectral())
Out[12]: <astropy.units.core._UnitContext at 0x10aa63b38>
In [13]: g(300 * u.THz)
Out[13]: <Quantity 1.799785424849269e-22 Jy>As mentioned above, the issue here is the enabled equivalencies apply to all parameters, whereas one might only want it to apply to one. For more complex units, I like the ability to define more complex input/output units. I would personally prefer if we recommended that these were defined using properties instead of lambda functions, for readability, e.g. @property
def input_units(self):
return self.intercept.unit / self.slope.unitinstead of input_units = lambda slope, intercept: intercept.unit / slope.unitFitting models with quantitiesI would advocate actually not working on this as part of this PR but doing it in a separate PR. For now, it would be sufficient to return an error saying that fitting models with quantities is not yet supported. And it would then be trivial later to do another PR where if we pass a model with units to a fitter, things would indeed be converted to some common base units as you suggest. Compound models
I agree, this sounds like a good approach Overriding input/output units
This appears to already be possible? In [14]: g.input_units = u.s
In [15]: g(3 * u.nm)
...
UnitConversionError: Units of input 'x', nm (length), could not be converted to required input units of s (time)Equivalencies
Agree - and as mentioned above, equivalencies should be specifiable at the parameter level. Parameter.value
I think |
|
@astrofrog , officially @embray does not have any time to work on this anymore, so I would say you can "take over" but give him credit to the existing work so far. |
Yep, that should be fixed (I'm surprised it allowed that...?)
Yes, this was on my to do list. It's currently possible to link the units of outputs to parameters and vice-versa. But it should also be possible to link the units of individual parameters to each other via some kind of unit constraint specified when defining the parameters. |
👍 |
Using a a property doesn't really work in the context of the current implementation, which IIRC wants to sometimes check unit consistency in cases where a is no different than using I prefer the lambda expression for short expressions because it makes clear that |
I think that makes some sense. Fitting could be added now, but it would be very slow. My idea for fitting would be to perform all unit conversions first, and then do the fit using the bare float values, and only restore units at the end. Otherwise you'd have to strip units before putting them through the optimizer, then somehow re-attach the units when the models I could try rebasing this if you want, but as @pllim said I don't have time to otherwise do work on it right now. |
I agree, this makes sense. Maybe one could imagine having a context manager internally that temporarily makes evaluate return plain values in a specified set of units, and we can use that context manager inside the fitters?
Shall I just take over this branch and open another pull request to continue it, since I'll have time to work on it over the next couple of weeks? |
👍 |
|
All right, in that case I am now working on this branch, and will open a new pull request with additional changes! |
|
@astrofrog is working on this over at #4615, so closing this one. |
This PR provides initial support for #1464, and builds on the much earlier PR #1796 by @nden with a rough / incomplete vision of how Models can be made to work with Quantities.
To start with, this makes evaluation of models given Quantity objects for parameters and/or inputs/independent variables basically just work. For example here's a simple custom model that uses Quantities:
However, there is more sophistication to this PR than that. Most of what makes it more interesting than just straight up supporting Quantities is that we can also specify constraints on units when defining the model. In this PR I added constraints to several of the built-in models (I eschewed polynomials, rotation, and projection models for now, but they can still be supported too).
The constraints are added at model class definition via
input_unitsandoutput_unitsattributes. You can read the docs for the attributes starting here.Just as an example, have a look at the new definition for
Gaussian1D. It hasinput_units = 'mean'. This means the model can have inputs in any units so long as they are compatible (including any active equivalencies) with the units of the models "mean" parameter. It also hasoutput_units = 'amplitude'which ensures that no matter how the model'sevaluatemethod is defined, the output when evaluating the model should be in the same units as the model's "amplitude" parameter. This also serves as a sanity check on the model's implementation.These
input_unitsandoutput_unitsattributes are collectively referred to as "unit specs", and may be more complicated (though it turns out for most models they are pretty simple). For more complicated examples, however, they may also be defined as functions. An example comes from theLinear1Dmodel, which hasinput_units = lambda slope, intercept: intercept.unit / slope.unit. This states that when evaluating aLinear1Dinstance the input's units must be the intercept's units divided by the slope's unit, so that it can be evaluated correctly. This example of course requires knowledge of what Linear1D computes, but of course it's completely reasonable to assume that someone implementing a model knows how it's defined :) And usually the implementations of the unit specs are much simpler than the model'sevaluateitself.Parameters can also be given a default/required unit, though I don't think I've defined any yet for any of the built-in models. Most of the time we want to leave parameter units flexible, but there are some cases (rotations for example) that it's obvious what to specify.
There are several TODO items for this feature that I will list below. It seems like a lot, but most of these are easy now that the groundwork is done, and some of them already have prototype implementations in a separate branch. However, I wanted to get feedback on the current WIP before adding the rest:
output_unitsare compatible. This will make for a good sanity check.input_unitsandoutput_unitsare defined at the class-level. But it may also be useful to write code that defines these at the instance level. This would allow writing code that requires input to a particular model to always be of some unit, without having to subclass the model type.getterandsetterattributes ofParameters. It is currently possible to define agetterandsetterfunction for a Parameter, which transforms the parameter's value between what the user specifies, and how the parameter is used internally for evaluation. In practice, this has only been used for angular parameters, to convert between degrees and radians. Being able to specify that angular parameters should have angular units obviates the need for this feature, which was always awkward to begin with.Parameter.valuereturn aQuantityor the raw value? Currently something likemodel.amplitude.valueormodel.amplitude.defaultdoes not return aQuantity. Instead theParameterobject is aQuantity-like object in that it has a.unitattribute (if units are defined on a parameter). Sometimes it might be convenient to get aQuantity, but on the other hand the current interface feels more consistent with the existingQuantityinterface.