Skip to content

New non-invasive interface for specifying parameters on arbitrary types #68

@bgroenks96

Description

@bgroenks96

@rafaqz Since we're talking about making breaking changes to the package anyway, I would like to submit another slightly more fundamental change (or possibly just extension) to the ModelParameters interface.

As you might recall, I recently incorporated ModelParameters into SpeedyWeather. However, one longstanding drawback of the Param interface is the need to intrusively modify existing struct types to assign separate type parameters for each field that may optionally be assigned a Param. This has the unfortunate effect of greatly increasing type complexity.

We wanted to avoid this in SpeedyWeather but still specify Params for all relevant fields on model component types. To achieve this, I developed an extension of ModelParameters that defines a parameters(obj; kwargs...) method which recursively constructs a nested named tuple of parameters defined on each type, e.g:

parameters(obj::MyType) = (
    fieldA = Param(obj.fieldA, desc="Field A"),
    fieldB = Param(obj.fieldB, desc="Field B"),
    ...
)

This solution has the benefit of being both flexible and non-invasive, since it does not require the Params to be set directly as field values but rather constructed only on demand in a separate type. However, it had the disadvantage of requiring the separate implementation of the parameters method for each type.

To ameliorate this, I developed a macro @parameterized which looks something like this (taking an example here from SpeedyWeather):

@parameterized @kwdef mutable struct Earth{NF<:AbstractFloat} <: AbstractPlanet

    "angular frequency of Earth's rotation [rad/s]"
    @param rotation::NF = DEFAULT_ROTATION
    
    "gravitational acceleration [m/s^2]"
    @param gravity::NF = DEFAULT_GRAVITY (bounds=Nonnegative,)
    
    "switch on/off daily cycle"
    daily_cycle::Bool = true
    
    "Seconds in a daily rotation"
    length_of_day::Second = Hour(24)

    "switch on/off seasonal cycle"
    seasonal_cycle::Bool = true

    "Seconds in an orbit around the sun"
    length_of_year::Second = Day(365.25)
    
    "time of spring equinox (year irrelevant)"
    equinox::DateTime = DateTime(2000, 3, 20) 

    "angle [˚] rotation axis tilt wrt to orbit"
    @param axial_tilt::NF = 23.4 (bounds=-90..90,)

    "Total solar irradiance at the distance of 1 AU [W/m²]"
    @param solar_constant::NF = 1365 (bounds=Nonnegative,)
end

All fields marked with @param (which is only a syntax marker not a real macro) will have parameters defined in an automatically generated dispatch of parameters. The named tuple following the field still permits the inclusion of any arbitrary number of additional properties for each parameter (e.g. bounds, units, etc.).

As an added bonus, a description field is automatically populated with the docstring for the struct field, thereby reducing duplication.

The parameters can then be handled completely separately from the original type itself, which is actually kind of nice. The separate parameter structure can be modified either via a Model type, with a ComponentArray or Setfield or however the user wants to handle it. The updated parameters are then applied via a reconstruct method (confusingly not the same as Flatten.reconstruct since its a nested data structure) which recursively updates the values using ConstructionBase.setproperties. So far this is type stable with the help of a generated function dispatch.

I actually quite like this system, and I think it has some major advantages over the current ModelParameters system. It also could easily accommodate #66 since we could add @const and/or @var tags to distinguish between Var and Const.

If you also like it, then I think there are three options here:

  1. We migrate the code from SpeedyWeather into ModelParameters and have the @parameterized/parameters interface as a complimentary alternative to the usual direct Param field assignment and params approach. The downside to this is that we would have two different interfaces, which is maybe confusing, and the overlap between method names (e.g. params and parameters) which perform different functions is also not great.
  2. We simply adopt this approach as the new primary interface for ModelParameters, possibly maintaining some backwards compatibility via a deprecated params method or something like that.
  3. We keep ModelParameters as it is and I can instead put this code into another package that explicitly extends ModelParameters.

Looking forward to your feedback!

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions