diff --git a/docs/make.jl b/docs/make.jl index 00c252651..a90aa11fa 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -21,8 +21,9 @@ makedocs( "2 Explore Results" => "howto/howto_2.md", "3 Monte Carlo + SA" => "howto/howto_3.md", "4 Timesteps, Params, and Vars" => "howto/howto_4.md", - "5 Port to v0.5.0" => "howto/howto_5.md", - "6 Port to v1.0.0" => "howto/howto_6.md" + "5 Dimensions" => "howto/howto_5.md,", + "6 Port to v0.5.0" => "howto/howto_6.md", + "7 Port to v1.0.0" => "howto/howto_7.md" ], "Advanced How-to Guides" => Any[ "Advanced How-to Guides Intro" => "howto_advanced/howto_adv_main.md", diff --git a/docs/src/howto/howto_4.md b/docs/src/howto/howto_4.md index 88c4ddcde..2733c6af4 100644 --- a/docs/src/howto/howto_4.md +++ b/docs/src/howto/howto_4.md @@ -106,7 +106,7 @@ When `set_param!` is called, it creates an external parameter by the name provid update_param!(m, :ParameterName, newvalues) ``` -Note here that `newvalues` must be the same type (or be able to convert to the type) of the old values stored in that parameter, and the same size as the model dimensions indicate. Also note that it if you have updated the time dimension of the model with `set_dimension!(m, :time, values)` you will need to update all parameters with a `:time` dimension, **even if the values have not changed**, so that the model can update the underlying time labels attached to the parameters. +Note here that `newvalues` must be the same type (or be able to convert to the type) of the old values stored in that parameter, and the same size as the model dimensions indicate. #### Setting parameters with a dictionary diff --git a/docs/src/howto/howto_5.md b/docs/src/howto/howto_5.md index 7e61ca7c2..590e937a6 100644 --- a/docs/src/howto/howto_5.md +++ b/docs/src/howto/howto_5.md @@ -1,107 +1,69 @@ -# How-to Guide 5: Port to Mimi v0.5.0 +# How-to Guide 5: Update the Time Dimension -The release of Mimi v0.5.0 is a breaking release, necessitating the adaptation of existing models' syntax and structure in order for those models to run on this new version. This guide provides an overview of the steps required to get most models using the v0.4.0 API working with v0.5.0. It is **not** a comprehensive review of all changes and new functionalities, but a guide to the minimum steps required to port old models between versions. For complete information on the new version and its functionalities, see the full documentation. - -This guide is organized into six main sections, each descripting an independent set of changes that can be undertaken in any order desired. - -1) Defining components -2) Constructing a model -3) Running the model -4) Accessing results -5) Plotting -6) Advanced topics - -**A Note on Function Naming**: There has been a general overhaul on function names, especially those in the explicity user-facing API, to be consistent with Julia conventions and the conventions of this Package. These can be briefly summarized as follows: - -- use `_` for readability -- append all functions with side-effects, i.e., non-pure functions that return a value but leave all else unchanged with a `!` -- the commonly used terms `component`, `variable`, and `parameter` are shortened to `comp`, `var`, and `param` -- functions that act upon a `component`, `variable`, or `parameter` are often written in the form `[action]_[comp/var/param]` - -## Defining Components - -The `run_timestep` function is now contained by the `@defcomp` macro, and takes the parameters `p, v, d, t`, referring to Parameters, Variables, and Dimensions of the component you defined. The fourth argument is an `AbstractTimestep`, i.e., either a `FixedTimestep` or a `VariableTimestep`. Similarly, the optional `init` function is also contained by `@defcomp`, and takes the parameters `p, v, d`. Thus, as described in the user guide, defining a single component is now done as follows: - -In this version, the fourth argument (`t` below) can no longer always be used simply as an `Int`. Indexing with `t` is still permitted, but special care must be taken when comparing `t` with conditionals or using it in arithmatic expressions. The full API as described later in this document in **Advanced Topics: Timesteps and available functions**. Since differential equations are commonly used as the basis for these models' equations, the most commonly needed change will be changing `if t == 1` to `if is_first(t)` - -```julia -@defcomp component1 begin - - # First define the state this component will hold - savingsrate = Parameter() - - # Second, define the (optional) init function for the component - function init(p, v, d) - end - - # Third, define the run_timestep function for the component - function run_timestep(p, v, d, t) - end - -end +A runnable model necessarily has a `time` dimension, originally set with the following call, but in some cases it may be desireable to alter this dimension by calling the following on a model which already has a time dimension set. +``` +set_dimension!(m, :time, time_keys) ``` -## Constructing a Model - -In an effort to standardize the function naming protocol within Mimi, and to streamline it with the Julia convention, several function names have been changed. The table below lists a **subset** of these changes, focused on the exported API functions most commonly used in model construction. - -| Old Syntax | New Syntax | -| ------------------------ |:-------------------------:| -|`addcomponent!` |`add_comp!` | -|`connectparameter` |`connect_param!` | -|`setleftoverparameters` |`set_leftover_params!` | -|`setparameter` |`set_param!` | -|`adddimension` |`add_dimension!` | -|`setindex` |`set_dimension!` | - -Changes to various optional keyword arguments: - -- `add_comp!`: Through Mimi v0.9.4, the optional keyword arguments `first` and `last` could be used to specify times for components that do not run for the full length of the model, like this: `add_comp!(mymodel, ComponentC; first=2010, last=2100)`. This functionality is currently disabled, and all components must run for the full length of the model's time dimension. This functionality may be re-implemented in a later version of Mimi. - -## Running a Model - -## Accessing Results - -## Plotting and the Explorer UI - -This release of Mimi does not include the plotting functionality previously offered by Mimi. While the previous files are still included, the functions are not exported as efforts are made to simplify and improve the plotting associated with Mimi. - -The new version does, however, include a new UI tool that can be used to visualize model results. This `explore` function is described in the User Guide under **Advanced Topics**. - -## Advanced Topics - -#### Timesteps and available functions - -As previously mentioned, some relevant function names have changed. These changes were made to eliminate ambiguity. For example, the new naming clarifies that `is_last` returns whether the timestep is on the last valid period to be run, not whether it has run through that period already. This check can still be achieved with `is_finished`, which retains its name and function. Below is a subset of such changes related to timesteps and available functions. - -| Old Syntax | New Syntax | -| ------------------------ |:-------------------------:| -|`isstart` |`is_first` | -|`isstop` |`is_last` | - -As mentioned in earlier in this document, the fourth argument in `run_timestep` is an `AbstractTimestep` i.e. a `FixedTimestep` or a `VariableTimestep` and is a type defined within Mimi in "src/time.jl". In this version, the fourth argument (`t` below) can no longer always be used simply as an `Int`. Defining the `AbstractTimestep` object as `t`, indexing with `t` is still permitted, but special care must be taken when comparing `t` with conditionals or using it in arithmatic expressions. Since differential equations are commonly used as the basis for these models' equations, the most commonly needed change will be changing `if t == 1` to `if is_first(t)`. - -The full API: - -- you may index into a variable or parameter with `[t]` or `[t +/- x]` as usual -- to access the time value of `t` (currently a year) as a `Number`, use `gettime(t)` -- useful functions for commonly used conditionals are `is_first(t)` and `is_last(t)` -- to access the index value of `t` as a `Number` representing the position in the time array, use `t.t`. Users are encouraged to avoid this access, and instead use the options listed above or a separate counter variable. each time the function gets called. - -#### Parameter connections between different length components - -#### More on parameter indices - -#### Updating an external parameter +---- +#### For example, one may wish to replace the FUND model's climate module with a different one, such as FAIR: -To update an external parameter, use the functions `update_param!` and `udpate_params!` (previously known as `update_external_parameter` and `update_external_parameters`, respectively.) Their calling signatures are: +For the purposes of this guide we focus on the first step of such modification. Since FUND runs yearly from 1950 to 3000 and FAIR yearly from 1765 to 2500, our modified model will need to run yearly from 1765 to 1950. -* `update_params!(md::ModelDef, parameters::Dict; update_timesteps = false)` +We start with FUND +``` +using Mimi +using MimiFUND +m = MimiFUND.get_model() +``` +where `MimiFUND.get_model` includes the call `set_dimension!(m, time, 1950:3000)`. -* `update_param!(md::ModelDef, name::Symbol, value; update_timesteps = false)` +---- +#### Now we need to change the `time` dimension to be 1765 to 2500: -For external parameters with a `:time` dimension, passing `update_timesteps=true` indicates that the time _keys_ (i.e., year labels) should also be updated in addition to updating the parameter values. +Before we do so, note some important rules and precautions. These are in place to avoid unexpected behavior, complications, or incorrect results caused by our under-the-hood assumptions, but if a use case arises where these are prohibitive please get in touch on the [forum](https://forum.mimiframework.org) and we can help you out. -#### Setting parameters with a dictionary +- The new time dimension cannot start later than the original time dimension. +- The new time dimension cannot end before the start of the original time dimension ie. it cannot completely exclude all times in the original time dimension. +- The new time dimension must use the same timestep lengths as the original dimension. -The function `set_leftover_params!` replaces the function `setleftoverparameters`. +---- +#### We now go ahead and change the `time` dimension to be 1765 to 2500: +``` +set_dimension!(m, :time, 1765:2500) +``` +At this point the model `m` can be run, and will run from 1765 to 2500 (Try running it and looking at `explore(m)` for parameters and variables with a `time` dimension!). In fact, we could start adding FAIR components to the model, which would automatically take on the entire model time dimension, ie. +``` +add_comp!(m, FAIR_component) # will run from 1765 to 1950 +``` +**However**, the FUND components will only run in the subset of years 1950 to 2500, using the same parameter values each year was previously associated with, and containing placeholder `missing` values in the parameter value spots from 1765 to 1949. More specifically: + +- The model's `time` dimension values are updated, and it will run for each year in the new 1765:1950 dimension. + ``` + julia> Mimi.time_labels(m) + 736-element Vector{Int64}: [1765, 1766, 1767, … 2498, 2499, 2500] + ``` +- The components `time` dimension values are updated, but (1) the components maintain the `first` year as set implicitly by the original `time` dimension (1950) so the run period start year does not change and (2) they maintain their `last` year as set implicitly by the original `time` dimension, unless that year is now later than the model's last year, in which case it is trimmed back to the `time` dimensions last year (2500). Thus, the components will run for the same run period, or a shorter one if the new time dimension ends before the component used to (in this case 1950:2500). + ``` + julia> component = m.md.namespace[:emissions] # get component def(ignore messy internals syntax) + julia> component.dim_dict[:time] + [1765, 1766, 1767, … 2498, 2499, 2500] + julia> component.first + 1950 + julia> component.last + 2500 + ``` +- All external parameters are trimmed and padded as needed so the model can still run, **and the values are still linked to their original years**. More specifically, if the new time dimension ends earlier than the original one than the parameter value vector/matrix is trimmed at the end. If the new time dimension starts earlier than the original, or ends later, the parameter values are padded with `missing`s at the front and/or back respectively. + ``` + julia> parameter_values = Mimi.external_params(m)[:currtaxn2o].values.data # get param values for use in next run (ignore messy internals syntax) + julia> size(parameter_values) + (736, 16) + julia> parameter_values[1:(1950-1765),:] # all missing + julia> parameter_values[(1950-1764),:] # hold set values + ``` + +---- +#### The following options are now available for further modifcations if this end state is not desireable: + +- If you want to update a component's run period, you may use the function `Mimi.set_first_last!(m, :ComponentName, first = new_first, last = new_last)` to specify when you want the component to run. +- You can update external parameters to have values in place of the assumed `missing`s using the `update_param!(m, :ParameterName, values)` function diff --git a/docs/src/howto/howto_6.md b/docs/src/howto/howto_6.md index 12ef7db53..edef2aab8 100644 --- a/docs/src/howto/howto_6.md +++ b/docs/src/howto/howto_6.md @@ -1,233 +1,107 @@ -# How-to Guide 6: Port from (>=) Mimi v0.5.0 to Mimi v1.0.0 +# How-to Guide 6: Port to Mimi v0.5.0 -The release of Mimi v1.0.0 is a breaking release, necessitating the adaptation of existing models' syntax and structure in order for those models to run on this new version. We have worked hard to keep these changes clear and as minimal as possible. +The release of Mimi v0.5.0 is a breaking release, necessitating the adaptation of existing models' syntax and structure in order for those models to run on this new version. This guide provides an overview of the steps required to get most models using the v0.4.0 API working with v0.5.0. It is **not** a comprehensive review of all changes and new functionalities, but a guide to the minimum steps required to port old models between versions. For complete information on the new version and its functionalities, see the full documentation. -This guide provides an overview of the steps required to get most models using the v0.9.5 API working with v1.0.0. It is **not** a comprehensive review of all changes and new functionalities, but a guide to the minimum steps required to port old models between versions. For complete information on the new version and its additional functionalities, see the full documentation. +This guide is organized into six main sections, each descripting an independent set of changes that can be undertaken in any order desired. -## Workflow Advice +1) Defining components +2) Constructing a model +3) Running the model +4) Accessing results +5) Plotting +6) Advanced topics -To port your model, we recommend you update to **Mimi v0.10.0**, which is identical to Mimi v1.0.0 **except** that it includes deprecation warnings for most breaking changes, instead of errors. This means that models written using Mimi v0.9.5 will, in most cases, run successfully under Mimi v0.10.0 and things that will cause errors in v1.0.0 will throw deprecation warnings. These can guide your changes, and thus a good workflow would be: +**A Note on Function Naming**: There has been a general overhaul on function names, especially those in the explicity user-facing API, to be consistent with Julia conventions and the conventions of this Package. These can be briefly summarized as follows: -1) Update your environment to use Mimi v0.10.0 with - ```julia - pkg> add Mimi#v0.10.0 - ``` -2) Read through this guide to get a sense for what has changed -3) Run your code and incrementally update it, using the deprecation warnings as guides for what to change and the instructions in this guide as explanations, until no warnings are thrown and you have changed anything relevant to your code that is explained in this gude. -4) Update to Mimi v1.0.0 with the following code, which will update Mimi to it's latest version, v1.0.0 - ```julia - pkg> free Mimi - ``` -5) Run your model! Things should run smoothly now. If not double check the guide, and feel free to reach out on the forum with any questions. Also, if you are curious about the reasons behind a change, just ask! +- use `_` for readability +- append all functions with side-effects, i.e., non-pure functions that return a value but leave all else unchanged with a `!` +- the commonly used terms `component`, `variable`, and `parameter` are shortened to `comp`, `var`, and `param` +- functions that act upon a `component`, `variable`, or `parameter` are often written in the form `[action]_[comp/var/param]` -This guide is organized into a few main sections, each descripting an independent set of changes that can be undertaken in any order desired. +## Defining Components -- Syntax Within the @defcomp Macro -- The set_param! Function -- The replace_comp! Function -- Different-length Components -- Marginal Models -- Simulation Syntax -- Composite Components (optional) +The `run_timestep` function is now contained by the `@defcomp` macro, and takes the parameters `p, v, d, t`, referring to Parameters, Variables, and Dimensions of the component you defined. The fourth argument is an `AbstractTimestep`, i.e., either a `FixedTimestep` or a `VariableTimestep`. Similarly, the optional `init` function is also contained by `@defcomp`, and takes the parameters `p, v, d`. Thus, as described in the user guide, defining a single component is now done as follows: -## Syntax Within the @defcomp Macro +In this version, the fourth argument (`t` below) can no longer always be used simply as an `Int`. Indexing with `t` is still permitted, but special care must be taken when comparing `t` with conditionals or using it in arithmatic expressions. The full API as described later in this document in **Advanced Topics: Timesteps and available functions**. Since differential equations are commonly used as the basis for these models' equations, the most commonly needed change will be changing `if t == 1` to `if is_first(t)` -#### Type-parameterization for Parameters - -*The Mimi Change:* - -To be consistent with julia syntax, Mimi now uses bracketing syntax to type-parameterize `Parameter`s inside the `@defcomp` macro instead of double-colon syntax. h +```julia +@defcomp component1 begin -*The User Change:* + # First define the state this component will hold + savingsrate = Parameter() -Where you previously indicated that the parameter `a` should be an `Int` with -```julia -@defcomp my_comp begin - a::Int = Parameter() - function run_timestep(p, v, d, t) + # Second, define the (optional) init function for the component + function init(p, v, d) end -end -``` -you should now use -```julia -@defcomp my_comp begin - a = Parameter{Int}() + + # Third, define the run_timestep function for the component function run_timestep(p, v, d, t) end -end -``` - -#### Integer Indexing - -*The Mimi Change:* -For safety, Mimi no longer allows indexing into `Parameter`s or `Varaible`s with the `run_timestep` function of the `@defcomp` macro with integers. Instead, this functionality is supported with two new types: `TimestepIndex` and `TimestepValue`. Complete details on indexing options can be found in How-to Guide 4: Work with Timesteps, Parameters, and Variables, but below we will describe the minimum steps to get your models working. - -*The User Change:* - -Where you previously used integers to index into a `Parameter` or `Variable`, you should now use the `TimestepIndex` type. For example, the code -```julia -function run_timestep(p, v, d, t) - v.my_var[t] = p.my_param[10] end ``` -should now read -```julia -function run_timestep(p, v, d, t) - v.my_var[t] = p.my_param[TimestepIndex(10)] -end -``` -Also, if you previously used logic to determine which integer index pertained to a specific year, and then used that integer for indexing, you should now use the `TimestepValue` type. For example, if you previously knew that the index 2 referred to the year 2012, and added that value to a parameter with -```julia -function run_timestep(p, v, d, t) - v.my_var[t] = p.my_param[t] + p.my_other_param[2] -end -``` -you should now use -```julia -function run_timestep(p, v, d, t) - v.my_var[t] = p.my_param[t] + p.my_other_param[TimestepValue(2012)] -end -``` - -#### `is_timestep` and `is_time` - -*The Mimi Change:* - -For simplicity and consistency with the change above, Mimi no longer supports the `is_timestep` or `is_time` functions and has replaced this functionality with comparison operators combined with the afformentioned `TimestepValue` and `TimestepIndex` types. - -*The User Change:* - -Any instance of the `is_timestep` function should be replaced with simple comparison with a `TimestepIndex` object ie. replace the logic `if is_timestep(t, 10) ...` with `if t == TimestepIndex(10) ...`. - -Any instance of the `is_time` function should be repalced with simple comparison with a `TimestepValue` object ie. replace the logic `if is_time(t, 2010) ...` with `if t == TimestepValue(2010) ...`. - -## The set_param! Function - -*The Mimi Change:* - -The `set_param!` method for setting a parameter value in a component now has the following signature: -``` -set_param!(m::Model, comp_name::Symbol, param_name::Symbol, ext_param_name::Symbol, val::Any) -``` -This function creates an external parameter called `ext_param_name` with value `val` in the model `m`'s list of external parameters, and connects the parameter `param_name` in component `comp_name` to this newly created external parameter. If there is already a parameter called `ext_param_name` in the model's list of external parameters, it errors. -There are two available shortcuts: -``` -# Shortcut 1 -set_param!(m::Model, param_name::Symbol, val::Any) -``` -This method creates an external parameter in the model called `param_name`, sets its value to `val`, looks at all the components in the model `m`, finds all the unbound parameters named `param_name`, and creates connections from all the unbound parameters that are named `param_name` to the newly created external parameter. If there is already a parameter called `param_name` in the external parameter list, it errors. +## Constructing a Model -``` -# Shortcut 2 -set_param!(m::Model, comp_name::Symbol, param_name::Symbol, val::Any) -``` -This method creates a new external parameter called `param_name` in the model `m` (if that already exists, it errors), sets its value to `val`, and then connects the parameter `param_name` in component `comp_name` to this newly created external parameter. +In an effort to standardize the function naming protocol within Mimi, and to streamline it with the Julia convention, several function names have been changed. The table below lists a **subset** of these changes, focused on the exported API functions most commonly used in model construction. -*The User Change:* +| Old Syntax | New Syntax | +| ------------------------ |:-------------------------:| +|`addcomponent!` |`add_comp!` | +|`connectparameter` |`connect_param!` | +|`setleftoverparameters` |`set_leftover_params!` | +|`setparameter` |`set_param!` | +|`adddimension` |`add_dimension!` | +|`setindex` |`set_dimension!` | -Any old code that uses the `set_param!` method with only 4 arguments (shortcut #2 shown above) will still work for setting parameters **if they are found in only one component** ... but if you have multiple components that have parameters with the same name, using the old 4-argument version of `set_param!` multiple times will cause an error. Instead, you need to determine what behavior you want across multiple components with parameters of the same name: -- If you want parameters with the same name that are found in multiple components to have the _same_ value, use the 3-argument method: `set_param!(m, :param_name, val)`. You only have to call this once and it will set the same value for all components with an unconnected parameter called `param_name`. -- If you want different components that have parameters with the same name to have _different_ values, then you need to call the 5-argument version of `set_param!` individually for each parameter value, such as: -``` -set_param!(m, :comp1, :foo, :foo1, 25) # creates an external parameter called :foo1 with value 25, and connects just comp1/foo to that value -set_param!(m, :comp2, :foo, :foo2, 30) # creates an external parameter called :foo2 with value 30, and connects just comp2/foo to that value -``` +Changes to various optional keyword arguments: -Also, you can no longer call `set_param!` to change the value of a parameter that has already been set in the model. If the parameter has already been set, you must use the following to change it: -``` -update_param!(m, ext_param_name, new_val) -``` -This updates the value of the external parameter called `ext_param_name` in the model `m`'s list of external parameters. Any component that have parameters connected to this external parameter will now be connected to this new value. - -## The replace_comp! Function - -*The Mimi Change:* +- `add_comp!`: Through Mimi v0.9.4, the optional keyword arguments `first` and `last` could be used to specify times for components that do not run for the full length of the model, like this: `add_comp!(mymodel, ComponentC; first=2010, last=2100)`. This functionality is currently disabled, and all components must run for the full length of the model's time dimension. This functionality may be re-implemented in a later version of Mimi. -For simplicity, the `replace_comp!` function has been replaced with a method augmenting the julia Base `replace!` function. +## Running a Model -*The User Change:* +## Accessing Results -Where you previously used -```julia -replace_comp!(m, new, old) -``` -to replace the `old` component with `new`, they should now use -```julia -replace!(m, old => new) -``` +## Plotting and the Explorer UI -## Different-length Components +This release of Mimi does not include the plotting functionality previously offered by Mimi. While the previous files are still included, the functions are not exported as efforts are made to simplify and improve the plotting associated with Mimi. -*The Mimi Change:* +The new version does, however, include a new UI tool that can be used to visualize model results. This `explore` function is described in the User Guide under **Advanced Topics**. -**Update: This Functionality has been reenabled, please feel free to use it again, your old code should now be valid again.** +## Advanced Topics -Through Mimi v0.9.4, the optional keyword arguments `first` and `last` could be used to specify times for components that do not run for the full length of the model, like this: `add_comp!(mymodel, ComponentC; first=2010, last=2100)`. This functionality is still disabled, as it was starting in v0.9.5, and all components must run for the full length of the model's time dimension. This functionality may be re-implemented in a later version of Mimi. +#### Timesteps and available functions -*The User Change:* +As previously mentioned, some relevant function names have changed. These changes were made to eliminate ambiguity. For example, the new naming clarifies that `is_last` returns whether the timestep is on the last valid period to be run, not whether it has run through that period already. This check can still be achieved with `is_finished`, which retains its name and function. Below is a subset of such changes related to timesteps and available functions. -Refactor your model so that all components are the same length. You may use the `run_timestep` function within each component to dictate it's behavior in different timesteps, including doing no calculations for a portion of the full model runtime. +| Old Syntax | New Syntax | +| ------------------------ |:-------------------------:| +|`isstart` |`is_first` | +|`isstop` |`is_last` | -## Marginal Models +As mentioned in earlier in this document, the fourth argument in `run_timestep` is an `AbstractTimestep` i.e. a `FixedTimestep` or a `VariableTimestep` and is a type defined within Mimi in "src/time.jl". In this version, the fourth argument (`t` below) can no longer always be used simply as an `Int`. Defining the `AbstractTimestep` object as `t`, indexing with `t` is still permitted, but special care must be taken when comparing `t` with conditionals or using it in arithmatic expressions. Since differential equations are commonly used as the basis for these models' equations, the most commonly needed change will be changing `if t == 1` to `if is_first(t)`. -*The Mimi Change:* - -For clarity, the previously named `marginal` attribute of a Mimi `MarginalModel` has been renamed to `modified`. Hence a `MarginalModel` is now described as a Mimi `Model` whose results are obtained by subtracting results of one `base` Model from those of another `marginal` Model that has a difference of `delta` with the signature: - -*The User Change:* - -Any previous access to the `marginal` attribute of a `MarginalModel`, `mm` below, should be changed from -```julia -model = mm.marginal -``` -to -```julia -model = mm.modified -``` -## Simulation Syntax - -#### Results Access - -*The Mimi Change:* - -For clarity of return types, Mimi no longer supports use of square brackets (a shortcut for julia Base `getindex`) to access the results of a Monte Carlo analysis, which are stored in the `SimulationInstance`. Instead, access to resulst is supported with the `getdataframe` function, which will return the results in the same type and format as the square bracket method used to return. - -*The User Change:* - -Results previously obtained with -```julia -results = si[:grosseconomy, :K] -``` -should now be obtained with -```julia -results = getdataframe(si, :grosseconomy, :K) -``` -#### Simulation Definition Modification Functions +The full API: -*The Mimi Change:* +- you may index into a variable or parameter with `[t]` or `[t +/- x]` as usual +- to access the time value of `t` (currently a year) as a `Number`, use `gettime(t)` +- useful functions for commonly used conditionals are `is_first(t)` and `is_last(t)` +- to access the index value of `t` as a `Number` representing the position in the time array, use `t.t`. Users are encouraged to avoid this access, and instead use the options listed above or a separate counter variable. each time the function gets called. -For consistency with julia syntax rules, the small set of unexported functions available to modify an existing `SimulationDefinition` have been renamed, moving from a camel case format to an underscore-based format as follows. +#### Parameter connections between different length components -*The User Change:* +#### More on parameter indices -Replace your functions as follows. +#### Updating an external parameter -- `deleteRV!` --> `delete_RV!` -- `addRV!` --> `add_RV!` -- `replaceRV!` --> `replace_RV!` -- `deleteTransform!` --> `delete_transform!` -- `addTransform!` --> `add_transform!` -- `deleteSave!` --> `delete_save!` -- `addSave!` --> `add_save!` +To update an external parameter, use the functions `update_param!` and `udpate_params!` (previously known as `update_external_parameter` and `update_external_parameters`, respectively.) Their calling signatures are: -## Composite Components (optional) +* `update_params!(md::ModelDef, parameters::Dict; update_timesteps = false)` -*The Mimi Change:* +* `update_param!(md::ModelDef, name::Symbol, value; update_timesteps = false)` -The biggest functionality **addition** of Mimi v1.0.0 is the inclusion of composite components. Prior versions of Mimi supported only "flat" models, i.e., with one level of components. This new version supports mulitple layers of components, with some components being "final" or leaf components, and others being "composite" components which themselves contain other leaf or composite components. This approach allows for a cleaner organization of complex models, and allows the construction of building blocks that can be re-used in multiple models. +For external parameters with a `:time` dimension, passing `update_timesteps=true` indicates that the time _keys_ (i.e., year labels) should also be updated in addition to updating the parameter values. -*The User Change:* +#### Setting parameters with a dictionary -All previous models are considered "flat" models, i.e. they have only one level of components, and do **not** need to be converted into multiple layer models to run. Thus this addition does not mean users need to alter their models, but we encourage you to check out the other documentation on composite components to learn how you can enhance your current models and built better onces in the future! +The function `set_leftover_params!` replaces the function `setleftoverparameters`. diff --git a/docs/src/howto/howto_7.md b/docs/src/howto/howto_7.md new file mode 100644 index 000000000..c9a7b1149 --- /dev/null +++ b/docs/src/howto/howto_7.md @@ -0,0 +1,233 @@ +# How-to Guide 7: Port from (>=) Mimi v0.5.0 to Mimi v1.0.0 + +The release of Mimi v1.0.0 is a breaking release, necessitating the adaptation of existing models' syntax and structure in order for those models to run on this new version. We have worked hard to keep these changes clear and as minimal as possible. + +This guide provides an overview of the steps required to get most models using the v0.9.5 API working with v1.0.0. It is **not** a comprehensive review of all changes and new functionalities, but a guide to the minimum steps required to port old models between versions. For complete information on the new version and its additional functionalities, see the full documentation. + +## Workflow Advice + +To port your model, we recommend you update to **Mimi v0.10.0**, which is identical to Mimi v1.0.0 **except** that it includes deprecation warnings for most breaking changes, instead of errors. This means that models written using Mimi v0.9.5 will, in most cases, run successfully under Mimi v0.10.0 and things that will cause errors in v1.0.0 will throw deprecation warnings. These can guide your changes, and thus a good workflow would be: + +1) Update your environment to use Mimi v0.10.0 with + ```julia + pkg> add Mimi#v0.10.0 + ``` +2) Read through this guide to get a sense for what has changed +3) Run your code and incrementally update it, using the deprecation warnings as guides for what to change and the instructions in this guide as explanations, until no warnings are thrown and you have changed anything relevant to your code that is explained in this gude. +4) Update to Mimi v1.0.0 with the following code, which will update Mimi to it's latest version, v1.0.0 + ```julia + pkg> free Mimi + ``` +5) Run your model! Things should run smoothly now. If not double check the guide, and feel free to reach out on the forum with any questions. Also, if you are curious about the reasons behind a change, just ask! + +This guide is organized into a few main sections, each descripting an independent set of changes that can be undertaken in any order desired. + +- Syntax Within the @defcomp Macro +- The set_param! Function +- The replace_comp! Function +- Different-length Components +- Marginal Models +- Simulation Syntax +- Composite Components (optional) + +## Syntax Within the @defcomp Macro + +#### Type-parameterization for Parameters + +*The Mimi Change:* + +To be consistent with julia syntax, Mimi now uses bracketing syntax to type-parameterize `Parameter`s inside the `@defcomp` macro instead of double-colon syntax. h + +*The User Change:* + +Where you previously indicated that the parameter `a` should be an `Int` with +```julia +@defcomp my_comp begin + a::Int = Parameter() + function run_timestep(p, v, d, t) + end +end +``` +you should now use +```julia +@defcomp my_comp begin + a = Parameter{Int}() + function run_timestep(p, v, d, t) + end +end +``` + +#### Integer Indexing + +*The Mimi Change:* + +For safety, Mimi no longer allows indexing into `Parameter`s or `Varaible`s with the `run_timestep` function of the `@defcomp` macro with integers. Instead, this functionality is supported with two new types: `TimestepIndex` and `TimestepValue`. Complete details on indexing options can be found in How-to Guide 4: Work with Timesteps, Parameters, and Variables, but below we will describe the minimum steps to get your models working. + +*The User Change:* + +Where you previously used integers to index into a `Parameter` or `Variable`, you should now use the `TimestepIndex` type. For example, the code +```julia +function run_timestep(p, v, d, t) + v.my_var[t] = p.my_param[10] +end +``` +should now read +```julia +function run_timestep(p, v, d, t) + v.my_var[t] = p.my_param[TimestepIndex(10)] +end +``` +Also, if you previously used logic to determine which integer index pertained to a specific year, and then used that integer for indexing, you should now use the `TimestepValue` type. For example, if you previously knew that the index 2 referred to the year 2012, and added that value to a parameter with +```julia +function run_timestep(p, v, d, t) + v.my_var[t] = p.my_param[t] + p.my_other_param[2] +end +``` +you should now use +```julia +function run_timestep(p, v, d, t) + v.my_var[t] = p.my_param[t] + p.my_other_param[TimestepValue(2012)] +end +``` + +#### `is_timestep` and `is_time` + +*The Mimi Change:* + +For simplicity and consistency with the change above, Mimi no longer supports the `is_timestep` or `is_time` functions and has replaced this functionality with comparison operators combined with the afformentioned `TimestepValue` and `TimestepIndex` types. + +*The User Change:* + +Any instance of the `is_timestep` function should be replaced with simple comparison with a `TimestepIndex` object ie. replace the logic `if is_timestep(t, 10) ...` with `if t == TimestepIndex(10) ...`. + +Any instance of the `is_time` function should be repalced with simple comparison with a `TimestepValue` object ie. replace the logic `if is_time(t, 2010) ...` with `if t == TimestepValue(2010) ...`. + +## The set_param! Function + +*The Mimi Change:* + +The `set_param!` method for setting a parameter value in a component now has the following signature: +``` +set_param!(m::Model, comp_name::Symbol, param_name::Symbol, ext_param_name::Symbol, val::Any) +``` +This function creates an external parameter called `ext_param_name` with value `val` in the model `m`'s list of external parameters, and connects the parameter `param_name` in component `comp_name` to this newly created external parameter. If there is already a parameter called `ext_param_name` in the model's list of external parameters, it errors. + +There are two available shortcuts: +``` +# Shortcut 1 +set_param!(m::Model, param_name::Symbol, val::Any) +``` +This method creates an external parameter in the model called `param_name`, sets its value to `val`, looks at all the components in the model `m`, finds all the unbound parameters named `param_name`, and creates connections from all the unbound parameters that are named `param_name` to the newly created external parameter. If there is already a parameter called `param_name` in the external parameter list, it errors. + +``` +# Shortcut 2 +set_param!(m::Model, comp_name::Symbol, param_name::Symbol, val::Any) +``` +This method creates a new external parameter called `param_name` in the model `m` (if that already exists, it errors), sets its value to `val`, and then connects the parameter `param_name` in component `comp_name` to this newly created external parameter. + +*The User Change:* + +Any old code that uses the `set_param!` method with only 4 arguments (shortcut #2 shown above) will still work for setting parameters **if they are found in only one component** ... but if you have multiple components that have parameters with the same name, using the old 4-argument version of `set_param!` multiple times will cause an error. Instead, you need to determine what behavior you want across multiple components with parameters of the same name: +- If you want parameters with the same name that are found in multiple components to have the _same_ value, use the 3-argument method: `set_param!(m, :param_name, val)`. You only have to call this once and it will set the same value for all components with an unconnected parameter called `param_name`. +- If you want different components that have parameters with the same name to have _different_ values, then you need to call the 5-argument version of `set_param!` individually for each parameter value, such as: +``` +set_param!(m, :comp1, :foo, :foo1, 25) # creates an external parameter called :foo1 with value 25, and connects just comp1/foo to that value +set_param!(m, :comp2, :foo, :foo2, 30) # creates an external parameter called :foo2 with value 30, and connects just comp2/foo to that value +``` + +Also, you can no longer call `set_param!` to change the value of a parameter that has already been set in the model. If the parameter has already been set, you must use the following to change it: +``` +update_param!(m, ext_param_name, new_val) +``` +This updates the value of the external parameter called `ext_param_name` in the model `m`'s list of external parameters. Any component that have parameters connected to this external parameter will now be connected to this new value. + +## The replace_comp! Function + +*The Mimi Change:* + +For simplicity, the `replace_comp!` function has been replaced with a method augmenting the julia Base `replace!` function. + +*The User Change:* + +Where you previously used +```julia +replace_comp!(m, new, old) +``` +to replace the `old` component with `new`, they should now use +```julia +replace!(m, old => new) +``` + +## Different-length Components + +*The Mimi Change:* + +**Update: This Functionality has been reenabled, please feel free to use it again, your old code should now be valid again.** + +Through Mimi v0.9.4, the optional keyword arguments `first` and `last` could be used to specify times for components that do not run for the full length of the model, like this: `add_comp!(mymodel, ComponentC; first=2010, last=2100)`. This functionality is still disabled, as it was starting in v0.9.5, and all components must run for the full length of the model's time dimension. This functionality may be re-implemented in a later version of Mimi. + +*The User Change:* + +Refactor your model so that all components are the same length. You may use the `run_timestep` function within each component to dictate it's behavior in different timesteps, including doing no calculations for a portion of the full model runtime. + +## Marginal Models + +*The Mimi Change:* + +For clarity, the previously named `marginal` attribute of a Mimi `MarginalModel` has been renamed to `modified`. Hence a `MarginalModel` is now described as a Mimi `Model` whose results are obtained by subtracting results of one `base` Model from those of another `marginal` Model that has a difference of `delta` with the signature: + +*The User Change:* + +Any previous access to the `marginal` attribute of a `MarginalModel`, `mm` below, should be changed from +```julia +model = mm.marginal +``` +to +```julia +model = mm.modified +``` +## Simulation Syntax + +#### Results Access + +*The Mimi Change:* + +For clarity of return types, Mimi no longer supports use of square brackets (a shortcut for julia Base `getindex`) to access the results of a Monte Carlo analysis, which are stored in the `SimulationInstance`. Instead, access to resulst is supported with the `getdataframe` function, which will return the results in the same type and format as the square bracket method used to return. + +*The User Change:* + +Results previously obtained with +```julia +results = si[:grosseconomy, :K] +``` +should now be obtained with +```julia +results = getdataframe(si, :grosseconomy, :K) +``` +#### Simulation Definition Modification Functions + +*The Mimi Change:* + +For consistency with julia syntax rules, the small set of unexported functions available to modify an existing `SimulationDefinition` have been renamed, moving from a camel case format to an underscore-based format as follows. + +*The User Change:* + +Replace your functions as follows. + +- `deleteRV!` --> `delete_RV!` +- `addRV!` --> `add_RV!` +- `replaceRV!` --> `replace_RV!` +- `deleteTransform!` --> `delete_transform!` +- `addTransform!` --> `add_transform!` +- `deleteSave!` --> `delete_save!` +- `addSave!` --> `add_save!` + +## Composite Components (optional) + +*The Mimi Change:* + +The biggest functionality **addition** of Mimi v1.0.0 is the inclusion of composite components. Prior versions of Mimi supported only "flat" models, i.e., with one level of components. This new version supports mulitple layers of components, with some components being "final" or leaf components, and others being "composite" components which themselves contain other leaf or composite components. This approach allows for a cleaner organization of complex models, and allows the construction of building blocks that can be re-used in multiple models. + +*The User Change:* + +All previous models are considered "flat" models, i.e. they have only one level of components, and do **not** need to be converted into multiple layer models to run. Thus this addition does not mean users need to alter their models, but we encourage you to check out the other documentation on composite components to learn how you can enhance your current models and built better onces in the future! diff --git a/docs/src/howto/howto_main.md b/docs/src/howto/howto_main.md index 1fcc08155..314acd117 100644 --- a/docs/src/howto/howto_main.md +++ b/docs/src/howto/howto_main.md @@ -18,7 +18,10 @@ If you find a bug in these guides, or have a clarifying question or suggestion, [How-to Guide 4: Work with Timesteps, Parameters, and Variables](@ref) -[How-to Guide 5: Port to Mimi v0.5.0](@ref) +[How-to Guide 5: Update the Time Dimension](@ref) -[How-to Guide 6: Port from (>=) Mimi v0.5.0 to Mimi v1.0.0](@ref) +[How-to Guide 6: Port to Mimi v0.5.0](@ref) + + +[How-to Guide 7: Port from (>=) Mimi v0.5.0 to Mimi v1.0.0](@ref) diff --git a/docs/src/ref/ref_structures_definitions.md b/docs/src/ref/ref_structures_definitions.md index c2ed860c4..a8cfec2d4 100644 --- a/docs/src/ref/ref_structures_definitions.md +++ b/docs/src/ref/ref_structures_definitions.md @@ -23,8 +23,6 @@ dim_dict::OrderedDict{Symbol, Union{Nothing, Dimension}} namespace::OrderedDict{Symbol, Any} first::Union{Nothing, Int} last::Union{Nothing, Int} -first_free::Bool -last_free::Bool is_uniform::Bool ``` The namespace of a leaf component can hold `ParameterDef`s and `VariableDef`s, both which are subclasses of `DatumDef` (see below for more details on these types). diff --git a/docs/src/tutorials/tutorial_3.md b/docs/src/tutorials/tutorial_3.md index e08567559..c48381793 100644 --- a/docs/src/tutorials/tutorial_3.md +++ b/docs/src/tutorials/tutorial_3.md @@ -28,7 +28,7 @@ When the original model calls [`set_param!`](@ref), Mimi creates an external par update_param!(mymodel, :parametername, newvalues) ``` -Note here that `newvalues` must be the same type (or be able to convert to the type) of the old values stored in that parameter, and the same size as the model dimensions indicate. Also note that it if you have updated the time dimension of the model with `set_dimension!(m, :time, values)` you will need to update all parameters with a `:time` dimension, **even if the values have not changed**, so that the model can update the underlying time labels (ie. year labels) to match your new model time labels (ie. year labels). +Note here that `newvalues` must be the same type (or be able to convert to the type) of the old values stored in that parameter, and the same size as the model dimensions indicate. If you wish to alter connections within an existing model, [`disconnect_param!`](@ref) and [`connect_param!`](@ref) can be used in conjunction with each other to update the connections within the model, although this is more likely to be done as part of larger changes involving components themslves, as discussed in the next subsection. @@ -83,20 +83,20 @@ update_param!(m, :fco22x, 3.000) run(m) ``` -A more complex example may be a situation where you want to update several parameters, including some with a `:time` dimension, in conjunction with altering the time index of the model itself. DICE uses a default time horizon of 2005 to 2595 with 10 year increment timesteps. If you wish to change this, say, to 2000 to 2500 by 10 year increment timesteps and use parameters that match this time, you could use the following code: +A more complex example may be a situation where you want to update several parameters, including some with a `:time` dimension, in conjunction with altering the time index of the model itself. DICE uses a default time horizon of 2005 to 2595 with 10 year increment timesteps. If you wish to change this, say, to 1995 to 2505 by 10 year increment timesteps and use parameters that match this time, you could use the following code: First you upate the `time` dimension of the model as follows: ```julia const ts = 10 -const years = collect(2000:ts:2500) +const years = collect(1995:ts:2505) nyears = length(years) set_dimension!(m, :time, years) ``` -Now you must update at least all parameters with a `:time` dimension, even if the length and values remain the same, so that the underlying time labels (ie. year labels) update to match your new model time labels (ie. year labels). +At this point all parameters with a `:time` dimension have been slightly modified under the hood, but the original values are still tied to their original years. In this case, for example, the external parameter has been shorted by 9 values (end from 2595 --> 2505) and padded at the front with a value of `missing` (start from 2005 --> 1995). Since some values, especially initializing values, are not time-agnostic, we maintain the relationship between values and time labels. If you wish to attach new values, you can use `update_param!` as below. In this case this is probably necessary, since having a `missing` in the first spot of a parameter with a `:time` dimension will likely cause an error when this value is accessed. -Create a dictionary `params` with one entry `(k, v)` per external parameter by name `k` to value `v`. Each key `k` must be a symbol or convert to a symbol matching the name of an external parameter that already exists in the model definition. Part of this dictionary may look like: +Create a dictionary `params` with one entry `(k, v)` per external parameter you want to update by name `k` to value `v`. Each key `k` must be a symbol or convert to a symbol matching the name of an external parameter that already exists in the model definition. Part of this dictionary may look like: ```julia params = Dict{Any, Any}() diff --git a/src/core/build.jl b/src/core/build.jl index 8e61599c6..88a8dbae9 100644 --- a/src/core/build.jl +++ b/src/core/build.jl @@ -359,7 +359,7 @@ function _build(md::ModelDef) t = dimension(md, :time) time_bounds = (firstindex(t), lastindex(t)) - propagate_time!(md, t = t) # this might not be needed, but is a final propagation to double check everything + _propagate_time_dim!(md, t) # this might not be needed, but is a final propagation to double check everything ci = _build(md, vdict, pdict, time_bounds) mi = ModelInstance(ci, md) diff --git a/src/core/connections.jl b/src/core/connections.jl index f4d840f67..3aab02ab6 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -650,3 +650,99 @@ function add_connector_comps!(obj::AbstractCompositeComponentDef) return nothing end + + +""" + _pad_parameters!(obj::ModelDef) + +Take each external parameter of the Model Definition `obj` and `update_param!` +with new data values that are altered to match a new time dimension by (1) trimming +the values down if the time dimension has been shortened and (2) padding with missings +as necessary. +""" +function _pad_parameters!(obj::ModelDef) + + model_times = time_labels(obj) + + for (name, param) in obj.external_params + if (param isa ArrayModelParameter) && (:time in param.dim_names) + + param_times = _get_param_times(param) + padded_data = _get_padded_data(param, param_times, model_times) + update_param!(obj, name, padded_data) + + end + end +end + +""" + _get_padded_data(param::ArrayModelParameter, param_times::Vector, model_times::Vector) + +Obtain the new data values for the Array Model Paramter `param` with current +time labels `param_times` such that they are altered to match a new time dimension +with keys `model_times` by (1) trimming the values down if the time dimension has +been shortened and (2) padding with missings as necessary. +""" +function _get_padded_data(param::ArrayModelParameter, param_times::Vector, model_times::Vector) + + data = param.values.data + ti = get_time_index_position(param) + + # first handle the back end + model_last = last(model_times) + param_last = last(param_times) + + if model_last < param_last # trim down the data + + trim_idx = findfirst(isequal(last(model_times)), param_times) + idxs = repeat(Any[:], ndims(data)) + idxs[ti] = 1:trim_idx + data = data[idxs...] + + elseif model_last > param_last # pad the data + + pad_length = length(model_times[findfirst(isequal(param_last), model_times)+1:end]) + dims = [size(data)...] + dims[ti] = pad_length + end_padding_rows = Array{Union{Missing, Number}}(missing, dims...) + data = vcat(data, end_padding_rows) + + end + + # now handle the front end + model_first = first(model_times) + param_first = first(param_times) + + # note we do not allow for any trimming off the front end + if model_first < param_first + + pad_length = length(model_times[1:findfirst(isequal(param_first), model_times)-1]) + dims = [size(data)...] + dims[ti] = pad_length + begin_padding_rows = Array{Union{Missing, Number}}(missing, dims...) + data = vcat(begin_padding_rows, data) + + end + + return data +end + +""" + _get_param_times(param::ArrayModelParameter{TimestepArray{FixedTimestep{FIRST, STEP, LAST}, T, N, ti, S}}) + +Return the time labels that parameterize the `TimestepValue` which in turn parameterizes +the ArrayModelParameter `param`. +""" +function _get_param_times(param::ArrayModelParameter{TimestepArray{FixedTimestep{FIRST, STEP, LAST}, T, N, ti, S}}) where {FIRST, STEP, LAST, T, N, ti, S} + return collect(FIRST:STEP:LAST) +end + +""" + _get_param_times(param::ArrayModelParameter{TimestepArray{VariableTimestep{TIMES}, T, N, ti, S}}) + +Return the time labels that parameterize the `TimestepValue` which in turn parameterizes +the ArrayModelParameter `param`. +""" +function _get_param_times(param::ArrayModelParameter{TimestepArray{VariableTimestep{TIMES}, T, N, ti, S}}) where {TIMES, T, N, ti, S} + return [TIMES...] +end diff --git a/src/core/defs.jl b/src/core/defs.jl index 9efb3267d..14d1d14f1 100644 --- a/src/core/defs.jl +++ b/src/core/defs.jl @@ -439,8 +439,8 @@ function set_param!(md::ModelDef, comp_def::AbstractComponentDef, param_name::Sy if has_parameter(md, ext_param_name) error("Cannot set parameter :$ext_param_name, the model already has an external parameter with this name.", - " Use `update_param(m, param_name, value)` to change the value, or use ", - "`set_param(m, comp_name, param_name, unique_param_name, value)` to set a value for only this component.") + " Use `update_param!(m, param_name, value)` to change the value, or use ", + "`set_param!(m, comp_name, param_name, unique_param_name, value)` to set a value for only this component.") end set_param!(md, param_name, value, dims = dims, comps = [comp_def], ext_param_name = ext_param_name) @@ -765,58 +765,18 @@ function _insert_comp!(obj::AbstractCompositeComponentDef, comp_def::AbstractCom end """ - propagate_time!(obj::AbstractComponentDef, t::Dimension; first::NothingInt=nothing, last::NothingInt=nothing) + _propagate_first_last!(obj::AbstractComponentDef; first::NothingInt=nothing, last::NothingInt=nothing) -Propagate a time dimension down through the comp def tree. This consists of two -primary functions which first push first and last through the comp def tree, and -then push the time dimension through, which sets any leftover first and last -attributes as well. +Propagate first and/or last through a component def tree. This function will override +any first and last that have been previously set. """ -function propagate_time!(obj::AbstractComponentDef; t::Union{Dimension, Nothing}=nothing, first::NothingInt=nothing, last::NothingInt=nothing) - - # the first step is pushing through fist and last, if they are set explicitly - if first !== nothing || last!== nothing - _propagate_firstlast!(obj, first=first, last=last) - end - - if t !== nothing - # propagate the actual time dimension through the components, and fill in - # any missing first and lasts as defaulting to the first and last elements of the - # time Dimension - _propagate_time_dim!(obj, t) - - # run over the object and check that first and last are within the time - # dimension of the parent - _check_times(obj, [keys(t)...]) - end - -end - -""" - _propagate_firstlast(obj::AbstractComponentDef; first::NothingInt=nothing, last::NothingInt=nothing) - -Propagate first and last through a component def treeIf first and last keyword -arguments are included as integers, then the object's first_free and/or last_free -flags are set to false respectively, these first and last are propagated through -and are immutable for the future (they will not vary freely with the model's -time dimension). -""" -function _propagate_firstlast!(obj::AbstractComponentDef; first::NothingInt=nothing, last::NothingInt=nothing) +function _propagate_first_last!(obj::AbstractComponentDef; first::NothingInt=nothing, last::NothingInt=nothing) - # set first - if !isnothing(first) && obj.first_free - obj.first_free = false - obj.first = first - end - - # set first - if !isnothing(last) && obj.last_free - obj.last_free = false - obj.last = last - end + !isnothing(first) ? obj.first = first : nothing + !isnothing(last) ? obj.last = last : nothing for c in compdefs(obj) # N.B. compdefs returns empty list for leaf nodes - _propagate_firstlast!(c, first=first, last=last) + _propagate_first_last!(c, first=first, last=last) end end @@ -825,26 +785,45 @@ end Propagate a time dimension down through the comp def tree. If first and last keyword arguments are not set in a given comp def they will be set to match the -time dimension, but first_free and /or last_free flags are left as true so these -can vary with the model's time dimension in the future. +time dimension. """ function _propagate_time_dim!(obj::AbstractComponentDef, t::Dimension) set_dimension!(obj, :time, t) - obj.first_free ? obj.first = firstindex(t) : nothing - obj.last_free ? obj.last = lastindex(t) : nothing + t_first = firstindex(t) + t_last = lastindex(t) + + curr_first = obj.first + curr_last = obj.last + + # Handle First + if isnothing(curr_first) || isa(obj, Mimi.ModelDef) # if we are working with an unset attribute or a ModelDef we always want to set first + obj.first = t_first + elseif t_first > curr_first # working with a component so we only want to move it if we're moving first forward (currently unreachable b/c error caught above) + obj.first = t_first + end + + # Handle Last + if isnothing(curr_last) || isa(obj, Mimi.ModelDef) + obj.last = t_last + elseif t_last < curr_last # working with a component so we only want to move it if we're moving last back (currently unreachable b/c caught error above) + obj.last = t_last + end for c in compdefs(obj) # N.B. compdefs returns empty list for leaf nodes _propagate_time_dim!(c, t) end + # run over the object and check that first and last are within the time + # dimension of the parent + _check_times(obj, [keys(t)...]) end """ function _check_times(obj::AbstractComponentDef, parent_time_keys::Array) - Check that all first and last times are properly contained within a comp_def + Check that all first and last times exist within contained within a comp_def `obj`'s parent time keys `parent_time_keys`. """ function _check_times(obj::AbstractComponentDef, parent_time_keys::Array) @@ -860,6 +839,38 @@ function _check_times(obj::AbstractComponentDef, parent_time_keys::Array) end end + +""" +function _check_first_last(obj::Union{Model, ModelDef}; first::NothingInt = nothing, last::NothingInt = nothing) + + Check that all first and last times are properly contained within a comp_def + `obj`'s time labels. +""" +function _check_first_last(obj::Union{Model, ModelDef}; first::NothingInt = nothing, last::NothingInt = nothing) + times = time_labels(obj) + !isnothing(first) && !(first in times) && error("The first index ($first) must exist within the model's time dimension $times.") + !isnothing(last) && !(last in times) && error("The last index ($last) must exist within the model's time dimension $times") +end + +""" + function set_first_last!(obj::AbstractCompositeComponentDef, comp_name::Symbol; first::NothingInt=nothing, last::NothingInt=nothing) + + Set the `first` and/or `last` attributes of model `obj`'s component `comp_name`, + after it has been added to the model. This will propagate the `first` and `last` + through any subcomponents of `comp_name` as well. Note that this will override + any previous `first` and `last` settings. + """ + function set_first_last!(obj::Model, comp_name::Symbol; first::NothingInt=nothing, last::NothingInt=nothing) + !has_comp(obj, comp_name) && error("Model does not contain a component named $comp_name") + + _check_first_last(obj, first = first, last = last) + + comp_def = compdef(obj, comp_name) + _propagate_first_last!(comp_def, first=first, last=last) + + dirty!(comp_def) +end + """ add_comp!( obj::AbstractCompositeComponentDef, @@ -867,8 +878,6 @@ end comp_name::Symbol=comp_def.comp_id.comp_name; first::NothingInt=nothing, last::NothingInt=nothing, - first_free::Bool=true, - last_free::Bool=true, before::NothingSymbol=nothing, after::NothingSymbol=nothing, rename::NothingPairList=nothing @@ -887,8 +896,6 @@ function add_comp!(obj::AbstractCompositeComponentDef, comp_name::Symbol=comp_def.comp_id.comp_name; first::NothingInt=nothing, last::NothingInt=nothing, - first_free::Bool=true, - last_free::Bool=true, before::NothingSymbol=nothing, after::NothingSymbol=nothing, rename::NothingPairList=nothing) # TBD: rename is not yet implemented @@ -905,13 +912,24 @@ function add_comp!(obj::AbstractCompositeComponentDef, comp_def.name = comp_name parent!(comp_def, obj) - # Handle time dimension for the copy, leave the time unset for the original - # component template - note that if the obj does not yet have a :time dimension - # set we can still set first and last, which is useful for calls to Component within - # the @defcomposite macro producing add_comp! calls - has_dim(obj, :time) ? t = dimension(obj, :time) : t = nothing - propagate_time!(comp_def, t = t, first=first, last=last) + # Handle time dimension for the component and leaving the time unset for the + # original component template + + # (1) Propagate the first and last from the add_comp! call through the component (default to nothing) + if has_dim(obj, :time) + _check_first_last(obj, first = first, last = last) # check that the first and last fall in the obj's time labels + end + _propagate_first_last!(comp_def; first = first, last = last) + + # (2) If the obj has a time dimension propgagate this through the component, + # which also sets remaining first and last to match the time dimension. + isa(obj, ModelDef) && !has_dim(obj, :time) && error("Cannot add a component to a Model without first setting the :time dimension") + if has_dim(obj, :time) + t = dimension(obj, :time) + _propagate_time_dim!(comp_def, t) + end + # Add dims and insert comp _add_anonymous_dims!(obj, comp_def) _insert_comp!(obj, comp_def, before=before, after=after) @@ -926,8 +944,6 @@ end comp_name::Symbol=comp_id.comp_name; first::NothingInt=nothing, last::NothingInt=nothing, - first_free::Bool=true, - last_free::Bool=true, before::NothingSymbol=nothing, after::NothingSymbol=nothing, rename::NothingPairList=nothing diff --git a/src/core/dimensions.jl b/src/core/dimensions.jl index ca75c2494..cf3512f74 100644 --- a/src/core/dimensions.jl +++ b/src/core/dimensions.jl @@ -86,28 +86,24 @@ Set the values of `ccd` dimension `name` to integers 1 through `count`, if `keys an integer; or to the values in the vector or range if `keys` is either of those types. """ function set_dimension!(ccd::AbstractCompositeComponentDef, name::Symbol, keys::Union{Int, Vector, Tuple, AbstractRange}) + redefined = has_dim(ccd, name) - # if redefined - # @warn "Redefining dimension :$name" - # end - dim = Dimension(keys) if name == :time - # check to make sure if we are setting time dimension for the Model that - # it doesn't start after, or end before, any of the components. Note that - # here we can dependent on the invariant that all subcomponents of a composite - # component have the same first and last bounds - ccd_first = [keys...][1] - ccd_last = [keys...][end] - for subcomp in compdefs(ccd) - subcomp.first_free || ccd_first > subcomp.first && error("Top time dimension must end after or at same time as all it's subcomponents, but $(ccd_first) is after $(subcomp.first).") - subcomp.last_free || ccd_last < subcomp.last && error("Top time dimension must start before or at same time as all it's subcomponents, but $(ccd_last) is before $(subcomp.last).") - end + # if we are redefining the time dimension of a model, the timestep length + # must match the timestep length of the old time dimension + redefined && _check_time_redefinition(ccd, keys) - propagate_time!(ccd, t = dim) + # propagate the time dimension through all sub-components + _propagate_time_dim!(ccd, dim) set_uniform!(ccd, isuniform(keys)) + + # if we are redefining the time dimension for a Model Definition + # pad the time arrays with missings and update their time labels + redefined && (ccd isa ModelDef) && _pad_parameters!(ccd) + end return set_dimension!(ccd, name, dim) @@ -144,7 +140,53 @@ function dim_names(ccd::AbstractCompositeComponentDef) return collect(dims) end +""" + _check_time_redefinition(obj::AbstractCompositeComponentDef, keys::Union{Int, Vector, Tuple, AbstractRange}) + +Run through all necesssary safety checks for redefining `obj`'s time dimenson to +a new dimension with keys `keys`. +""" +function _check_time_redefinition(obj::AbstractCompositeComponentDef, keys::Union{Int, Vector, Tuple, AbstractRange}) where T + + # get useful variables + curr_keys = time_labels(obj) + curr_first = obj.first + curr_last = obj.last + + new_keys = [keys...] + new_first = first(new_keys) + new_last = last(new_keys) + + # (1) check that the shift is legal + isa(obj, ModelDef) ? obj_name = "model" : obj_name = "component $(nameof(obj))" + new_first > curr_first && error("Cannot redefine the time dimension to start at $new_first because it is after the $obj_name's current start $curr_first.") + curr_first > new_last && error("Cannot redefine the time dimension to end at $new_last because it is before the $obj_name's current start $curr_first") + + # (2) check first and last + !(curr_first in new_keys) && error("The current first index ($curr_first) must exist within the model's new time dimension $new_keys.") # can be assumed since we cannot move the time forward + curr_last >= new_last && !(new_last in curr_keys) && error("The new last index ($new_last) must exist within the model's current time dimension $curr_keys, since the time redefinition contracts to an earlier year.") + curr_last < new_last && !(curr_last in new_keys) && error("The current last index ($curr_last) must exist within the model's redefined time dimension $new_keys, since the time redefinition expands to a later year.") + + # (3) check that the overlap region between the current keys and new keys holds same keys + if length(curr_keys) > 1 && length(new_keys) > 1 + if isuniform(curr_keys) # fixed timesteps + step_size(curr_keys) != step_size(new_keys) && error("Cannot redefine the time dimension to have a timestep size of $(step_size(new_keys)), must match the timestep size of current time dimension, $(step_size(curr_keys))") + + else # variable timesteps + start_idx = 1 # can be assumed since we cannot move the time forward + new_last < curr_last ? end_idx = findfirst(isequal(new_last), curr_keys) : end_idx = length(curr_keys) + expected_overlap = curr_keys[start_idx:end_idx] + + start_idx = findfirst(isequal(curr_first), new_keys) + end_idx = start_idx + length(expected_overlap) - 1 + observed_overlap = new_keys[start_idx:end_idx] + + expected_overlap != observed_overlap && error("Cannot redefine the time dimension, the overlapping portion of the current and new times must be identical.") + end + end + +end + dim_names(comp_def::AbstractComponentDef, datum_name::Symbol) = dim_names(datumdef(comp_def, datum_name)) dim_count(def::AbstractDatumDef) = length(dim_names(def)) - diff --git a/src/core/model.jl b/src/core/model.jl index 21ccbd34d..d71b178c5 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -127,8 +127,6 @@ is deprecated, but temporarily remains as a dummy argument to allow warning dete m::Model, comp_id::ComponentId, comp_name::Symbol=comp_id.comp_name; first::NothingInt=nothing, last::NothingInt=nothing, - first_free::Bool=true, - last_free::Bool=true, before::NothingSymbol=nothing, after::NothingSymbol=nothing, rename::NothingPairList=nothing @@ -152,8 +150,6 @@ end m::Model, comp_def::AbstractComponentDef, comp_name::Symbol=comp_id.comp_name; first::NothingInt=nothing, last::NothingInt=nothing, - first_free::Bool=true, - last_free::Bool=true, before::NothingSymbol=nothing, after::NothingSymbol=nothing, rename::NothingPairList=nothing diff --git a/src/core/types/defs.jl b/src/core/types/defs.jl index feb80c072..efb029e2b 100644 --- a/src/core/types/defs.jl +++ b/src/core/types/defs.jl @@ -42,8 +42,6 @@ end namespace::OrderedDict{Symbol, Any} first::Union{Nothing, Int} last::Union{Nothing, Int} - first_free::Bool - last_free::Bool is_uniform::Bool # Store a reference to the AbstractCompositeComponent that contains this comp def. @@ -75,7 +73,6 @@ end self.dim_dict = OrderedDict{Symbol, Union{Nothing, Dimension}}() self.namespace = OrderedDict{Symbol, Any}() self.first = self.last = nothing - self.first_free = self.last_free = true self.is_uniform = true self.parent = nothing return self diff --git a/test/runtests.jl b/test/runtests.jl index 00f10e7d0..b75e5dc5c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -110,7 +110,7 @@ Electron.prep_test_env() @info("test_firstlast.jl") @time include("test_firstlast.jl") - @info("test_explorer_model.jl") + @info("test_explorer_model.jl") # BROKEN @time include("test_explorer_model.jl") @info("test_explorer_sim.jl") diff --git a/test/test_components.jl b/test/test_components.jl index e9951bf63..7f2df64af 100644 --- a/test/test_components.jl +++ b/test/test_components.jl @@ -5,7 +5,8 @@ using Test import Mimi: compdefs, compdef, compkeys, has_comp, first_period, - last_period, compmodule, compname, compinstance, dim_keys, dim_values + last_period, compmodule, compname, compinstance, dim_keys, dim_values, + set_first_last! my_model = Model() @@ -46,6 +47,9 @@ end end end +# Testing that you cannot add a component without a time dimension +@test_throws ErrorException add_comp!(my_model, testcomp1) + # Start building up the model set_dimension!(my_model, :time, 2015:5:2110) add_comp!(my_model, testcomp1) @@ -104,34 +108,34 @@ end # 1. Test resetting the time dimension without explicit first/last values -cd = testcomp1 -@test cd.first === nothing # original component definition's first and last values are unset -@test cd.last === nothing +comp_def = testcomp1 +@test comp_def.first === nothing # original component definition's first and last values are unset +@test comp_def.last === nothing m = Model() set_dimension!(m, :time, 2001:2005) add_comp!(m, testcomp1, :C) # Don't set the first and last values here -cd = compdef(m.md, :C) # Get the component definition in the model -@test cd.first === 2001 -@test cd.last === 2005 +comp_def = compdef(m.md, :C) # Get the component definition in the model +@test comp_def.first === 2001 +@test comp_def.last === 2005 set_param!(m, :C, :par1, zeros(5)) -Mimi.build!(m) # Build the model -ci = compinstance(m, :C) # Get the component instance -@test ci.first == 2001 # The component instance's first and last values should match the model's index -@test ci.last == 2005 +Mimi.build!(m) # Build the model +ci = compinstance(m, :C) # Get the component instance +@test ci.first == 2001 && ci.last == 2005 # no change -set_dimension!(m, :time, 2005:2020) # Reset the time dimension -cd = compdef(m.md, :C) # Get the component definition in the model -@test cd.first === 2005 -@test cd.last === 2020 +set_dimension!(m, :time, 2000:2020) # Reset the time dimension +comp_def = compdef(m.md, :C) # Get the component definition in the model +@test comp_def.first === 2001 && comp_def.last === 2005 # no change -update_param!(m, :par1, zeros(16)) +update_param!(m, :par1, zeros(21)) Mimi.build!(m) # Build the model -ci = compinstance(m, :C) # Get the component instance -@test ci.first == 2005 # The component instance's first and last values should match the model's index -@test ci.last == 2020 +ci = compinstance(m, :C) # Get the component instance +@test ci.first == 2001 && ci.last == 2005 # no change +set_first_last!(m, :C, first = 2000, last = 2020) +comp_def = compdef(m.md, :C) # Get the component definition in the model +@test comp_def.first == 2000 && comp_def.last == 2020 # change! # 2. Test resetting the time dimension with explicit first/last values @@ -139,24 +143,22 @@ m = Model() set_dimension!(m, :time, 2000:2100) add_comp!(m, testcomp1, :C; first = 2010, last = 2090) -cd = compdef(m.md, :C) # Get the component definition in the model -@test cd.first == 2010 # First and last values are defined in the comp def because they were explicitly given -@test cd.last == 2090 +comp_def = compdef(m.md, :C) # Get the component definition in the model +@test comp_def.first == 2010 && comp_def.last == 2090 -set_dimension!(m, :time, 2010:2090) -set_param!(m, :C, :par1, zeros(81)) +set_dimension!(m, :time, 1950:2090) +set_param!(m, :C, :par1, zeros(141)) Mimi.build!(m) # Build the model ci = compinstance(m, :C) # Get the component instance -@test ci.first == 2010 # The component instance's first and last values are the same as in the comp def -@test ci.last == 2090 +@test ci.first == 2010 && ci.last == 2090 # The component instance's first and last values are the same as in the comp def -set_dimension!(m, :time, 2000:2200) # Reset the time dimension -update_param!(m, :par1, zeros(201)) # Have to reset the parameter to have the same width as the model time dimension +set_dimension!(m, :time, 1940:2200) # Reset the time dimension +update_param!(m, :par1, zeros(261)) # Have to reset the parameter to have the same width as the model time dimension -cd = compdef(m.md, :C) # Get the component definition in the model -@test cd.first == 2010 # First and last values should still be the same -@test cd.last == 2090 +comp_def = compdef(m.md, :C) # Get the component definition in the model +@test comp_def.first == 2010 # First and last values should still be the same +@test comp_def.last == 2090 Mimi.build!(m) # Build the model ci = compinstance(m, :C) # Get the component instance diff --git a/test/test_dimensions.jl b/test/test_dimensions.jl index b98f97d48..90b9314fe 100644 --- a/test/test_dimensions.jl +++ b/test/test_dimensions.jl @@ -5,7 +5,11 @@ using Test import Mimi: compdef, AbstractDimension, RangeDimension, Dimension, key_type, first_period, last_period, - ComponentReference, ComponentPath + ComponentReference, ComponentPath, ComponentDef, time_labels + +## +## Constants +## dim_varargs = Dimension(:foo, :bar, :baz) # varargs dim_vec = Dimension([:foo, :bar, :baz]) # Vector @@ -13,6 +17,10 @@ dim_range = Dimension(2010:2100) # AbstractRange rangedim = RangeDimension(2010:2100) # RangeDimension type dim_vals = Dimension(4) # Same as 1:4 +## +## Test a Bunch of Small Functionalities and Helpers +## + @test key_type(dim_varargs) == Symbol @test key_type(dim_vec) == Symbol @test key_type(dim_range) == Int @@ -33,7 +41,6 @@ dim_vals = Dimension(4) # Same as 1:4 @test lastindex(dim_range) == 2100 @test lastindex(dim_vals) == 4 - @test Base.keys(rangedim) == [2010:2100...] @test Base.values(rangedim) == [1:91...] @@ -45,7 +52,6 @@ end @test dim_varargs[:] == [1,2,3] # @test rangedim[2011] == 2 # TODO: this errors.. - @test get(dim_varargs, :bar, 999) == 2 @test get(dim_varargs, :new, 4) == 4 #adds a key/value pair @test get(rangedim, 2010, 1) == 1 @@ -76,37 +82,7 @@ end @test getindex(dim_varargs, :bar) == 2 @test getindex(dim_varargs, :) == [1,2,3] - -# Test resetting the time dimension - -@defcomp foo2 begin - x = Parameter(index=[time]) - y = Variable(index=[4]) -end - -m = Model() -set_dimension!(m, :time, 2000:2100) - -@test_throws ErrorException add_comp!(m, foo2; first = 2005, last = 2105) # Can't add a component longer than a model - -foo2_ref = add_comp!(m, foo2) - -foo2_ref = ComponentReference(m, :foo2) -my_foo2 = compdef(foo2_ref) - -# Test parameter connections -@test_throws ErrorException set_param!(m, :foo2, :x, 1990:2200) # too long -@test_throws ErrorException set_param!(m, :foo2, :x, 2005:2095) # too short -set_param!(m, :foo2, :x, 2000:2100) #Shouldn't error - -set_dimension!(m, :time, 2010:2050) - -@test first_period(m.md) == 2010 -@test last_period(m.md) == 2050 - - # Test that d.time returns AbstracTimesteps that can be used as indexes - @defcomp bar begin v1 = Variable(index = [time]) @@ -128,7 +104,9 @@ add_comp!(m, bar) run(m) @test m[:bar, :v1] == fixed_years +m = Model() set_dimension!(m, :time, variable_years) +add_comp!(m, bar) run(m) @test m[:bar, :v1] == variable_years @@ -139,4 +117,64 @@ dims = [:time] @test variable_dimensions(m, :bar, :v1) == dims @test variable_dimensions(m, (:bar,), :v1) == dims +## +## Test time dimension (and resetting it!) +## + +@defcomp foo2 begin + x = Parameter(index=[time]) + y = Variable(index=[4]) +end + +# build model, set dims, and add components +m = Model() +@test_throws ErrorException add_comp!(m, foo2) # cannot add a component before time dimension is set +set_dimension!(m, :time, 2000:2100) + +@test_throws ErrorException add_comp!(m, foo2; first = 2000, last = 2105) # 2105 cannot be found in the model's time dimension +@test_throws ErrorException add_comp!(m, foo2; first = 1950, last = 2100) # 1950 cannot be found in the model's time dimension + +foo2_ref1 = add_comp!(m, foo2) +foo2_ref2 = ComponentReference(m, :foo2) +@test foo2_ref1 === foo2_ref2 +my_foo2 = compdef(foo2_ref1) + +@test first_period(m.md) == first_period(m.md.namespace[:foo2]) == 2000 +@test last_period(m.md) == last_period(m.md.namespace[:foo2]) == 2100 + +# Set Parameters +original_x_vals = collect(2000:2100) +@test_throws ErrorException set_param!(m, :foo2, :x, 1990:2200) # too long +@test_throws ErrorException set_param!(m, :foo2, :x, 2005:2095) # too short +set_param!(m, :foo2, :x, original_x_vals) + +run(m) + +# Reset Dimension +@test_throws ErrorException set_dimension!(m, :time, 2050:2100) # can't move time forward +@test_throws ErrorException set_dimension!(m, :time, 2105:2200) # can't move new first past old last +set_dimension!(m, :time, 1990:2050) + +@test first_period(m.md) == 1990 +@test last_period(m.md) == 2050 +@test first_period(m.md.namespace[:foo2]) == 2000 # no change +@test last_period(m.md.namespace[:foo2]) == 2050 # trimmed with model + +# check that parameters were padded properly +new_x_vals = m.md.external_params[:x].values.data +@test length(new_x_vals) == length(time_labels(m)) +@test new_x_vals[11:end] == original_x_vals[1:51] +@test all(ismissing, new_x_vals[1:10]) + +run(m) # should still run because parameters were adjusted under the hood + +# reset again with late end +set_dimension!(m, :time, 1990:2200) +new_x_vals = m.md.external_params[:x].values.data +@test length(new_x_vals) == length(time_labels(m)) +@test all(ismissing, new_x_vals[1:10]) +@test new_x_vals[11:61] == original_x_vals[1:51] +@test all(ismissing, new_x_vals[62:end]) + +run(m) end #module diff --git a/test/test_firstlast.jl b/test/test_firstlast.jl index ae5d3bbc5..2491e61a9 100644 --- a/test/test_firstlast.jl +++ b/test/test_firstlast.jl @@ -3,7 +3,7 @@ module TestFirstLast using Mimi using Test -import Mimi: time_labels +import Mimi: time_labels, set_first_last! # # Define some Components @@ -84,10 +84,10 @@ set_dimension!(m, :time, collect(2015:5:2115)) # check that the first, last, and time have been updated properly for both the # ModelDef and ComponentDef(s) @test collect(2015:5:2115) == time_labels(m.md) == [keys(m.md.namespace[:emissions].dim_dict[:time])...] == [keys(m.md.namespace[:grosseconomy].dim_dict[:time])...] -@test m.md.first == m.md.namespace[:grosseconomy].first # grosseconomy first and last vary with model limits -@test m.md.last == m.md.namespace[:grosseconomy].last # grosseconomy first and last vary with model limits -@test m.md.namespace[:emissions].first == 2020 # emissions first and last are fixed -@test m.md.namespace[:emissions].last == 2105 # emissions first and last are fixed +@test m.md.namespace[:grosseconomy].first == 2015 # same as original model dim +@test m.md.namespace[:grosseconomy].last == 2110 # same as original model dim +@test m.md.namespace[:emissions].first == 2020 # explicitly set +@test m.md.namespace[:emissions].last == 2105 # explicitly set # reset any parameters that have a time dimension update_param!(m, :l, [(1. + 0.015)^t *6404 for t in 1:21]) @@ -100,8 +100,8 @@ run(m) # test that there are missing values in :emissions variables outside of the component's # run period, and no missing values in the :grosseconomy variables @test ismissing(m[:emissions, :E][1]) - @test sum(ismissing.(m[:emissions, :E][20:21])) == 2 - @test sum(ismissing.(m[:emissions, :E][2:19])) == 0 +@test all(ismissing, m[:emissions, :E][20:21]) +@test sum(ismissing.(m[:emissions, :E][2:19])) == 0 @test sum(ismissing.(m[:grosseconomy, :l])) == 0 # @@ -416,10 +416,39 @@ set_param!(m, :par_1_1, collect(1:length(time_labels(m)))) run(m) # check that first and last moved through properly -@test m.md.first == m.md.namespace[:top].first == m.md.namespace[:top].namespace[:A].first == m.md.namespace[:top].namespace[:A].namespace[:Comp1].first== 2005 -@test m.md.last == m.md.namespace[:top].last == m.md.namespace[:top].namespace[:A].last ==m.md.namespace[:top].namespace[:A].namespace[:Comp1].last == 2020 +@test m.md.namespace[:top].first == m.md.namespace[:top].namespace[:A].first == m.md.namespace[:top].namespace[:A].namespace[:Comp1].first== 2005 +@test m.md.namespace[:top].last == m.md.namespace[:top].namespace[:A].last ==m.md.namespace[:top].namespace[:A].namespace[:Comp1].last == 2020 @test m.md.namespace[:top].namespace[:B].first == m.md.namespace[:top].namespace[:B].namespace[:Comp3].first== 2010 @test m.md.namespace[:top].namespace[:B].last ==m.md.namespace[:top].namespace[:B].namespace[:Comp3].last == 2015 + + # + # Test set_first_last! function + # + + m = Model() + set_dimension!(m, :time, collect(2015:5:2110)) # 20 timesteps + add_comp!(m, grosseconomy) + add_comp!(m, emissions) + + # check that the attributes of the ModelDef and ComponentDef(s) have been set + # as expected + @test collect(2015:5:2110) == time_labels(m.md) == [keys(m.md.namespace[:emissions].dim_dict[:time])...] == [keys(m.md.namespace[:grosseconomy].dim_dict[:time])...] + @test m.md.first == m.md.namespace[:grosseconomy].first == m.md.namespace[:emissions].first + @test m.md.last == m.md.namespace[:grosseconomy].last == m.md.namespace[:emissions].last + + # now set the emissions first and last and check + set_first_last!(m, :emissions, first = 2020, last = 2105) + + @test collect(2015:5:2110) == time_labels(m.md) == [keys(m.md.namespace[:emissions].dim_dict[:time])...] == [keys(m.md.namespace[:grosseconomy].dim_dict[:time])...] + @test m.md.first == m.md.namespace[:grosseconomy].first + @test m.md.last == m.md.namespace[:grosseconomy].last + @test m.md.namespace[:emissions].first == 2020 + @test m.md.namespace[:emissions].last == 2105 + + # check warnings + @test_throws ErrorException set_first_last!(m, :grosseconomy, first = 2000) # too early + @test_throws ErrorException set_first_last!(m, :grosseconomy, last = 3000) + end #module diff --git a/test/test_model_structure.jl b/test/test_model_structure.jl index db68b56eb..1c26c7ea0 100644 --- a/test/test_model_structure.jl +++ b/test/test_model_structure.jl @@ -42,9 +42,9 @@ end m = Model() -# TBD: This is not necessarily an error with composites. -# make sure you can't add a component before setting time dimension -# @test_throws ErrorException add_comp!(m, A) +# make sure you can't add a component before setting time dimension (only true for +# adding a component to a model, not adding to a composite component) +@test_throws ErrorException add_comp!(m, A) set_dimension!(m, :time, 2015:5:2100) diff --git a/test/test_parametertypes.jl b/test/test_parametertypes.jl index cd5f04021..2156b4950 100644 --- a/test/test_parametertypes.jl +++ b/test/test_parametertypes.jl @@ -5,7 +5,8 @@ using Test import Mimi: external_params, external_param, TimestepMatrix, TimestepVector, - ArrayModelParameter, ScalarModelParameter, FixedTimestep, import_params! + ArrayModelParameter, ScalarModelParameter, FixedTimestep, import_params!, + set_first_last!, _get_param_times # # Test that parameter type mismatches are caught @@ -78,6 +79,7 @@ extpars = external_params(m.mi.md) @test isa(extpars[:a], ArrayModelParameter) @test isa(extpars[:b], ArrayModelParameter) +@test _get_param_times(extpars[:a]) == _get_param_times(extpars[:b]) == 2000:2100 @test isa(extpars[:c], ArrayModelParameter) @test isa(extpars[:d], ScalarModelParameter) @@ -104,7 +106,7 @@ set_param!(m, :a, Array{Int,2}(zeros(101, 3))) # should be able to convert from update_param!(m, :d, 5) # should work, will convert to float new_extpars = external_params(m) # Since there are changes since the last build, need to access the updated dictionary in the model definition @test extpars[:d].value == 0.5 # The original dictionary still has the old value -@test new_extpars[:d].value == 5. # The new dicitonary has the updated value +@test new_extpars[:d].value == 5. # The new dictionary has the updated value @test_throws ErrorException update_param!(m, :e, 5) # wrong type; should be array @test_throws ErrorException update_param!(m, :e, ones(10)) # wrong size update_param!(m, :e, [4,5,6,7]) @@ -129,33 +131,57 @@ update_param!(m, :e, [4,5,6,7]) end end -# 1. Test with Fixed Timesteps +# 1. update_param! with Fixed Timesteps m = Model() set_dimension!(m, :time, 2000:2004) add_comp!(m, MyComp2, first=2001, last=2003) set_param!(m, :MyComp2, :x, [1, 2, 3, 4, 5]) +# Year x Model MyComp2 +# 2000 1 first +# 2001 2 first +# 2002 3 +# 2003 4 last +# 2004 5 last -@test_throws ErrorException set_dimension!(m, :time, 2002:2004) # model starts after component -@test_throws ErrorException set_dimension!(m, :time, 2000:2002) # model ends before component - -# these tests specifically look for the case of updating a TimestepArray's data -# without time labels, thus simply calling param.values.data = new_data ... the addition -# of SubArray to data's type options made it necessary to convert types explicitly -# which may be causing a performance hit update_param!(m, :x, [2.,3.,4.,5.,6.]) update_param!(m, :x, zeros(5)) update_param!(m, :x, [1,2,3,4,5]) -set_dimension!(m, :time, 2001:2003) -update_param!(m, :x, [2, 3, 4]) +set_dimension!(m, :time, 1999:2001) +# Year x Model MyComp2 +# 1999 missing first +# 2000 1 +# 2001 2 last first, last + +x = external_param(m.md, :x) +@test ismissing(x.values.data[1]) +@test x.values.data[2:3] == [1.0, 2.0] +@test _get_param_times(x) == 1999:2001 +run(m) # should be runnable + +update_param!(m, :x, [2, 3, 4]) # change x to match +# Year x Model MyComp2 +# 1999 2 first +# 2000 3 +# 2001 4 last first, last + x = external_param(m.md, :x) -@test x.values isa Mimi.TimestepArray{Mimi.FixedTimestep{2001, 1, 2003}, Union{Missing,Float64}, 1} +@test x.values isa Mimi.TimestepArray{Mimi.FixedTimestep{1999, 1, 2001}, Union{Missing,Float64}, 1} @test x.values.data == [2., 3., 4.] run(m) -@test m[:MyComp2, :y][1] == 2 # 2001 -@test m[:MyComp2, :y][2] == 3 # 2002 +@test ismissing(m[:MyComp2, :y][1]) # 1999 +@test ismissing(m[:MyComp2, :y][2]) # 2000 +@test m[:MyComp2, :y][3] == 4 # 2001 +set_first_last!(m, :MyComp2, first = 1999, last = 2001) +# Year x Model MyComp2 +# 1999 2 first first +# 2000 3 +# 2001 4 last last + +run(m) +@test m[:MyComp2, :y] == [2, 3, 4] # 2. Test with Variable Timesteps @@ -163,17 +189,47 @@ m = Model() set_dimension!(m, :time, [2000, 2005, 2020]) add_comp!(m, MyComp2) set_param!(m, :MyComp2, :x, [1, 2, 3]) +# Year x Model MyComp2 +# 2000 1 first first +# 2005 2 +# 2010 3 last last + +set_dimension!(m, :time, [2000, 2005, 2020, 2100]) +# Year x Model MyComp2 +# 2000 1 first first +# 2005 2 +# 2020 3 last +# 2100 missing last + +x = external_param(m.md, :x) +@test ismissing(x.values.data[4]) +@test x.values.data[1:3] == [1.0, 2.0, 3.0] + +update_param!(m, :x, [2, 3, 4, 5]) # change x to match +# Year x Model MyComp2 +# 2000 2 first first +# 2005 3 +# 2020 4 last +# 2100 5 last -set_dimension!(m, :time, [2005, 2020, 2050]) - -update_param!(m, :x, [2, 3, 4]) x = external_param(m.md, :x) -@test x.values isa Mimi.TimestepArray{Mimi.VariableTimestep{(2005, 2020, 2050)}, Union{Missing,Float64}, 1} -@test x.values.data == [2., 3., 4.] +@test x.values isa Mimi.TimestepArray{Mimi.VariableTimestep{(2000, 2005, 2020, 2100)}, Union{Missing,Float64}, 1} +@test x.values.data == [2., 3., 4., 5.] run(m) -@test m[:MyComp2, :y][1] == 2 # 2005 -@test m[:MyComp2, :y][2] == 3 # 2020 +@test m[:MyComp2, :y][1] == 2 # 2000 +@test m[:MyComp2, :y][2] == 3 # 2005 +@test m[:MyComp2, :y][3] == 4 # 2020 +@test ismissing(m[:MyComp2, :y][4]) # 2100 - past last attribute for component +set_first_last!(m, :MyComp2, first = 2000, last = 2020) +# Year x Model MyComp2 +# 2000 1 first first +# 2005 2 +# 2020 3 last last + +run(m) +@test m[:MyComp2, :y][1:3] == [2., 3., 4.] +@test ismissing(m[:MyComp2, :y][4]) # 3. Test updating from a dictionary @@ -182,17 +238,18 @@ set_dimension!(m, :time, [2000, 2005, 2020]) add_comp!(m, MyComp2) set_param!(m, :MyComp2, :x, [1, 2, 3]) -set_dimension!(m, :time, [2005, 2020, 2050]) +set_dimension!(m, :time, [2000, 2005, 2020, 2100]) -update_params!(m, Dict(:x=>[2, 3, 4])) +update_params!(m, Dict(:x=>[2, 3, 4, 5])) x = external_param(m.md, :x) -@test x.values isa Mimi.TimestepArray{Mimi.VariableTimestep{(2005, 2020, 2050)}, Union{Missing,Float64}, 1} -@test x.values.data == [2., 3., 4.] +@test x.values isa Mimi.TimestepArray{Mimi.VariableTimestep{(2000, 2005, 2020, 2100)}, Union{Missing,Float64}, 1} +@test x.values.data == [2., 3., 4., 5.] run(m) -@test m[:MyComp2, :y][1] == 2 # 2005 -@test m[:MyComp2, :y][2] == 3 # 2020 -@test m[:MyComp2, :y][3] == 4 # 2050 +@test m[:MyComp2, :y][1] == 2 # 2000 +@test m[:MyComp2, :y][2] == 3 # 2005 +@test m[:MyComp2, :y][3] == 4 # 2020 +@test ismissing(m[:MyComp2, :y][4]) # 2100 # 4. Test updating the time index to a different length @@ -200,16 +257,41 @@ m = Model() set_dimension!(m, :time, 2000:2002) # length 3 add_comp!(m, MyComp2) set_param!(m, :MyComp2, :x, [1, 2, 3]) +# Year x Model MyComp2 +# 2000 1 first first +# 2001 2 +# 2002 3 last last set_dimension!(m, :time, 1999:2003) # length 5 - update_param!(m, :x, [2, 3, 4, 5, 6]) +# Year x Model MyComp2 +# 1999 2 first +# 2000 3 first +# 2001 4 +# 2002 5 last +# 2003 6 last + x = external_param(m.md, :x) @test x.values isa Mimi.TimestepArray{Mimi.FixedTimestep{1999, 1, 2003}, Union{Missing, Float64}, 1, 1} @test x.values.data == [2., 3., 4., 5., 6.] run(m) -@test m[:MyComp2, :y] == [2., 3., 4., 5., 6.] +@test ismissing(m[:MyComp2, :y][1]) +@test m[:MyComp2, :y][2:4] == [3., 4., 5.] +@test ismissing(m[:MyComp2, :y][5]) + +set_first_last!(m, :MyComp2, first = 1999, last = 2001) +# Year x Model MyComp2 +# 1999 2 first first +# 2000 3 +# 2001 4 last +# 2002 5 +# 2003 6 last + +run(m) +@test ismissing(m[:MyComp2, :y][4]) +@test ismissing(m[:MyComp2, :y][5]) +@test m[:MyComp2, :y][1:3] == [2., 3., 4.] # 5. Test all the warning and error cases @@ -235,21 +317,19 @@ update_param!(m, :z, 1) @test external_param(m.md, :z).value == 1 # Reset the time dimensions -set_dimension!(m, :time, 2005:2007) +set_dimension!(m, :time, 1999:2001) update_params!(m, Dict(:x=>[3,4,5], :y=>[10,20], :z=>0)) # Won't error when updating from a dictionary -@test external_param(m.md, :x).values isa Mimi.TimestepArray{Mimi.FixedTimestep{2005,1, 2007},Union{Missing,Float64},1} +@test external_param(m.md, :x).values isa Mimi.TimestepArray{Mimi.FixedTimestep{1999,1, 2001},Union{Missing,Float64},1} @test external_param(m.md, :x).values.data == [3.,4.,5.] @test external_param(m.md, :y).values == [10.,20.] @test external_param(m.md, :z).value == 0 - #------------------------------------------------------------------------------ # Test the three different set_param! methods for a Symbol type parameter #------------------------------------------------------------------------------ - @defcomp A begin p1 = Parameter{Symbol}() end