Skip to content

Conversation

@embray
Copy link
Member

@embray embray commented Jun 12, 2015

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:

>>> from astropy import units as u
>>> from astropy.modeling import custom_model
>>> @custom_model
... def Square(x, amplitude=1):
...     # Note: model defined without any reference to specific quantities
...     return amplitude * x**2
...
>>> s = Square(2 * u.m)  # Instantiate model with Quantity for the amplitude
>>> s.amplitude
Parameter('amplitude', value=2.0, unit=m)
>>> s.amplitude / (2.0 * u.m)  # Quantity arithmetic works on parameters
<Quantity 1.0>
>>> s(3 / u.s)  # The model can be evaluated on Quantities, and what you get is what you get
<Quantity 18.0 m / s2>

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_units and output_units attributes. You can read the docs for the attributes starting here.
Just as an example, have a look at the new definition for Gaussian1D. It has input_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 has output_units = 'amplitude' which ensures that no matter how the model's evaluate method 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_units and output_units attributes 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 the Linear1D model, which has input_units = lambda slope, intercept: intercept.unit / slope.unit. This states that when evaluating a Linear1D instance 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's evaluate itself.

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:

  • The biggest TODO is fitting support. Right now fitting won't generally give reliable results. This is because parameters are stored internally in whatever units the user specified them in. But for efficient fitting what we really need to do is convert parameter values to a common set of base units when storing them in the model, as discussed in Models should support Quantity #1464. That's the only major barrier to fitting support.
  • Support with compound models. Fortunately this is mostly straightforward to implement. Creating compound models from models with units will place more constraints on what operations can be done on two models. This is a good thing. For example, it will not be possible to add two models together unless all their output_units are compatible. This will make for a good sanity check.
  • More sophisticated unit specs for parameters--currently parameters can be given a required unit but don't support fancier unit specs like inputs and outputs do. For example, it might be useful to require one parameter's units to be equivalent to another parameter's units.
  • Possible support for per-instance unit spec overrides? Right now input_units and output_units are 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.
  • Clearer support for unit equivalencies. Right now equivalencies will just work both when instantiating and evaluating a model. However, it might be useful to have equivalencies baked into a model somehow. For example, when evaluating a model it might be good to automatically apply all equivalencies that were active when the model was instantiated.
  • Implement unit specs on more of the built-in models, assuming people like this scheme.
  • More tests.
  • More documentation.
  • If Consider adding special "arbitrary" unit? #3793 gets implemented, that will enable some simplification of the code for this.
  • Deprecate getter and setter attributes of Parameters. It is currently possible to define a getter and setter function 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.
  • Open question: Should Parameter.value return a Quantity or the raw value? Currently something like model.amplitude.value or model.amplitude.default does not return a Quantity. Instead the Parameter object is a Quantity-like object in that it has a .unit attribute (if units are defined on a parameter). Sometimes it might be convenient to get a Quantity, but on the other hand the current interface feels more consistent with the existing Quantity interface.
  • Anything else that comes out of review of this WIP?

embray added 6 commits May 15, 2015 17:20
…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.
Copy link
Contributor

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).

Copy link
Member Author

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.

@mhvk
Copy link
Contributor

mhvk commented Jun 13, 2015

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!

Copy link
Member Author

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.

embray added 6 commits June 25, 2015 13:45
…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.
@astrofrog
Copy link
Member

@embray - can we remove the 1.1 milestone? (to try and do the feature freeze this week)

@astrofrog
Copy link
Member

Don't think this can realistically go in 1.1, removing milestone.

Copy link
Contributor

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.

@hamogu
Copy link
Member

hamogu commented Jan 25, 2016

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.
That's not a pushback against this feature, just a feature request for an easy opt-out should there ever be models with hard required units.

@embray
Copy link
Member Author

embray commented Jan 27, 2016

@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.

@astrofrog
Copy link
Member

@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.

@astrofrog
Copy link
Member

@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 units

I 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 mean and stddev should be the same. We'll need a way to specify that, since at the moment I can set inconsistent units:

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:

More sophisticated unit specs for parameters--currently parameters can be given a required unit but don't support fancier unit specs like inputs and outputs do. For example, it might be useful to require one parameter's units to be equivalent to another parameter's units.

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 models

As you mentioned, you can set input_units and output_units, which are set to the name of the parameter from which to use the units that any input should be in when evaluating the models. So in the case of the Gaussian:

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.unit

instead of

input_units = lambda slope, intercept: intercept.unit / slope.unit

Fitting models with quantities

I 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

Creating compound models from models with units will place more constraints on what operations can be done on two models. This is a good thing. For example, it will not be possible to add two models together unless all their output_units are compatible.

I agree, this sounds like a good approach

Overriding input/output units

Possible support for per-instance unit spec overrides? Right now input_units and output_units are 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.

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

Clearer support for unit equivalencies. Right now equivalencies will just work both when instantiating and evaluating a model. However, it might be useful to have equivalencies baked into a model somehow. For example, when evaluating a model it might be good to automatically apply all equivalencies that were active when the model was instantiated.

Agree - and as mentioned above, equivalencies should be specifiable at the parameter level.

Parameter.value

Open question: Should Parameter.value return a Quantity or the raw value? Currently something like model.amplitude.value or model.amplitude.default does not return a Quantity. Instead the Parameter object is a Quantity-like object in that it has a .unit attribute (if units are defined on a parameter). Sometimes it might be convenient to get a Quantity, but on the other hand the current interface feels more consistent with the existing Quantity interface.

I think .value has to return the value (and .unit the unit) otherwise it will be confusing. We can add .quantity to return it as a quantity?

@pllim
Copy link
Member

pllim commented Feb 12, 2016

@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.

@embray
Copy link
Member Author

embray commented Feb 16, 2016

@astrofrog

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.

Yep, that should be fixed (I'm surprised it allowed that...?)

In some cases, some units should be linked, so for example the units for mean and stddev should be the same. We'll need a way to specify that, since at the moment I can set inconsistent units:

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.

@embray
Copy link
Member Author

embray commented Feb 16, 2016

@astrofrog

Any active equivalency can be taken into account, but what about also allowing equivalencies to be attached to parameters?

👍

@embray
Copy link
Member Author

embray commented Feb 16, 2016

@astrofrog

I would personally prefer if we recommended that these were defined using properties instead of lambda functions

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 self might not exist. That said there's no reason to use property, and writing

def input_inputs(...):
    ...

is no different than using input_units = lambda ... You don't even have to put @staticmethod if you don't want to since the metaclass knows what to do with this, but maybe putting @staticmethod also makes it more clear that it is not an instance method. That should be allowed if it isn't already.

I prefer the lambda expression for short expressions because it makes clear that input_units is generally treated as a "class attribute" even though it can also be a function. A strange but true fact.

@embray
Copy link
Member Author

embray commented Feb 16, 2016

@astrofrog

I think .value has to return the value (and .unit the unit) otherwise it will be confusing. We can add .quantity to return it as a quantity?

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 evaluate is called to update the residuals, and it will be quite a slog.

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.

@astrofrog
Copy link
Member

@embray

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 evaluate is called to update the residuals, and it will be quite a slog.

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?

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.

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?

@pllim
Copy link
Member

pllim commented Feb 16, 2016

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?

👍

@astrofrog
Copy link
Member

All right, in that case I am now working on this branch, and will open a new pull request with additional changes!

@pllim
Copy link
Member

pllim commented May 2, 2016

@astrofrog is working on this over at #4615, so closing this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants