diff --git a/README.md b/README.md index a0a94d8b6..74fbaf441 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,11 @@ Mimi is a [Julia](http://julialang.org) package that provides a component model ## NEWS +We recently tagged and released a feature relese revamping the API surrounding parameters, please see https://www.mimiframework.org/Mimi.jl/dev/howto/howto_5/ and https://www.mimiframework.org/Mimi.jl/dev/howto/howto_9/. + On 7/15/2020 we officially tagged and released Mimi v1.0.0, which has some new features, documentation, and quite a bit of internals work as well. Since this is a major version change, there are some breaking changes that may require you to update your code. We have done the updates for the existing models in the Mimi registry (FUND, DICE, etc.), and will release new major versions of those today as well, so if you are using the latest version of Mimi and the latest version of the packages, all should run smoothly. -**Please view the how to guide here: https://www.mimiframework.org/Mimi.jl/stable/howto/howto_6/ for a run-down of how you should update your own code.** +**Please view the how to guide here: https://www.mimiframework.org/Mimi.jl/stable/howto/howto_7/ for a run-down of how you should update your own code.** In addition please do not hesitate to ask any questions on the forum, we are working hard to keep this transition smooth. diff --git a/contrib/test_all_models.jl b/contrib/test_all_models.jl index 96b10d2a5..306755b08 100644 --- a/contrib/test_all_models.jl +++ b/contrib/test_all_models.jl @@ -10,24 +10,27 @@ # julia --color=yes test_all_models.jl # +using Pkg +pkg_that_errored = [] +# first set of packages to test packages_to_test = [ - "MimiDICE2010" => ("https://github.com/anthofflab/MimiDICE2010.jl", "master"), - "MimiDICE2013" => ("https://github.com/anthofflab/MimiDICE2013.jl", "master"), - "MimiRICE2010" => ("https://github.com/anthofflab/MimiRICE2010.jl", "master"), - "MimiFUND" => ("https://github.com/fund-model/MimiFUND.jl", "master"), - "MimiPAGE2009" => ("https://github.com/anthofflab/MimiPAGE2009.jl", "master"), + "MimiDICE2010" => ("https://github.com/anthofflab/MimiDICE2010.jl", "df"), # note here we use the df branch + "MimiDICE2013" => ("https://github.com/anthofflab/MimiDICE2013.jl", "df"), # note here we use the df branch "MimiDICE2016" => ("https://github.com/AlexandrePavlov/MimiDICE2016.jl", "master"), + "MimiDICE2016R2" => ("https://github.com/anthofflab/MimiDICE2016R2.jl", "master"), + "MimiRICE2010" => ("https://github.com/anthofflab/MimiRICE2010.jl", "master"), + "MimiFUND" => ("https://github.com/fund-model/MimiFUND.jl", "mcs"), # note here we use the mcs branch + "MimiPAGE2009" => ("https://github.com/anthofflab/MimiPAGE2009.jl", "mcs"), # note here we use the mcs branch + "MimiPAGE2020" => ("https://github.com/lrennels/MimiPAGE2020.jl", "mcs"), # note using lrennels fork mcs branch, and testing this takes a LONG time :) "MimiSNEASY" => ("https://github.com/anthofflab/MimiSNEASY.jl", "master"), "MimiFAIR" => ("https://github.com/anthofflab/MimiFAIR.jl", "master"), "MimiMAGICC" => ("https://github.com/anthofflab/MimiMAGICC.jl", "master"), - "MimiHector" => ("https://github.com/anthofflab/MimiHector.jl", "master") + "MimiHector" => ("https://github.com/anthofflab/MimiHector.jl", "master"), ] -using Pkg - mktempdir() do folder_name - pkg_that_errored = [] + Pkg.activate(folder_name) Pkg.develop(PackageSpec(path=joinpath(@__DIR__, ".."))) @@ -54,6 +57,39 @@ mktempdir() do folder_name for p in pkg_that_errored println(p) end - end +# test separately because needs MimiFUND 3.8.6 +packages_to_test = [ + "MimiIWG" => ("https://github.com/rffscghg/MimiIWG.jl", "mcs") # note here we use the mcs branch +] + +mktempdir() do folder_name + + Pkg.activate(folder_name) + + Pkg.develop(PackageSpec(path=joinpath(@__DIR__, ".."))) + + Pkg.add([i isa Pair ? PackageSpec(url=i[2][1], rev=i[2][2]) : PackageSpec(i) for i in packages_to_test]) + + Pkg.resolve() + + for pkg_info in packages_to_test + pkg = pkg_info isa Pair ? pkg_info[1] : pkg_info + @info "Now testing $pkg." + try + Pkg.test(PackageSpec(pkg)) + catch err + push!(pkg_that_errored, pkg) + end + end +end + +println() +println() +println() + +println("The following packages errored:") +for p in pkg_that_errored + println(p) +end diff --git a/docs/make.jl b/docs/make.jl index 02f234593..b3f305fda 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -20,10 +20,12 @@ makedocs( "1 Construct + Run a Model" => "howto/howto_1.md", "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 Update Time Dimension" => "howto/howto_5.md", - "6 Port to v0.5.0" => "howto/howto_6.md", - "7 Port to v1.0.0" => "howto/howto_7.md" + "4 Timesteps" => "howto/howto_4.md", + "5 Parameters + Variables" => "howto/howto_5.md", + "6 Update Time Dimension" => "howto/howto_6.md", + "7 Port to v0.5.0" => "howto/howto_7.md", + "8 Port to v1.0.0" => "howto/howto_8.md", + "9 Port to New Param API" => "howto/howto_9.md" ], "Advanced How-to Guides" => Any[ "Advanced How-to Guides Intro" => "howto_advanced/howto_adv_main.md", diff --git a/docs/src/explanations/exp_pkgs.md b/docs/src/explanations/exp_pkgs.md index fbec848b6..83e0d5eed 100644 --- a/docs/src/explanations/exp_pkgs.md +++ b/docs/src/explanations/exp_pkgs.md @@ -23,6 +23,8 @@ run(m) ## Registries and The Mimi Registry +*Update (5/3/2020): Please note that going forward we are moving away from this model and encouraging registration in the General Registry to keep things simple and seamless for users instead of requiring extra maintenance and communication by our team. This will not be a breaking change, so current models registered with the Mimi registry will work as expected.* + Packages can be registered in a [Registry](https://julialang.github.io/Pkg.jl/v1/registries/), and "registries contain information about packages, such as available releases and dependencies, and where they can be downloaded. The [General registry](https://github.com/JuliaRegistries/General) is the default one, and is installed automatically". The Mimi registry is a custom registry maintained by the Mimi development team that colocates several Mimi models in one central registry in the same way julia colates packages in the General registry, where `Mimi` and other packages you commonly may use are located. While the development team maintains this registry and has some basic requirements such as continuous integration tesing (CI) and proper package structure as dictated by julia, they do not claim responsibility or knowledge of the content or quality of the models themselves. diff --git a/docs/src/howto/howto_1.md b/docs/src/howto/howto_1.md index bf933b865..095999637 100644 --- a/docs/src/howto/howto_1.md +++ b/docs/src/howto/howto_1.md @@ -1,6 +1,6 @@ # How-to Guide 1: Construct and Run a Model -This how-to guide pairs nicely with Tutorial 4: Create a Model and Tutorial 6: Create a Model with Composite Components, and serves as a higher-level version and refresher for those with some experience with Mimi. If this is your first time constructing and running a Mimi model, we recommend you start with Tutorial 4 (and Tutorial 6 if you are interested in composite components), which will give you more detailed step-by step instructions. +This how-to guide pairs nicely with [Tutorial 4: Create a Model](@ref) and [Tutorial 6: Create a Model Including Composite Components](@ref), and serves as a higher-level version and refresher for those with some experience with Mimi. If this is your first time constructing and running a Mimi model, we recommend you start with Tutorial 4 (and Tutorial 6 if you are interested in composite components), which will give you more detailed step-by step instructions. ## Defining Components @@ -48,13 +48,13 @@ The API for using the fourth argument, represented as `t` in this explanation, i To access the data in a parameter or to assign a value to a variable, you must use the appropriate index or indices (in this example, either the Timestep or region or both). -By default, all parameters and variables defined in the `@defcomp` will be allocated storage as scalars or Arrays of type `Float64.` For a description of other data type options, see How-to Guide 4: Work with Timesteps, Parameters, and Variables +By default, all parameters and variables defined in the [`@defcomp`](@ref) will be allocated storage as scalars or Arrays of type `Float64.` For a description of other data type options, see How-to Guide 5: Work with Parameters and Variables ### Composite Components Composite components can contain any number of subcomponents, **which can be either leaf components or more composite components**. To the degree possible, composite components are designed to operate in the same way as leaf components, although there are a few necessary differences: -- Leaf components are defined using the macro `@defcomp`, while Composite components are defined using `@defcomposite`. Each macro supports syntax and semantics specific to the type of component. +- Leaf components are defined using the macro [`@defcomp`](@ref), while Composite components are defined using [`@defcomposite`](@ref). Each macro supports syntax and semantics specific to the type of component. - Leaf components support user-defined `run_timestep()` functions, whereas composites have a built-in `run_timestep()` function that iterates over its subcomponents and calls their `run_timestep()` function. @@ -103,9 +103,9 @@ Now we construct a composite component `MyCompositeComponent` which holds the tw end ``` -The `connect` calls are responsible for making internal connections between any two components held by a composite component, similar to `connect_param!` described in the Model section below. +The `connect` calls are responsible for making internal connections between any two components held by a composite component, similar to [`connect_param!`](@ref) described in the Model section below. -As mentioned above, conflict resolution refers to cases where two subcomponents have identically named parameters, and thus the user needs to explicitly demonstrate that they are aware of this and create a new external parameter that will point to all subcomponent parameters with that name. For example, given leaf components `A` and `B`: +As mentioned above, conflict resolution refers to cases where two subcomponents have identically named parameters, and thus the user needs to explicitly demonstrate that they are aware of this and create a new shared model parameter that will point to all subcomponent parameters with that name. For example, given leaf components `A` and `B`: ```julia @defcomp Leaf1 begin @@ -161,21 +161,29 @@ add_comp!(m, ComponentA) add_comp!(m, ComponentA, :GDP) ``` -The first argument to `add_comp!` is the model, the second is the name of the ComponentId defined by `@defcomp`. If an optional third symbol is provided (as in the second line above), this will be used as the name of the component in this model. This allows you to add multiple versions of the same component to a model, with different names. +The first argument to [`add_comp!`](@ref) is the model, the second is the name of the ComponentId defined by [`@defcomp`](@ref). If an optional third symbol is provided (as in the second line above), this will be used as the name of the component in this model. This allows you to add multiple versions of the same component to a model, with different names. -The `add_comp` function has two more optional keyword arguments, `first` and `last`, which can be used to indicate a fixed start and/or end time (year in this case) that the compnonent should run for (within the bounds of the model's time dimension). For example, the following indicates that `ComponentA` should only run from 1900 to 2000. +The [`add_comp!`](@ref) function has two more optional keyword arguments, `first` and `last`, which can be used to indicate a fixed start and/or end time (year in this case) that the compnonent should run for (within the bounds of the model's time dimension). For example, the following indicates that `ComponentA` should only run from 1900 to 2000. ```julia add_comp!(m, ComponentA; first = 1900, last = 2000) ``` -The next step is to set the values for all the parameters in the components. Parameters can either have their values assigned from external data, or they can internally connect to the values from variables in other components of the model. +The next step is to set the values for all the parameters in the components. Parameters can either have their values assigned from external data, or they can internally connect to the values from variables in other components of the model. When assigned from external data, parameters are externally connected to a model parameter, which can be a shared model parameter with its own name and connected to more than one component-parameter pair, or an unshared model paarameter accessible only through the component-parameter pair names and connected solely to that parameter. -To make an external connection, the syntax is as follows: +To make an external connection to an unshared model parameter, the syntax is as follows: ```julia -set_param!(m, :ComponentName, :ParameterName, 0.8) # a scalar parameter -set_param!(m, :ComponentName, :ParameterName2, rand(351, 3)) # a two-dimensional parameter +update_param!(m, :ComponentName, :ParameterName1, 0.8) # a scalar parameter +update_param!(m, :ComponentName, :ParameterName2, rand(351, 3)) # a two-dimensional parameter +``` + +To make an external connection to a shared model parameter, the syntax is as follows: + +```julia +add_shared_param!(m, :ModelParameterName, 1.0) # add a shared model parameter to the model +connect_param!(m, :ComponentName, :ParameterName3, :ModelParameterName) # connect component parameter +connect_param!(m, :ComponentName, :ParameterName4, :ModelParameterName) ``` To make an internal connection, the syntax is as follows. @@ -301,11 +309,11 @@ end m = Model() set_dimension!(m, :time, 2005:2020) add_comp!(m, top, nameof(top)) -set_param!(m, :fooA1, 1) -set_param!(m, :fooA2, 2) -set_param!(m, :foo3, 10) -set_param!(m, :foo4, 20) -set_param!(m, :par_1_1, collect(1:length(2005:2020))) +update_param!(m, :top, :fooA1, 1) +update_param!(m, :top, :fooA2, 2) +update_param!(m, :top, :foo3, 10) +update_param!(m, :top, :foo4, 20) +update_param!(m, :top, :par_1_1, collect(1:length(2005:2020))) run(m) ``` Take a look at what you've created now using `explore(m)`, a peek into what you can learn in How To Guide 2! diff --git a/docs/src/howto/howto_2.md b/docs/src/howto/howto_2.md index 2174b42e4..7b6569c90 100644 --- a/docs/src/howto/howto_2.md +++ b/docs/src/howto/howto_2.md @@ -32,9 +32,9 @@ getdataframe(m, :Component1=>:Var1, :Component2=>:Var2) # request variables from Mimi provides support for plotting using [VegaLite](https://github.com/vega/vega-lite) and [VegaLite.jl](https://github.com/fredo-dedup/VegaLite.jl). -Plotting support is provided by the **Explorer UI**, rooted in `VegaLite`. The `explore` function allows the user to view and explore the variables and parameters of a model run. The explorer can be used in two primary ways. +Plotting support is provided by the **Explorer UI**, rooted in `VegaLite`. The [`explore`](@ref) function allows the user to view and explore the variables and parameters of a model run. The explorer can be used in two primary ways. -In order to invoke the explorer UI and explore all of the variables and parameters in a model, simply call the function `explore` with the model run as the required argument as shown below. This will produce a new browser window containing a selectable list of parameters and variables, organized by component, each of which produces a graphic. The exception here being that if the parameter or variable is a single scalar value, the value will appear alongside the name in the left-hand list. +In order to invoke the explorer UI and explore all of the variables and parameters in a model, simply call the function [`explore`](@ref) with the model run as the required argument as shown below. This will produce a new browser window containing a selectable list of parameters and variables, organized by component, each of which produces a graphic. The exception here being that if the parameter or variable is a single scalar value, the value will appear alongside the name in the left-hand list. ```julia run(m) @@ -43,7 +43,7 @@ explore(m) ![Explorer Model Example](../figs/explorer_model_example.png) -Alternatively, in order to view just one parameter or variable, call the (unexported) function `Mimi.plot` as below to return a plot object and automatically display the plot in a viewer, assuming `Mimi.plot` is the last command executed. Note that `plot` is not exported in order to avoid namespace conflicts, but a user may import it if desired. This call will return the type `VegaLite.VLSpec`, which you may interact with using the API described in the [VegaLite.jl](https://github.com/fredo-dedup/VegaLite.jl) documentation. For example, [VegaLite.jl](https://github.com/fredo-dedup/VegaLite.jl) plots can be saved as [PNG](https://en.wikipedia.org/wiki/Portable_Network_Graphics), [SVG](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics), [PDF](https://en.wikipedia.org/wiki/PDF) and [EPS](https://en.wikipedia.org/wiki/Encapsulated_PostScript) files. You may save a plot using the `save` function. +Alternatively, in order to view just one parameter or variable, call the (unexported) function `plot` as below to return a plot object and automatically display the plot in a viewer, assuming `plot` is the last command executed. Note that `plot` is not exported in order to avoid namespace conflicts, so needs to be called with Mimi.plot or a user may import it if desired. This call will return the type `VegaLite.VLSpec`, which you may interact with using the API described in the [VegaLite.jl](https://github.com/fredo-dedup/VegaLite.jl) documentation. For example, [VegaLite.jl](https://github.com/fredo-dedup/VegaLite.jl) plots can be saved as [PNG](https://en.wikipedia.org/wiki/Portable_Network_Graphics), [SVG](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics), [PDF](https://en.wikipedia.org/wiki/PDF) and [EPS](https://en.wikipedia.org/wiki/Encapsulated_PostScript) files. You may save a plot using the `save` function. Note that saving an interactive plot in a non-interactive file format, such as .pdf or .svg will result in a warning `WARN Can not resolve event source: window`, but the plot will be saved as a static image. If you wish to preserve interactive capabilities, you may save it using the .vegalite file extension. If you then open this file in Jupyter lab, the interactive aspects will be preserved. @@ -55,4 +55,4 @@ save("figure.svg", p) ``` ![Plot Model Example](../figs/plot_model_example.png) -These two functions, `explore` and `plot` also have methods applicable to the sensitivity analysis support described in the next section. Details can be found in the sensitivity analysis how-to guide How-to Guide 3: Conduct Sensitivity Analysis as well as Tutorial 4: Sensitivity Analysis (SA) Support. +These two functions, [`explore`](@ref) and `plot` also have methods applicable to the sensitivity analysis support described in the next section. Details can be found in the sensitivity analysis how-to guide How-to Guide 3: Conduct Sensitivity Analysis as well as Tutorial 4: Sensitivity Analysis (SA) Support. diff --git a/docs/src/howto/howto_3.md b/docs/src/howto/howto_3.md index a55015c9d..ecf7acda8 100644 --- a/docs/src/howto/howto_3.md +++ b/docs/src/howto/howto_3.md @@ -2,20 +2,20 @@ Mimi includes a host of routines which support running Monte Carlo simulations and various sensitivity analysis methods on Mimi models. Tutorial 5: Monte Carlo Simulations and Sensitivity Analysis Support is a good starting point for learning about these methods. This how-to guide includes more detail and optionality, covering more advanced options such as non-stochastic scenarios and running multiple models, which are not yet included in the tutorial. -## Overview +## Overview Running Monte Carlo simulations, and proximal sensitivity analysis, in Mimi can be broken down into three primary user-facing elements: -1. The `@defsim` macro, which defines random variables (RVs) which are assigned distributions and associated with model parameters, and override the default (random) sampling method. +1. The [`@defsim`](@ref) macro, which defines random variables (RVs) which are assigned distributions and associated with model parameters, and override the default (random) sampling method. -2. The `run` function, which runs a simulation instance, setting the model(s) on which a simulation definition can be run with `set_models!`, generates all trial data with `generate_trials!`, and has several with optional parameters and optional callback functions to customize simulation behavior. +2. The [`run`](@ref) function, which runs a simulation instance, setting the model(s) on which a simulation definition can be run within that, generates all trial data with `generate_trials!`, and has several with optional parameters and optional callback functions to customize simulation behavior. 3. The `analyze` function, which takes a simulation instance, analyzes the results and returns results specific to the type of simulation passed in. The rest of this document will be organized as follows: -1. The `@defsim` macro -2. The `run` function +1. The [`@defsim`](@ref) macro +2. The [`run`](@ref) function 3. The `analyze` function 4. Plotting and the Explorer UI 5. Other Useful Functions @@ -25,7 +25,7 @@ The rest of this document will be organized as follows: ## 1. The `@defsim` macro -The first step in a Mimi sensitivity analysis is using the `@defsim` macro to define and return a `SimulationDef{T}`. This simulation definition contains all the definition information in a form that can be applied at run-time. The `T` in `SimulationDef{T}` is any type that your application would like to live inside the `SimulationDef` struct, and most importantly specifies the sampling strategy to be used in your sensitivity analysis. +The first step in a Mimi sensitivity analysis is using the [`@defsim`](@ref) macro to define and return a `SimulationDef{T}`. This simulation definition contains all the definition information in a form that can be applied at run-time. The `T` in `SimulationDef{T}` is any type that your application would like to live inside the `SimulationDef` struct, and most importantly specifies the sampling strategy to be used in your sensitivity analysis. We have implemented four types for `T <: AbstractSimulationData`: @@ -50,7 +50,7 @@ const DeltaSimulationDef = SimulationDef{DeltaData} const DeltaSimulationInstance = SimulationInstance{DeltaData} ``` -In order to build the information required at run-time, the `@defsim` macro carries out several tasks including the following. +In order to build the information required at run-time, the [`@defsim`](@ref) macro carries out several tasks including the following. ### Define Random Variables (RVs) @@ -64,7 +64,7 @@ If using Latin Hypercube Sampling (LHS) is used, the following function must als In addition to the distributions available in the `Distributions` package, Mimi provides the following options. Note that these are not exported by default, so they need to either be explicitly imported (ie. `import Mimi: EmpiricalDistribution`) or prefixed with `Mimi.` when implemented (ie. `Mimi.EmpiricalDistribution(vals, probs)`): -- `EmpiricalDistribution`, which takes a vector of values and (optional) vector of probabilities and produces samples from these values using the given probabilities, if provided, or equal probability otherwise. To use this in a `@defsim`, you might do: +- `EmpiricalDistribution`, which takes a vector of values and (optional) vector of probabilities and produces samples from these values using the given probabilities, if provided, or equal probability otherwise. To use this in a [`@defsim`](@ref), you might do: ```julia using CSVFiles @@ -81,7 +81,7 @@ In addition to the distributions available in the `Distributions` package, Mimi # create your simulation @defsim begin ... - trc_transientresponse = EmpiricalDistribution(values, probs) + RandomVariable1 = EmpiricalDistribution(values, probs) ... end ``` @@ -89,7 +89,7 @@ In addition to the distributions available in the `Distributions` package, Mimi - `SampleStore{T}`, which stores a vector of samples that are produced in order by the `rand` function. This allows the user to to store a predefined set of values (useful for regression testing) and it is used by the LHS method, which draws all required samples at once at equal probability intervals and then shuffles the values. It is also used when rank correlations are specified, since this requires re-ordering draws from random variables. -- `ReshapedDistribution`, which supports use of vector-valued distributions, i.e., those that generate vectors of values for each single draw. An example (that motivated this addition) is the `Dirichlet` distribution, which produces a vector of values that sum to 1. To use this in `@defsim`, you might do: +- `ReshapedDistribution`, which supports use of vector-valued distributions, i.e., those that generate vectors of values for each single draw. An example (that motivated this addition) is the `Dirichlet` distribution, which produces a vector of values that sum to 1. To use this in [`@defsim`](@ref), you might do: ```julia rd = ReshapedDistribution([5, 5], Dirichlet(25,1)) @@ -100,13 +100,13 @@ In addition to the distributions available in the `Distributions` package, Mimi **For all applications in this section, it is important to note that for each trial, a random variable on the right hand side of an assignment will take on the value of a *single* draw from the given distribution. This means that even if the random variable is applied to more than one parameter on the left hand side (such as assigning to a slice), each of these parameters will be assigned the same value, not different draws from the distribution.** -The macro next defines how to apply the values generated by each RV to model parameters based on a pseudo-assignment operator: +The macro next defines how to apply the values generated by each RV to model parameters based on a pseudo-assignment operator. The left hand side of these assignments can be either a `param`, which must refer to a shared model parameter, or `comp.param` which refers to an unshared model parameter specific to a component. -- `param = RV` replaces the values in the parameter with the value of the RV for the current trial. -- `param += RV` replaces the values in the parameter with the sum of the original value and the value of the RV for the current trial. -- `param *= RV` replaces the values in the parameter with the product of the original value and the value of the RV for the current trial. +- `param = RV` or `comp.param = RV` replaces the values in the parameter with the value of the RV for the current trial. +- `param += RV` or `comp.param += RV` replaces the values in the parameter with the sum of the original value and the value of the RV for the current trial. +- `param *= RV` or `comp.param *= RV` replaces the values in the parameter with the product of the original value and the value of the RV for the current trial. -As described below, in `@defsim`, you can apply distributions to specific slices of array parameters, and you can "bulk assign" distributions to elements of a vector or matrix using a more condensed syntax. Note that these relationship assignments are referred to as **transforms**, and are referred to later in this documentation in the `add_transform!` and `delete_transform!` helper functions. +As described below, in [`@defsim`](@ref), you can apply distributions to specific slices of array parameters, and you can "bulk assign" distributions to elements of a vector or matrix using a more condensed syntax. Note that these relationship assignments are referred to as **transforms**, and are referred to later in this documentation in the `add_transform!` and `delete_transform!` helper functions. #### Apply RVs to model parameters: Assigning to array slices @@ -114,7 +114,7 @@ Options for applying distributions to array slices is accomplished using array access syntax on the left-hand side of an assignment. The assignment may use any of these assignment operators: `=`, `*=`, or `+=`, as described above. Slices can be indicated using a variety of specifications. Assume we -define two parameters in `@defcomp` as +define two parameters in [`@defcomp`](@ref) as ``` foo = Parameter(index=[regions]) bar = Parameter(index=[time, regions]) @@ -170,7 +170,7 @@ of RV value, i.e., you cannot combine this with the `*=` or `+=` operators. ### Specify a Sampling Strategies -As previously mentioned and included in the tutorial, the `@defsim` macro uses the call to `sampling` to type-parameterize the `SimulationDef` with one of three types, which in turn direct the sampling strategy of the simulation. This is done with the `sampling` line of the macro. +As previously mentioned and included in the tutorial, the [`@defsim`](@ref) macro uses the call to `sampling` to type-parameterize the `SimulationDef` with one of three types, which in turn direct the sampling strategy of the simulation. This is done with the `sampling` line of the macro. 1. Simple random-sampling Monte Carlo Simulation (`MCSData`), 2. Latin Hypercube Sampling (`LHSData`) @@ -188,13 +188,13 @@ Certain sampling strategies support (or necessitate) further customization. Thes - extra parameters (Sobol): Sobol sampling allows specification of the sample size N and whether or not one wishes to calculate second-order effects. -## 2. The `run` function +## 2. The [`run`](@ref) function -In it's simplest use, the `run` function generates and iterates over generated trial data, perturbing a chosen subset of Mimi's "external parameters", based on the defined distributions, and then runs the given Mimi model(s). The function retuns an instance of `SimulationInstance`, holding a copy of the original `SimulationDef` with additional trial information as well as a list of references ot the models and the results. Optionally, trial values and/or model results are saved to CSV files. +In it's simplest use, the [`run`](@ref) function generates and iterates over generated trial data, perturbing a chosen subset of Mimi's model parameters, based on the defined distributions, and then runs the given Mimi model(s). The function retuns an instance of `SimulationInstance`, holding a copy of the original `SimulationDef` with additional trial information as well as a list of references ot the models and the results. Optionally, trial values and/or model results are saved to CSV files. ### Function signature -The full signature for the `run` is: +The full signature for the [`run`](@ref) is: ``` function Base.run(sim_def::SimulationDef{T}, models::Union{Vector{Model}, Model}, samplesize::Int; @@ -213,7 +213,7 @@ Using this function allows a user to run the simulation definition `sim_def` for Optionally the user may run the `models` for `ntimesteps`, if specified, else to the maximum defined time period. Note that trial data are applied to all the associated models even when running only a portion of them. -If provided, the generated trials and results will be saved in the indicated `trials_output_filename` and `results_output_dir` respectively. If `results_in_memory` is set to false, then results will be cleared from memory and only stored in the `results_output_dir`. After `run`, the results of a `SimulationInstance` can be accessed using the `getdataframe` function with the following signature, which returns a `DataFrame`. +If provided, the generated trials and results will be saved in the indicated `trials_output_filename` and `results_output_dir` respectively. If `results_in_memory` is set to false, then results will be cleared from memory and only stored in the `results_output_dir`. After [`run`](@ref), the results of a `SimulationInstance` can be accessed using the `getdataframe` function with the following signature, which returns a `DataFrame`. ``` getdataframe(sim_inst::SimulationInstance, comp_name::Symbol, datum_name::Symbol; model::Int = 1) @@ -232,15 +232,15 @@ scenario_func(sim_inst::SimulationInstance, tup::Tuple) By default, the scenario loop encloses the simulation loop, but the scenario loop can be placed inside the simulation loop by specifying `scenario_placement=INNER`. When `INNER` is specified, the `scenario_func` is called after any `pre_trial_func` but before the model is run. -Finally, `run` returns the type `SimulationInstance` that contains a copy of the original `SimulationDef` in addition to trials information (`trials`, `current_trial`, and `current_data`), the model list `models`, and results information in `results`. +Finally, [`run`](@ref) returns the type `SimulationInstance` that contains a copy of the original `SimulationDef` in addition to trials information (`trials`, `current_trial`, and `current_data`), the model list `models`, and results information in `results`. -### Internal Functions to `run` +### Internal Functions to [`run`](@ref) -The following functions are internal to `run`, and do not need to be understood by users but may be interesting to understand. +The following functions are internal to [`run`](@ref), and do not need to be understood by users but may be interesting to understand. #### The set_models! function -The `run` function sets the model or models to run using `set_models!` function and saving references to these in the `SimulationInstance` instance. The `set_models!` function has several methods for associating the model(s) to run with the `SimulationDef`: +The [`run`](@ref) function sets the model or models to run using `set_models!` function and saving references to these in the `SimulationInstance` instance. The `set_models!` function has several methods for associating the model(s) to run with the `SimulationDef`: ``` set_models!(sim_inst::SimulationInstance, models::Vector{Model}) @@ -262,7 +262,7 @@ Also note that if the `filename` argument is used, all random variable draws are ### Non-stochastic Scenarios -In many cases, scenarios (which we define as a choice of values from a discrete set for one or more parameters) need to be considered in addition to the stochastic parameter variation. To support scenarios, `run` also offers iteration over discrete scenario values, which are passed to `run` via the keyword parameter `scenario_args::Dict{Symbol, Vector}`. For example, to iterate over scenario values "a", and "b", as well as, say, discount rates `0.025, 0.05, 0.07`, you could provide the argument: +In many cases, scenarios (which we define as a choice of values from a discrete set for one or more parameters) need to be considered in addition to the stochastic parameter variation. To support scenarios, [`run`](@ref) also offers iteration over discrete scenario values, which are passed to [`run`](@ref) via the keyword parameter `scenario_args::Dict{Symbol, Vector}`. For example, to iterate over scenario values "a", and "b", as well as, say, discount rates `0.025, 0.05, 0.07`, you could provide the argument: `scenario_args=Dict([:name => ["a", "b"], :rate => [0.025, 0.05, 0.07]])` @@ -328,18 +328,18 @@ This function wraps the `analyze` function in the [GlobalSensitivityAnalysis.jl] As described in the User Guide, Mimi provides support for plotting using [VegaLite](https://github.com/vega/vega-lite) and [VegaLite.jl](https://github.com/fredo-dedup/VegaLite.jl) within the Mimi Explorer UI and `Mimi.plot` function. These functions not only work for `Model`s, but for `SimulationInstance`s as well. -In order to invoke the explorer UI and explore all of the saved variables from the `save` list of a `SimulationInstance`, simply call the function `explore` with the simulation as the required argument as shown below. This will produce a new browser window containing a selectable list of variables, each of which produces a graphic. +In order to invoke the explorer UI and explore all of the saved variables from the `save` list of a `SimulationInstance`, simply call the function [`explore`](@ref) with the simulation as the required argument as shown below. This will produce a new browser window containing a selectable list of variables, each of which produces a graphic. ```julia run(sim_inst) explore(sim_inst) ``` -There are several optional keyword arguments for the `explore` method, as shown by the full function signature: +There are several optional keyword arguments for the [`explore`](@ref) method, as shown by the full function signature: ```julia explore(sim_inst::SimulationInstance; title="Electron", model_index::Int = 1, scen_name::Union{Nothing, String} = nothing, results_output_dir::Union{Nothing, String} = nothing) ``` -The `title` is the optional title of the application window, the `model_index` defines which model in your list of `models` passed to `run` you would like to explore (defaults to 1), and `scen_name` is the name of the specific scenario you would like to explore if there is a scenario dimension to your simulation. Note that if there are multiple scenarios, this is a **required** argument. Finally, if you have saved the results of your simulation to disk and cleared them from memory using `run`'s `results_in_memory` keyword argument flag set to `false`, you **must** provide a `results_output_dir` which indicates the parent folder for all outputs and potential subdirectories, identical to that passed to `run`. +The `title` is the optional title of the application window, the `model_index` defines which model in your list of `models` passed to [`run`](@ref) you would like to explore (defaults to 1), and `scen_name` is the name of the specific scenario you would like to explore if there is a scenario dimension to your simulation. Note that if there are multiple scenarios, this is a **required** argument. Finally, if you have saved the results of your simulation to disk and cleared them from memory using [`run`](@ref)'s `results_in_memory` keyword argument flag set to `false`, you **must** provide a `results_output_dir` which indicates the parent folder for all outputs and potential subdirectories, identical to that passed to [`run`](@ref). ![Explorer Simulation Example](../figs/explorer_sim_example.png) @@ -352,7 +352,7 @@ p = Mimi.plot(sim_inst, :component1, :parameter1) save("figure.svg", p) ``` -Note the function signature below, which has the same keyword arguments and requirements as the aforementioned `explore` method, save for `title`. +Note the function signature below, which has the same keyword arguments and requirements as the aforementioned [`explore`](@ref) method, save for `title`. ```julia plot(sim_inst::SimulationInstance, comp_name::Symbol, datum_name::Symbol; interactive::Bool = false, model_index::Int = 1, scen_name::Union{Nothing, String} = nothing, results_output_dir::Union{Nothing, String} = nothing) ``` @@ -370,7 +370,8 @@ A small set of unexported functions are available to modify an existing `Simulat * `replace_RV!(sim_def::SimulationDef, name::Symbol, dist::Distribution)` - replace the random variable with name `name` in Simulation Definition with a random variable of the same `name` but with the distribution `Distribution` * `delete_transform!(sim_def::SimulationDef, name::Symbol)!` - Delete all data transformations in Simulation Definition `sim_def` (i.e., replacement, addition or multiplication) of original data values with values drawn from the random variable named `name` -* `add_transform!(sim_def::SimulationDef, paramname::Symbol, op::Symbol, rvname::Symbol, dims::Vector=[])!` - Create a new `TransformSpec` based on `paramname`, `op`, `rvname` and `dims` to the Simulation Definition `sim_def`. The symbol `rvname` must refer to an existing random variable, and `paramname` must refer to an existing parameter. If `dims` are provided, these must be legal subscripts of `paramname`. Op must be one of :+=, :*=, or :(=). +* `add_transform!(sim_def::SimulationDef, paramname::Symbol, op::Symbol, rvname::Symbol, dims::Vector=[])!` - Create a new `TransformSpec` based on `paramname`, `op`, `rvname` and `dims` to the Simulation Definition `sim_def`. The symbol `rvname` must refer to an existing random variable, and `paramname` must refer to an existing shared model parameter that can thus be accessed by that name. Use the following signature if your `paramname` is an unshared model parameter specific to a component. If `dims` are provided, these must be legal subscripts of `paramname`. Op must be one of :+=, :*=, or :(=). +* `add_transform!(sim_def::SimulationDef, compname::Symbol, paramname::Symbol, op::Symbol, rvname::Symbol, dims::Vector=[])!` - Create a new TransformSpec based on `compname`, `paramname`, `op`, `rvname` and `dims` to the Simulation definition `sim_def`, and update the Simulation's NamedTuple type. The symbol `rvname` must refer to an existing RV, and `compname` and `paramname` must holding an existing component and parameter. If `dims` are provided, these must be legal subscripts of `paramname`. Op must be one of :+=, :*=, or :(=). For example, say a user starts off with a SimulationDefinition `MySimDef` with a parameter `MyParameter` drawn from the random variable `MyRV` with distribution `Uniform(0,1)`. @@ -439,7 +440,7 @@ N = 100 sd = @defsim begin # Define random variables. The rv() is required to disambiguate an # RV definition name = Dist(args...) from application of a distribution - # to an external parameter. This makes the (less common) naming of an + # to a model parameter. This makes the (less common) naming of an # RV slightly more burdensome, but it's only required when defining # correlations or sharing an RV across parameters. rv(name1) = Normal(1, 0.2) diff --git a/docs/src/howto/howto_4.md b/docs/src/howto/howto_4.md index 2733c6af4..92b41c533 100644 --- a/docs/src/howto/howto_4.md +++ b/docs/src/howto/howto_4.md @@ -1,6 +1,6 @@ -# How-to Guide 4: Work with Timesteps, Parameters, and Variables +# How-to Guide 4: Work with Timesteps -## Timesteps and available functions +## Timesteps and Available Functions An `AbstractTimestep` i.e. a `FixedTimestep` or a `VariableTimestep` is a type defined within Mimi in "src/time.jl". It is used to represent and keep track of time indices when running a model. @@ -66,58 +66,3 @@ TimestepIndex(1):TimestepIndex(10) # implicit step size of 1 TimestepIndex(1):2:TimestepIndex(10) # explicit step of type Int ``` Both `TimestepIndex` and `TimestepArray` have methods to support addition and subtraction of integers. Note that the addition or subtraction is relative to the definition of the `time` dimension, so while `TimestepIndex(1) + 1 == TimestepIndex(2)`, `TimestepValue(2000) + 1` could be equivalent to `TimestepValue(2001)` **if** 2001 is the next year in the time dimension, or `TimestepValue(2005)` if the array has a step size of 5. Hence adding or subtracting is relative to the definition of the `time` dimension. - - -## DataType specification of Parameters and Variables - -By default, the Parameters and Variables defined by a user will be allocated storage arrays of type `Float64` when a model is constructed. This default "number_type" can be overriden when a model is created, with the following syntax: -``` -m = Model(Int64) # creates a model with default number type Int64 -``` -But you can also specify individual Parameters or Variables to have different data types with the following syntax in a `@defcomp` macro: -``` -@defcomp example begin - p1 = Parameter{Bool}() # ScalarModelParameter that is a Bool - p2 = Parameter{Bool}(index = [regions]) # ArrayModelParameter with one dimension whose eltype is Bool - p3 = Parameter{Matrix{Int64}}() # ScalarModelParameter that is a Matrix of Integers - p4 = Parameter{Int64}(index = [time, regions]) # ArrayModelParameter with two dimensions whose eltype is Int64 -end -``` -If there are "index"s listed in the Parameter definition, then it will be an `ArrayModelParameter` whose `eltype` is the type specified in the curly brackets. If there are no "index"s listed, then the type specified in the curly brackets is the actual type of the parameter value, and it will be represent by Mimi as a `ScalarModelParameter`. - -## More on parameter indices - -As mentioned above, a parameter can have no index (a scalar), or one or multiple of the model's indexes. A parameter can also have an index specified in the following ways: - -```julia -@defcomp MyComponent begin - p1 = Parameter(index=[4]) # an array of length 4 - p2 = Parameter{Array{Float64, 2}}() # a two dimensional array of unspecified length -end -``` - -In both of these cases, the parameter's values are stored of as an array (p1 is one dimensional, and p2 is two dimensional). But with respect to the model, they are considered "scalar" parameters, simply because they do not use any of the model's indices (namely 'time', or 'regions'). - -## Updating an external parameter - -When `set_param!` is called, it creates an external parameter by the name provided, and stores the provided scalar or array value. It is possible to later change the value associated with that parameter name using the functions described below. - -```julia -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. - -#### Setting parameters with a dictionary - -In larger models it can be beneficial to set some of the external parameters using a dictionary of values. To do this, use the following function: - -```julia -set_leftover_params!(m, parameters) -``` - -Where `parameters` is a dictionary of type `Dict{String, Any}` where the keys are strings that match the names of the unset parameters in the model, and the values are the values to use for those parameters. - -## Using NamedArrays for setting parameters - -When a user sets a parameter, Mimi checks that the size and dimensions match what it expects for that component. If the user provides a NamedArray for the values, Mimi will further check that the names of the dimensions match the expected dimensions for that parameter, and that the labels match the model's index values for those dimensions. Examples of this can be found in "test/test_parameter_labels.jl". diff --git a/docs/src/howto/howto_5.md b/docs/src/howto/howto_5.md index 590e937a6..3f01b4b3b 100644 --- a/docs/src/howto/howto_5.md +++ b/docs/src/howto/howto_5.md @@ -1,69 +1,240 @@ -# How-to Guide 5: Update the Time Dimension +# How-to Guide 5: Work with Parameters and Variables -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. +## Parameters + +Component parameters in Mimi obtain values either (1) from a variable calculated by another component and passed through an internal connection or (2) from an externally set value stored in a model parameter. For the latter case, model parameters can be unshared, such that they can only connect to one component/parameter pair and must be accessed by specifying both the component and component's parameter name, or shared, such that they can connect to mulitple component/parameter pairs and have a unique name they can be referenced with. + +In the next few subsections we will present the API for setting, connecting, and updating parameters as presented by different potential use cases. The API consistes of only a few primary functions: + +- [`update_param!`](@ref) +- [`add_shared_param!`](@ref) +- [`disconnect_param!`](@ref) +- [`connect_param!`](@ref) + +along with the useful functions for batch setting: +- [`update_params!`](@ref) +- [`update_leftover_params!`](@ref) + +### Parameters when Creating a Model + +Take the example case of a user starting out building a two-component toy model. +```julia +@defcomp A begin + p1 = Parameter(default = 2) + p2 = Parameter(index = [time]) + + v1 = Variable() + + function run_timestep(p, v, d, t) + v.v1 = p.p1 + end +end + +@defcomp B begin + p3 = Parameter() + p4 = Parameter(index = [time]) + p5 = Parameter() + + v2 = Variable() + function run_timestep(p, v, d, t) + v.v2 = p.p3 + end +end + +m = Model() +set_dimension!(m, :time, 2000:2005) +add_comp!(m, A) +add_comp!(m, B) +``` +After the calls to [`add_comp!`](@ref), all four parameters are connected to a respective unshared model parameter. These unshared model parameters for `A`'s, `p2`, `B`'s `p3` and `p4` hold sentinel values of `nothing`, while that connected to `A`'s `p1` holds the value 2 as designated by the call to the `default` argument. + +At this point, you cannot `run(m)`, you will encounter: +```julia +run(m) +ERROR: Cannot build model; the following parameters still have values of nothing +and need to be updated or set: + p2 + p3 + p4 + p5 +``` +Per the above, we need to update these parameters so that they are connected to a non-`nothing` value. We have three cases here, (1) we want to update the value of an unshared parameter from `nothing` to a value, (2) we want to add a shared parameter and connect one or, more commonly, several component parameters to it, or (3) we want to connect a parameter to another component's variable. + +**Case 1:** In the first case, we simply call [`update_param!`](@ref) ie. +```julia +update_param!(m, :B, :p3, 5) +``` +The dimensions and datatype of the `value` set above will need to match those designated for the component's parameter, or corresponding appropriate error messages will be thrown. + +**Case 2:** In the second case, we will explicitly create and add a shared model parameter with [`add_shared_param!`](@ref) and then connect the parameters with [`connect_param!`](@ref) ie. +```julia +add_shared_param!(m, :shared_param, [1,2,3,4,5,6], dims = [:time]) +connect_param!(m, :A, :p2, :shared_param) +connect_param!(m, :B, :p4, :shared_param) +``` +The shared model parameter can have any name, including the same name as one of the component parameters, without any namespace collision with those, although for clarity we suggest using a unique name. + +Importantly, [`add_shared_param!`](@ref) has two optional keyword arguments, `dims` and `data_type`, which mirror specifications you gave in your [`@defcomp`](@ref) parameter definition and might be needed. Again we include error messages to alert you of this. Specifically: + +- **dims::Vector{Symbol}:** If your shared model parameter will be connected to parameters with dimensions, like one defined in [`@defcomp`](@ref) with `p = Parameter(index = [time])`, you'll need to specify dimensions with `add_shared_param!(m, :model_param_name, value; dims = [time])`. +- **data_type::DataType:** If your shared model parameter will be connected to parameters with dimensions, like one defined in [`@defcomp`](@ref) with `p = Parameter{Int64}()`, you *may* need to specify dimensions with `add_shared_param!(m, :model_param_name, value; data_type = Int64)` although we will try to interpret this under the hood for you. + +Appropriate error messages will instruct you to designate these if you forget to do so, and also recognize related problems with connections to parameters. + + +**Case 3.:** In the third case we want to connect `B`'s `p5` to `A`'s `v1`, and we can do so with: +```julia +connect_param!(m, :B, :p5, :A, :v1) +``` + +Now all your parameters are properly connected and you may run your model. ``` -set_dimension!(m, :time, time_keys) +run(m) ``` ----- -#### For example, one may wish to replace the FUND model's climate module with a different one, such as FAIR: +### Parameters when Modifying a Model + +Now say we have been given our model `m` above and we want to make some changes. Below we use some explicit examples that together should cover quite a few general cases. If something is not covered here that would be a useful case for us to explicitly explain, **don't hesitate to reach out**. We have also aimed to include useful warnings and error messages to point you in the right direction. + +To **update a parameter connected to an unshared model parameter**, use the same [`update_param!`](@ref) function as above: +```julia +update_param!(m, :A, :p1, 5) +``` +Trying this call when `A`'s parameter `p1` is connected to a shared parameter will error, and instruct you on the steps to use to either update the shared model parameter, or disconnect `A`'s `p1` from that shared model parameter and then proceed, both as explained below. -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. +To **update parameters connected to a shared model parameter**, use [`update_param!`](@ref) with different arguments, specifying the shared model parameter name: +```julia +update_param!(m, :shared_param, 5) +``` -We start with FUND +To **connect a parameter to another component's variable**, the below will disconnect any existing connections from `B`'s `p3` ([`disconnect_param!`](@ref) under the hood) and make the internal parameter connection to `A`'s `v1`: +```julia +connect_param!(m, :B, :p3, :A, :v1) +``` +Symmetrically, a subsequent call to [`update_param!`](@ref) would remove the internal connection and connect instead to an unshared model parameter as was done in the original `m`: +```julia +update_param!(m, :B, :p3, 10) ``` -using Mimi -using MimiFUND -m = MimiFUND.get_model() + +To **move from an external connection to a shared model parameter to an external connection to an unshared model parameter** use [`disconnect_param!`](@ref) followed by [`update_param!`](@ref) : +```julia +disconnect_param!(m, :A, :p2) +update_param!(m, :A, :p2, [101, 102, 103, 104, 105, 106]) ``` -where `MimiFUND.get_model` includes the call `set_dimension!(m, time, 1950:3000)`. +noting that this last call could also be a [`connect_param!`](@ref) to another parameter or variable etc., `A`'s `p2` is now free to be reset in any way you want. + +### Other Details + +#### Units + +In some cases you may have a model that specifies the units of parameters: +```julia +@defcomp A begin + p1 = Parameter(unit = "\$") + function run_timestep(p, v, d, t) + end +end + +@defcomp B begin + p2 = Parameter(unit = "thousands of \$") + function run_timestep(p, v, d, t) + end +end ----- -#### Now we need to change the `time` dimension to be 1765 to 2500: +m = Model() +set_dimension!(m, :time, 2000:2005) +add_comp!(m, A) +add_comp!(m, B) +``` +If you want to connect `p1` and `p2` to the same shared model parameter, you will encounter an error because the units do not match: +```julia +add_shared_param!(m, :shared_param, 100) +connect_param!(m, :A, :p1, :shared_param) # no error here +connect_param!(m, :B, :p2, :shared_param) -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. +ERROR: Units of compdef:p2 (thousands of $) do not match the following other +parameters connected to the same shared model parameter shared_param. To override +this error and connect anyways, set the `ignoreunits` flag to true: +`connect_param!(m, comp_def, param_name, model_param_name; ignoreunits = true)`. +MISMATCHES OCCUR WITH: [A:p1 with units $] +``` +As you see in the error message, if you want to override this error, you can use the `ignoreunits` flag: +```julia +connect_param!(m, :B, :p2, :shared_param, ignoreunits=true) +``` +#### Batch Update all Unset 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. +When building up a model, you may end up with several parameters that have not been explicitly updated that you want to batch update with pre-computer and saved values (ie. in a `csv` file). Before this update, the values still hold the a unusable sentinal value of `nothing` from intialization. A model with such parameters is not runnable. ----- -#### We now go ahead and change the `time` dimension to be 1765 to 2500: +The [`update_leftover_params!`](@ref) call takes a model and dictionary and updates the values of each the sentinal `nothing` model parameters by searching for their corresponding `(component_name, parameter_name)` pair in the provided dictionary with entries `k => v`, where `k` is a Tuple of Strings or Symbols `(component_name, parameter_name)`. The signature for this function is +``` +update_leftover_params!(m::Model, parameters::Dict) ``` -set_dimension!(m, :time, 1765:2500) +For example, given a model `m` with with component `A`'s parameters `p1` and `p2` which have not been updated from `nothing`, along with component `B`'s parameter `p1` that has not been updated. In this case the following will update those parameters and make the model runnable: +``` +parameters = Dict((:A, :p1) => 1, (:A, :p2) => :foo, (:B, :p1) => 100) +update_leftover_params!(m, parameeters) +``` +Note that your dictionary `parameters` **must include all leftover parameters that need to be set**, not just a subset of them, or it will error when it cannot find a desired key. + +#### Batch Update Specified Parameters with a Dictionary + +You can batch update a defined set of parameters using a `Dict` and the function [`update_params!`](@ref). You can do so for any set of unshared or shared model parameters. The signature for this function is: +```julia +update_params!(m::Model, parameters::Dict) ``` -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. +For each (k, v) pair in the provided `parameters` dictionary, [`update_param!`](@ref) is called to update the model parameter identified by the key to value v. For updating unshared parameters, each key k must be a Tuple matching the name of a component in `m` and the name of an parameter in that component. For updating shared parameters, each key k must be a symbol or convert to a symbol matching the name of a shared model parameter that already exists in the model. + +For example, given a model `m` with a shared model parameter `shared_param` connected to several component parameters, and two unshared model parameters `p1` and `p2` in a component `A`: +```julia +# update shared model parameters and unshared model parameters seprately +shared_dict = Dict(:shared_param => 1) +unshared_dict = Dict((:A, :p5) => 2, (:A, :p6) => 3) +update_params!(m, shared_dict) +update_params!(m, unshared_dict) + +# update both at the same time +dict = Dict(:shared_param => 1, (:A, :p5) => 2, (:A, :p6) => 3) +update_params!(m, dict) ``` -add_comp!(m, FAIR_component) # will run from 1765 to 1950 + +#### Anonymous Parameter Indices + +As mentioned above, a parameter can have no index (a scalar), or one or multiple of the model's indexes. A parameter can also have an index specified in the following ways: + +```julia +@defcomp MyComponent begin + p1 = Parameter(index=[4]) # an array of length 4 + p2 = Parameter{Array{Float64, 2}}() # a two dimensional array of unspecified length +end ``` -**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: +In both of these cases, the parameter's values are stored of as an array (p1 is one dimensional, and p2 is two dimensional). But with respect to the model, they are considered "scalar" parameters, simply because they do not use any of the model's indices (namely 'time', or 'regions'). + +#### Using NamedArrays for Setting Parameters + +When a user sets a parameter, Mimi checks that the size and dimensions match what it expects for that component. If the user provides a NamedArray for the values, Mimi will further check that the names of the dimensions match the expected dimensions for that parameter, and that the labels match the model's index values for those dimensions. Examples of this can be found in "test/test_parameter_labels.jl". + +## Variables + +[TODO] + +## DataType specification of Parameters and Variables + +By default, the Parameters and Variables defined by a user will be allocated storage arrays of type `Float64` when a model is constructed. This default "number_type" can be overriden when a model is created, with the following syntax: +```julia +m = Model(Int64) # creates a model with default number type Int64 +``` +But you can also specify individual Parameters or Variables to have different data types with the following syntax in a [`@defcomp`](@ref) macro: +```julia +@defcomp example begin + p1 = Parameter{Bool}() # ScalarModelParameter that is a Bool + p2 = Parameter{Bool}(index = [regions]) # ArrayModelParameter with one dimension whose eltype is Bool + p3 = Parameter{Matrix{Int64}}() # ScalarModelParameter that is a Matrix of Integers + p4 = Parameter{Int64}(index = [time, regions]) # ArrayModelParameter with two dimensions whose eltype is Int64 +end +``` +If there are "index"s listed in the Parameter definition, then it will be an `ArrayModelParameter` whose `eltype` is the type specified in the curly brackets. If there are no "index"s listed, then the type specified in the curly brackets is the actual type of the parameter value, and it will be represent by Mimi as a `ScalarModelParameter`. -- 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 +If you use this functionality and then `connect_param!` these Parameters to model parameters, you may need to +use the `data_type` keyword argument to specifiy the desired `DataType` of your connected parameter. diff --git a/docs/src/howto/howto_6.md b/docs/src/howto/howto_6.md index edef2aab8..d4f525753 100644 --- a/docs/src/howto/howto_6.md +++ b/docs/src/howto/howto_6.md @@ -1,107 +1,69 @@ -# How-to Guide 6: Port to Mimi v0.5.0 +# How-to Guide 6: 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 model 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.model_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 shared model parameters to have values in place of the assumed `missing`s using the [`update_param!`](@ref) function diff --git a/docs/src/howto/howto_7.md b/docs/src/howto/howto_7.md index c9a7b1149..e4b180f0e 100644 --- a/docs/src/howto/howto_7.md +++ b/docs/src/howto/howto_7.md @@ -1,233 +1,107 @@ -# How-to Guide 7: Port from (>=) Mimi v0.5.0 to Mimi v1.0.0 +# How-to Guide 7: 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 `update_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_8.md b/docs/src/howto/howto_8.md new file mode 100644 index 000000000..e50a26ab8 --- /dev/null +++ b/docs/src/howto/howto_8.md @@ -0,0 +1,233 @@ +# How-to Guide 8: 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_9.md b/docs/src/howto/howto_9.md new file mode 100644 index 000000000..0d59d2bb8 --- /dev/null +++ b/docs/src/howto/howto_9.md @@ -0,0 +1,202 @@ +# How-to Guide 9: Port to New Parameter API +### ... phasing out `set_param!` for all `update_param!` + +In the most recent feature release, Mimi presents a new, encouraged API for working with parameters that will hopefully be (1) simpler (2) clearer and (3) avoid unexpected behavior created by too much "magic" under the hood, per user requests. + +The following will first summarize the new, encouraged API and then take the next section to walk through the suggested ways to move from the older API, which includes [`set_param!`](@ref), to the new API, which phases out [`set_param!`](@ref). This release **should not be breaking** meaning that moving from the older to newer API may be done on your own time, although we would encourage taking the time to do so. Per usual, use the forum to ask any questions you may have, we will monitor closely to help work through corner cases etc. + +## The New API + +Here we briefly summarize the new, encouraged parameter API. We encourage users to follow-up by reading [How-to Guide 5: Work with Parameters and Variables](@ref)'s "Parameters" section for a detailed description of this new API, since the below is only a summary for brevity and to avoid duplication. We also note a related change to the [`@defsim`](@ref) Monte Carlo Simulation macro. + +### Parameters + +Component parameters in Mimi obtain values either (1) from a variable calculated by another component and passed through an internal connection or (2) from an externally set value stored in a model parameter. For the latter case, model parameters can be unshared, such that they can only connect to one component/parameter pair and must be accessed by specifying both the component and component's parameter name, or shared, such that they can connect to mulitple component/parameter pairs and have a unique name they can be referenced with. + +In the next few subsections we will present the API for setting, connecting, and updating parameters as presented by different potential use cases. The API consistes of only a few primary functions: + +- [`update_param!`](@ref) +- [`add_shared_param!`](@ref) +- [`disconnect_param!`](@ref) +- [`connect_param!`](@ref) + +along with the useful functions for batch setting: +- [`update_params!`](@ref) +- [`update_leftover_params!`](@ref) + +### Monte Carlo Simulations + +We have introduced new syntax to the monte carlo simulation definition macro [`@defsim`](@ref) to handle both shared and unshared parameters. + +Previously, one would always assign a random variable to a model parameter with syntax like: +```julia +rv(myrv) = Normal(0,1) +myparameter = myrv +# or the shortcut: +myparameter = Normal(0,1) +``` +Now, this syntax will only work if `myparameter` is a shared model parameter and thus accesible with that name. If the parameter is an unshared model parameter, use dot syntax like +```julia +rv(myrv) = Normal(0,1) +mycomponent.myparameter = myrv +# or the shortcut: +mycomponent.myparameter = Normal(0,1) +``` + +## Porting to the New API + +On a high level, calls to [`set_param!`](@ref) always related to **shared** model parameters, so it very likely that almost all of your current parameters are shared model parameters. The exception is parameters that are set by `default = ...` arguments in their [`@defcomp`](@ref) and then never reset, these will automatically be **unshared** model parameters. + +The changes you will want to make consist of (1) deciding which parameters you actually want to be connected to shared model parameters vs those you want to be connected to unshared model parameters (probably the majority) and (2) updating your code accordingly. You also may need to make related updates to [`@defsim`](@ref) Monte Carlo simulation definitions. + +**This section is not exhaustive, especially since [`set_param!`](@ref) has quite a few different methods for different permutations of arguments, so please don't hesitate to get in touch with questions about your specific use cases!** + +### `set_param!` and `update_param!` + +*The Mimi Change* + +A call to [`set_param!`](@ref) is equivalent to the the now suggested combination of calls to [`add_shared_param!`](@ref) and [`connect_param!`](@ref). For example: +```julia +set_param!(m, comp_name, param_name, model_param_name, value) +``` +is equivalent to +```julia +add_shared_param!(m, model_param_name, value) +connect_param!(m, comp_name, param_name, model_param_name) +``` +and similarly a call to +```julia +set_param!(m, comp_name, param_name, value) +``` +is equivalent to +```julia +add_shared_param!(m, model_param_name, value) # shared parameter gets the same name as the component parameter +connect_param!(m, comp_name, param_name, param_name) # once per component with a parameter named `param_name` +``` + +A call to [`update_param!`](@ref) retains the same functionality, such that +```julia +update_param!(m, model_param_name, value) +``` +will update a shared model parameter with name `model_param_name` to `value`, thus updating all component/parameter pairs externally connected to this shared model parameter. In addition, we now present a new [`update_param!`](@ref): +```julia +update_param!(m, comp_name, param_name, value) +``` +which will update the unshared model parameter externally connected to `comp_name`'s `param_name` to `value`. If `comp_name`'s `param_name` is connected to a shared model parameter, this call will error and present specific suggestions for either updating the shared model parameter or explicitly disconnecting your desired parameter before proceeding. + +Finally, [`add_shared_param!`](@ref) has two optional keyword arguments, `dims` and `data_type`, which mirror specifications you gave in your [`@defcomp`](@ref) parameter definition and might be needed. Again we include error messages to alert you of this. Specifically: + +- **dims::Vector{Symbol}:** If your shared model parameter will be connected to parameters with dimensions, like one defined in [`@defcomp`](@ref) with `p = Parameter(index = [time])`, you'll need to specify dimensions with `add_shared_param!(m, :model_param_name, value; dims = [time])`. +- **data_type::DataType:** If your shared model parameter will be connected to parameters with dimensions, like one defined in [`@defcomp`](@ref) with `p = Parameter{Int64}()`, you *may* need to specify dimensions with `add_shared_param!(m, :model_param_name, value; data_type = Int64)` although we will try to interpret this under the hood for you. + +Appropriate error messages will instruct you to designate these if you forget to do so, and also recognize related problems with connections to parameters. + + +*The User Change* + +Taking a look at your code, if you see a call to [`set_param!`](@ref), first decide if this is a case where you want to create a shared model parameter that can be connected to several component/parameter pairs. In many cases you will see a call to [`set_param!`](@ref) with four arguments: +```julia +set_param!(m, comp_name, param_name, value) +``` +and the desired behavior is that this component/parameter pair be connected to an unshared model parameter. To do this, change [`set_param!`](@ref) to [`update_param!`](@ref) with the same arguments: +```julia +update_param!(m, comp_name, param_name, value) +``` +This will simply update the value of the unshared model parameter specific to `comp_name` and `param_name`, which will be the sentinal value `nothing` if it has not been touched since `add_comp!`. Recall that now you do not have a model parameter accessible using just `param_name`, your unshared model parameter has a hidden and under-the-hood unique name to prevent collisions, but you will only be able to access the model parameter value with a combination of `comp_name` and `param_name`. Updating this parameter in the future thus uses the same syntax: +```julia +update_param!(m, comp_name, param_name, new_value) +``` + +Now, suppose you actually do want to create a shared model parameter. In this case, you may see a call to [`set_param!`](@ref) like: +```julia +set_param!(m, param_name, value) +``` +and you may want to keep this as the creation of and connection to a shared model parameter. In this case, you will use a combination of calls: +```julia +add_shared_param!(m, param_name, value) +connect_param!(m, comp_name_1, param_name, param_name) +connect_param!(m, comp_name_2, param_name, param_name) +``` +where the call to [`connect_param!`](@ref) must be made once for each component/parameter pair you want to connect to the shared model parameter, which previously was done under the hood by searching for all component's with a parameter with the name `param_name`. Note that in this new syntax, it's actually preferable not to use the same `param_name` for your shared model parameter. + +To keep your scripts understandable, we would actually recommend using a different parameter name, like follows. You can also connect parameters to this shared model parameter that do not share its name. **In essense Mimi will not make assumptions that component's with the same parameter name should get the same value**, you must be explicit: +```julia +add_shared_param!(m, model_param_name, value) +connect_param!(m, comp_name_1, param_name_1, model_param_name) +connect_param!(m, comp_name_2, param_name_2, model_param_name) +``` +Now you have a shared model parameter accessible with `model_param_name` and updating this parameter in the future can thus use the three argument [`update_param!`](@ref) syntax: +```julia +update_param!(m, model_param_name, new_value) +``` + +### `update_params!` + +*The Mimi Change* + +Previously, one could batch update a set of parameters using a `Dict` and the function [`update_params!`](@ref), which you passed a model `m` and a dictionary `parameters` with entries `k => v` where the key `k` was a Symbol matching the name of a shared model parameter and `v` the desired value. This will still work for shared model parameters, but we have added a new type of entry `k => v` where `k` is a Tuple of `(component_name, parameter_name)`. + +The signature for this function is: +```julia +update_params!(m::Model, parameters::Dict) +``` +For each (k, v) pair in the provided `parameters` dictionary, [`update_params!`](@ref) is called to update the model parameter identified by the key to value v. For updating unshared parameters, each key k must be a Tuple matching the name of a component in `m` and the name of an parameter in that component. For updating shared parameters, each key `k` must be a Symbol (or convert to a Symbol) matching the name of a shared model parameter that already exists in the model. + +For example, given a model `m` with a shared model parameter `model_param_name` connected to several component parameters, and two unshared model parameters `p1` and `p2` in a component `A`: +```julia +# update shared model parameters and unshared model parameters seprately +shared_dict = Dict(:model_param_name => 1) +unshared_dict = Dict((:A, :p5) => 2, (:A, :p6) => 3) +update_params!(m, shared_dict) +update_params!(m, unshared_dict) + +# update both at the same time +dict = Dict(:model_param_name => 1, (:A, :p5) => 2, (:A, :p6) => 3) +update_params!(m, dict) +``` + +*The User Change* + +Current calls to [`update_params!`](@ref) will still work as long as the keys are shared model parameters, if they no longer exist in your model as shared model parameters you'll need to make the key a Tuple like above. + +### `update_leftover_params!` + +*The Mimi Change* + +Previously, one could batch set all unset parameters in a model using a `Dict` and the function [`set_leftover_params!`](@ref), which you passed a model `m` and a dictionary `parameters` with entries `k => v` where the key `k` was a Symbol or String matching the name of a shared model parameter and `v` the desired value. This will still work, and will always create a new shared model parameter for each key. + +We have added a new function [`update_leftover_params!`](@ref) that does the same high-level operation, but updates the values of the already created unshared model parameters for each provided key entry `k => v`, where `k` is a Tuple of Strings or Symbols `(component_name, parameter_name)`. This avoids creation of undesired shared model parameters, and the connection of more than one component-parameter pair to the same shared model parameter without explicit direction from the user. + +*The User Change* + +We recommend moving to use of `update_leftover_params!` by changing your dictionary keys to be `(component_name, parameter_name)`. If previous calls to `set_leftover_params!` created shared model parameters with multiple connected component-parameter pairs **and you want to maintain this behavior**, you should do this explicitly with the aforementioned combination of `add_shared_param!` and a series of calls to `connect_param!`. + + +### Monte Carlo Simulations with `@defsim` + +*The Mimi Change* + +Previously, one would always assign a random variable to a model parameter with syntax like: +```julia +myparameter = Normal(0,1) +``` +or +```julia +rv(myrv) = Normal(0,1) +myparameter = myrv +``` +Now, this syntax will only work if `myparameter` is a shared model parameter and thus accesible with that name. If the parameter is an unshared model parameter, use dot syntax like +``` +mycomponent.myparameter = Normal(0,1) +``` +or +```julia +rv(myrv) = Normal(0,1) +mycomponent.myparameter = myrv +``` + +*The User Change* + +In an attempt to make this transition smooth, if you use the former syntax with an unshared model parameter, such as one that is set with a `default`, we will throw a warning and try under the hood to resolve which unshared model parameter you are trying to refer to. If we can figure it out without unsafe assumptions, we will warn about the assumption we are asking and proceed. If we can't do so safely, we will error. If you encounter this error case, just get in touch and we will help you update your code since this release is not supposed to break code! + +The easiest way to make this update is to run your existing code and look for warning and error messages which should give explicit descriptions of how to move forward to silence the warnings or resolve the errors. diff --git a/docs/src/howto/howto_main.md b/docs/src/howto/howto_main.md index 314acd117..f9b60da34 100644 --- a/docs/src/howto/howto_main.md +++ b/docs/src/howto/howto_main.md @@ -15,13 +15,19 @@ If you find a bug in these guides, or have a clarifying question or suggestion, [How-to Guide 3: Conduct Monte Carlo Simulations and Sensitivity Analysis](@ref) -[How-to Guide 4: Work with Timesteps, Parameters, and Variables](@ref) +[How-to Guide 4: Work with Timesteps](@ref) -[How-to Guide 5: Update the Time Dimension](@ref) +[How-to Guide 5: Work with Parameters and Variables](@ref) -[How-to Guide 6: Port to Mimi v0.5.0](@ref) +[How-to Guide 6: Update the Time Dimension](@ref) -[How-to Guide 7: Port from (>=) Mimi v0.5.0 to Mimi v1.0.0](@ref) +[How-to Guide 7: Port to Mimi v0.5.0](@ref) + + +[How-to Guide 8: Port from (>=) Mimi v0.5.0 to Mimi v1.0.0](@ref) + + +[How-to Guide 9: Port to New Parameter API](@ref) diff --git a/docs/src/howto_advanced/howto_adv_buildinit.md b/docs/src/howto_advanced/howto_adv_buildinit.md index 2b72e6248..b5d8dfd64 100644 --- a/docs/src/howto_advanced/howto_adv_buildinit.md +++ b/docs/src/howto_advanced/howto_adv_buildinit.md @@ -22,9 +22,9 @@ Note that you can retrieve values from a ModelInstance in the same way you index ## The init function -The `init` function can optionally be called within `@defcomp` and **before** `run_timestep`. Similarly to `run_timestep`, this function is called with parameters `init(p, v, d)`, where the component state (defined by the first three arguments) has fields for the Parameters, Variables, and Dimensions of the component you defined. +The `init` function can optionally be called within [`@defcomp`](@ref) and **before** `run_timestep`. Similarly to `run_timestep`, this function is called with parameters `init(p, v, d)`, where the component state (defined by the first three arguments) has fields for the Parameters, Variables, and Dimensions of the component you defined. -If defined for a specific component, this function will run **before** the timestep loop, and should only be used for parameters or variables without a time index e.g. to compute the values of scalar variables that only depend on scalar parameters. Note that when using `init`, it may be necessary to add special handling in the `run_timestep` function for the first timestep, in particular for difference equations. A skeleton `@defcomp` script using both `run_timestep` and `init` would appear as follows: +If defined for a specific component, this function will run **before** the timestep loop, and should only be used for parameters or variables without a time index e.g. to compute the values of scalar variables that only depend on scalar parameters. Note that when using `init`, it may be necessary to add special handling in the `run_timestep` function for the first timestep, in particular for difference equations. A skeleton [`@defcomp`](@ref) script using both `run_timestep` and `init` would appear as follows: ```julia @defcomp component1 begin diff --git a/docs/src/howto_advanced/howto_adv_datumrefs.md b/docs/src/howto_advanced/howto_adv_datumrefs.md index becf08eec..906c04d0e 100644 --- a/docs/src/howto_advanced/howto_adv_datumrefs.md +++ b/docs/src/howto_advanced/howto_adv_datumrefs.md @@ -4,7 +4,7 @@ While it is not encouraged in the customary use of Mimi, some scenarios may make ## Component References -Component references allow you to write cleaner model code when connecting components. The `add_comp!` function returns a reference to the component that you just added: +Component references allow you to write cleaner model code when connecting components. The [`add_comp!`](@ref) function returns a reference to the component that you just added: ```jldoctest faq1; output = false using Mimi @@ -25,7 +25,7 @@ typeof(MyComp) # note the type is a Mimi Component Definition Mimi.ComponentDef ``` -If you want to get a reference to a component after the `add_comp!` call has been made, you can construct the reference as: +If you want to get a reference to a component after the [`add_comp!`](@ref) call has been made, you can construct the reference as: ```jldoctest faq1; output = false mycomponent = Mimi.ComponentReference(m, :MyComp) @@ -36,11 +36,11 @@ typeof(mycomponent) # note the type is a Mimi Component Reference Mimi.ComponentReference ``` -You can use this component reference in place of the `set_param!` and `connect_param!` calls: +You can use this component reference in place of the [`update_param!`](@ref) and [`connect_param!`](@ref) calls: -#### References in place of `set_param!` +#### References in place of `update_param!` -The line `set_param!(model, :MyComponent, :myparameter, myvalue)` can be written as `mycomponent[:myparameter] = myvalue`, where `mycomponent` is a component reference. +The line `update_param!(model, :MyComponent, :myparameter, myvalue)` can be written as `mycomponent[:myparameter] = myvalue`, where `mycomponent` is a component reference. #### References in place of `connect_param!` diff --git a/docs/src/internals/Mimi Meeting_5_26_2021.ipynb b/docs/src/internals/Mimi Meeting_5_26_2021.ipynb new file mode 100644 index 000000000..12e76122f --- /dev/null +++ b/docs/src/internals/Mimi Meeting_5_26_2021.ipynb @@ -0,0 +1,145 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Mimi Meeting 4/26/2021" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "@defcomp Foo\n", + " p1 = Parameter()\n", + " p2 = Parameter(default = 1)\n", + " p3 = Parameter()\n", + " p4 = Parameter(default = 2)\n", + "end\n", + "\n", + "@defcomp Bar\n", + " p1 = Parameter()\n", + " p2 = Parameter()\n", + " p5 = Parameter(default = 5)\n", + " p6 = Parameter(default = 6)\n", + "end" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Simple Cases:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m = Model()\n", + "\n", + "add_comp!(m, Foo)\n", + "\n", + "# above the add_comp! call calls create_nonshared_param! for each parameter, and for the parameters with \n", + "# default values sets a value, otherwise it sets to a sentinal value or type for a missing parameter value\n", + "\n", + "update_param!(m, :Foo, :p1, 5) # updates nonshared param Foo.p1 to 5\n", + "\n", + "set_param!(m, :p2, 10) # now we create a new shared model parameter called p2\n", + "\n", + "update_param!(m, :Foo, :p2, 7) # Errors with a message that Foo.p2 is connected to a shared model \n", + " # parameter, and you can't use the comp.param method of update_param! \n", + " # in that case" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Old Way to Handle Parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m = Model()\n", + "add_comp!(m, Foo)\n", + "\n", + "# at this point there are no shared model parameters, everything is unconnected\n", + "\n", + "set_param!(m, :Foo, :p1, 5) # now there is a shared model parameter with the name :p1 connected to Foo\n", + "set_param!(m, :Bar, :p1, 5) # errors because we already have a :p1 model parameter\n", + "update_param!(m, :p1, 5)\n", + "\n", + "set_param!(m, :p2, 8) # now there is a shared model parameter with the name :p2 connected to Foo and Bar\n", + "update_param!(m, :p2, 5)\n", + "\n", + "# defaults handled at runtime" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Old Way to Handle Parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m = Model()\n", + "add_comp!(m, Foo)\n", + "\n", + "# at this point there are nonshared model parameters for each component/parameter pair, and the ones with \n", + "# defaults have values while the others have sentinal NaN or missing types\n", + "\n", + "update_param!(m, :Foo, :p1, 5)\n", + "update_param!(m, :p1, 5) # errors because there is no shared :p1\n", + "\n", + "create_shared_param!(m, :p2_shared, 5) # create's a shared parameter :p2\n", + "connect_param!(m, :Foo, :p2, :p2_shared) # connects Foo's :p2 to m's :p2_shared\n", + "connect_param!(m, :Bar, :p2, :p2_shared) # connects Bar's :p2 to m's :p2_shared" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Julia 1.6.0", + "language": "julia", + "name": "julia-1.6" + }, + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "1.6.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/src/internals/Mimi Meetings -5_26_2021.png b/docs/src/internals/Mimi Meetings -5_26_2021.png new file mode 100644 index 000000000..414498382 Binary files /dev/null and b/docs/src/internals/Mimi Meetings -5_26_2021.png differ diff --git a/docs/src/internals/proposals.md b/docs/src/internals/proposals.md index bb89badd0..da43d63b3 100644 --- a/docs/src/internals/proposals.md +++ b/docs/src/internals/proposals.md @@ -160,6 +160,6 @@ Variable dicts are keyed by a string of the form `"$component_name:$variable_nam #### Parameter dict -Parameter dicts are keyed by "external" names (symbols), with values represented in the same format as shown above for Variable dicts. +Parameter dicts are keyed by model parameter names (symbols), with values represented in the same format as shown above for Variable dicts. diff --git a/docs/src/ref/ref_API.md b/docs/src/ref/ref_API.md index cda519728..ebbc3fb78 100644 --- a/docs/src/ref/ref_API.md +++ b/docs/src/ref/ref_API.md @@ -2,9 +2,12 @@ ```@docs @defcomp +@defsim +@defcomposite MarginalModel Model -add_comp! +add_comp! +add_shared_param! connect_param! create_marginal_model delete_param! @@ -20,17 +23,23 @@ get_var_value hasvalue is_first is_last +is_time +is_timestep modeldef -parameter_names +nameof parameter_dimensions +parameter_names replace! -set_dimension! -set_leftover_params! -set_param! +replace_comp! +run +set_dimension! +set_leftover_params! +set_param! TimestepIndex TimestepValue -variable_dimensions -variable_names update_param! update_params! +update_leftover_params! +variable_dimensions +variable_names ``` diff --git a/docs/src/ref/ref_composites.md b/docs/src/ref/ref_composites.md index 5da2bbf14..19268c34c 100644 --- a/docs/src/ref/ref_composites.md +++ b/docs/src/ref/ref_composites.md @@ -4,6 +4,8 @@ Prior versions of Mimi supported only "flat" models, i.e., with one level of com To the degree possible, composite components are designed to operate the same as leaf components, though there are necessarily differences: -1. Leaf components are defined using the macro `@defcomp`, while composites are defined using `@defcomposite`. Each macro supports syntax and semantics specific to the type of component. +1. Leaf components are defined using the macro [`@defcomp`](@ref), while composites are defined using [`@defcomposite`](@ref). Each macro supports syntax and semantics specific to the type of component. 2. Leaf components support user-defined `run_timestep()` functions, whereas composites have a built-in `run_timestep()` function that iterates over its subcomponents and calls their `run_timestep()` function. The `init()` function is handled analogously. + +... [TODO] diff --git a/docs/src/ref/ref_main.md b/docs/src/ref/ref_main.md index 3f010c1bc..abcc56b8b 100644 --- a/docs/src/ref/ref_main.md +++ b/docs/src/ref/ref_main.md @@ -17,4 +17,5 @@ If you find a bug in these reference guides, or have a clarifying question or su - [Reference Guide: Structures - Instances](@ref) describes the core _instance_ data structures used to implement Mimi 1.0. + - [Reference Guide: Composite Components](@ref) describes the introduction of composite components in Mimi 1.0. diff --git a/docs/src/ref/ref_structures_classes_types.md b/docs/src/ref/ref_structures_classes_types.md index 323e0587f..d0dda1c19 100644 --- a/docs/src/ref/ref_structures_classes_types.md +++ b/docs/src/ref/ref_structures_classes_types.md @@ -2,6 +2,8 @@ ## Classes.jl +**NOTE: We plan to soon phase out use of Classes.jl for simplicity** + Most of the core data structures are defined using the `Classes.jl` package, which was developed for Mimi, but separated out as a generally useful julia package. The main features of `Classes` are: 1. Classes can subclass other classes, thereby inheriting the same list of fields as a starting point, which can then be extended with further fields. diff --git a/docs/src/ref/ref_structures_definitions.md b/docs/src/ref/ref_structures_definitions.md index a8cfec2d4..b771fb104 100644 --- a/docs/src/ref/ref_structures_definitions.md +++ b/docs/src/ref/ref_structures_definitions.md @@ -2,7 +2,7 @@ ## Model Definition -Models are composed of two separate structures, which we refer to as the "definition" side and the "instance" or "instantiated" side. The definition side is operated on by the user via the `@defcomp` and `@defcomposite` macros, and the public API (`add_comp!`, `set_param!`, `connect_param!`, etc.). +Models are composed of two separate structures, which we refer to as the "definition" side and the "instance" or "instantiated" side. The definition side is operated on by the user via the [`@defcomp`](@ref) and [`@defcomposite`](@ref) macros, and the public API ([`add_comp!`](@ref), [`update_param!`](@ref), [`connect_param!`](@ref), etc.). The instantiated model can be thought of as a "compiled" version of the model definition, with its data structures oriented toward run-time efficiency. It is constructed by Mimi in the `build()` function, which is called by the `run()` function. @@ -29,7 +29,7 @@ The namespace of a leaf component can hold `ParameterDef`s and `VariableDef`s, b ## Composite components -Composite components are defined using the `@defcomposite` macro which generates a composite component definition of the type `CompositeComponentDef` which has the following fields, in addition to the fields of a `ComponentDef`: +Composite components are defined using the [`@defcomposite`](@ref) macro which generates a composite component definition of the type `CompositeComponentDef` which has the following fields, in addition to the fields of a `ComponentDef`: ``` # CompositeComponentDef <: ComponentDef internal_param_conns::Vector{InternalParameterConnection} @@ -41,7 +41,7 @@ The namespace of a composite component can hold `CompositeParameterDef`s and`Com Note: we use "datum" to refer collectively to parameters and variables. Parameters are values that are fed into a component, and variables are values calculated by a component's `run_timestep` function. -Datum are defined with the `@defcomp` and `@defcomposite` macros, and have the following fields: +Datum are defined with the [`@defcomp`](@ref) and [`@defcomposite`](@ref) macros, and have the following fields: ``` # DatumDef name::Symbol @@ -79,11 +79,11 @@ datum_name::Symbol # name of the parameter or variable in the subcomponent's na ## ModelDef -A `ModelDef` is a top-level composite that also stores external parameters and a list of external parameter connections. It contains the following additional fields: +A `ModelDef` is a top-level composite that also stores model parameters and a list of model parameter connections. It contains the following additional fields: ``` # ModelDef <: CompositeComponentDef external_param_conns::Vector{ExternalParameterConnection} -external_params::Dict{Symbol, ModelParameter} +model_params::Dict{Symbol, ModelParameter} number_type::DataType dirty::Bool ``` @@ -91,9 +91,6 @@ Note: a ModelDef's namespace will only hold `AbstractComponentDef`s. ## Parameter Connections -Parameters hold values defined exogneously to the model ("external" parameters) or to the -component ("internal" parameters). - `InternalParameterConnection` Internal parameters are defined by connecting a parameter in one component to a variable in another component. This struct holds the names and `ComponentPath`s of the parameter @@ -102,10 +99,10 @@ internal parameter connections result in direct references from the parameter to storage allocated for the variable. `ExternalParameterConnection` -Values that are exogenous to the model are defined in external parameters whose values are -assigned using the public API function `set_param!()`, or by setting default values in -`@defcomp` or `@defcomposite`, in which case, the default values are assigned via an -internal call to `set_param!()`. +Values that are exogenous to the model are defined in model parameters whose values are +assigned using the public API function [`update_param!()`](@ref), or by setting default values in +[`@defcomp`](@ref) or [`@defcomposite`](@ref), in which case, the default values are assigned via an +internal call to [`update_param!()`](@ref). External connections are stored in the `ModelDef`, along with the actual `ModelParameter`s, which may be scalar values or arrays, as described below. @@ -119,13 +116,13 @@ src_var_name::Symbol dst_comp_path::ComponentPath dst_par_name::Symbol ignoreunits::Bool -backup::Union{Symbol, Nothing} # a Symbol identifying the external param providing backup data, or nothing +backup::Union{Symbol, Nothing} # a Symbol identifying the model param providing backup data, or nothing backup_offset::Union{Int, Nothing} # ExternalParameterConnection <: AbstractConnection comp_path::ComponentPath param_name::Symbol # name of the parameter in the component -external_param::Symbol # name of the parameter stored in the model's external_params +model_param_name::Symbol # name of the parameter stored in the model's model_params ``` ## Model parameters @@ -133,4 +130,4 @@ external_param::Symbol # name of the parameter stored in the model's external_p `ModelParameter` This is an abstract type that is the supertype of both `ScalarModelParameter{T}` and `ArrayModelParameter{T}`. These two parameterized types are used to store values set -for external model parameters. +for model parameters. diff --git a/docs/src/ref/ref_structures_instances.md b/docs/src/ref/ref_structures_instances.md index 32cc5e64b..c78400193 100644 --- a/docs/src/ref/ref_structures_instances.md +++ b/docs/src/ref/ref_structures_instances.md @@ -35,7 +35,7 @@ md::ModelDef nt::NamedTuple{Tuple{Symbol}, Tuple{Type}} # Type is either ScalarModelParameter (for scalar parameters) or TimestepArray (for array parameters) comp_paths::Vector{ComponentPath} ``` -Note: In the `ComponentInstanceParameters`, the values stored in the named tuple point to the actual variable arrays in the other components for things that are internally connected, or to the actual value stored in the mi.md.external_params dictionary if it's an external parameter. +Note: In the `ComponentInstanceParameters`, the values stored in the named tuple point to the actual variable arrays in the other components for things that are internally connected, or to the actual value stored in the mi.md.model_params dictionary if it's a model parameter. ``` # ComponentInstanceVariables (only exist in leaf component instances) nt::NamedTuple{Tuple{Symbol}, Tuple{Type}} # Type is either ScalarModelParameter (for scalar variables) or TimestepArray (for array variables) diff --git a/docs/src/tutorials/tutorial_1.md b/docs/src/tutorials/tutorial_1.md index a6764cd45..eca134be5 100644 --- a/docs/src/tutorials/tutorial_1.md +++ b/docs/src/tutorials/tutorial_1.md @@ -52,7 +52,7 @@ You will have to run this command every time you want to use Mimi in julia. You ## Mimi Registry -To access the models in the [MimiRegistry](https://github.com/mimiframework/Mimi.jl), you first need to connect your julia installation with the central Mimi registry of Mimi models. This central registry is like a catalogue of models that use Mimi that is maintained by the Mimi project. To add this registry, run the following command at the julia package REPL: +To access the models in the [MimiRegistry](https://github.com/mimiframework/Mimi.jl), you first need to connect your julia installation with the central Mimi registry of Mimi models. This central registry is like a catalogue of models that use Mimi that is maintained by the Mimi project. For more information about the Mimi Registry see [Explanations: Models as Packages](@ref), and note that for simplicity we aim to start phasing out use of a Mimi Registry for the General Registry as explained there. To add this registry, run the following command at the julia package REPL: ```julia pkg> registry add https://github.com/mimiframework/MimiRegistry.git diff --git a/docs/src/tutorials/tutorial_2.md b/docs/src/tutorials/tutorial_2.md index 4ec107405..6c4c608e9 100644 --- a/docs/src/tutorials/tutorial_2.md +++ b/docs/src/tutorials/tutorial_2.md @@ -10,8 +10,6 @@ Working through the following tutorial will require: **If you have not yet prepared these, go back to the first tutorial to set up your system.** -Note that we have recently released Mimi v1.0.0, which is a breaking release and thus we cannot promise backwards compatibility with version lower than v1.0.0 although several of these tutorials may run properly with older versions. For assistance updating your own model to v1.0.0, or if you are curious about the primary changes made, see the How-to Guide on porting to Mimi v1.0.0. Mimi v0.10.0 is functionally dentical to Mimi v1.0.0, but includes deprecation warnings instead of errors to assist users in porting to v1.0.0. - #### Step 1. Download FUND The first step in this process is downloading the FUND model, which is now made easy with the Mimi registry. Assuming you have already done the one-time run of the following command to connect your julia installation with the central Mimi registry of Mimi models, as instructed in the first tutorial, @@ -38,7 +36,7 @@ using MimiFUND # output ``` -Now we can access the public API of FUND, including the function `MimiFUND.get_model`. This function returns a copy of the default FUND model. Here we will first get the model, and then use the `run` function to run it. +Now we can access the public API of FUND, including the function `MimiFUND.get_model`. This function returns a copy of the default FUND model. Here we will first get the model, and then use the [`run`](@ref) function to run it. ```jldoctest tutorial2; output = false, filter = r".*"s m = MimiFUND.get_model() diff --git a/docs/src/tutorials/tutorial_3.md b/docs/src/tutorials/tutorial_3.md index 97c9bd21a..5acf63e64 100644 --- a/docs/src/tutorials/tutorial_3.md +++ b/docs/src/tutorials/tutorial_3.md @@ -10,27 +10,33 @@ Working through the following tutorial will require: **If you have not yet prepared these, go back to the first tutorial to set up your system.** -Note that we have recently released Mimi v1.0.0, which is a breaking release and thus we cannot promise backwards compatibility with version lower than v1.0.0 although several of these tutorials may run properly with older versions. For assistance updating your own model to v1.0.0, or if you are curious about the primary changes made, see the How-to Guide on porting to Mimi v1.0.0. Mimi v0.10.0 is functionally dentical to Mimi v1.0.0, but includes deprecation warnings instead of errors to assist users in porting to v1.0.0. - ## Introduction -There are various ways to modify an existing model, and this tutorial aims to introduce the Mimi API relevant to this broad category of tasks. It is important to note that regardless of the goals and complexities of your modifications, the API aims to allow for modification **without alteration of the original code for the model being modified**. Instead, you will download and run the existing model, and then use API calls to modify it. This means that in practice, you should not need to alter the source code of the model you are modifying. Thus, it is easy to keep up with any external updates or improvements made to that model. +There are various ways to modify an existing model, and this tutorial aims to introduce the Mimi API relevant to this broad category of tasks. It is important to note that regardless of the goals and complexities of your modifications, the API aims to allow for modification **without alteration of the original code for the model being modified**. Instead, you will download and run the existing model, and then use API calls to modify it. This means that in practice, you should not need to alter the source code of the model you are modifying. This should make it simple to keep up with any external updates or improvements made to that model. Possible modifications range in complexity, from simply altering parameter values, to adjusting an existing component, to adding a brand new component. ## Parametric Modifications: The API -Several types of changes to models revolve around the parameters themselves, and may include updating the values of parameters and changing parameter connections without altering the elements of the components themselves or changing the general component structure of the model. The most useful functions of the common API in these cases are likely **[`update_param!`](@ref)/[`update_params!`](@ref), [`disconnect_param!`](@ref), and [`connect_param!`](@ref)**. For detail on these functions see the API reference guide, Reference Guide: The Mimi API. +Several types of changes to models revolve around the parameters themselves, and may include updating the values of parameters and changing parameter connections without altering the elements of the components themselves or changing the general component structure of the model. The most useful functions of the common API in these cases are likely [`update_param!`](@ref)/[`update_params!`](@ref), [`add_shared_param!`](@ref), [`disconnect_param!`](@ref) and [`connect_param!`](@ref). For detail on these functions see the [How-to Guide 5: Work with Parameters and Variables](@ref) and the API reference guide, [Reference Guide: The Mimi API](@ref). -When the original model calls [`set_param!`](@ref), Mimi creates an external parameter by the name provided, and stores the provided scalar or array value. The functions [`update_param!`](@ref) and [`update_params!`](@ref) allow you to change the value associated with this external parameter. +By the Mimi structure, the parameters in a model you start with receive their values either from an exogenously set model parameters (shared or unshared as described in How To Guide 5) through an external parameter connection, or from another component's variable through an internal parameter connection. +The functions [`update_param!`](@ref) and [`update_params!`](@ref) allow you to change the value associated with a given model parameter, and thus value connected to the respective component-parameter pair(s) connected to it. If the model parameter is a shared model parameter you can use the following to update it: +```julia +update_param!(mymodel, :model_parameter_name, newvalues) +``` +If the model parameter is unshared, and thus the value can only be connected to one component/parameter pair, you can use the following to update it: ```julia -update_param!(mymodel, :parametername, newvalues) +update_param!(mymodel, :comp_name, :param_name, 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. + +**If you are unsure whether the component-parameter pair you wish to update is connected to a shared or unshared model parameter** use the latter, four argument call above and an error message will give you specific instructions on how to proceed. As described in How To Guide 5, parameters default to being unshared. -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. +The functions [`disconnect_param!`](@ref) and [`connect_param!`](@ref) can be used to alter or add connections within an existing model. These two 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 themselves, as discussed in the next subsection. -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. +**Once again, for specific instructions and details on various cases of updating and changing parameters, and their connections, please view [How-to Guide 5: Work with Parameters and Variables](@ref). We do not repeat all information here for brevity and to avoid duplication.** ## Parametric Modifications: DICE Example @@ -70,13 +76,13 @@ Thus there are no required arguments, although the user can input `params`, a di #### Step 3. Altering Parameters -In the case that you wish to alter an exogenous parameter, you may use the [`update_param!`](@ref) function. Per usual, you will start by importing the Mimi package to your space with +In the case that you wish to alter an parameter retrieving an exogenously set value from a model parameter, you may use the [`update_param!`](@ref) function. Per usual, you will start by importing the Mimi package to your space with ```julia using Mimi ``` -In DICE the parameter `fco22x` is the forcings of equilibrium CO2 doubling in watts per square meter, and exists in the components `climatedynamics` and `radiativeforcing`. We can change this value from its default value of `3.200` to `3.000` in both components, using the following code: +In DICE the parameter `fco22x` is the forcings of equilibrium CO2 doubling in watts per square meter, and is a shared model parameter (named `fco22x`) and connected to component parameters with the same name, `fco22x`, in components `climatedynamics` and `radiativeforcing`. We can change this value from its default value of `3.200` to `3.000` in both components, using the following code: ```julia update_param!(m, :fco22x, 3.000) @@ -85,7 +91,7 @@ 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 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: +First you update the `time` dimension of the model as follows: ```julia const ts = 10 @@ -94,9 +100,11 @@ nyears = length(years) set_dimension!(m, :time, years) ``` -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. +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 model 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!`](@ref) 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 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: +Updating the `:time` dimension can be tricky, depending on your use case, so **we recommend reading [How-to Guide 6: Update the Time Dimension](@ref)** if you plan to do this often in your work. + +To batch update **shared** model parameters, create a dictionary `params` with one entry `(k, v)` per model 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 a shared model parameter that already exists in the model definition. Part of this dictionary may look like: ```julia params = Dict{Any, Any}() @@ -107,13 +115,24 @@ params[:S] = repeat([0.23], nyears) ... ``` +To batch update **unshared** model parameters, follow a similar pattern but use tuples (:comp_name, :param_name) as your dictionary keys, which might look like: + +```julia +params = Dict{Any, Any}() +params[(:comp1, :a1)] = 0.00008162 +params[(:comp1, :a2)] = 0.00204626 +... +params[(:comp2, :S)] = repeat([0.23], nyears) +... +``` +Finally, you can combine these two dictionaries and Mimi will recognize and resolve the two different key types under the hood. + Now you simply update the parameters listen in `params` and re-run the model with ```julia update_params!(m, params) run(m) ``` - ## Component and Structural Modifications: The API Most model modifications will include not only parametric updates, but also structural changes and component modification, addition, replacement, and deletion along with the required re-wiring of parameters etc. The most useful functions of the common API, in these cases are likely **[`replace!`](@ref), [`add_comp!`](@ref)** along with **`delete!`** and the requisite functions for parameter setting and connecting. For detail on the public API functions look at the API reference. diff --git a/docs/src/tutorials/tutorial_4.md b/docs/src/tutorials/tutorial_4.md index 3e8934a27..d86daa89b 100644 --- a/docs/src/tutorials/tutorial_4.md +++ b/docs/src/tutorials/tutorial_4.md @@ -11,13 +11,11 @@ Working through the following tutorial will require: **If you have not yet prepared these, go back to the first tutorial to set up your system.** -Note that we have recently released Mimi v1.0.0, which is a breaking release and thus we cannot promise backwards compatibility with version lower than v1.0.0 although several of these tutorials may run properly with older versions. For assistance updating your own model to v1.0.0, or if you are curious about the primary changes made, see the How-to Guide on porting to Mimi v1.0.0. Mimi v0.10.0 is functionally dentical to Mimi v1.0.0, but includes deprecation warnings instead of errors to assist users in porting to v1.0.0. - ## Constructing A One-Region Model In this example, we construct a stylized model of the global economy and its changing greenhouse gas emission levels through time. The overall strategy involves creating components for the economy and emissions separately, and then defining a model where the two components are coupled together. -There are two main steps to creating a component, both within the `@defcomp` macro which defines a component: +There are two main steps to creating a component, both within the [`@defcomp`](@ref) macro which defines a component: * List the parameters and variables. * Use the `run_timestep` function `run_timestep(p, v, d, t)` to set the equations of that component. @@ -26,25 +24,26 @@ Starting with the economy component, each variable and parameter is listed. If e Next, the `run_timestep` function must be defined along with the various equations of the `grosseconomy` component. In this step, the variables and parameters are linked to this component and must be identified as either a variable or a parameter in each equation. For this example, `v` will refer to variables while `p` refers to parameters. -It is important to note that `t` below is an `AbstractTimestep`, and the specific API for using this argument are described in detail in the how to guide How-to Guide 4: Work with Timesteps, Parameters, and Variables +It is important to note that `t` below is an `AbstractTimestep`, and the specific API for using this argument are described in detail in the how to guide How-to Guide 4: Work with Timesteps. ```jldoctest tutorial4; output = false using Mimi # start by importing the Mimi package to your space @defcomp grosseconomy begin YGROSS = Variable(index=[time]) # Gross output - K = Variable(index=[time]) # Capital - l = Parameter(index=[time]) # Labor - tfp = Parameter(index=[time]) # Total factor productivity - s = Parameter(index=[time]) # Savings rate - depk = Parameter() # Depreciation rate on capital - Note that it has no time index - k0 = Parameter() # Initial level of capital - share = Parameter() # Capital share + K = Variable(index=[time]) # Capital + l = Parameter(index=[time]) # Labor + tfp = Parameter(index=[time]) # Total factor productivity + s = Parameter(index=[time]) # Savings rate + depk = Parameter() # Depreciation rate on capital - Note that it has no time index + k0 = Parameter() # Initial level of capital + share = Parameter() # Capital share function run_timestep(p, v, d, t) # Define an equation for K if is_first(t) - # Note the use of v. and p. to distinguish between variables and parameters + # Note the use of v. and p. to distinguish between variables and + # parameters v.K[t] = p.k0 else v.K[t] = (1 - p.depk)^5 * v.K[t-1] + v.YGROSS[t-1] * p.s[t-1] * 5 @@ -63,14 +62,14 @@ Next, the component for greenhouse gas emissions must be created. Although the ```jldoctest tutorial4; output = false @defcomp emissions begin - E = Variable(index=[time]) # Total greenhouse gas emissions + E = Variable(index=[time]) # Total greenhouse gas emissions sigma = Parameter(index=[time]) # Emissions output ratio YGROSS = Parameter(index=[time]) # Gross output - Note that YGROSS is now a parameter function run_timestep(p, v, d, t) - # Define an equation for E - v.E[t] = p.YGROSS[t] * p.sigma[t] # Note the p. in front of YGROSS + # Define an equation for E + v.E[t] = p.YGROSS[t] * p.sigma[t] # Note the p. in front of YGROSS end end @@ -82,7 +81,7 @@ We can now use Mimi to construct a model that binds the `grosseconomy` and `emis * Once the model is defined, [`set_dimension!`](@ref) is used to set the length and interval of the time step. * We then use [`add_comp!`](@ref) to incorporate each component that we previously created into the model. It is important to note that the order in which the components are listed here matters. The model will run through each equation of the first component before moving onto the second component. One can also use the optional `first` and `last` keyword arguments to indicate a subset of the model's time dimension when the component should start and end. -* Next, [`set_param!`](@ref) is used to assign values to each parameter in the model, with parameters being uniquely tied to each component. If _population_ was a parameter for two different components, it must be assigned to each one using [`set_param!`](@ref) two different times. The syntax is `set_param!(model_name, :component_name, :parameter_name, value)` +* Next, [`update_param!`](@ref) is used to assign values each component parameter with an external connection to an unshared model parameter. If _population_ was a parameter for two different components, it must be assigned to each one using [`update_param!`](@ref) two different times. The syntax is `update_param!(model_name, :component_name, :parameter_name, value)`. Alternatively if these parameters are always meant to use the same value, one could use [`add_shared_param!`](@ref) to create a shared model parameter and add it to the model, and then use [`connect_param!`](@ref) to connect both. This syntax would use `add_shared_param!(model_name, :model_param_name, value)` followed by `connect_param!(model_name, :component_name, :parameter_name, :model_param_name)` twice, once for each component. * If any variables of one component are parameters for another, [`connect_param!`](@ref) is used to couple the two components together. In this example, _YGROSS_ is a variable in the `grosseconomy` component and a parameter in the `emissions` component. The syntax is `connect_param!(model_name, :component_name_parameter, :parameter_name, :component_name_variable, :variable_name)`, where `:component_name_variable` refers to the component where your parameter was initially calculated as a variable. * Finally, the model can be run using the command `run(model_name)`. * To access model results, use `model_name[:component, :variable_name]`. @@ -101,18 +100,19 @@ function construct_model() add_comp!(m, grosseconomy) add_comp!(m, emissions) - # Set parameters for the grosseconomy component - set_param!(m, :grosseconomy, :l, [(1. + 0.015)^t *6404 for t in 1:20]) - set_param!(m, :grosseconomy, :tfp, [(1 + 0.065)^t * 3.57 for t in 1:20]) - set_param!(m, :grosseconomy, :s, ones(20).* 0.22) - set_param!(m, :grosseconomy, :depk, 0.1) - set_param!(m, :grosseconomy, :k0, 130.) - set_param!(m, :grosseconomy, :share, 0.3) - - # Set parameters for the emissions component - set_param!(m, :emissions, :sigma, [(1. - 0.05)^t *0.58 for t in 1:20]) + # Update parameters for the grosseconomy component + update_param!(m, :grosseconomy, :l, [(1. + 0.015)^t *6404 for t in 1:20]) + update_param!(m, :grosseconomy, :tfp, [(1 + 0.065)^t * 3.57 for t in 1:20]) + update_param!(m, :grosseconomy, :s, ones(20).* 0.22) + update_param!(m, :grosseconomy, :depk, 0.1) + update_param!(m, :grosseconomy, :k0, 130.) + update_param!(m, :grosseconomy, :share, 0.3) + + # Update parameters for the emissions component + update_param!(m, :emissions, :sigma, [(1. - 0.05)^t *0.58 for t in 1:20]) + + # connect parameters for the emissions component connect_param!(m, :emissions, :YGROSS, :grosseconomy, :YGROSS) - # Note that connect_param! was used here. return m @@ -124,7 +124,7 @@ construct_model (generic function with 1 method) ``` -Note that as an alternative to using many of the `set_param!` calls above, one may use the `default` keyword argument in `@defcomp` when first defining a `Variable` or `Parameter`, as shown in `examples/tutorial/01-one-region-model/one-region-model-defaults.jl`. +Note that as an alternative to using many of the [`update_param!`](@ref) calls above, one may use the `default` keyword argument in [`@defcomp`](@ref) when first defining a `Variable` or `Parameter`, as shown in `examples/tutorial/01-one-region-model/one-region-model-defaults.jl`. Now we can run the model and examine the results: @@ -155,7 +155,7 @@ We can now modify our two-component model of the globe to include multiple regio * When using [`@defcomp`](@ref), a regions index must be specified. In addition, for variables that have a regional index it is necessary to include `(index=[regions])`. This can be combined with the time index as well, `(index=[time, regions])`. * In the `run_timestep` function, unlike the time dimension, regions must be specified and looped through in any equations that contain a regional variable or parameter. * [`set_dimension!`](@ref) must be used to specify your regions in the same way that it is used to specify your timestep. -* When using [`set_param!`](@ref) for values with a time and regional dimension, an array is used. Each row corresponds to a time step, while each column corresponds to a separate region. For regional values with no timestep, a vector can be used. It is often easier to create an array of parameter values before model construction. This way, the parameter name can be entered into [`set_param!`](@ref) rather than an entire equation. +* When using [`update_param!`](@ref) for values with a time and regional dimension, an array is used. Each row corresponds to a time step, while each column corresponds to a separate region. For regional values with no timestep, a vector can be used. It is often easier to create an array of parameter values before model construction. This way, the parameter name can be entered into [`update_param!`](@ref) rather than an entire equation. * When constructing regionalized models with multiple components, it is often easier to save each component as a separate file and to then write a function that constructs the model. When this is done, `using Mimi` must be speficied for each component. This approach will be used here. To create a three-regional model, we will again start by constructing the grosseconomy and emissions components, making adjustments for the regional index as needed. Each component should be saved as a separate file. @@ -299,15 +299,14 @@ function construct_MyModel() add_comp!(m, grosseconomy) add_comp!(m, emissions) - set_param!(m, :grosseconomy, :l, l) - set_param!(m, :grosseconomy, :tfp, tfp) - set_param!(m, :grosseconomy, :s, s) - set_param!(m, :grosseconomy, :depk,depk) - set_param!(m, :grosseconomy, :k0, k0) - set_param!(m, :grosseconomy, :share, 0.3) + update_param!(m, :grosseconomy, :l, l) + update_param!(m, :grosseconomy, :tfp, tfp) + update_param!(m, :grosseconomy, :s, s) + update_param!(m, :grosseconomy, :depk,depk) + update_param!(m, :grosseconomy, :k0, k0) + update_param!(m, :grosseconomy, :share, 0.3) - # set parameters for emissions component - set_param!(m, :emissions, :sigma, sigma) + update_param!(m, :emissions, :sigma, sigma) connect_param!(m, :emissions, :YGROSS, :grosseconomy, :YGROSS) return m @@ -348,4 +347,4 @@ explore(m) ``` ---- -Next, feel free to move on to the next tutorial, which will go into depth on how to **run a sensitvity analysis** on a own model. +Next, feel free to move on to the next tutorial, which will go into depth on how to **run a sensitivity analysis** on a own model. diff --git a/docs/src/tutorials/tutorial_5.md b/docs/src/tutorials/tutorial_5.md index 50408b693..27cf40f56 100644 --- a/docs/src/tutorials/tutorial_5.md +++ b/docs/src/tutorials/tutorial_5.md @@ -11,8 +11,6 @@ Working through the following tutorial will require: **If you have not yet prepared these, go back to the first tutorial to set up your system.** -Note that we have recently released Mimi v1.0.0, which is a breaking release and thus we cannot promise backwards compatibility with version lower than v1.0.0 although several of these tutorials may run properly with older versions. For assistance updating your own model to v1.0.0, or if you are curious about the primary changes made, see the How-to Guide on porting to Mimi v1.0.0. Mimi v0.10.0 is functionally dentical to Mimi v1.0.0, but includes deprecation warnings instead of errors to assist users in porting to v1.0.0. - MimiDICE2010 is required for the second example in this tutorial. If you are not yet comfortable with downloading and running a registered Mimi model, refer to Tutorial 2 for instructions. ## The API @@ -136,15 +134,14 @@ function construct_MyModel() add_comp!(m, grosseconomy) add_comp!(m, emissions) - set_param!(m, :grosseconomy, :l, l) - set_param!(m, :grosseconomy, :tfp, tfp) - set_param!(m, :grosseconomy, :s, s) - set_param!(m, :grosseconomy, :depk,depk) - set_param!(m, :grosseconomy, :k0, k0) - set_param!(m, :grosseconomy, :share, 0.3) + update_param!(m, :grosseconomy, :l, l) + update_param!(m, :grosseconomy, :tfp, tfp) + update_param!(m, :grosseconomy, :s, s) + update_param!(m, :grosseconomy, :depk,depk) + update_param!(m, :grosseconomy, :k0, k0) + update_param!(m, :grosseconomy, :share, 0.3) - # set parameters for emissions component - set_param!(m, :emissions, :sigma, sigma) + update_param!(m, :emissions, :sigma, sigma) connect_param!(m, :emissions, :YGROSS, :grosseconomy, :YGROSS) return m @@ -172,23 +169,28 @@ Mimi.Model #### Step 2. Define the Simulation -The `@defsim` macro is the first step in the process, and returns a `SimulationDef`. The following syntax allows users to define random variables (RVs) as distributions, and associate model parameters with the defined random variables. +The [`@defsim`](@ref) macro is the first step in the process, and returns a `SimulationDef`. The following syntax allows users to define random variables (RVs) as distributions, and associate model parameters with the defined random variables. -There are two ways of assigning random variables to model parameters in the `@defsim` macro. Notice that both of the following syntaxes are used in the following example. +There are two ways of assigning random variables to model parameters in the [`@defsim`](@ref) macro. Notice that both of the following syntaxes are used in the following example. The first is the following: ```julia rv(rv1) = Normal(0, 0.8) # create a random variable called "rv1" with the specified distribution -param1 = rv1 # then assign this random variable "rv1" to the parameter "param1" in the model +param1 = rv1 # then assign this random variable "rv1" to the shared model parameter "param1" in the model +comp1.param2 = rv1 # then assign this random variable "rv1" to the unshared model parameter "param2" in component `comp1` ``` -The second is a shortcut, in which you can directly assign the distribution on the right-hand side to the name of the model parameter on the left hand side. With this syntax, a single random variable is created under the hood and then assigned to `param1`. +The second is a shortcut, in which you can directly assign the distribution on the right-hand side to the name of the model parameter on the left hand side. With this syntax, a single random variable is created under the hood and then assigned to our shared model parameter `param1` and unshared model parameter `param2`. ```julia param1 = Normal(0, 0.8) +comp1.param2 = Normal(1,0) ``` + +Note here that if we have a shared model parameter we can assign based on its name, but if we have an unshared model parameter specific to one component/parameter pair we need to specify both. If the component is not specified Mimi will throw a warning and try to resolve under the hood with assumptions, proceeding if possible and erroring if not. + **It is important to note** that for each trial, a random variable on the right hand side of an assignment, be it using an explicitly defined random variable with `rv(rv1)` syntax or using shortcut syntax as above, will take on the value of a **single** draw from the given distribution. This means that even if the random variable is applied to more than one parameter on the left hand side (such as assigning to a slice), each of these parameters will be assigned the same value, not different draws from the distribution -The `@defsim` macro also selects the sampling method. Simple random sampling (also called Monte Carlo sampling) is the default. Other options include Latin Hypercube sampling and Sobol sampling. Below we show just one example of a `@defsim` call, but the How-to guide referenced at the beginning of this tutorial gives a more comprehensive overview of the options. +The [`@defsim`](@ref) macro also selects the sampling method. Simple random sampling (also called Monte Carlo sampling) is the default. Other options include Latin Hypercube sampling and Sobol sampling. Below we show just one example of a [`@defsim`](@ref) call, but the How-to guide referenced at the beginning of this tutorial gives a more comprehensive overview of the options. ```jldoctest tutorial5; output = false, filter = r".*"s using Mimi @@ -208,18 +210,18 @@ sd = @defsim begin sampling(LHSData, corrlist=[(:name1, :name2, 0.7), (:name1, :name3, 0.5)]) # assign RVs to model Parameters - share = Uniform(0.2, 0.8) + grosseconomy.share = Uniform(0.2, 0.8) # you can use the *= operator to replace the values in the parameter with the # product of the original value and the value of the RV for the current # trial (note that in both lines below, all indexed values will be mulitplied by the # same draw from the given random parameter (name2 or Uniform(0.8, 1.2)) - sigma[:, Region1] *= name2 - sigma[2020:5:2050, (Region2, Region3)] *= Uniform(0.8, 1.2) + emissions.sigma[:, Region1] *= name2 + emissions.sigma[2020:5:2050, (Region2, Region3)] *= Uniform(0.8, 1.2) # For parameters that have a region dimension, you can assign an array of distributions, # keyed by region label, which must match the region labels in the model - depk = [Region1 => Uniform(0.7, .9), + grosseconomy.depk = [Region1 => Uniform(0.7, .9), Region2 => Uniform(0.8, 1.), Region3 => Truncated(Normal(), 0, 1)] @@ -235,9 +237,9 @@ end #### Step 3. Run Simulation -Next, use the `run` function to run the simulation for the specified simulation definition, model (or list of models), and number of trials. View the internals documentation [here](https://github.com/mimiframework/Mimi.jl/blob/master/docs/src/internals/montecarlo.md) for **critical and useful details on the full signature of the `run` function**. +Next, use the [`run`](@ref) function to run the simulation for the specified simulation definition, model (or list of models), and number of trials. View the internals documentation [here](https://github.com/mimiframework/Mimi.jl/blob/master/docs/src/internals/montecarlo.md) for **critical and useful details on the full signature of the [`run`](@ref) function**. -In its simplest use, the `run` function generates and iterates over a sample of trial data from the distributions of the random variables defined in the `SimulationDef`, perturbing the subset of Mimi's "external parameters" that have been assigned random variables, and then runs the given Mimi model(s) for each set of trial data. The function returns a `SimulationInstance`, which holds a copy of the original `SimulationDef` in addition to trials information (`trials`, `current_trial`, and `current_data`), the model list `models`, and results information in `results`. Optionally, trial values and/or model results are saved to CSV files. Note that if there is concern about in-memory storage space for the results, use the `results_in_memory` flag set to `false` to incrementally clear the results from memory. +In its simplest use, the [`run`](@ref) function generates and iterates over a sample of trial data from the distributions of the random variables defined in the `SimulationDef`, perturbing the subset of Mimi's model parameters that have been assigned random variables, and then runs the given Mimi model(s) for each set of trial data. The function returns a `SimulationInstance`, which holds a copy of the original `SimulationDef` in addition to trials information (`trials`, `current_trial`, and `current_data`), the model list `models`, and results information in `results`. Optionally, trial values and/or model results are saved to CSV files. Note that if there is concern about in-memory storage space for the results, use the `results_in_memory` flag set to `false` to incrementally clear the results from memory. ```jldoctest tutorial5; output = false, filter = r".*"s # Run 100 trials, and optionally save results to the indicated directories @@ -253,7 +255,7 @@ E_results = getdataframe(si, :emissions, :E) ``` #### Step 4. Explore and Plot Results -As described in the internals documentation [here](https://github.com/mimiframework/Mimi.jl/blob/master/docs/src/internals/montecarlo.md), Mimi provides both `explore` and `Mimi.plot` to explore the results of both a run `Model` and a run `SimulationInstance`. +As described in the internals documentation [here](https://github.com/mimiframework/Mimi.jl/blob/master/docs/src/internals/montecarlo.md), Mimi provides both [`explore`](@ref) and `Mimi.plot` to explore the results of both a run `Model` and a run `SimulationInstance`. To view your results in an interactive application viewer, simply call: @@ -267,7 +269,7 @@ If desired, you may also include a `title` for your application window. If more explore(si; title = "MyWindow", model_index = 1) # we do not indicate scen_name here since we have no scenarios ``` -To view the results for one of the saved variables from the `save` command in `@defsim`, use the (unexported to avoid namespace collisions) `Mimi.plot` function. This function has the same keyword arguments and requirements as `explore` (except for `title`), and three required arguments: the `SimulationInstance`, the component name (as a `Symbol`), and the variable name (as a `Symbol`). +To view the results for one of the saved variables from the `save` command in `@defsim`, use the (unexported to avoid namespace collisions) `Mimi.plot` function. This function has the same keyword arguments and requirements as [`explore`](@ref) (except for `title`), and three required arguments: the `SimulationInstance`, the component name (as a `Symbol`), and the variable name (as a `Symbol`). ```julia Mimi.plot(si, :grosseconomy, :K) diff --git a/examples/01-onecomponent.jl b/examples/01-onecomponent.jl index 3f28c40c8..7b45dfa8d 100644 --- a/examples/01-onecomponent.jl +++ b/examples/01-onecomponent.jl @@ -6,7 +6,7 @@ using Mimi @defcomp component1 begin # First define the state this component will hold - savingsrate = Parameter() + savingsrate = Parameter(default = 1.0) # Second, define the (optional) init function for the component function init(p, v, d) diff --git a/examples/compositecomp-model.jl b/examples/compositecomp-model.jl index 97f6c7f44..cb8b9d535 100644 --- a/examples/compositecomp-model.jl +++ b/examples/compositecomp-model.jl @@ -86,13 +86,14 @@ end # model m = Model() + md = m.md set_dimension!(m, :time, 2005:2020) add_comp!(m, top, nameof(top)) -set_param!(m, :fooA1, 1) -set_param!(m, :fooA2, 2) -set_param!(m, :foo3, 10) -set_param!(m, :foo4, 20) -set_param!(m, :par_1_1, collect(1:length(time_labels(md)))) +update_param!(m, :top, :fooA1, 1) +update_param!(m, :top, :fooA2, 2) +update_param!(m, :top, :foo3, 10) +update_param!(m, :top, :foo4, 20) +update_param!(m, :top, :par_1_1, collect(1:length(Mimi.time_labels(md)))) run(m) diff --git a/examples/tutorial/01-one-region-model/one-region-model.jl b/examples/tutorial/01-one-region-model/one-region-model.jl index 981b202f4..a4cb21f04 100644 --- a/examples/tutorial/01-one-region-model/one-region-model.jl +++ b/examples/tutorial/01-one-region-model/one-region-model.jl @@ -43,18 +43,17 @@ function construct_model() add_comp!(m, grosseconomy) add_comp!(m, emissions) - # Set parameters for the grosseconomy component - set_param!(m, :grosseconomy, :l, [(1. + 0.015)^t *6404 for t in 1:20]) - set_param!(m, :grosseconomy, :tfp, [(1 + 0.065)^t * 3.57 for t in 1:20]) - set_param!(m, :grosseconomy, :s, ones(20).* 0.22) - set_param!(m, :grosseconomy, :depk, 0.1) - set_param!(m, :grosseconomy, :k0, 130.) - set_param!(m, :grosseconomy, :share, 0.3) - - # Set parameters for the emissions component - set_param!(m, :emissions, :sigma, [(1. - 0.05)^t *0.58 for t in 1:20]) + # Update parameters for the grosseconomy component + update_param!(m, :grosseconomy, :l, [(1. + 0.015)^t *6404 for t in 1:20]) + update_param!(m, :grosseconomy, :tfp, [(1 + 0.065)^t * 3.57 for t in 1:20]) + update_param!(m, :grosseconomy, :s, ones(20).* 0.22) + update_param!(m, :grosseconomy, :depk, 0.1) + update_param!(m, :grosseconomy, :k0, 130.) + update_param!(m, :grosseconomy, :share, 0.3) + + # Update and connect parameters for the emissions component + update_param!(m, :emissions, :sigma, [(1. - 0.05)^t *0.58 for t in 1:20]) connect_param!(m, :emissions, :YGROSS, :grosseconomy, :YGROSS) - # Note that connect_param! was used here. return m diff --git a/examples/tutorial/02-multi-region-model/multi-region-model.jl b/examples/tutorial/02-multi-region-model/multi-region-model.jl index addc268f7..7e48bf83b 100644 --- a/examples/tutorial/02-multi-region-model/multi-region-model.jl +++ b/examples/tutorial/02-multi-region-model/multi-region-model.jl @@ -19,15 +19,16 @@ function construct_MyModel() add_comp!(m, grosseconomy) add_comp!(m, emissions) - set_param!(m, :grosseconomy, :l, l) - set_param!(m, :grosseconomy, :tfp, tfp) - set_param!(m, :grosseconomy, :s, s) - set_param!(m, :grosseconomy, :depk, depk) - set_param!(m, :grosseconomy, :k0, k0) - set_param!(m, :grosseconomy, :share, 0.3) - - # set parameters for emissions component - set_param!(m, :emissions, :sigma, sigma) + # update parameters for grosseconomy component + update_param!(m, :grosseconomy, :l, l) + update_param!(m, :grosseconomy, :tfp, tfp) + update_param!(m, :grosseconomy, :s, s) + update_param!(m, :grosseconomy, :depk, depk) + update_param!(m, :grosseconomy, :k0, k0) + update_param!(m, :grosseconomy, :share, 0.3) + + # update and connect parameters for emissions component + update_param!(m, :emissions, :sigma, sigma) connect_param!(m, :emissions, :YGROSS, :grosseconomy, :YGROSS) return m diff --git a/src/Mimi.jl b/src/Mimi.jl index bf901a930..2aab7ec92 100644 --- a/src/Mimi.jl +++ b/src/Mimi.jl @@ -16,6 +16,7 @@ export MarginalModel, Model, add_comp!, + add_shared_param!, # components, connect_param!, create_marginal_model, @@ -47,6 +48,7 @@ export TimestepValue, update_param!, update_params!, + update_leftover_params!, # variables, variable_dimensions, variable_names diff --git a/src/components/adder.jl b/src/components/adder.jl index f873aec46..d8b719950 100644 --- a/src/components/adder.jl +++ b/src/components/adder.jl @@ -13,4 +13,3 @@ using Mimi v.output[t] = @allow_missing(p.input[t]) + p.add[t] end end - diff --git a/src/core/build.jl b/src/core/build.jl index 88a8dbae9..18465bbc0 100644 --- a/src/core/build.jl +++ b/src/core/build.jl @@ -1,5 +1,11 @@ -connector_comp_name(i::Int) = Symbol("ConnectorComp$i") +""" + _substitute_views!(vals::Array{T, N}, comp_def) where {T, N} +For each value in `vals`, if the value is a `TimestepArray` swap in a new +TimestepArray with the same type parameterization but with its `data` field +holding a view of the original value's `data` defined by the first and last +indices of Component `comp_def`. +""" # helper function to substitute views for data function _substitute_views!(vals::Array{T, N}, comp_def) where {T, N} times = [keys(comp_def.dim_dict[:time])...] @@ -12,6 +18,13 @@ function _substitute_views!(vals::Array{T, N}, comp_def) where {T, N} end end +""" + _get_view(val::TimestepArray{T_TS, T, N, ti, S}, first_idx, last_idx) where {T_TS, T, N, ti, S} + +Return a TimestepArray with the same type parameterization as the `val` TimestepArray, +but with its `data` field holding a view of the `val.data` based on the entered +`first-idx` and `last_idx`. +""" function _get_view(val::TimestepArray{T_TS, T, N, ti, S}, first_idx, last_idx) where {T_TS, T, N, ti, S} idxs = Array{Any}(fill(:, N)) @@ -21,7 +34,12 @@ function _get_view(val::TimestepArray{T_TS, T, N, ti, S}, first_idx, last_idx) w return TimestepArray{T_TS, T, N, ti}(val.data isa SubArray ? view(val.data.parent, idxs...) : view(val.data, idxs...)) end -# Return the datatype to use for instance variables/parameters +""" + _instance_datatype(md::ModelDef, def::AbstractDatumDef) + +Return the datatype of the AbstractDataumDef `def` in ModelDef `md`, which will +be used to create ModelInstance instance variables and parameters. +""" function _instance_datatype(md::ModelDef, def::AbstractDatumDef) dtype = def.datatype == Number ? number_type(md) : def.datatype dims = dim_names(def) @@ -51,6 +69,13 @@ function _instance_datatype(md::ModelDef, def::AbstractDatumDef) return T end +""" + _instantiate_datum(md::ModelDef, def::AbstractDatumDef) + +Return the parameterized datum, broadly either Scalar or Array, pertaining to +AbstractDatumDef `def` in the Model Def `md`, that will support instantiate of parameters +and variables. +""" # Create the Ref or Array that will hold the value(s) for a Parameter or Variable function _instantiate_datum(md::ModelDef, def::AbstractDatumDef) dtype = _instance_datatype(md, def) @@ -106,7 +131,12 @@ function _instantiate_component_vars(md::ModelDef, comp_def::ComponentDef) return ComponentInstanceVariables(names, types, values, paths) end -# Creates the top-level vars for the model +""" + function _instantiate_vars(md::ModelDef) + +Create the top-level variables for the Model Def `md` and return the dictionary +of the resulting ComponentInstanceVariables. +""" function _instantiate_vars(md::ModelDef) vdict = Dict{ComponentPath, Any}() recurse(md, cd -> vdict[cd.comp_path] = _instantiate_component_vars(md, cd); leaf_only=true) @@ -194,20 +224,29 @@ function _get_leaf_level_epcs(md::ModelDef, epc::ExternalParameterConnection) par_sub_paths, param_names = _find_paths_and_names(comp, epc.param_name) leaf_epcs = ExternalParameterConnection[] - external_param_name = epc.external_param + model_param_name = epc.model_param_name top_path = epc.comp_path for (par_sub_path, param_name) in zip(par_sub_paths, param_names) param_path = ComponentPath(top_path, par_sub_path) - epc = ExternalParameterConnection(param_path, param_name, external_param_name) + epc = ExternalParameterConnection(param_path, param_name, model_param_name) push!(leaf_epcs, epc) end return leaf_epcs end -# Collect all parameters with connections to allocated variable storage +# generic helper function to get connector component name +connector_comp_name(i::Int) = Symbol("ConnectorComp$i") + +""" + _collect_params(md::ModelDef, var_dict::Dict{ComponentPath, Any}) + +Collect all parameters in ModelDef `md` with connections to allocated variable +storage in `var_dict` and return a dictionary of (comp_path, par_name) => ModelParameter +elements. +""" function _collect_params(md::ModelDef, var_dict::Dict{ComponentPath, Any}) # @info "Collecting params for $(comp_def.comp_id)" @@ -228,7 +267,7 @@ function _collect_params(md::ModelDef, var_dict::Dict{ComponentPath, Any}) end for epc in external_param_conns(md) - param = external_param(md, epc.external_param) + param = model_param(md, epc.model_param_name) leaf_level_epcs = _get_leaf_level_epcs(md, epc) for leaf_epc in leaf_level_epcs pdict[(leaf_epc.comp_path, leaf_epc.param_name)] = (param isa ScalarModelParameter ? param : value(param)) @@ -244,13 +283,19 @@ function _collect_params(md::ModelDef, var_dict::Dict{ComponentPath, Any}) conn_comp = compdef(md, connector_comp_name(i)) conn_path = conn_comp.comp_path - param = external_param(md, backup) + param = model_param(md, backup) pdict[(conn_path, :input2)] = (param isa ScalarModelParameter ? param : value(param)) end return pdict end +""" + _instantiate_params(comp_def::ComponentDef, par_dict::Dict{Tuple{ComponentPath, Symbol}, Any}) + +Create the top-level parameters for the Model Def `md` using the parameter dictionary +`par_dict` and return the resulting ComponentInstanceParameters. +""" function _instantiate_params(comp_def::ComponentDef, par_dict::Dict{Tuple{ComponentPath, Symbol}, Any}) # @info "Instantiating params for $(comp_def.comp_path)" comp_path = comp_def.comp_path @@ -263,7 +308,16 @@ function _instantiate_params(comp_def::ComponentDef, par_dict::Dict{Tuple{Compon return ComponentInstanceParameters(names, types, vals, paths) end -# Return a built leaf or composite LeafComponentInstance +""" + _build(comp_def::ComponentDef, + var_dict::Dict{ComponentPath, Any}, + par_dict::Dict{Tuple{ComponentPath, Symbol}, Any}, + time_bounds::Tuple{Int, Int}) + +Return a built leaf or composite LeafComponentInstance created using ComponentDef +`comp_def`, variables and parameters from `var_dict` and `par_dict` and the time +bounds set by `time_bounds`. +""" function _build(comp_def::ComponentDef, var_dict::Dict{ComponentPath, Any}, par_dict::Dict{Tuple{ComponentPath, Symbol}, Any}, @@ -278,6 +332,16 @@ function _build(comp_def::ComponentDef, return LeafComponentInstance(comp_def, vars, pars, time_bounds) end +""" + _build(comp_def::AbstractCompositeComponentDef, + var_dict::Dict{ComponentPath, Any}, + par_dict::Dict{Tuple{ComponentPath, Symbol}, Any}, + time_bounds::Tuple{Int, Int}) + +Return a built CompositeComponentInstance created using AbstractCompositeComponentDef +`comp_def`, variables and parameters from `var_dict` and `par_dict` and the time +bounds set by `time_bounds`. +""" function _build(comp_def::AbstractCompositeComponentDef, var_dict::Dict{ComponentPath, Any}, par_dict::Dict{Tuple{ComponentPath, Symbol}, Any}, @@ -294,6 +358,12 @@ function _build(comp_def::AbstractCompositeComponentDef, return CompositeComponentInstance(comps, comp_def, time_bounds, variables, parameters) end +""" + _get_variables(comp_def::AbstractCompositeComponentDef) + +Return a vector of NamedTuples for all variables in the CompositeComponentInstance +`comp_def`. +""" # helper functions for to create the variables and parameters NamedTuples for a # CompositeComponentInstance function _get_variables(comp_def::AbstractCompositeComponentDef) @@ -307,6 +377,12 @@ function _get_variables(comp_def::AbstractCompositeComponentDef) return variables end +""" + _get_parameters(comp_def::AbstractCompositeComponentDef) + +Return a vector of NamedTuples for all parameters in the CompositeComponentInstance +`comp_def`. +""" function _get_parameters(comp_def::AbstractCompositeComponentDef) namespace = comp_def.namespace @@ -319,35 +395,21 @@ function _get_parameters(comp_def::AbstractCompositeComponentDef) end """ - _set_defaults!(md::ModelDef) + _build(md::ModelDef) -Look for default values for any unset parameters and set those values. The -depth-first search starts stores results in a dict, so higher-level settings -(i.e., closer to ModelDef in the hierarchy) overwrite lower-level ones. +Build ModelDef `md` (lowest build function called by `build(md::ModelDef)`) and return the ModelInstance.. """ -function _set_defaults!(md::ModelDef) - not_set = unconnected_params(md) - isempty(not_set) && return - - for ref in not_set - comp_name, par_name = ref.comp_name, ref.datum_name - pardef = md[comp_name][par_name] - default_value = pardef.default - default_value === nothing || set_param!(md, par_name, default_value) - end -end - function _build(md::ModelDef) # @info "_build(md)" add_connector_comps!(md) - # check if all parameters are set - not_set = unconnected_params(md) - - if ! isempty(not_set) - params = join([p.datum_name for p in not_set], "\n ") - error("Cannot build model; the following parameters are not set:\n $params") + # check if any of the parameters initialized with the value(s) of nothing + # are still nothing + nothingparams = nothing_params(md) + if ! isempty(nothingparams) + params = join([p.datum_name for p in nothingparams], "\n ") + error("Cannot build model; the following parameters still have values of `nothing` and need to be updated:\n $params") end vdict = _instantiate_vars(md) @@ -366,15 +428,23 @@ function _build(md::ModelDef) return mi end +""" + build(m::Model) + +Build Model `m` and return the ModelInstance. +""" function build(m::Model) # Reference a copy in the ModelInstance to avoid changes underfoot md = deepcopy(m.md) - _set_defaults!(md) # apply defaults to unset parameters in the model instance's copy of the model definition - mi = _build(md) return mi end +""" + build!(m::Model) + +Build Model `m` for and dirty `m`'s ModelDef. +""" function build!(m::Model) m.mi = build(m) m.md.dirty = false @@ -400,11 +470,21 @@ function create_marginal_model(base::Model, delta::Float64=1.0) mm = MarginalModel(base, delta) end +""" + Base.run(mm::MarginalModel; ntimesteps::Int=typemax(Int)) + +Run the marginal model `mm` once with `ntimesteps`. +""" function Base.run(mm::MarginalModel; ntimesteps::Int=typemax(Int)) run(mm.base, ntimesteps=ntimesteps) run(mm.modified, ntimesteps=ntimesteps) end +""" + build!(mm::MarginalModel) + +Build MarginalModel `mm` by building both its `base` and `modified models`. +""" function build!(mm::MarginalModel) build!(mm.base) build!(mm.modified) diff --git a/src/core/connections.jl b/src/core/connections.jl index 3aab02ab6..aaf51618f 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -20,6 +20,15 @@ function disconnect_param!(obj::AbstractCompositeComponentDef, comp_def::Abstrac filter!(x -> !(x.dst_comp_path == path && x.dst_par_name == param_name), obj.internal_param_conns) if obj isa ModelDef + + # if disconnecting an unshared parameter, it will become unreachable since + # it's name is a random, unique symbol so remove it from the ModelDef's + # list of model parameters + model_param_name = get_model_param_name(obj, nameof(comp_def), param_name; missing_ok = true) + if !isnothing(model_param_name) && !(model_param(obj, model_param_name).is_shared) + delete!(obj.model_params, model_param_name); + end + filter!(x -> !(x.comp_path == path && x.param_name == param_name), obj.external_param_conns) end dirty!(obj) @@ -40,23 +49,39 @@ end # Default string, string unit check function verify_units(unit1::AbstractString, unit2::AbstractString) = (unit1 == unit2) -function _check_labels(obj::AbstractCompositeComponentDef, - comp_def::AbstractComponentDef, param_name::Symbol, ext_param::ArrayModelParameter) +""" + _check_attributes(obj::AbstractCompositeComponentDef, + comp_def::AbstractComponentDef, param_name::Symbol, + mod_param::ArrayModelParameter) + +Check that the attributes of the ArrayModelParameter `mod_param` match the attributes +of the model parameter `param_name` in component `comp_def` of object `obj`, +including datatype and dimensions. +""" +function _check_attributes(obj::AbstractCompositeComponentDef, + comp_def::AbstractComponentDef, param_name::Symbol, mod_param::ArrayModelParameter) + + is_nothing_param(mod_param) && return + param_def = parameter(comp_def, param_name) - t1 = eltype(ext_param.values) + t1 = eltype(mod_param.values) t2 = eltype(param_def.datatype) if !(t1 <: Union{Missing, t2}) - error("Mismatched datatype of parameter connection: Component: $(comp_def.comp_id) ($t1), Parameter: $param_name ($t2)") + error("Mismatched datatype of parameter connection: Component: $(nameof(comp_def)) ", + "Parameter: $param_name ($t2) to Model Parameter ($t1). Mimi requires that ", + "the model parameter type be a subtype of the component parameter type (Unioned with Missing for arrays) ", + "($t1 <: Union{Missing, $t2}) If you are using `add_shared_param!` try ", + "using the `data_type` keyword argument to specifiy data_type = $(eltype(param_def.datatype))") end - comp_dims = dim_names(param_def) - param_dims = dim_names(ext_param) + param_dims = dim_names(param_def) + model_dims = dim_names(mod_param) - if ! isempty(param_dims) && size(param_dims) != size(comp_dims) - d1 = size(comp_dims) + if ! isempty(param_dims) && size(param_dims) != size(model_dims) + d1 = size(model_dims) d2 = size(param_dims) - error("Mismatched dimensions of parameter connection: Component: $(comp_def.comp_id) ($d1), Parameter: $param_name ($d2)") + error("Mismatched dimensions of parameter connection: Component: $(nameof(comp_def)) Parameter: $param_name ($d2) to Model Parameter ($d1)") end # Don't check sizes for ConnectorComps since they won't match. @@ -66,43 +91,113 @@ function _check_labels(obj::AbstractCompositeComponentDef, # index_values = indexvalues(obj) - for (i, dim) in enumerate(comp_dims) + for (i, dim) in enumerate(param_dims) if isa(dim, Symbol) - param_length = size(ext_param.values)[i] + param_length = size(mod_param.values)[i] comp_length = dim_count(obj, dim) if param_length != comp_length - error("Mismatched data size for a parameter connection: dimension :$dim in $(comp_def.comp_id) has $comp_length elements; external parameter :$param_name has $param_length elements.") + error("Mismatched data size for a parameter connection: dimension :$dim in $(nameof(comp_def))'s parameter $param_name has $comp_length elements; model parameter has $param_length elements") end end end end """ - connect_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, param_name::Symbol, ext_param_name::Symbol; - check_labels::Bool=true) + _check_attributes(obj::AbstractCompositeComponentDef, + comp_def::AbstractComponentDef, param_name::Symbol, + mod_param::ScalarModelParameter) + +Check that the attributes of the ScalarModelParameter `mod_param` match the attributes +of the model parameter `param_name` in component `comp_def` of object `obj`, +including datatype. +""" +function _check_attributes(obj::AbstractCompositeComponentDef, + comp_def::AbstractComponentDef, param_name::Symbol, + mod_param::ScalarModelParameter) + + is_nothing_param(mod_param) && return + + param_def = parameter(comp_def, param_name) + t1 = typeof(mod_param.value) + t2 = param_def.datatype + + if !(t1 <: Union{Missing, t2}) + error("Mismatched datatype of parameter connection: Component: $(nameof(comp_def)) ", + "Parameter: $param_name ($t2) to Model Parameter with type ($t1). If you are using ", + "`add_shared_param`! try using the `data_type` keyword argument to specifiy ", + "data_type = $(param_def.datatype).") + end + +end + +""" + connect_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, param_name::Symbol, model_param_name::Symbol; + check_attributes::Bool=true, ignoreunits::Bool=false)) Connect a parameter `param_name` in the component `comp_name` of composite `obj` to -the external parameter `ext_param_name`. +the model parameter `model_param_name`. """ function connect_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, - param_name::Symbol, ext_param_name::Symbol; - check_labels::Bool=true) + param_name::Symbol, model_param_name::Symbol; + check_attributes::Bool=true, ignoreunits::Bool = false) comp_def = compdef(obj, comp_name) - connect_param!(obj, comp_def, param_name, ext_param_name, check_labels=check_labels) + connect_param!(obj, comp_def, param_name, model_param_name, check_attributes=check_attributes, ignoreunits = ignoreunits) end -function connect_param!(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, - param_name::Symbol, ext_param_name::Symbol; check_labels::Bool=true) - ext_param = external_param(obj, ext_param_name) +""" + connect_param!(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, + param_name::Symbol, model_param_name::Symbol; check_attributes::Bool=true, + ignoreunits::Bool = false) - if ext_param isa ArrayModelParameter && check_labels - _check_labels(obj, comp_def, param_name, ext_param) +Connect a parameter `param_name` in the component `comp_def` of composite `obj` to +the model parameter `model_param_name`. +""" +function connect_param!(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, + param_name::Symbol, model_param_name::Symbol; check_attributes::Bool=true, + ignoreunits::Bool = false) + + mod_param = model_param(obj, model_param_name) + + # check the attributes between the shared model parameter and the component parameter + check_attributes && _check_attributes(obj, comp_def, param_name, mod_param) + + # check for collisions + if is_shared(mod_param) + conns = filter(i -> i.model_param_name == model_param_name, external_param_conns(obj)) + if !(isempty(conns)) # need to check collisions + pairs = [compdef(obj, conn.comp_path) => conn.param_name for conn in conns] + push!(pairs, comp_def => param_name) + + # which fields to check for collisions in subcomponents + # NB: we don't need the types of the parameters to connected to + # exactly match, if they both satisfy _check_attributes above with the + # model parameter that is good enough --> we take :datatype out of the + # fields list below + fields = ignoreunits ? [:dim_names] : [:dim_names, :unit] + + collisions = _find_collisions(fields, Vector(pairs)) + + if ! isempty(collisions) + if :unit in collisions + error("Cannot connect $(nameof(comp_def)):$(param_name) to shared model ", + "parameter $model_param_name, it has a conflicting ", + ":unit value ($(parameter_unit(comp_def, param_name))) with ", + "other parameters connected to this shared model parameter. To ignore ", + "this set the `ignoreunits` flag in `connect_param!` to false.") + else + spec = join(collisions, " and ") + error("Cannot connect $(nameof(comp_def)):$(param_name) to shared model parameter ", + "$model_param_name, it has conflicting values for the $spec of other ", + "parameters connected to this shared model parameter.") + end + end + end end disconnect_param!(obj, comp_def, param_name) # calls dirty!() comp_path = @or(comp_def.comp_path, ComponentPath(obj.comp_path, comp_def.name)) - conn = ExternalParameterConnection(comp_path, param_name, ext_param_name) + conn = ExternalParameterConnection(comp_path, param_name, model_param_name) add_external_param_conn!(obj, conn) return nothing @@ -121,7 +216,7 @@ variable `src_var_name` in another component `src_comp_path` of the same model u check match units between the two. The `backup_offset` argument, which is only valid when `backup` data has been set, indicates that the backup data should be used for a specified number of timesteps after the source component begins. ie. the value would be -`1` if the destination componentm parameter should only use the source component +`1` if the destination component parameter should only use the source component data for the second timestep and beyond. """ function _connect_param!(obj::AbstractCompositeComponentDef, @@ -183,7 +278,9 @@ function _connect_param!(obj::AbstractCompositeComponentDef, end - set_external_array_param!(obj, dst_par_name, values, dst_dims) + # NB: potentially unsafe way to add parameter/might be duplicating work so + # advise shifting to create_model_param ... but leaving it as is for now + add_model_array_param!(obj, dst_par_name, values, dst_dims) backup_param_name = dst_par_name else @@ -223,6 +320,23 @@ Try calling: return nothing end +""" + connect_param!(obj::AbstractCompositeComponentDef, + dst_comp_name::Symbol, dst_par_name::Symbol, + src_comp_name::Symbol, src_var_name::Symbol, + backup::Union{Nothing, Array}=nothing; ignoreunits::Bool=false, + backup_offset::Union{Nothing, Int} = nothing) + +Bind the parameter `dst_par_name` of one component `dst_comp_name` of composite `obj` to a +variable `src_var_name` in another component `src_comp_name` of the same model using +`backup` to provide default values and the `ignoreunits` flag to indicate the need to +check match units between the two. The `backup_offset` argument, which is only valid +when `backup` data has been set, indicates that the backup data should be used for +a specified number of timesteps after the source component begins. ie. the value would be +`1` if the destination component parameter should only use the source component +data for the second timestep and beyond. +""" + function connect_param!(obj::AbstractCompositeComponentDef, dst_comp_name::Symbol, dst_par_name::Symbol, src_comp_name::Symbol, src_var_name::Symbol, @@ -263,7 +377,7 @@ Split a string of the form "/path/to/component:datum_name" into the component pa """ function split_datum_path(obj::AbstractCompositeComponentDef, s::AbstractString) elts = split(s, ":") - length(elts) != 2 && error("Cannot split datum path '$s' into ComponentPath and datum name") + length(elts) != 2 && error("Cannot split datum path '$s' into ComponentPath and datum name.") return (ComponentPath(obj, elts[1]), Symbol(elts[2])) end @@ -295,7 +409,7 @@ end connection_refs(obj::ModelDef) Return a vector of UnnamedReference's to parameters from subcomponents that are either found in -internal connections or that have been already connected to external parameter values. +internal connections or that have been already connected to model parameter values. """ function connection_refs(obj::ModelDef) refs = UnnamedReference[] @@ -311,6 +425,43 @@ function connection_refs(obj::ModelDef) return refs end +""" + nothing_params(obj::AbstractCompositeComponentDef) + +Return a list of UnnamedReference's to parameters that are connected to a model +parameter with a value of nothing. +""" +function nothing_params(obj::AbstractCompositeComponentDef) + + refs = UnnamedReference[] + + for conn in obj.external_param_conns + param = model_param(obj, conn.model_param_name) + if is_nothing_param(param) + push!(refs, UnnamedReference(conn.comp_path.names[end], conn.param_name)) + end + end + return refs +end + +""" + is_nothing_param(param::ScalarModelParameter) + +Return true if `param`'s value is nothing, and false otherwise. +""" +function is_nothing_param(param::ScalarModelParameter) + return isnothing(param.value) +end + +""" + is_nothing_param(param::ArrayModelParameter) + +Return true if `param`'s values is nothing, and false otherwise. +""" +function is_nothing_param(param::ArrayModelParameter) + return isnothing(param.values) +end + """ unconnected_params(obj::AbstractCompositeComponentDef) @@ -322,47 +473,93 @@ function unconnected_params(obj::AbstractCompositeComponentDef) end """ - set_leftover_params!(m::Model, parameters::Dict) + update_leftover_params!(md::ModelDef, parameters::Dict) -Set all of the parameters in model `m` that don't have a value and are not connected +Update all of the parameters in `ModelDef` `md` that don't have a value and are not connected to some other component to a value from a dictionary `parameters`. This method assumes -the dictionary keys are strings that match the names of unset parameters in the model. +the dictionary keys are Tuples of Symbols (or convertible to Symbols ie. Strings) +of (comp_name, param_name) that match the component-parameter pair of +unset parameters in the model. All resulting connected model parameters will be +unshared model parameters. """ -function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T - for param_ref in unconnected_params(md) +function update_leftover_params!(md::ModelDef, parameters) + parameters = Dict(Symbol.(k) => v for (k, v) in parameters) + for param_ref in nothing_params(md) + param_name = param_ref.datum_name comp_name = param_ref.comp_name + key = (comp_name, param_name) + if haskey(parameters, key) + value = parameters[key] + update_param!(md, comp_name, param_name, value) + else + error("Cannot set parameter (:$comp_name, :$param_name), not found in provided dictionary.") + end + end + nothing +end + +""" + set_leftover_params!(md::ModelDef, parameters::Dict) + +Set all of the parameters in `ModelDef` `md` that don't have a value and are not connected +to some other component to a value from a dictionary `parameters`. This method assumes +the dictionary keys are Symbols (or convertible into Symbols ie. Strings) that +match the names of unset parameters in the model. All resulting connected model +parameters will be shared model parameters. + +Note that this function `set_leftover_params! has been deprecated, and uses should +be transitioned to using `update_leftover_params!` with keys specific to component-parameter +pairs i.e. (comp_name, param_name) => value in the dictionary. +""" +function set_leftover_params!(md::ModelDef, parameters::Dict) where T + # @warn "The function `set_leftover_params! has been deprecated, please use `update_leftover_params!` with keys specific to component, parameter pairs i.e. (comp_name, param_name) => value in the dictionary.") + parameters = Dict(Symbol.(k) => v for (k, v) in parameters) + for param_ref in nothing_params(md) + + param_name = param_ref.datum_name + comp_name = param_ref.comp_name + comp_def = find_comp(md, comp_name) param_def = comp_def[param_name] - # Only set the unconnected parameter if it doesn't have a default - if param_def.default === nothing - # check whether we need to create the external parameter - if external_param(md, param_name, missing_ok=true) === nothing - if haskey(parameters, string(param_name)) - value = parameters[string(param_name)] - param_dims = parameter_dimensions(md, comp_name, param_name) - - set_external_param!(md, param_name, value; param_dims = param_dims) - else - error("Cannot set parameter :$param_name, not found in provided dictionary and no default value detected.") - end + # check whether we need to add the model parameter to the ModelDef + if isnothing(model_param(md, param_name, missing_ok=true)) + if haskey(parameters, param_name) + value = parameters[param_name] + param = create_model_param(md, param_def, value; is_shared = true) + add_model_param!(md, param_name, param) + else + error("Cannot set shared model parameter :$param_name, not found in provided dictionary.") end - connect_param!(md, comp_name, param_name, param_name) end + connect_param!(md, comp_name, param_name, param_name) end nothing end +""" + internal_param_conns(obj::AbstractCompositeComponentDef, dst_comp_path::ComponentPath) -# Find internal param conns to a given destination component +Return internal param conns to a given destination component on `dst_comp_path` in `obj`. +""" function internal_param_conns(obj::AbstractCompositeComponentDef, dst_comp_path::ComponentPath) return filter(x->x.dst_comp_path == dst_comp_path, internal_param_conns(obj)) end +""" + internal_param_conns(obj::AbstractCompositeComponentDef, comp_name::Symbol) + +Return internal param conns to a given destination component `comp_name` in `obj`. +""" function internal_param_conns(obj::AbstractCompositeComponentDef, comp_name::Symbol) return internal_param_conns(obj, ComponentPath(obj.comp_path, comp_name)) end +""" + add_internal_param_conn!(obj::AbstractCompositeComponentDef, conn::InternalParameterConnection) + +Add an internal param conns `conn` to the internal parameter connection lists of `obj`. +""" function add_internal_param_conn!(obj::AbstractCompositeComponentDef, conn::InternalParameterConnection) push!(obj.internal_param_conns, conn) dirty!(obj) @@ -372,21 +569,88 @@ end # These should all take ModelDef instead of AbstractCompositeComponentDef as 1st argument # -# Find external param conns for a given comp +""" + external_param_conns(obj::ModelDef, comp_path::ComponentPath) + +Find external param conns for a given comp on path `comp_path` in `obj`. +""" function external_param_conns(obj::ModelDef, comp_path::ComponentPath) return filter(x -> x.comp_path == comp_path, external_param_conns(obj)) end +""" + external_param_conns(obj::ModelDef, comp_name::Symbol) + +Find external param conns for a given comp `comp_name` in `obj`. +""" function external_param_conns(obj::ModelDef, comp_name::Symbol) return external_param_conns(obj, ComponentPath(obj.comp_path, comp_name)) end -function external_param(obj::ModelDef, name::Symbol; missing_ok=false) - haskey(obj.external_params, name) && return obj.external_params[name] +""" + model_param(obj::ModelDef, name::Symbol; missing_ok=false) + +Return the ModelParameter in `obj` with name `name`. If `missing_ok` is set +to `true`, return nothing if parameter is not found, otherwise error. +""" +function model_param(obj::ModelDef, name::Symbol; missing_ok=false) + haskey(obj.model_params, name) && return obj.model_params[name] + + missing_ok && return nothing + + error("$name not found in model parameter list.") +end + +""" + model_param(obj::ModelDef, comp_name::Symbol, param_name::Symbol; missing_ok = false) + +Return the ModelParameter in `obj` connected to component `comp_name`'s parameter +`param_name`. If `missing_ok` is set to `true`, return nothing if parameter is not +found, otherwise error. +""" +function model_param(obj::ModelDef, comp_name::Symbol, param_name::Symbol; missing_ok = false) + + model_param_name = get_model_param_name(obj, comp_name, param_name; missing_ok = true) + + if isnothing(model_param_name) + missing_ok && return nothing + error("Model parameter connected to $comp_name's parameter $param_name not found in model's parameter connections list.") + else + return model_param(obj, model_param_name) + end + +end + +""" + get_model_param_name(obj::ModelDef, comp_name::Symbol, param_name::Symbol; missing_ok=false) + +Get the model parameter name for the exernal parameter conneceted to comp_name's +parameter param_name. The keyword argument `missing_ok` defaults to false so +if no parameter is found an error is thrown, if it is set to true the function will +return `nothing`. +""" +function get_model_param_name(obj::ModelDef, comp_name::Symbol, param_name::Symbol; missing_ok=false) + for conn in obj.external_param_conns + if conn.comp_path.names[end] == comp_name && conn.param_name == param_name + return conn.model_param_name + end + end missing_ok && return nothing - error("$name not found in external parameter list") + error("Model parameter connected to $comp_name's parameter $param_name not found in model's parameter connections list.") +end + +""" + get_model_param_name(obj::Model, comp_name::Symbol, param_name::Symbol; missing_ok=false) + +Get the model parameter name for the exernal parameter connected to comp_name's +parameter param_name. The keyword argument `missing_ok` defaults to false so +if no parameter is found an error is thrown, if it is set to true the function will +return `nothing`. +""" +function get_model_param_name(obj::Model, comp_name::Symbol, param_name::Symbol; missing_ok=false) + get_model_param_name(obj.md, comp_name, param_name; missing_ok = missing_ok) end function add_external_param_conn!(obj::ModelDef, conn::ExternalParameterConnection) @@ -394,87 +658,131 @@ function add_external_param_conn!(obj::ModelDef, conn::ExternalParameterConnecti dirty!(obj) end -function set_external_param!(obj::ModelDef, name::Symbol, value::ModelParameter) - # if haskey(obj.external_params, name) - # @warn "Redefining external param :$name in $(obj.comp_path) from $(obj.external_params[name]) to $value" +""" + add_model_param!(md::ModelDef, name::Symbol, value::ModelParameter) + +Add an model parameter with name `name` and Model Parameter `value` to ModelDef `md`. +""" +function add_model_param!(md::ModelDef, name::Symbol, value::ModelParameter) + # if haskey(obj.model_params, name) + # @warn "Redefining model param :$name in $(obj.comp_path) from $(obj.model_params[name]) to $value" # end - obj.external_params[name] = value - dirty!(obj) + md.model_params[name] = value + dirty!(md) return value end -function set_external_param!(obj::ModelDef, name::Symbol, value::Number; - param_dims::Union{Nothing,Array{Symbol}} = nothing) - set_external_scalar_param!(obj, name, value) + +""" + add_model_param!(md::ModelDef, name::Symbol, value::Number; + param_dims::Union{Nothing,Array{Symbol}} = nothing, + is_shared::Bool = false) + +Create and add a model parameter with name `name` and Model Parameter `value` +to ModelDef `md`. The Model Parameter will be created with value `value`, dimensions +`param_dims` which can be left to be created automatically from the Model Def, and +an is_shared attribute `is_shared` which defaults to false. + +WARNING: this has been mostly replaced by combining create_model_param with add_model_param +method using the paramdef ... certain checks are not done here ... should be careful +using it and only do so under the hood. +""" +function add_model_param!(md::ModelDef, name::Symbol, value::Number; + param_dims::Union{Nothing,Array{Symbol}} = nothing, + is_shared::Bool = false) + add_model_scalar_param!(md, name, value, is_shared = is_shared) end -function set_external_param!(obj::ModelDef, name::Symbol, +""" + add_model_param!(md::ModelDef, name::Symbol, value::Number; + param_dims::Union{Nothing,Array{Symbol}} = nothing, + is_shared::Bool = false) + +Create and add a model parameter with name `name` and Model Parameter `value` +to ModelDef `md`. The Model Parameter will be created with value `value`, dimensions +`param_dims` which can be left to be created automatically from the Model Def, and +an is_shared attribute `is_shared` which defaults to false. +""" +function add_model_param!(md::ModelDef, name::Symbol, value::Union{AbstractArray, AbstractRange, Tuple}; - param_dims::Union{Nothing,Array{Symbol}} = nothing) + param_dims::Union{Nothing,Array{Symbol}} = nothing, + is_shared::Bool = false) + ti = get_time_index_position(param_dims) - if ti != nothing - value = convert(Array{number_type(obj)}, value) + if !isnothing(ti) + value = convert(Array{number_type(md)}, value) num_dims = length(param_dims) - values = get_timestep_array(obj, eltype(value), num_dims, ti, value) + values = get_timestep_array(md, eltype(value), num_dims, ti, value) else values = value end - set_external_array_param!(obj, name, values, param_dims) + add_model_array_param!(md, name, values, param_dims, is_shared = is_shared) end """ - set_external_array_param!(obj::ModelDef, - name::Symbol, value::TimestepVector, dims) + add_model_array_param!(md::ModelDef, + name::Symbol, value::TimestepVector, + dims; is_shared::Bool = false) Add a one dimensional time-indexed array parameter indicated by `name` and -`value` to the composite `obj`. In this case `dims` must be `[:time]`. +`value` to the Model Def `md`. The `is_shared` attribute of the ArrayModelParameter +will default to false. In this case `dims` must be `[:time]`. """ -function set_external_array_param!(obj::ModelDef, - name::Symbol, value::TimestepVector, dims) - param = ArrayModelParameter(value, [:time]) # must be :time - set_external_param!(obj, name, param) +function add_model_array_param!(md::ModelDef, + name::Symbol, value::TimestepVector, + dims; is_shared::Bool = false) + param = ArrayModelParameter(value, [:time], is_shared) # must be :time + add_model_param!(md, name, param) end """ - set_external_array_param!(obj::ModelDef, - name::Symbol, value::TimestepMatrix, dims) + add_model_array_param!(md::ModelDef, + name::Symbol, value::TimestepMatrix, dims; + is_shared::Bool = false) Add a multi-dimensional time-indexed array parameter `name` with value -`value` to the composite `obj`. In this case `dims` must be `[:time]`. +`value` to the Model Def `md`. The `is_shared` attribute of the ArrayModelParameter +will default to false. In this case `dims` must contain `[:time]`. """ -function set_external_array_param!(obj::ModelDef, - name::Symbol, value::TimestepArray, dims) - param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims) - set_external_param!(obj, name, param) +function add_model_array_param!(md::ModelDef, + name::Symbol, value::TimestepArray, dims; + is_shared::Bool = false) + !(:time in dims) && error("When adding an `ArrayModelParameter` the dimensions array must include `:time`, but here it is $dims.") + param = ArrayModelParameter(value, dims, is_shared) + add_model_param!(md, name, param) end """ - set_external_array_param!(obj::ModelDef, - name::Symbol, value::AbstractArray, dims) + add_model_array_param!(md::ModelDef, + name::Symbol, value::AbstractArray, dims; + is_shared::Bool = false) -Add an array type parameter `name` with value `value` and `dims` dimensions to the composite `obj`. +Add an array type parameter `name` with value `value` and `dims` dimensions to the +Model Def `md`. The `is_shared` attribute of the ArrayModelParameter will default to +false. """ -function set_external_array_param!(obj::ModelDef, - name::Symbol, value::AbstractArray, dims) - param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims) - set_external_param!(obj, name, param) +function add_model_array_param!(md::ModelDef, + name::Symbol, value::AbstractArray, dims; + is_shared::Bool = false) + param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims, is_shared) + add_model_param!(md, name, param) end """ - set_external_scalar_param!(obj::ModelDef, name::Symbol, value::Any) + add_model_scalar_param!(md::ModelDef, name::Symbol, value::Any; is_shared::Bool = false) -Add a scalar type parameter `name` with the value `value` to the composite `obj`. +Add a scalar type parameter `name` with the value `value` to the Model Def `md`. """ -function set_external_scalar_param!(obj::ModelDef, name::Symbol, value::Any) - param = ScalarModelParameter(value) - set_external_param!(obj, name, param) +function add_model_scalar_param!(md::ModelDef, name::Symbol, value::Any; is_shared::Bool = false) + param = ScalarModelParameter(value, is_shared) + add_model_param!(md, name, param) end """ update_param!(obj::AbstractCompositeComponentDef, name::Symbol, value; update_timesteps = nothing) -Update the `value` of an external model parameter in composite `obj`, referenced +Update the `value` of a model parameter in composite `obj`, referenced by `name`. The update_timesteps keyword argument is deprecated, we keep it here just to provide warnings. """ @@ -483,8 +791,15 @@ function update_param!(obj::AbstractCompositeComponentDef, name::Symbol, value; _update_param!(obj::AbstractCompositeComponentDef, name, value) end +""" + update_param!(mi::ModelInstance, name::Symbol, value) + +Update the `value` of a model parameter in `ModelInstance` `mi`, referenced +by `name`. This is an UNSAFE updat as it does not dirty the model, and should +be used carefully and specifically for things like our MCS work. +""" function update_param!(mi::ModelInstance, name::Symbol, value) - param = mi.md.external_params[name] + param = mi.md.model_params[name] if param isa ScalarModelParameter param.value = value @@ -497,22 +812,99 @@ function update_param!(mi::ModelInstance, name::Symbol, value) return nothing end -function _update_param!(obj::AbstractCompositeComponentDef, - name::Symbol, value) - param = external_param(obj, name, missing_ok=true) - if param === nothing - error("Cannot update parameter; $name not found in composite's external parameters.") - end +""" + update_param!(mi::ModelInstance, comp_name::Symbol, param_name::Symbol, value) + +Update the `value` of a model parameter in `ModelInstance` `mi`, connected to +component `comp_name`'s parameter `param_name`. This is an UNSAFE updat as it does +not dirty the model, and should be used carefully and specifically for things like +our MCS work. +""" +function update_param!(mi::ModelInstance, comp_name::Symbol, param_name::Symbol, value) + param = mi.md.model_params[get_model_param_name(mi.md, comp_name, param_name)] if param isa ScalarModelParameter - _update_scalar_param!(param, name, value) + param.value = value + elseif param.values isa TimestepArray + copyto!(param.values.data, value) + else + copyto!(param.values, value) + end + + return nothing +end + +""" + update_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, value) + +Update the `value` of the unshared model parameter in Model Def `md` connected to component +`comp_name`'s parameter `param_name`. +""" +function update_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, value) + + model_param_name = get_model_param_name(md, comp_name, param_name; missing_ok = true) + + # check if we need a new parameter, maybe because it was previously a nothing + # parameter that got disconnected + if isnothing(model_param_name) + + comp_def = find_comp(md, comp_name) + param_def = comp_def[param_name] + + param = create_model_param(md, param_def, value; is_shared = false) + + model_param_name = gensym() + add_model_param!(md, model_param_name, param) + + connect_param!(md, comp_name, param_name, model_param_name) + dirty!(md) + + # update existing parameter else - _update_array_param!(obj, name, value) + mod_param = model_param(md, model_param_name) + is_shared(mod_param) && error("$comp_name:$param_name is connected to a ", + "a shared model parameter with name $model_param_name in the model, ", + "to update the shared model parameter please call `update_param!(m, $model_param_name, value)` ", + "to explicitly update a shared parameter that may be connected to ", + "several components. If you want to disconnect $comp_name:$param_name ", + "from the shared model parameter and connect it to it's own unshared ", + "model parameter, first use `disconnect_param!` and then you can use this same ", + "call to `update_param!`.") + + # update the parameter + _update_param!(md, model_param_name, value) end +end +""" + _update_param!(obj::AbstractCompositeComponentDef, name::Symbol, value) + +Update the `value` of the model parameter `name` in Model Def `md`. +""" +function _update_param!(obj::AbstractCompositeComponentDef, name::Symbol, value) + param = model_param(obj, name, missing_ok=true) + if param === nothing + error("Cannot update parameter $name; $name not found in composite's model parameters.") + end + + # handle nothing params + if is_nothing_param(param) + _update_nothing_param!(obj, name, value) + else + if param isa ScalarModelParameter + _update_scalar_param!(param, name, value) + else + _update_array_param!(obj, name, value) + end + end dirty!(obj) end +""" + _update_scalar_param!(param::ScalarModelParameter, name, value) + +Update the `value` of the scalar model parameter `param`. +""" function _update_scalar_param!(param::ScalarModelParameter, name, value) if ! (value isa typeof(param.value)) try @@ -525,10 +917,15 @@ function _update_scalar_param!(param::ScalarModelParameter, name, value) nothing end +""" + _update_array_param!(obj::AbstractCompositeComponentDef, name, value) + +Update the `value` of the array model parameter `name` in object `obj`. +""" function _update_array_param!(obj::AbstractCompositeComponentDef, name, value) # Get original parameter - param = external_param(obj, name) + param = model_param(obj, name) # Check type of provided parameter if !(typeof(value) <: AbstractArray) @@ -559,7 +956,10 @@ function _update_array_param!(obj::AbstractCompositeComponentDef, name, value) T = eltype(value) ti = get_time_index_position(param) new_timestep_array = get_timestep_array(obj, T, N, ti, value) - set_external_param!(obj, name, ArrayModelParameter(new_timestep_array, dim_names(param))) + # NB: potentially unsafe way to add parameter/might be duplicating work so + # advise shifting to create_model_param ... but leaving it as is for now + # since this is a special case of replacing an existing model param + add_model_param!(obj, name, ArrayModelParameter(new_timestep_array, dim_names(param), param.is_shared)) else copyto!(param.values.data, value) end @@ -572,22 +972,63 @@ function _update_array_param!(obj::AbstractCompositeComponentDef, name, value) end """ - update_params!(obj::AbstractCompositeComponentDef, parameters::Dict{T, Any}; update_timesteps = nothing) where T + _update_nothing_param!(obj::AbstractCompositeComponentDef, name::Symbol, value) + +Update the `value` of the model parameter `name` in object `obj` where the model +parameter has an initial value of nothing likely from instanitate during `add_comp!`. +""" +function _update_nothing_param!(obj::AbstractCompositeComponentDef, name::Symbol, value) + + # get the component def and param def + conn = filter(i -> i.model_param_name == name, obj.external_param_conns)[1] + param_name = conn.param_name + + comp_def = find_comp(obj, conn.comp_path) + param_def = comp_def[param_name] + + # create the unshared model parameter + param = create_model_param(obj, param_def, value) + + # Need to check the dimensions of the parameter data against component + # before adding it to the model's parameter list + _check_attributes(obj, comp_def, param_name, param) + + # add the unshared model parameter to the model def, which will replace the + # old one and thus keep the connection in tact + add_model_param!(obj, name, param) +end + +""" + update_params!(obj::AbstractCompositeComponentDef, parameters::Dict; update_timesteps = nothing) For each (k, v) in the provided `parameters` dictionary, `update_param!` -is called to update the 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 -component definition. +is called to update the model parameter identified by k to value v. + +For updating unshared parameters, each key k must be a Tuple matching the name of a +component in `obj` and the name of an parameter in that component. + +For updating shared parameters, each key k must be a symbol or convert to a symbol +matching the name of a shared model parameter that already exists in the model. """ function update_params!(obj::AbstractCompositeComponentDef, parameters::Dict; update_timesteps = nothing) !isnothing(update_timesteps) ? @warn("Use of the `update_timesteps` keyword argument is no longer supported or needed, time labels will be adjusted automatically if necessary.") : nothing - parameters = Dict(Symbol(k) => v for (k, v) in parameters) - for (param_name, value) in parameters - _update_param!(obj, param_name, value) + parameters = Dict(Symbol.(k) => v for (k, v) in parameters) + for (k, v) in parameters + if k isa Tuple + model_param_name = get_model_param_name(obj, first(k), last(k)) + else + model_param_name = k + end + _update_param!(obj, model_param_name, v) end nothing end +""" + add_connector_comps!(obj::AbstractCompositeComponentDef) + +Add all the needed Mimi connector components to object `obj`. +""" function add_connector_comps!(obj::AbstractCompositeComponentDef) conns = internal_param_conns(obj) i = 1 # counter to track the number of connector comps added @@ -605,7 +1046,7 @@ function add_connector_comps!(obj::AbstractCompositeComponentDef) for conn in need_conn_comps add_backup!(obj, conn.backup) - num_dims = length(size(external_param(obj, conn.backup).values)) + num_dims = length(size(model_param(obj, conn.backup).values)) if ! (num_dims in (1, 2)) error("Connector components for parameters with > 2 dimensions are not implemented.") @@ -622,6 +1063,10 @@ function add_connector_comps!(obj::AbstractCompositeComponentDef) conn_comp = add_comp!(obj, conn_comp_def, conn_comp_name, before=comp_name) conn_path = conn_comp.comp_path + # remove the connections added in add_comp! + disconnect_param!(obj, conn_comp, :input1) + disconnect_param!(obj, conn_comp, :input2) + # add a connection between src_component and the ConnectorComp add_internal_param_conn!(obj, InternalParameterConnection(conn.src_comp_path, conn.src_var_name, conn_path, :input1, @@ -655,7 +1100,7 @@ end """ _pad_parameters!(obj::ModelDef) -Take each external parameter of the Model Definition `obj` and `update_param!` +Take each model 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. @@ -664,8 +1109,12 @@ 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) + for (name, param) in obj.model_params + # there is only a chance we only need to pad a parameter if: + # (1) it is an ArrayModelParameter + # (2) it has a time dimension + # (3) it does not have a values attribute of nothing, as assigned on initialization + if (param isa ArrayModelParameter) && (:time in param.dim_names) && !is_nothing_param(param) param_times = _get_param_times(param) padded_data = _get_padded_data(param, param_times, model_times) @@ -746,3 +1195,283 @@ 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 + +""" + add_shared_param!(md::ModelDef, name::Symbol, value::Any; dims::Array{Symbol}=Symbol[]) + +User-facing API function to add a shared parameter to Model Def `md` with name +`name` and value `value`, and an array of dimension names `dims` which dfaults to +an empty vector. The `is_shared` attribute of the added Model Parameter will be `true`. + +The `value` can by a scalar, an array, or a NamedAray. Optional keyword argument 'dims' is a list +of the dimension names of the provided data, and will be used to check that they match the +model's index labels. Optional keyword argument `datatype` allows user to specify a datatype +to use for the shared model parameter. +""" +function add_shared_param!(md::ModelDef, name::Symbol, value::Any; dims::Array{Symbol}=Symbol[], data_type::DataType=Nothing) + + # Check provided name: make sure shared model parameter name does not exist already + has_parameter(md, name) && error("Cannot add parameter :$name, the model already has a shared parameter with this name.") + + # Check provided dims: + # (1) handle NamedArray + # (2) make sure provided dims names exist in the model + # (3) make sure number of provided dims matches value + + if value isa NamedArray + !isempty(dims) && dims !== dimnames(value) && @warn "Provided dims are $dims, provided NamedArray value has dims $(dimnames(value)), will use value dims $(dimnames(value))." + dims = dimnames(value) + end + + for dim in dims + isa(dim, Symbol) && !has_dim(md, dim) && error("Model doesn't have dimension :$dim indicated in the dims of added shared parameter, $dims.") + end + + if value isa AbstractArray && ndims(value) != length(dims) + error("Please provide $(ndims(value)) dimension names for value, $(length(dims))", + " were given but provided value has $(ndims(value)). This is done with the `dims` keyword argument ", + " ie. : `add_shared_param!(md, name, value; dims = [:time])") + end + + # get the data type to use to create ParameterDef, which we either get from + # the data_type argument and just check against provided data in `value`, or we + # infer from the provided data in `value` with the caveat that any number + # type will be raised to number_type(md) for now (except Bools) + value, data_type = _resolve_datatype(md, value, data_type) + + # create the ParameterDef + + # note here that this will take our `data_type` and provide some logic including + # if data_type == Number it will create a ParameterDef with datatype md.number_type + # which is also what we do above + param_def = ParameterDef(name, nothing, data_type, dims, "", "", nothing) + + # create the model parameter + param = create_model_param(md, param_def, value; is_shared = true) + + # double check the dimensions between the model and the created parameter + param_dims = dim_names(param_def) + for (i, dim) in enumerate(param_dims) + if isa(dim, Symbol) + param_length = size(param.values)[i] + model_length = dim_count(md, dim) + if param_length != model_length + error("Mismatched data size for new shared param: dimension :$dim in model has $model_length elements; parameter :$name value $param_length elements.") + end + end + end + + # add the shared model parameter to the model def + add_model_param!(md, name, param) + +end + +# helper functions to return the data_type and (maybe converted) value to use +# in creation of ParameterDef that will parameterize our new added shared model +# parameter + +function _resolve_datatype(md::ModelDef, value::Any, data_type::DataType) + + # if a data_type is not provided get it from `value` + if data_type <: Nothing + value, data_type = _resolve_datatype_nothing(md, value, data_type) + + # otherwise check data_type against DataType of `value `` + else + value, data_type = _resolve_datatype_value(md, value, data_type) + end + + return value, data_type +end + +function _resolve_datatype_nothing(md::ModelDef, value::Any, data_type::DataType) + + value_data_type = value isa AbstractArray ? eltype(value) : typeof(value) + + # if it is not a DataType, try manually converting first ... + if !(value_data_type isa DataType) + try value = convert(DataType, value_data_type) + catch; end + end + + # if it is still not a DataType, try converting it to a Number and if + # successful convert the values and update the data_type + if !(value_data_type isa DataType) + try value = convert.(Number, value) + catch; end + value_data_type = eltype(value) + end + + # if it still isn't a datatype, then I give up just go with Any + if !(value_data_type isa DataType) + value_data_type = Any + end + + # raise to Number to lower the constraints, except for a Boolean make a + # corner case exception + if value_data_type <: Number && !(value_data_type <: Bool) + value_data_type = number_type(md) + end + + return value, value_data_type +end + +function _resolve_datatype_value(md::ModelDef, value::Any, data_type::DataType) + + value_data_type = value isa AbstractArray ? eltype(value) : typeof(value) + if value_data_type != data_type + + # mirrors what we do in _update_param! + if value isa AbstractArray + try + value = convert(Array{data_type}, value) + catch e + error("Mismatched datatypes: elements of provided `value` have a ", + "DataType ($value_data_type) and cannot be converted to the provided ", + "DataType in `data_type` argument ($data_type). Please resolve by ", + "converting the data you provided or changing the `data_type` argument.") + end + else + try + value = convert(data_type, value) + catch e + error("Mismatched datatypes: `value` has a ", + "DataType ($value_data_type) and do not match the provided ", + "DataType in `data_type` argument ($data_type). Please resolve by ", + "converting the data you provided or changing the `data_type` argument.") + end + end + end + + return value, data_type +end + +""" + create_model_param(md::ModelDef, param_def::AbstractParameterDef, value::Any; is_shared::Bool = false) + +Create a new model parameter to be added to Model Def `md` with specifications +matching parameter definition `param_def` and with `value`. The keyword argument +is_shared defaults to false, and thus an unshared parameter would be created, whereas +setting `is_shared` to true creates a shared parameter. +""" +function create_model_param(md::ModelDef, param_def::AbstractParameterDef, value::Any; is_shared::Bool = false) + if dim_count(param_def) > 0 + return create_array_model_param(md, param_def, value; is_shared = is_shared) + else + return create_scalar_model_param(md, param_def, value; is_shared = is_shared) + end +end + +""" + create_array_model_param(md::ModelDef, param_def::AbstractParameterDef, value::Any; is_shared::Bool = false) + +Create a new array model parameter to be added to Model Def `md` with specifications +matching parameter definition `param_def` and with `value`. The keyword argument +is_shared defaults to false, and thus an unshared parameter would be created, whereas +setting `is_shared` to true creates a shared parameter. +""" +function create_array_model_param(md::ModelDef, param_def::AbstractParameterDef, value::Any; is_shared::Bool = false) + + # gather info + param_name = nameof(param_def) + param_dims = dim_names(param_def) + num_dims = dim_count(param_def) + data_type = param_def.datatype + + # data type + dtype = Union{Missing, (data_type == Number ? number_type(md) : data_type)} + + # create a sentinal unshared parameter + if isnothing(value) + param = ArrayModelParameter(value, param_dims, is_shared) + + # have a value - in the initiliazation of parameters case this is a default + # value set in defcomp + else + + # check dimensions + if value isa NamedArray + dims = dimnames(value) + dims !== nothing && check_parameter_dimensions(md, value, dims, param_name) + end + + # convert the number type and, if NamedArray, convert to Array + if dtype <: AbstractArray + value = convert(dtype, value) + else + # check that number of dimensions matches + value_dims = length(size(value)) + if num_dims != value_dims + error("Mismatched data size: dimension :$param_name", + " in has $num_dims dimensions; indicated value", + " has $value_dims dimensions.") + end + value = convert(Array{dtype, num_dims}, value) + end + + # create TimestepArray if there is a time dim + ti = get_time_index_position(param_dims) + if ti !== nothing # there is a time dimension + T = eltype(value) + values = get_timestep_array(md, T, num_dims, ti, value) + else + values = value + end + + param = ArrayModelParameter(values, param_dims, is_shared) + end + return param +end + +""" + create_scalar_model_param(md::ModelDef, param_def::AbstractParameterDef, value::Any; is_shared::Bool = false) + +Create a new scalar model parameter to be added to Model Def `md` with specifications +matching parameter definition `param_def` and with `value`. The keyword argument +is_shared defaults to false, and thus an unshared parameter would be created, whereas +setting `is_shared` to true creates a shared parameter. +""" +function create_scalar_model_param(md::ModelDef, param_def::AbstractParameterDef, value::Any; is_shared::Bool = false) + + # gather info + param_name = nameof(param_def) + param_dims = dim_names(param_def) + num_dims = dim_count(param_def) + data_type = param_def.datatype + + # get data type + dtype = Union{Missing, (data_type == Number ? number_type(md) : data_type)} + + # create a sentinal unshared parameter + if isnothing(value) + param = ScalarModelParameter(value, is_shared) + + # have a value - in the initiliazation of parameters case this is a default + # value set in defcomp + else + value = convert(dtype, value) + param = ScalarModelParameter(value, is_shared) + end + + return param +end + +## +## DEPRECATIONS - Should move from warning --> error --> removal +## + +# -- throw errors -- + +# -- throw warnings -- + +@deprecate external_param(obj::ModelDef, name::Symbol; missing_ok=false) model_param(obj, name,; missing_ok = missing_ok) + +@deprecate set_external_param!(obj::ModelDef, name::Symbol, value::ModelParameter) add_model_param!(obj, name, value) +@deprecate set_external_param!(obj::ModelDef, name::Symbol, value::Number; param_dims::Union{Nothing,Array{Symbol}} = nothing, is_shared::Bool = false) add_model_param!(obj, name, value; param_dims = param_dims, is_shared = is_shared) +@deprecate set_external_param!(obj::ModelDef, name::Symbol, value::Union{AbstractArray, AbstractRange, Tuple}; param_dims::Union{Nothing,Array{Symbol}} = nothing, is_shared::Bool = false) add_model_param!(obj, name, value; param_dims = param_dims, is_shared = is_shared) + +@deprecate set_external_array_param!(obj::ModelDef, name::Symbol, value::TimestepVector, dims; is_shared::Bool = false) add_model_array_param!(obj, name, value, dims; is_shared = is_shared) +@deprecate set_external_array_param!(obj::ModelDef, name::Symbol, value::TimestepArray, dims; is_shared::Bool = false) add_model_array_param(obj, name, value, dims; is_shared = is_shared) +@deprecate set_external_array_param!(obj::ModelDef, name::Symbol, value::AbstractArray, dims; is_shared::Bool = false) add_model_array_param(obj, name, value, dims; is_shared = is_shared) + +@deprecate set_external_scalar_param!(obj::ModelDef, name::Symbol, value::Any; is_shared::Bool = false) add_model_scalar_param(obj, name, value; is_shared = is_shared) diff --git a/src/core/defcomp.jl b/src/core/defcomp.jl index 5be3e5dd4..3e0f7b7ac 100644 --- a/src/core/defcomp.jl +++ b/src/core/defcomp.jl @@ -200,7 +200,7 @@ macro defcomp(comp_name, ex) continue end - # DEPRECATION - EVENTUALLY REMOVE + # DEPRECATION if @capture(elt, name_::datum_type_ = elt_type_(args__)) error("The following syntax has been deprecated in @defcomp: \"$name::$datum_type = $elt_type(...)\". Use curly bracket syntax instead: \"$name = $elt_type{$datum_type}(...)\"") elseif ! @capture(elt, name_ = (elt_type_{datum_type_}(args__) | elt_type_(args__))) diff --git a/src/core/defcomposite.jl b/src/core/defcomposite.jl index dd328302c..49ccee3ff 100644 --- a/src/core/defcomposite.jl +++ b/src/core/defcomposite.jl @@ -1,42 +1,5 @@ using MacroTools -# From 1/16/2020 meeting -# -# c1 = Component(A) -# Component(B) # equiv B = Component(B) -# -# x3 = Parameter(a.p1, a.p2, b.p3, default=3, description="asflijasef", visibility=:private) -# -# This creates external param x3, and connects b.p3 and ANY parameter in any child named p1 to it -# AND now no p1 in any child can be connected to anything else. Use Not from the next if you want -# an exception for that -# x3 = Parameter(p1, b.p3, default=3, description="asflijasef", visibility=:private) -# -# x3 = Parameter(p1, p2, Not(c3.p1), b.p3, default=3, description="asflijasef", visibility=:private) -# -# connect(B.p2, c1.v4) -# connect(B.p3, c1.v4) -# -# x2 = Parameter(c2.x2, default=35) -# -# BUBBLE UP PHASE -# -# for p in unique(unbound_parameters) -# x1 = Parameter(c1.x1) -# end -# -# if any(unbound_parameter) then error("THIS IS WRONG") -# -# -# Expressions to parse in @defcomposite: -# -# 1. name_ = Component(compname_) -# 2. Component(compname_) => (compname = Component(compname_)) -# 3. pname_ = Parameter(args__) # args can be: pname, comp.pname, or keyword=value -# 4. connect(a.param, b.var) -# -# - # splitarg produces a tuple for each arg of the form (arg_name, arg_type, slurp, default) _arg_name(arg_tup) = arg_tup[1] _arg_type(arg_tup) = arg_tup[2] @@ -162,7 +125,7 @@ end # TBD: finish documenting this! """ - defcomposite(cc_name::Symbol, ex::Expr) + defcomposite(cc_name, ex) Define a Mimi CompositeComponentDef `cc_name` with the expressions in `ex`. Expressions are all shorthand for longer-winded API calls, and include the following: diff --git a/src/core/defs.jl b/src/core/defs.jl index 14d1d14f1..69c28fcd8 100644 --- a/src/core/defs.jl +++ b/src/core/defs.jl @@ -1,23 +1,35 @@ -Base.length(obj::AbstractComponentDef) = 0 # no sub-components -Base.length(obj::AbstractCompositeComponentDef) = length(components(obj)) +# +# Components +# + + +# `compdef` methods to obtain component definitions using various arguments +""" + function compdef(comp_id::ComponentId) + +Return the component definition with ComponentId `comp_id`. +""" function compdef(comp_id::ComponentId) # @info "compdef: mod=$(comp_id.module_obj) name=$(comp_id.comp_name)" return getfield(comp_id.module_obj, comp_id.comp_name) end compdef(cr::ComponentReference) = find_comp(cr) - compdef(obj::AbstractCompositeComponentDef, path::ComponentPath) = find_comp(obj, path) - compdef(obj::AbstractCompositeComponentDef, comp_name::Symbol) = components(obj)[comp_name] +compdefs(obj::AbstractCompositeComponentDef) = values(components(obj)) +compdefs(c::ComponentDef) = [] # Allows method to be called harmlessly on leaf component defs, which simplifies recursive funcs. + +# other helper functions has_comp(obj::AbstractCompositeComponentDef, comp_name::Symbol) = haskey(components(obj), comp_name) -compdefs(obj::AbstractCompositeComponentDef) = values(components(obj)) compkeys(obj::AbstractCompositeComponentDef) = keys(components(obj)) -# Allows method to be called harmlessly on leaf component defs, which simplifies recursive funcs. -compdefs(c::ComponentDef) = [] +Base.length(obj::AbstractComponentDef) = 0 # no sub-components +Base.length(obj::AbstractCompositeComponentDef) = length(components(obj)) +Base.getindex(comp::AbstractComponentDef, key::Symbol) = comp.namespace[key] +@delegate Base.haskey(comp::AbstractComponentDef, key::Symbol) => namespace compmodule(comp_id::ComponentId) = comp_id.module_obj compname(comp_id::ComponentId) = comp_id.comp_name @@ -27,6 +39,10 @@ compname(obj::AbstractComponentDef) = compname(obj.comp_id) compnames() = map(compname, compdefs()) +# +# Helper Functions with methods for multiple Def types +# + dirty(md::ModelDef) = md.dirty function dirty!(obj::AbstractComponentDef) @@ -57,16 +73,20 @@ last_period(root::AbstractCompositeComponentDef, comp::AbstractComponentDef) = find_first_period(comp_def::AbstractComponentDef) = @or(first_period(comp_def), first_period(get_root(comp_def))) find_last_period(comp_def::AbstractComponentDef) = @or(last_period(comp_def), last_period(get_root(comp_def))) +# +# Models +# + """ delete!(md::ModelDef, comp_name::Symbol; deep::Bool=false) Delete a `component` by name from `md`. -If `deep=true` then any external model parameters connected only to +If `deep=true` then any model parameters connected only to this component will also be deleted. """ function Base.delete!(md::ModelDef, comp_name::Symbol; deep::Bool=false) if ! has_comp(md, comp_name) - error("Cannot delete '$comp_name': component does not exist.") + error("Cannot delete '$comp_name': component does not exist in model.") end comp_def = compdef(md, comp_name) @@ -81,13 +101,13 @@ function Base.delete!(md::ModelDef, comp_name::Symbol; deep::Bool=false) # Remove external parameter connections - if deep # Find and delete external_params that were connected only to the deleted component if specified - # Get all external parameters this component is connected to - comp_ext_params = map(x -> x.external_param, filter(x -> x.comp_path == comp_path, md.external_param_conns)) + if deep # Find and delete model_params that were connected only to the deleted component if specified + # Get all model parameters this component is connected to + comp_model_params = map(x -> x.model_param_name, filter(x -> x.comp_path == comp_path, md.external_param_conns)) # Identify which ones are not connected to any other components - unbound_filter = x -> length(filter(epc -> epc.external_param == x, md.external_param_conns)) == 1 - unbound_comp_params = filter(unbound_filter, comp_ext_params) + unbound_filter = x -> length(filter(epc -> epc.model_param_name == x, md.external_param_conns)) == 1 + unbound_comp_params = filter(unbound_filter, comp_model_params) # Delete these parameters [delete_param!(md, param_name) for param_name in unbound_comp_params] @@ -101,32 +121,29 @@ function Base.delete!(md::ModelDef, comp_name::Symbol; deep::Bool=false) end """ - delete_param!(md::ModelDef, external_param_name::Symbol) + delete_param!(md::ModelDef, model_param_name::Symbol) -Delete `external_param_name` from `md`'s list of external parameters, and also -remove all external parameters connections that were connected to `external_param_name`. +Delete `model_param_name` from `md`'s list of model parameters, and also +remove all external parameters connections that were connected to `model_param_name`. """ -function delete_param!(md::ModelDef, external_param_name::Symbol) - if external_param_name in keys(md.external_params) - delete!(md.external_params, external_param_name) +function delete_param!(md::ModelDef, model_param_name::Symbol) + if model_param_name in keys(md.model_params) + delete!(md.model_params, model_param_name) else - error("Cannot delete $external_param_name, not found in external parameter list.") + error("Cannot delete $model_param_name, not found in model's parameter list.") end - # Remove external parameter connections - epc_filter = x -> x.external_param != external_param_name + # Remove model parameter connections + epc_filter = x -> x.model_param_name != model_param_name filter!(epc_filter, md.external_param_conns) dirty!(md) end -@delegate Base.haskey(comp::AbstractComponentDef, key::Symbol) => namespace - -Base.getindex(comp::AbstractComponentDef, key::Symbol) = comp.namespace[key] - # # Component namespaces # + """ istype(T::DataType) @@ -167,6 +184,8 @@ variables(obj::AbstractCompositeComponentDef) = values(filter(istype(CompositeVa variables(comp_id::ComponentId) = variables(compdef(comp_id)) """ + _ns_has(comp_def::AbstractComponentDef, name::Symbol, T::DataType) + Return true if the component namespace has an item `name` that isa `T` """ function _ns_has(comp_def::AbstractComponentDef, name::Symbol, T::DataType) @@ -174,6 +193,8 @@ function _ns_has(comp_def::AbstractComponentDef, name::Symbol, T::DataType) end """ + _ns_get(obj::AbstractComponentDef, name::Symbol, T::DataType) + Get a named element from the namespace of `obj` and verify its type. """ function _ns_get(obj::AbstractComponentDef, name::Symbol, T::DataType) @@ -186,6 +207,8 @@ function _ns_get(obj::AbstractComponentDef, name::Symbol, T::DataType) end """ + _save_to_namespace(comp::AbstractComponentDef, key::Symbol, value::NamespaceElement) + Save a value to a component's namespace. Allow replacement of existing values for a key only with items of the same type; otherwise an error is thrown. """ @@ -212,27 +235,62 @@ end # Dimensions # +""" + step_size(values::Vector{Int}) + +Return the step size for vector of `values`, where the vector is assumed to be uniform. +""" step_size(values::Vector{Int}) = (length(values) > 1 ? values[2] - values[1] : 1) -# -# TBD: should these be defined as methods of CompositeComponentDef, i.e., not for leaf comps -# +""" + step_size(obj::AbstractComponentDef) + +Return the step size of the time dimension labels of `obj`. +""" function step_size(obj::AbstractComponentDef) keys = time_labels(obj) return step_size(keys) end +""" + first_and_step(obj::AbstractComponentDef) + +Return the step size and first value of the time dimension labels of `obj`. +""" function first_and_step(obj::AbstractComponentDef) keys = time_labels(obj) return first_and_step(keys) end +""" + first_and_step(values::Vector{Int}) + +Return the step size and first value of the vector of `values`, where the vector +is assumed to be uniform. +""" first_and_step(values::Vector{Int}) = (values[1], step_size(values)) +""" + first_and_last(obj::AbstractComponentDef) + +Return the first and last time labels of `obj`. +""" first_and_last(obj::AbstractComponentDef) = (obj.first, obj.last) +""" + time_labels(obj::AbstractComponentDef) + +Return the time labels of `obj`, defined as the keys of the `:time` +dimension +""" time_labels(obj::AbstractComponentDef) = dim_keys(obj, :time) +""" + check_parameter_dimensions(md::ModelDef, value::AbstractArray, dims::Vector, name::Symbol) + +Check to make sure that the labels for dimensions `dims` in parameter `name` match +Model Def `md`'s index values `value`. +""" function check_parameter_dimensions(md::ModelDef, value::AbstractArray, dims::Vector, name::Symbol) for dim in dims if has_dim(md, dim) @@ -291,7 +349,7 @@ parameter(obj::AbstractCompositeComponentDef, comp_name::Symbol, param_name::Symbol) = parameter(compdef(obj, comp_name), param_name) has_parameter(comp_def::AbstractComponentDef, name::Symbol) = _ns_has(comp_def, name, AbstractParameterDef) -has_parameter(md::ModelDef, name::Symbol) = haskey(md.external_params, name) +has_parameter(md::ModelDef, name::Symbol) = haskey(md.model_params, name) function parameter_unit(obj::AbstractComponentDef, param_name::Symbol) param = parameter(obj, param_name) @@ -323,8 +381,9 @@ function parameter_dimensions(obj::AbstractComponentDef, comp_name::Symbol, para return parameter_dimensions(compdef(obj, comp_name), param_name) end - """ + find_params(obj::AbstractCompositeComponentDef, param_name::Symbol) + Find and return a vector of tuples containing references to a ComponentDef and a ParameterDef for all instances of parameters with name `param_name`, below the composite `obj`. If none are found, an empty vector is returned. @@ -394,8 +453,11 @@ function recurse(obj::ComponentDef, f::Function, args...; composite_only || f(obj, args...) nothing end +""" + subcomp_params(obj::AbstractComponentDef) -# return UnnamedReference's for all subcomponents' parameters +Return UnnamedReference's for all parameters of the subcomponents of `obj`. +""" function subcomp_params(obj::AbstractCompositeComponentDef) params = UnnamedReference[] for (name, sub_obj) in obj.namespace @@ -423,67 +485,111 @@ function set_param!(md::ModelDef, comp_name::Symbol, value_dict::Dict{Symbol, An end end +""" + set_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, value; dims=nothing) + +Set the value of parameter `param_name` in component `comp_name` of Model Def `md` +to `value`. This will create a shared model parameter with name `param_name` +and connect `comp_name`'s parameter `param_name` to it. + +The `value` can by a scalar, an array, or a NamedAray. Optional keyword argument 'dims' is a list +of the dimension names of the provided data, and will be used to check that they match the +model's index labels. +""" function set_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, value; dims=nothing) set_param!(md, comp_name, param_name, param_name, value, dims=dims) end -function set_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, ext_param_name::Symbol, value; dims=nothing) +""" + set_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, model_param_name::Symbol, + value; dims=nothing) + +Set the value of parameter `param_name` in component `comp_name` of Model Def `md` +to `value`. This will create a shared model parameter with name `model_param_name` +and connect `comp_name`'s parameter `param_name` to it. + +The `value` can by a scalar, an array, or a NamedAray. Optional keyword argument 'dims' is a list +of the dimension names of the provided data, and will be used to check that they match the +model's index labels. +""" +function set_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, model_param_name::Symbol, value; dims=nothing) comp_def = compdef(md, comp_name) @or(comp_def, error("Top-level component with name $comp_name not found")) - set_param!(md, comp_def, param_name, ext_param_name, value, dims=dims) + set_param!(md, comp_def, param_name, model_param_name, value, dims=dims) end -function set_param!(md::ModelDef, comp_def::AbstractComponentDef, param_name::Symbol, ext_param_name::Symbol, value; dims=nothing) - has_parameter(comp_def, param_name) || +""" + set_param!(md::ModelDef, comp_def::AbstractComponentDef, param_name::Symbol, + model_param_name::Symbol, value; dims=nothing) + +Set the value of parameter `param_name` in component `comp_def` of Model Def `md` +to `value`. This will create a shared model parameter with name `model_param_name` +and connect `comp_name`'s parameter `param_name` to it. + +The `value` can by a scalar, an array, or a NamedAray. Optional keyword argument 'dims' is a list +of the dimension names of the provided data, and will be used to check that they match the +model's index labels. +""" +function set_param!(md::ModelDef, comp_def::AbstractComponentDef, param_name::Symbol, model_param_name::Symbol, value; dims=nothing) + + # error if cannot find the parameter in the component + if !has_parameter(comp_def, param_name) error("Cannot find parameter :$param_name in component $(pathof(comp_def))") - 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.") + # error if the model_param_name is already found in the model + elseif has_parameter(md, model_param_name) + + error("Cannot set parameter :$model_param_name, the model already has a parameter with this name.", + " IF you wish to change the value of unshared parameter :$param_name connected to component :$(nameof(compdef))", + " use `update_param!(m, comp_name, param_name, value).", + " IF you wish to change the value of the existing shared parameter :$model_param_name, ", + " use `update_param!(m, param_name, value)` to change the value of the shared parameter.", + " IF you wish to create a new shared parameter connected to component :$(nameof(compdef)), use ", + "`add_shared_param` paired with `connect_param!`.") end - set_param!(md, param_name, value, dims = dims, comps = [comp_def], ext_param_name = ext_param_name) + set_param!(md, param_name, value, dims = dims, comps = [comp_def], model_param_name = model_param_name) end """ set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing) -Set the value of a parameter in all components of the model that have a parameter of -the specified name. +Set the value of parameter `param_name in all components of the Model Def `md` +that have a parameter of the specified name to `value`. This will create a shared +model parameter with name `param_name` and connect all component parameters with +that name to it. The `value` can by a scalar, an array, or a NamedAray. Optional keyword argument 'dims' is a list of the dimension names of the provided data, and will be used to check that they match the model's index labels. """ -function set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing, ignoreunits::Bool=false, comps=nothing, ext_param_name=nothing) +function set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing, ignoreunits::Bool=false, comps=nothing, model_param_name=nothing) + + # find components for connection # search immediate subcomponents for this parameter if comps === nothing comps = [comp for (compname, comp) in components(md) if has_parameter(comp, param_name)] end - - if ext_param_name === nothing - ext_param_name = param_name - end - - if isempty(comps) - error("Cannot set parameter :$param_name; not found in ModelDef or children") - end - + isempty(comps) && error("Cannot set parameter :$param_name; not found in ModelDef or children") + + # check for collisions # which fields to check for collisions in subcomponents - fields = ignoreunits ? (:dim_names, :datatype) : (:dim_names, :datatype, :unit) + fields = ignoreunits ? [:dim_names, :datatype] : [:dim_names, :datatype, :unit] collisions = _find_collisions(fields, [comp => param_name for comp in comps]) if ! isempty(collisions) if :unit in collisions - error("Cannot set parameter :$param_name in the model, components have conflicting values for the :unit field of this parameter. ", - "Call `set_param!` with optional keyword argument `ignoreunits = true` to override.") + error("Cannot set shared parameter :$param_name in the model, components have conflicting values for the :unit field of this parameter. ", + "IF you wish to set a shared parameter, call `set_param!` with optional keyword argument `ignoreunits = true` to override.", + "IF you wish to leave these parameters as unshared parameters and just update values, update them with separate calls to `update_param!(m, comp_name, param_name, value)`.") else spec = join(collisions, " and ") - error("Cannot set parameter :$param_name in the model, components have conflicting values for the $spec of this parameter. ", - "Set these parameters with separate calls to `set_param!(m, comp_name, param_name, unique_param_name, value)`.") + error("Cannot set shared parameter :$param_name in the model, components have conflicting values for the $spec of this parameter. ", + "IF you wish to set a shared parameter for each component, set these parameters with separate calls to `set_param!(m, comp_name, param_name, unique_param_name, value)`.", + "IF you wish to leave these parameters as unshared parameters and just update values, update them with separate calls to `update_param!(m, comp_name, param_name, value)`.") end end + # check dimensions if value isa NamedArray dims = dimnames(value) end @@ -492,78 +598,29 @@ function set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing, ignor check_parameter_dimensions(md, value, dims, param_name) end - comp_def = comps[1] # since we alread checked that the found comps have no conflicting fields in their parameter definitions, we can just use the first one for reference below - param_def = comp_def[param_name] - param_dims = param_def.dim_names - num_dims = length(param_dims) - - data_type = param_def.datatype - dtype = Union{Missing, (data_type == Number ? number_type(md) : data_type)} - - if num_dims > 0 - - # convert the number type and, if NamedArray, convert to Array - if dtype <: AbstractArray - value = convert(dtype, value) - else - # check that number of dimensions matches - value_dims = length(size(value)) - if num_dims != value_dims - error("Mismatched data size for a set parameter call: dimension :$param_name", - " in has $num_dims dimensions; indicated value", - " has $value_dims dimensions.") - end - value = convert(Array{dtype, num_dims}, value) - end - - ti = get_time_index_position(param_dims) - - if ti !== nothing # there is a time dimension - T = eltype(value) - - if num_dims == 0 - values = value - else - - # Use the first from the Model def, not the component, since we now say that the - # data needs to match the dimensions of the model itself, so we need to allocate - # the full time length even if we pad it with missings. - first = first_period(md) - first === nothing && @warn "set_param!: first === nothing" - - last = last_period(md) - last === nothing && @warn "set_param!: last === nothing" - - if isuniform(md) - stepsize = step_size(md) - values = TimestepArray{FixedTimestep{first, stepsize, last}, T, num_dims, ti}(value) - else - times = time_labels(md) - first_index = findfirst(isequal(first), times) - values = TimestepArray{VariableTimestep{(times[first_index:end]...,)}, T, num_dims, ti}(value) - end - end - else - values = value - end - - param = ArrayModelParameter(values, param_dims) - # Need to check the dimensions of the parameter data against each component before addeding it to the model's external parameters - for comp in comps - _check_labels(md, comp, param_name, param) - end - set_external_param!(md, ext_param_name, param) - + # create shared model parameter - since we alread checked that the found + # comps have no conflicting fields in their parameter definitions, we can + # just use the first one for reference + param_def = comps[1][param_name] + param = create_model_param(md, param_def, value; is_shared = true) + + # Need to check the dimensions of the parameter data against each component + # before adding it to the model's model parameters + for comp in comps + _check_attributes(md, comp, param_name, param) + end - else # scalar parameter case - value = convert(dtype, value) - set_external_scalar_param!(md, ext_param_name, value) + # add the shared model parameter to the model def + if model_param_name === nothing + model_param_name = param_name end + add_model_param!(md, model_param_name, param) - # connect_param! calls dirty! so we don't have to + # connect for comp in comps - # Set check_labels=false because we already checked above before setting the param - connect_param!(md, comp, param_name, ext_param_name, check_labels=false) + # Set check_attributes = false because we already checked above + # connect_param! calls dirty! so we don't have to + connect_param!(md, comp, param_name, model_param_name, check_attributes = false, ignoreunits = ignoreunits) end nothing end @@ -571,6 +628,8 @@ end # # Variables # + +# `variable` methods to get variable given various arguments variable(obj::ComponentDef, name::Symbol) = _ns_get(obj, name, VariableDef) variable(obj::AbstractCompositeComponentDef, name::Symbol) = _ns_get(obj, name, CompositeVariableDef) @@ -584,8 +643,18 @@ function variable(obj::AbstractCompositeComponentDef, comp_path::ComponentPath, return variable(comp_def, var_name) end +""" + has_variable(comp_def::ComponentDef, name::Symbol) + +Return `true` if component `comp_def` has a variable `name`, otherwise return false. +""" has_variable(comp_def::ComponentDef, name::Symbol) = _ns_has(comp_def, name, VariableDef) +""" + has_variable(comp_def::AbstractCompositeComponentDef, name::Symbol) + +Return `true` if component `comp_def` has a variable `name`, otherwise return false. +""" has_variable(comp_def::AbstractCompositeComponentDef, name::Symbol) = _ns_has(comp_def, name, CompositeVariableDef) """ @@ -595,8 +664,12 @@ Return a list of all variable names for a given component `comp_name` in a model """ variable_names(obj::AbstractCompositeComponentDef, comp_name::Symbol) = variable_names(compdef(obj, comp_name)) -variable_names(comp_def::AbstractComponentDef) = [nameof(var) for var in variables(comp_def)] +""" + variable_names(comp_def::AbstractComponentDef) +Return a list of all variable names for a given component `comp_def`. +""" +variable_names(comp_def::AbstractComponentDef) = [nameof(var) for var in variables(comp_def)] function variable_unit(obj::AbstractCompositeComponentDef, comp_path::ComponentPath, var_name::Symbol) var = variable(obj, comp_path, var_name) @@ -662,12 +735,21 @@ end # Other # -# Return the number of timesteps a given component in a model will run for. +""" + function getspan(obj::AbstractComponentDef, comp_name::Symbol) + +Return the number of timesteps a given component `comp_name` in `obj` will run for. +""" function getspan(obj::AbstractComponentDef, comp_name::Symbol) comp_def = compdef(obj, comp_name) return getspan(obj, comp_def) end +""" + function getspan(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef) + +Return the number of timesteps a given component `comp_def` in `obj` will run for. +""" function getspan(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef) first = first_period(obj, comp_def) last = last_period(obj, comp_def) @@ -764,6 +846,63 @@ function _insert_comp!(obj::AbstractCompositeComponentDef, comp_def::AbstractCom nothing end +""" + _initialize_parameters!(md::ModelDef, comp_def::AbstractComponentDef) + +Add and connect an unshared model parameter to `md` for each parameter in +`comp_def`. +""" +function _initialize_parameters!(md::ModelDef, comp_def::AbstractComponentDef) + for param_def in parameters(comp_def) + + param_name = nameof(param_def) + comp_name = nameof(comp_def) + + # Make some checks to see if the parameter needs to be created, because it was either: + + # (1) externally created and connected, as checked with unconnected_params + # or alternatively by checking !isnothing(get_model_param_name(md, nameof(comp_def), + # nameof(param_def); missing_ok = true)) + # (2) internally connected and thus the old shared parameter has been + # deleted, as checked by unconnected_params + + connected = UnnamedReference(comp_name, param_name) in connection_refs(md) + !connected && _initialize_parameter!(md, comp_def, param_def) + + end + nothing +end + +""" + _initialize_parameter!(md::ModelDef, comp_def::AbstractComponentDef, param_def::AbstractParameterDef) + +Add and connect an unshared model parameter to `md` for parameter `param_def` in +`comp_def`. +""" +function _initialize_parameter!(md::ModelDef, comp_def::AbstractComponentDef, param_def::AbstractParameterDef) + + param_name = nameof(param_def) + comp_name = nameof(comp_def) + + model_param_name = gensym() + value = param_def.default + + # create the unshared model parameter with a value of param_def.default, + # which will be nothing if it not set explicitly + param = create_model_param(md, param_def, value) + + # Need to check the dimensions of the parameter data against component + # before adding it to the model's parameter list + _check_attributes(md, comp_def, param_name, param) + + # add the unshared model parameter to the model def + add_model_param!(md, model_param_name, param) + + # connect - don't need to check attributes since did it above + connect_param!(md, comp_def, param_name, model_param_name; check_attributes = false) + +end + """ _propagate_first_last!(obj::AbstractComponentDef; first::NothingInt=nothing, last::NothingInt=nothing) @@ -889,7 +1028,7 @@ Note that a copy of `comp_id` is made in the composite and assigned the give nam argument `rename` can be a list of pairs indicating `original_name => imported_name`. The optional arguments `first` and `last` indicate the times bounding the run period for the given component, which must be within the bounds of the model and if explicitly set are fixed. These default -to flexibly changing with the model's `:time` dimension. +to flexibly changing with the model's `:time` dimension. """ function add_comp!(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, @@ -913,7 +1052,7 @@ function add_comp!(obj::AbstractCompositeComponentDef, parent!(comp_def, obj) # Handle time dimension for the component and leaving the time unset for the - # original component template + # original component template using steps (1) and (2) # (1) Propagate the first and last from the add_comp! call through the component (default to nothing) if has_dim(obj, :time) @@ -933,6 +1072,9 @@ function add_comp!(obj::AbstractCompositeComponentDef, _add_anonymous_dims!(obj, comp_def) _insert_comp!(obj, comp_def, before=before, after=after) + # Create an unshared model parameter for each of the new component's parameters + isa(obj, ModelDef) && _initialize_parameters!(obj, comp_def) + # Return the comp since it's a copy of what was passed in return comp_def end @@ -1038,7 +1180,7 @@ function _replace!(obj::AbstractCompositeComponentDef, error("Cannot replace and reconnect; new component does not contain the necessary variables.") end - # Check external parameter connections + # Check model parameter connections remove = [] for epc in external_param_conns(obj, comp_name) param_name = epc.param_name @@ -1058,13 +1200,15 @@ function _replace!(obj::AbstractCompositeComponentDef, end filter!(epc -> !(epc in remove), external_param_conns(obj)) - # Delete the old component from composite's namespace only, leaving parameter connections + # Delete the old component from composite's namespace only, leaving parameter + # connections delete!(obj.namespace, comp_name) else # Delete the old component and all its internal and external parameter connections delete!(obj, comp_name) end - # Re-add - return add_comp!(obj, comp_id, comp_name; before=before, after=after) + ref = add_comp!(obj, comp_id, comp_name; before=before, after=after) + + return ref end diff --git a/src/core/dimensions.jl b/src/core/dimensions.jl index cf3512f74..d9919575e 100644 --- a/src/core/dimensions.jl +++ b/src/core/dimensions.jl @@ -109,6 +109,11 @@ function set_dimension!(ccd::AbstractCompositeComponentDef, name::Symbol, keys:: return set_dimension!(ccd, name, dim) end +""" + set_dimension!(obj::AbstractComponentDef, name::Symbol, dim::Dimension) + +Set the dimension `name` in `obj` to `dim`. +""" function set_dimension!(obj::AbstractComponentDef, name::Symbol, dim::Dimension) dirty!(obj) obj.dim_dict[name] = dim @@ -121,6 +126,13 @@ function set_dimension!(obj::AbstractComponentDef, name::Symbol, dim::Dimension) return dim end +""" + function add_dimension!(comp::AbstractComponentDef, name) + +Add a dimension of name `name` to `comp`, where the dimension will be `nothing` +unless `name` is an Int in which case we create an "anonymouse" dimension on +the fly with keys `1` through `count` where `count` = `name`. +""" function add_dimension!(comp::AbstractComponentDef, name) # generally, we add dimension name with nothing instead of a Dimension instance, # but in the case of an Int name, we create the "anonymous" dimension on the fly. @@ -131,6 +143,11 @@ end # Note that this operates on the registered comp, not one added to a composite add_dimension!(comp_id::ComponentId, name) = add_dimension!(compdef(comp_id), name) +""" + dim_names(ccd::AbstractCompositeComponentDef) + +Return a list of the dimension names of `ccd`. +""" function dim_names(ccd::AbstractCompositeComponentDef) dims = OrderedSet{Symbol}() # use a set to eliminate duplicates for cd in compdefs(ccd) @@ -140,6 +157,20 @@ function dim_names(ccd::AbstractCompositeComponentDef) return collect(dims) end +""" + dim_names(comp_def::AbstractComponentDef, datum_name::Symbol) + +Return a list of the dimension names of datum `datum_name` in `comp_def`. +""" +dim_names(comp_def::AbstractComponentDef, datum_name::Symbol) = dim_names(datumdef(comp_def, datum_name)) + +""" + dim_count(def::AbstractDatumDef) + +Return number of dimensions in `def`. +""" +dim_count(def::AbstractDatumDef) = length(dim_names(def)) + """ _check_time_redefinition(obj::AbstractCompositeComponentDef, keys::Union{Int, Vector, Tuple, AbstractRange}) @@ -186,7 +217,3 @@ function _check_time_redefinition(obj::AbstractCompositeComponentDef, keys::Unio 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/instances.jl b/src/core/instances.jl index ceb233e00..e54ba0e05 100644 --- a/src/core/instances.jl +++ b/src/core/instances.jl @@ -109,8 +109,18 @@ function get_var_value(ci::AbstractComponentInstance, name::Symbol) end end +""" + set_param_value(ci::AbstractComponentInstance, name::Symbol, value) + +Set the value of parameter `name` in component `ci` to `value`. +""" set_param_value(ci::AbstractComponentInstance, name::Symbol, value) = setproperty!(ci.parameters, name, value) +""" + set_var_value(ci::AbstractComponentInstance, name::Symbol, value) + +Set the value of variable `name` in component `ci` to `value`. +""" set_var_value(ci::AbstractComponentInstance, name::Symbol, value) = setproperty!(ci.variables, name, value) """ @@ -319,6 +329,12 @@ function run_timestep(cci::AbstractCompositeComponentInstance, clock::Clock, dim return nothing end +""" + Base.run(mi::ModelInstance, ntimesteps::Int=typemax(Int), + dimkeys::Union{Nothing, Dict{Symbol, Vector{T} where T <: DimensionKeyTypes}}=nothing) + +Run the `ModelInstance` `mi` once with `ntimesteps` and dimension keys `dimkeys`. +""" function Base.run(mi::ModelInstance, ntimesteps::Int=typemax(Int), dimkeys::Union{Nothing, Dict{Symbol, Vector{T} where T <: DimensionKeyTypes}}=nothing) diff --git a/src/core/model.jl b/src/core/model.jl index d71b178c5..22610fe8b 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -11,11 +11,33 @@ Return the `ModelDef` contained by Model `m`. """ modeldef(m::Model) = m.md +""" + modelinstance(m::Model) + +Return the `ModelInstance` contained by Model `m`. +""" modelinstance(m::Model) = m.mi + +""" + modelinstance_def(m::Model) + +Return the `ModelDef` of the `ModelInstance` contained by Model `m`. +""" modelinstance_def(m::Model) = modeldef(modelinstance(m)) +""" + is_built(m::Model) + +Return true if Model `m` is built, otherwise return false. +""" is_built(m::Model) = !(dirty(m.md) || modelinstance(m) === nothing) +""" + is_built(mm::MarginalModel) + +Return true if `MarginalModel` `mm` is built, meaning both its `base` and `modified` +`Model`s are built, otherwise return false. +""" is_built(mm::MarginalModel) = (is_built(mm.base) && is_built(mm.modified)) @delegate compinstance(m::Model, name::Symbol) => mi @@ -26,17 +48,21 @@ is_built(mm::MarginalModel) = (is_built(mm.base) && is_built(mm.modified)) @delegate internal_param_conns(m::Model) => md @delegate external_param_conns(m::Model) => md -@delegate external_params(m::Model) => md -@delegate external_param(m::Model, name::Symbol; missing_ok=false) => md +@delegate model_params(m::Model) => md +@delegate model_param(m::Model, comp_name::Symbol, param_name::Symbol; missing_ok = false) => md +@delegate model_param(m::Model, name::Symbol; missing_ok=false) => md @delegate connected_params(m::Model) => md @delegate unconnected_params(m::Model) => md +@delegate nothing_params(m::Model) => md @delegate add_connector_comps!(m::Model) => md """ - connect_param!(m::Model, dst_comp_name::Symbol, dst_par_name::Symbol, src_comp_name::Symbol, src_var_name::Symbol, - backup::Union{Nothing, Array}=nothing; ignoreunits::Bool=false, backup_offset::Union{Int, Nothing}=nothing) + connect_param!(m::Model, dst_comp_name::Symbol, dst_par_name::Symbol, + src_comp_name::Symbol, src_var_name::Symbol, + backup::Union{Nothing, Array}=nothing; ignoreunits::Bool=false, + backup_offset::Union{Int, Nothing}=nothing) Bind the parameter `dst_par_name` of one component `dst_comp_name` of model `m` to a variable `src_var_name` in another component `src_comp_name` of the same model @@ -47,19 +73,21 @@ a specified number of timesteps after the source component begins. ie. the value `1` if the destination componentm parameter should only use the source component data for the second timestep and beyond. """ -@delegate connect_param!(m::Model, - dst_comp_name::Symbol, dst_par_name::Symbol, - src_comp_name::Symbol, src_var_name::Symbol, - backup::Union{Nothing, Array}=nothing; - ignoreunits::Bool=false, backup_offset::Union{Int, Nothing} = nothing) => md +@delegate connect_param!(m::Model, dst_comp_name::Symbol, dst_par_name::Symbol, + src_comp_name::Symbol, src_var_name::Symbol, + backup::Union{Nothing, Array}=nothing; ignoreunits::Bool=false, + backup_offset::Union{Nothing, Int} = nothing) => md + """ - connect_param!(m::Model, comp_name::Symbol, param_name::Symbol, ext_param_name::Symbol) + connect_param!(m::Model, comp_name::Symbol, param_name::Symbol, model_param_name::Symbol; + check_attributes::Bool=true, ignoreunits::Bool=false)) -Bind the parameter `param_name` in the component `comp_name` of model `m` to the external parameter -`ext_param_name` already present in the model's list of external parameters. +Connect a parameter `param_name` in the component `comp_name` of composite `obj` to +the model parameter `model_param_name`. """ -@delegate connect_param!(m::Model, comp_name::Symbol, param_name::Symbol, ext_param_name::Symbol) => md +@delegate connect_param!(m::Model, comp_name::Symbol, param_name::Symbol, model_param_name::Symbol; + check_attributes::Bool=true, ignoreunits::Bool = false) => md """ connect_param!(m::Model, dst::Pair{Symbol, Symbol}, src::Pair{Symbol, Symbol}, backup::Array; ignoreunits::Bool=false) @@ -88,37 +116,104 @@ Remove any parameter connections for a given parameter `param_name` in a given c """ @delegate disconnect_param!(m::Model, comp_name::Symbol, param_name::Symbol) => md -# TBD: these may not be needed as delegators -@delegate set_external_param!(m::Model, name::Symbol, value::ModelParameter) => md +""" + add_model_param!(m::Model, name::Symbol, value::ModelParameter) -@delegate set_external_param!(m::Model, name::Symbol, - value::Union{Number, AbstractArray, AbstractRange, Tuple}; - param_dims::Union{Nothing,Array{Symbol}} = nothing) => md +Add an model parameter with name `name` and Model Parameter `value` to Model `m`. +""" +@delegate add_model_param!(m::Model, name::Symbol, value::ModelParameter) => md + +""" + add_model_param!(m: Model, name::Symbol, value::Number; + param_dims::Union{Nothing,Array{Symbol}} = nothing, + is_shared::Bool = false) + +Create and add a model parameter with name `name` and Model Parameter `value` +to Model `m`. The Model Parameter will be created with value `value`, dimensions +`param_dims` which can be left to be created automatically from the Model Def, and +an is_shared attribute `is_shared` which defaults to false. +""" +@delegate add_model_param!(m::Model, name::Symbol, + value::Union{Number, AbstractArray, AbstractRange, Tuple}; + param_dims::Union{Nothing,Array{Symbol}} = nothing) => md +""" + add_model_param!(m::Model, name::Symbol, + value::Union{AbstractArray, AbstractRange, Tuple}; + param_dims::Union{Nothing,Array{Symbol}} = nothing, + is_shared::Bool = false) + +Create and add a model parameter with name `name` and Model Parameter `value` +to Model `m`. The Model Parameter will be created with value `value`, dimensions +`param_dims` which can be left to be created automatically from the Model Def, and +an is_shared attribute `is_shared` which defaults to false. +""" +@delegate add_model_param!(m::Model, name::Symbol, + value::Union{AbstractArray, AbstractRange, Tuple}; + param_dims::Union{Nothing,Array{Symbol}} = nothing, + is_shared::Bool = false) => md +""" + add_internal_param_conn(m::Model, conn::InternalParameterConnection) +Add internal parameter connection `conn` to model `m`. +""" @delegate add_internal_param_conn!(m::Model, conn::InternalParameterConnection) => md -# @delegate doesn't handle the 'where T' currently. This is the only instance of it for now... -function set_leftover_params!(m::Model, parameters::Dict{T, Any}) where T - set_leftover_params!(m.md, parameters) -end +""" + set_leftover_params!(m::Model, parameters::Dict) + +Set all of the parameters in `Model` `m` that don't have a value and are not connected +to some other component to a value from a dictionary `parameters`. This method assumes +the dictionary keys are strings (or convertible into Strings ie. Symbols) that +match the names of unset parameters in the model, and all resulting new model +parameters will be shared parameters. + +Note that this function `set_leftover_params! has been deprecated, and uses should +be transitioned to using `update_leftover_params!` with keys specific to component-parameter +pairs i.e. (comp_name, param_name) => value in the dictionary. +""" +@delegate set_leftover_params!(m::Model, parameters) => md + +""" + update_leftover_params!(m::Model, parameters::Dict) + +Update all of the parameters in `Model` `m` that don't have a value and are not connected +to some other component to a value from a dictionary `parameters`. This method assumes +the dictionary keys are Tuples of Symbols (or convertible to Symbols ie. Strings) +of (comp_name, param_name) that match the component-parameter pair of +unset parameters in the model. All resulting connected model parameters will be +unshared model parameters. +""" +@delegate update_leftover_params!(m::Model, parameters) => md + """ update_param!(m::Model, name::Symbol, value; update_timesteps = nothing) -Update the `value` of an external model parameter in model `m`, referenced by +Update the `value` of an model parameter in model `m`, referenced by `name`. The update_timesteps keyword argument is deprecated, we keep it here just to provide warnings. """ @delegate update_param!(m::Model, name::Symbol, value; update_timesteps = nothing) => md """ - update_params!(m::Model, parameters::Dict{T, Any}; update_timesteps = nothing) where T + update_param!(m::Model, comp_name::Symbol, param_name::Symbol, value) + +Update the `value` of the unshared model parameter in Model `m`'s Model Def connected +to component `comp_name`'s parameter `param_name`. +""" +@delegate update_param!(m::Model, comp_name::Symbol, param_name::Symbol, value) => md + +""" + update_params!(m::Model, parameters::Dict; update_timesteps = nothing) + +For each (k, v) in the provided `parameters` dictionary, `update_param!` +is called to update the model parameter identified by k to value v. + +For updating unshared parameters, each key k must be a Tuple matching the name of a +component in `obj` and the name of an parameter in that component. -For each (k, v) in the provided `parameters` dictionary, `update_param!`` -is called to update the 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 t -hat already exists in the model definition. The update_timesteps keyword argument -is deprecated, but temporarily remains as a dummy argument to allow warning detection. +For updating shared parameters, each key k must be a symbol or convert to a symbol +matching the name of a shared model parameter that already exists in the model. """ @delegate update_params!(m::Model, parameters::Dict; update_timesteps = nothing) => md @@ -167,46 +262,6 @@ function add_comp!(m::Model, comp_def::AbstractComponentDef, comp_name::Symbol=c return add_comp!(m, comp_def.comp_id, comp_name; kwargs...) end -# DEPRECATION - EVENTUALLY REMOVE -""" - replace_comp!( - m::Model, comp_id::ComponentId, comp_name::Symbol=comp_id.comp_name; - before::NothingSymbol=nothing, - after::NothingSymbol=nothing, - reconnect::Bool=true - ) - -Deprecated function for replacing the component with name `comp_name` in model `m` with the -new component specified by `comp_id`. Use the following syntax instead: - -`replace!(m, comp_name => Mimi.compdef(comp_id); kwargs...)` - -See docstring for `replace!` for further description of available functionality. -""" -function replace_comp!(m::Model, comp_id::ComponentId, comp_name::Symbol=comp_id.comp_name; kwargs...) - error("Function `replace_comp!(m, comp_id, comp_name; kwargs...)` has been deprecated. Use `replace!(m, comp_name => Mimi.compdef(comp_id); kwargs...)` instead.") -end - -# DEPRECATION - EVENTUALLY REMOVE -""" - replace_comp!( - m::Model, comp_def::ComponentDef, comp_name::Symbol=comp_id.comp_name; - before::NothingSymbol=nothing, - after::NothingSymbol=nothing, - reconnect::Bool=true - ) - -Deprecated function for replacing the component with name `comp_name` in model `m` with the -new component specified by `comp_def`. Use the following syntax instead: - -`replace!(m, comp_name => comp_def; kwargs...)` - -See docstring for `replace!` for further description of available functionality. -""" -function replace_comp!(m::Model, comp_def::ComponentDef, comp_name::Symbol=comp_def.comp_id.comp_name; kwargs...) - error("Function `replace_comp!(m, comp_def, comp_name; kwargs...)` has been deprecated. Use `replace!(m, comp_name => comp_def; kwargs...)` instead.") -end - """ replace!( m::Model, @@ -248,7 +303,11 @@ Return an iterator on the components in a model's model instance. @delegate time_labels(m::Model) => md -# Return the number of timesteps a given component in a model will run for. +""" + getspan(m::Model, comp_name::Symbol) + +Return the number of timesteps a given component in a model will run for. +""" @delegate getspan(m::Model, comp_name::Symbol) => md """ @@ -267,6 +326,11 @@ function datumdef(comp_def::AbstractComponentDef, item::Symbol) end end +""" + datumdef(m::Model, comp_name::Symbol, item::Symbol) + +Return a DatumDef for `item` in the given component `comp_name` of model `m`. +""" datumdef(m::Model, comp_name::Symbol, item::Symbol) = datumdef(compdef(m.md, comp_name), item) """ @@ -376,36 +440,51 @@ variables(m::Model, comp_name::Symbol) = variables(compdef(m, comp_name)) @delegate variable_names(m::Model, comp_name::Symbol) => md """ - set_external_array_param!(m::Model, name::Symbol, value::Union{AbstractArray, TimestepArray}, dims) + add_shared_param!(m::Model, name::Symbol, value::Any; dims::Array{Symbol}=Symbol[], datatype::DataType=Nothing) + +User-facing API function to add a shared parameter to Model `m` with name +`name` and value `value`, and an array of dimension names `dims` which dfaults to +an empty vector. The `is_shared` attribute of the added Model Parameter will be `true`. + +The `value` can by a scalar, an array, or a NamedAray. Optional keyword argument 'dims' is a list +of the dimension names of the provided data, and will be used to check that they match the +model's index labels. This must be included if the `value` is not a scalar, and defaults +to an empty vector. Optional keyword argument `datatype` allows user to specify a datatype +to use for the shared model parameter. +""" +@delegate add_shared_param!(m::Model, name::Symbol, value::Any; dims::Array{Symbol}=Symbol[], data_type::DataType=Nothing) => md + +""" + add_model_array_param!(m::Model, name::Symbol, value::Union{AbstractArray, TimestepArray}, dims) Add a one or two dimensional (optionally, time-indexed) array parameter `name` with value `value` to the model `m`. """ -@delegate set_external_array_param!(m::Model, name::Symbol, value::Union{AbstractArray, TimestepArray}, dims) => md +@delegate add_model_array_param!(m::Model, name::Symbol, value::Union{AbstractArray, TimestepArray}, dims) => md """ - set_external_scalar_param!(m::Model, name::Symbol, value::Any) + add_model_scalar_param!(m::Model, name::Symbol, value::Any) Add a scalar type parameter `name` with value `value` to the model `m`. """ -@delegate set_external_scalar_param!(m::Model, name::Symbol, value::Any) => md +@delegate add_model_scalar_param!(m::Model, name::Symbol, value::Any) => md """ delete!(m::Model, component::Symbol; deep::Bool=false) Delete a `component` by name from a model `m`'s ModelDef, and nullify the ModelInstance. -If `deep=true` then any external model parameters connected only to +If `deep=true` then any model model parameters connected only to this component will also be deleted. """ @delegate Base.delete!(m::Model, comp_name::Symbol; deep::Bool=false) => md """ - delete_param!(m::Model, external_param_name::Symbol) + delete_param!(m::Model, model_param_name::Symbol) -Delete `external_param_name` from a model `m`'s ModelDef's list of external parameters, and -also remove all external parameters connections that were connected to `external_param_name`. +Delete `model_param_name` from a model `m`'s ModelDef's list of model parameters, and +also remove all external parameters connections that were connected to `model_param_name`. """ -@delegate delete_param!(m::Model, external_param_name::Symbol) => md +@delegate delete_param!(m::Model, model_param_name::Symbol) => md """ set_param!(m::Model, comp_name::Symbol, param_name::Symbol, value; dims=nothing) @@ -418,15 +497,15 @@ that they match the model's index labels. @delegate set_param!(m::Model, comp_name::Symbol, param_name::Symbol, value; dims=nothing) => md """ - set_param!(m::Model, comp_name::Symbol, param_name::Symbol, ext_param_name::Symbol, value; dims=nothing) + set_param!(m::Model, comp_name::Symbol, param_name::Symbol, model_param_name::Symbol, value; dims=nothing) Set the parameter `param_name` of a component `comp_name` in a model `m` to a given `value`, -storing the value in the model's external parameter list by the provided name `ext_param_name`. +storing the value in the model's parameter list by the provided name `model_param_name`. The `value` can by a scalar, an array, or a NamedAray. Optional keyword argument 'dims' is a list of the dimension names of the provided data, and will be used to check that they match the model's index labels. """ -@delegate set_param!(m::Model, comp_name::Symbol, param_name::Symbol, ext_param_name::Symbol, value; dims=nothing) => md +@delegate set_param!(m::Model, comp_name::Symbol, param_name::Symbol, model_param_name::Symbol, value; dims=nothing) => md """ @@ -440,7 +519,8 @@ the specified name. @delegate import_params!(m::Model) => md """ - run(m::Model) + Base.run(m::Model; ntimesteps::Int=typemax(Int), rebuild::Bool=false, + dim_keys::Union{Nothing, Dict{Symbol, Vector{T} where T <: DimensionKeyTypes}}=nothing) Run model `m` once. """ @@ -459,3 +539,59 @@ function Base.run(m::Model; ntimesteps::Int=typemax(Int), rebuild::Bool=false, run(mi, ntimesteps, dim_keys) nothing end + +## +## DEPRECATIONS - Should move from warning --> error --> removal +## + +# -- throw errors -- + +""" + replace_comp!( + m::Model, comp_def::ComponentDef, comp_name::Symbol=comp_id.comp_name; + before::NothingSymbol=nothing, + after::NothingSymbol=nothing, + reconnect::Bool=true + ) + +Deprecated function for replacing the component with name `comp_name` in model `m` with the +new component specified by `comp_def`. Use the following syntax instead: + +`replace!(m, comp_name => comp_def; kwargs...)` + +See docstring for `replace!` for further description of available functionality. +""" +function replace_comp!(m::Model, comp_def::ComponentDef, comp_name::Symbol=comp_def.comp_id.comp_name; kwargs...) + error("Function `replace_comp!(m, comp_def, comp_name; kwargs...)` has been deprecated. Use `replace!(m, comp_name => comp_def; kwargs...)` instead.") +end + +""" + replace_comp!( + m::Model, comp_id::ComponentId, comp_name::Symbol=comp_id.comp_name; + before::NothingSymbol=nothing, + after::NothingSymbol=nothing, + reconnect::Bool=true + ) + +Deprecated function for replacing the component with name `comp_name` in model `m` with the +new component specified by `comp_id`. Use the following syntax instead: + +`replace!(m, comp_name => Mimi.compdef(comp_id); kwargs...)` + +See docstring for `replace!` for further description of available functionality. +""" +function replace_comp!(m::Model, comp_id::ComponentId, comp_name::Symbol=comp_id.comp_name; kwargs...) + error("Function `replace_comp!(m, comp_id, comp_name; kwargs...)` has been deprecated. Use `replace!(m, comp_name => Mimi.compdef(comp_id); kwargs...)` instead.") +end + +# -- throw warnings -- + +@delegate set_external_param!(m::Model, name::Symbol, + value::Union{Number, AbstractArray, AbstractRange, Tuple}; + param_dims::Union{Nothing,Array{Symbol}} = nothing) => md + +@delegate set_external_param!(m::Model, name::Symbol, value::ModelParameter) => md +@delegate set_external_array_param!(m::Model, name::Symbol, value::Union{AbstractArray, TimestepArray}, dims) => md +@delegate set_external_scalar_param!(m::Model, name::Symbol, value::Any) => md +@delegate external_params(m::Model) => md +@delegate external_param(m::Model, name::Symbol; missing_ok=false) => md diff --git a/src/core/references.jl b/src/core/references.jl index d3b048b49..5a8800ce6 100644 --- a/src/core/references.jl +++ b/src/core/references.jl @@ -2,7 +2,7 @@ set_param!(ref::ComponentReference, name::Symbol, value) Set a component parameter as `set_param!(reference, name, value)`. -This creates a unique name :compname_paramname in the model's external parameter list, +This creates a unique name :compname_paramname in the model's model parameter list, and sets the parameter only in the referenced component to that value. """ function set_param!(ref::ComponentReference, name::Symbol, value) @@ -15,7 +15,7 @@ end update_param!(ref::ComponentReference, name::Symbol, value) Update a component parameter as `update_param!(reference, name, value)`. -This uses the unique name :compname_paramname in the model's external parameter list, +This uses the unique name :compname_paramname in the model's model parameter list, and updates the parameter only in the referenced component to that value. """ function update_param!(ref::ComponentReference, name::Symbol, value) @@ -28,7 +28,7 @@ end Base.setindex!(ref::ComponentReference, value, name::Symbol) Set a component parameter as `reference[name] = value`. -This creates a unique name :compname_paramname in the model's external parameter list, +This creates a unique name :compname_paramname in the model's model parameter list, and sets the parameter only in the referenced component to that value. """ function Base.setindex!(ref::ComponentReference, value, name::Symbol) diff --git a/src/core/time.jl b/src/core/time.jl index 2704d13d1..4493c00a6 100644 --- a/src/core/time.jl +++ b/src/core/time.jl @@ -20,16 +20,6 @@ function gettime(ts::VariableTimestep) return ts.current end -# DEPRECATION - EVENTUALLY REMOVE -""" - is_time(ts::AbstractTimestep, t::Int) - -Deprecated fucntion to return true or false, true if the current time (year) for `ts` is `t` - """ - function is_time(ts::AbstractTimestep, t::Int) - error("`is_time(ts, t)` is deprecated. Use comparison operators with TimestepValue objects instead: `ts == TimestepValue(t)`") - end - """ is_first(ts::AbstractTimestep) @@ -39,16 +29,6 @@ function is_first(ts::AbstractTimestep) return ts.t == 1 end -# DEPRECATION - EVENTUALLY REMOVE -""" - is_timestep(ts::AbstractTimestep, t::Int) - -Deprecated function to return true or false, true if `ts` timestep is step `t`. - """ - function is_timestep(ts::AbstractTimestep, t::Int) - error("`is_timestep(ts, t)` is deprecated. Use comparison operators with TimestepIndex objects instead: `ts == TimestepIndex(t)`") - end - """ is_last(ts::FixedTimestep) @@ -68,14 +48,31 @@ function is_last(ts::VariableTimestep{TIMES}) where {TIMES} return gettime(ts) == TIMES[end] end +""" + finished(ts::FixedTimestep{FIRST, STEP, LAST}) where {FIRST, STEP, LAST} + +Return true if `ts` has finished running, ie. the current time is after the +`LAST` of `ts`, otherwise return false. +""" function finished(ts::FixedTimestep{FIRST, STEP, LAST}) where {FIRST, STEP, LAST} return gettime(ts) > LAST end +""" + finished(ts::VariableTimestep{TIMES}) where {TIMES} + +Return true if `ts` has finished running, ie. the current time is after the +last member of `TIMES` of `ts`, otherwise return false. +""" function finished(ts::VariableTimestep{TIMES}) where {TIMES} return gettime(ts) > TIMES[end] end +""" + next_timestep(ts::FixedTimestep{FIRST, STEP, LAST}) where {FIRST, STEP, LAST} + +Return the subsequent timestep of `ts`. +""" function next_timestep(ts::FixedTimestep{FIRST, STEP, LAST}) where {FIRST, STEP, LAST} if finished(ts) error("Cannot get next timestep, this is last timestep.") @@ -83,6 +80,11 @@ function next_timestep(ts::FixedTimestep{FIRST, STEP, LAST}) where {FIRST, STEP, return FixedTimestep{FIRST, STEP, LAST}(ts.t + 1) end +""" + next_timestep(ts::VariableTimestep{TIMES}) where {TIMES} + +Return the subsequent timestep of `ts`. +""" function next_timestep(ts::VariableTimestep{TIMES}) where {TIMES} if finished(ts) error("Cannot get next timestep, this is last timestep.") @@ -90,6 +92,8 @@ function next_timestep(ts::VariableTimestep{TIMES}) where {TIMES} return VariableTimestep{TIMES}(ts.t + 1) end +# extend Base with arithmetic methods for timesteps + function Base.:-(ts::FixedTimestep{FIRST, STEP, LAST}, val::Int) where {FIRST, STEP, LAST} if val != 0 && is_first(ts) error("Cannot get previous timestep, this is first timestep.") @@ -175,6 +179,7 @@ function Base.:<(tv::TimestepValue, ts::AbstractTimestep) end # Colon support + function Base.:(:)(start::T, step::Int, stop::T) where {T<:TimestepIndex} indices = [start.index:step:stop.index...] return TimestepIndex.(indices) @@ -188,6 +193,11 @@ end # CLOCK # +""" + Clock(time_keys::Vector{Int}) + +Return a `Clock` object with times `time_keys`. +""" function Clock(time_keys::Vector{Int}) last = time_keys[end] @@ -201,10 +211,20 @@ function Clock(time_keys::Vector{Int}) end end +""" + timestep(c::Clock) + +Return `Clock` `c`'s current timestep. +""" function timestep(c::Clock) return c.ts end +""" + time_index(c::Clock) + +Return `Clock` `c`'s time index. +""" function time_index(c::Clock) return c.ts.t end @@ -212,17 +232,27 @@ end """ gettime(c::Clock) -Return the current time of the timestep held by the `c` clock. +Return the time of the timestep held by the `c` clock. """ function gettime(c::Clock) return gettime(c.ts) end +""" + advance(c::Clock) + +Advance `Clock` `c` to the next timestep. +""" function advance(c::Clock) c.ts = next_timestep(c.ts) nothing end +""" + advance(c::Clock) + +Return true if the timestep `ts` of `Clock` `c` has finished running. +""" function finished(c::Clock) return finished(c.ts) end @@ -232,6 +262,11 @@ function Base.reset(c::Clock) nothing end +""" + timesteps(c::Clock) + +Return the timesteps held by `Clock` `c`. +""" function timesteps(c::Clock) c = deepcopy(c) timesteps = AbstractTimestep[] @@ -240,4 +275,30 @@ function timesteps(c::Clock) advance(c) end return timesteps -end \ No newline at end of file +end + +## +## DEPRECATIONS - Should move from warning --> error --> removal +## + +# -- throw errors -- + +""" + is_time(ts::AbstractTimestep, t::Int) + +Deprecated function to return true or false, true if the current time (year) for `ts` is `t` + """ + function is_time(ts::AbstractTimestep, t::Int) + error("`is_time(ts, t)` is deprecated. Use comparison operators with TimestepValue objects instead: `ts == TimestepValue(t)`") + end + + """ + is_timestep(ts::AbstractTimestep, t::Int) + +Deprecated function to return true or false, true if `ts` timestep is step `t`. + """ + function is_timestep(ts::AbstractTimestep, t::Int) + error("`is_timestep(ts, t)` is deprecated. Use comparison operators with TimestepIndex objects instead: `ts == TimestepIndex(t)`") + end + + # -- throw warnings -- diff --git a/src/core/time_arrays.jl b/src/core/time_arrays.jl index 857c98f63..272b202cc 100644 --- a/src/core/time_arrays.jl +++ b/src/core/time_arrays.jl @@ -29,8 +29,6 @@ function get_time_index_position(obj::AbstractCompositeComponentDef, comp_name:: end const AnyIndex = Union{Int, Vector{Int}, Tuple, Colon, OrdinalRange} -# DEPRECATION - EVENTUALLY REMOVE -const AnyIndex_NonColon = Union{Int, Vector{Int}, Tuple, OrdinalRange} # Helper function for getindex; throws a MissingException if data is missing, otherwise returns data function _missing_data_check(data, t) @@ -59,18 +57,6 @@ function _single_index_check(data, idxs) end end -# DEPRECATION - EVENTUALLY REMOVE -# Helper function for getindex; throws an error if one indexes into a TimestepArray with an integer -function _throw_int_getindex_error() - error("Indexing with getindex into a TimestepArray with Integer(s) is deprecated, please index with a TimestepIndex(index::Int) instead ie. instead of t[2] use t[TimestepIndex(2)]") -end - -# DEPRECATION - EVENTUALLY REMOVE -# Helper function for setindex; throws an error if one indexes into a TimestepArray with an integer -function _throw_int_setindex_error() - error("Indexing with setindex into a TimestepArray with Integer(s) is deprecated, please index with a TimestepIndex(index::Int) instead ie. instead of t[2] use t[TimestepIndex(2)]") -end - # Helper macro used by connector macro allow_missing(expr) let e = gensym("e") @@ -232,18 +218,6 @@ function Base.setindex!(v::TimestepVector, val, ts::TimestepIndex) setindex!(v.data, val, ts.index) end -# DEPRECATION - EVENTUALLY REMOVE -# int indexing version supports old-style components and internal functions, not -# part of the public API - - function Base.getindex(v::TimestepVector, i::AnyIndex_NonColon) - _throw_int_getindex_error() -end - -function Base.setindex!(v::TimestepVector, val, i::AnyIndex_NonColon) - _throw_int_setindex_error() -end - # # c. TimestepMatrix # @@ -366,19 +340,6 @@ function Base.setindex!(mat::TimestepMatrix, val, ts::TimestepIndex, idx::AnyInd setindex!(mat.data, val, ts.index, idx) end -# DEPRECATION - EVENTUALLY REMOVE -# int indexing version supports old-style components and internal functions, not -# part of the public API - -function Base.getindex(mat::TimestepMatrix, idx1::AnyIndex_NonColon, idx2::AnyIndex_NonColon) - _throw_int_getindex_error() -end - -function Base.setindex!(mat::TimestepMatrix, val, idx1::Int, idx2::Int) - _throw_int_setindex_error() -end - - # # TimestepArray methods # @@ -504,28 +465,6 @@ function Base.setindex!(arr::TimestepArray{VariableTimestep{TIMES}, T, N, ti}, v setindex!(arr.data, val, idxs1..., ts.index, idxs2...) end -# DEPRECATION - EVENTUALLY REMOVE -# Colon support - this allows the time dimension to be indexed with a colon - function Base.getindex(arr::TimestepArray{FixedTimestep{FIRST, STEP, LAST}, T, N, ti}, idxs::AnyIndex...) where {FIRST, STEP, LAST, T, N, ti} - isa(idxs[ti], AnyIndex_NonColon) ? _throw_int_getindex_error() : nothing - return arr.data[idxs...] -end - -function Base.getindex(arr::TimestepArray{VariableTimestep{TIMES}, T, N, ti}, idxs::AnyIndex...) where {TIMES, T, N, ti} - isa(idxs[ti], AnyIndex_NonColon) ? _throw_int_getindex_error() : nothing - return arr.data[idxs...] -end - -function Base.setindex!(arr::TimestepArray{FixedTimestep{FIRST, STEP, LAST}, T, N, ti}, val, idxs::AnyIndex...) where {FIRST, STEP, LAST, T, N, ti} - isa(idxs[ti], AnyIndex_NonColon) ? _throw_int_setindex_error() : nothing - setindex!(arr.data, val, idxs...) -end - -function Base.setindex!(arr::TimestepArray{VariableTimestep{TIMES}, T, N, ti}, val, idxs::AnyIndex...) where {TIMES, T, N, ti} - isa(idxs[ti], AnyIndex_NonColon) ? _throw_int_setindex_error() : nothing - setindex!(arr.data, val, idxs...) -end - # Indexing with arrays of TimestepIndexes or TimestepValues function Base.getindex(arr::TimestepArray{TS, T, N, ti}, idxs::Union{Array{TimestepIndex,1}, AnyIndex}...) where {TS, T, N, ti} idxs1, ts_array, idxs2 = split_indices(idxs, ti) @@ -612,3 +551,65 @@ function hasvalue(arr::TimestepArray{VariableTimestep{A_TIMES}, T, N, ti}, return A_TIMES[1] <= gettime(ts) <= last_period(arr) && all([1 <= idx <= size(arr, i) for (i, idx) in enumerate(idxs)]) end + +## +## DEPRECATIONS - Should move from warning --> error --> removal +## + +# -- throw errors -- + +const AnyIndex_NonColon = Union{Int, Vector{Int}, Tuple, OrdinalRange} + +# Helper function for getindex; throws an error if one indexes into a TimestepArray with an integer +function _throw_int_getindex_error() + error("Indexing with getindex into a TimestepArray with Integer(s) is deprecated, please index with a TimestepIndex(index::Int) instead ie. instead of t[2] use t[TimestepIndex(2)]") +end + +# Helper function for setindex; throws an error if one indexes into a TimestepArray with an integer +function _throw_int_setindex_error() + error("Indexing with setindex into a TimestepArray with Integer(s) is deprecated, please index with a TimestepIndex(index::Int) instead ie. instead of t[2] use t[TimestepIndex(2)]") +end + +# int indexing version supports old-style components and internal functions, not +# part of the public API + +function Base.getindex(v::TimestepVector, i::AnyIndex_NonColon) + _throw_int_getindex_error() +end + +function Base.setindex!(v::TimestepVector, val, i::AnyIndex_NonColon) + _throw_int_setindex_error() +end + +# int indexing version supports old-style components and internal functions, not +# part of the public API + +function Base.getindex(mat::TimestepMatrix, idx1::AnyIndex_NonColon, idx2::AnyIndex_NonColon) + _throw_int_getindex_error() +end + +function Base.setindex!(mat::TimestepMatrix, val, idx1::Int, idx2::Int) + _throw_int_setindex_error() +end + +# Colon support - this allows the time dimension to be indexed with a colon + +function Base.getindex(arr::TimestepArray{FixedTimestep{FIRST, STEP, LAST}, T, N, ti}, idxs::AnyIndex...) where {FIRST, STEP, LAST, T, N, ti} + isa(idxs[ti], AnyIndex_NonColon) ? _throw_int_getindex_error() : nothing + return arr.data[idxs...] +end + +function Base.getindex(arr::TimestepArray{VariableTimestep{TIMES}, T, N, ti}, idxs::AnyIndex...) where {TIMES, T, N, ti} + isa(idxs[ti], AnyIndex_NonColon) ? _throw_int_getindex_error() : nothing + return arr.data[idxs...] +end + +function Base.setindex!(arr::TimestepArray{FixedTimestep{FIRST, STEP, LAST}, T, N, ti}, val, idxs::AnyIndex...) where {FIRST, STEP, LAST, T, N, ti} + isa(idxs[ti], AnyIndex_NonColon) ? _throw_int_setindex_error() : nothing + setindex!(arr.data, val, idxs...) +end + +function Base.setindex!(arr::TimestepArray{VariableTimestep{TIMES}, T, N, ti}, val, idxs::AnyIndex...) where {TIMES, T, N, ti} + isa(idxs[ti], AnyIndex_NonColon) ? _throw_int_setindex_error() : nothing + setindex!(arr.data, val, idxs...) +end diff --git a/src/core/types/core.jl b/src/core/types/core.jl index 1e8921170..714f75229 100644 --- a/src/core/types/core.jl +++ b/src/core/types/core.jl @@ -1,6 +1,10 @@ using Classes using DataStructures +# +# General +# + """ @or(args...) @@ -23,6 +27,10 @@ abstract type MimiStruct end const AbstractMimiType = Union{MimiStruct, AbstractMimiClass} +# +# Components +# + # To identify components, @defcomp creates a variable with the name of # the component whose value is an instance of this type. struct ComponentId <: MimiStruct @@ -90,6 +98,7 @@ end # Unclear whether this is really any better than simply using # a dict for all cases. Might scrap this in the end. # + mutable struct RangeDimension{T <: DimensionRangeTypes} <: AbstractDimension range::T end diff --git a/src/core/types/defs.jl b/src/core/types/defs.jl index efb029e2b..d1eaedf61 100644 --- a/src/core/types/defs.jl +++ b/src/core/types/defs.jl @@ -14,9 +14,6 @@ Return the name of `def`. `NamedDef`s include `DatumDef`, `ComponentDef`, and ` """ Base.nameof(obj::AbstractNamedObj) = obj.name -# Deprecate old definition in favor of standard name -@deprecate name(obj::AbstractNamedObj) nameof(obj) - # Similar structure is used for variables and parameters (parameters merely adds `default`) @class mutable DatumDef <: NamedObj begin comp_path::Union{Nothing, ComponentPath} @@ -156,7 +153,7 @@ global const NamespaceElement = Union{LeafNamespaceElement, CompositeNa @class mutable CompositeComponentDef <: ComponentDef begin internal_param_conns::Vector{InternalParameterConnection} - # Names of external params that the ConnectorComps will use as their :input2 parameters. + # Names of model params that the ConnectorComps will use as their :input2 parameters. backups::Vector{Symbol} function CompositeComponentDef(comp_id::Union{Nothing, ComponentId}=nothing) @@ -206,7 +203,7 @@ ComponentPath(obj::AbstractCompositeComponentDef, names::Symbol...) = ComponentP @class mutable ModelDef <: CompositeComponentDef begin external_param_conns::Vector{ExternalParameterConnection} - external_params::Dict{Symbol, ModelParameter} + model_params::Dict{Symbol, ModelParameter} number_type::DataType dirty::Bool @@ -216,16 +213,16 @@ ComponentPath(obj::AbstractCompositeComponentDef, names::Symbol...) = ComponentP CompositeComponentDef(self) # call super's initializer ext_conns = Vector{ExternalParameterConnection}() - ext_params = Dict{Symbol, ModelParameter}() + model_params = Dict{Symbol, ModelParameter}() # N.B. @class-generated method - return ModelDef(self, ext_conns, ext_params, number_type, false) + return ModelDef(self, ext_conns, model_params, number_type, false) end end external_param_conns(md::ModelDef) = md.external_param_conns -external_params(md::ModelDef) = md.external_params +model_params(md::ModelDef) = md.model_params # # Reference types offer a more convenient syntax for interrogating Components. @@ -252,3 +249,16 @@ end end var_name(comp_ref::VariableReference) = getfield(comp_ref, :var_name) + +## +## DEPRECATIONS - Should move from warning --> error --> removal +## + +# -- throw errors -- + +# -- throw warnings -- + +@deprecate external_params(md::ModelDef) model_params(md) + +# Deprecate old definition in favor of standard name +@deprecate name(obj::AbstractNamedObj) nameof(obj) diff --git a/src/core/types/model.jl b/src/core/types/model.jl index 279d3a390..0d3bae38f 100644 --- a/src/core/types/model.jl +++ b/src/core/types/model.jl @@ -55,7 +55,14 @@ function Base.getindex(mm::MarginalModel, comp_path::ComponentPath, name::Symbol return (mm.modified.mi[comp_path, name] .- mm.base.mi[comp_path, name]) ./ mm.delta end -# DEPRECATION - EVENTUALLY REMOVE (and go back to default getproperty behavior) +## +## DEPRECATIONS - Should move from warning --> error --> removal +## + +# -- throw errors -- + +# -- throw warnings -- + function Base.getproperty(base::MarginalModel, s::Symbol) if (s == :marginal) error("Use of `MarginalModel.marginal` is deprecated in favor of `MarginalModel.modified`.") diff --git a/src/core/types/params.jl b/src/core/types/params.jl index 9769a8eb0..95ac13d0e 100644 --- a/src/core/types/params.jl +++ b/src/core/types/params.jl @@ -4,18 +4,21 @@ abstract type ModelParameter <: MimiStruct end -# TBD: rename as ScalarParameter, ArrayParameter, and AbstractParameter? - mutable struct ScalarModelParameter{T} <: ModelParameter value::T + is_shared::Bool + + function ScalarModelParameter{T}(value::T; is_shared::Bool = false) where T + new(value, is_shared) + end - function ScalarModelParameter{T}(value::T) where T - new(value) + function ScalarModelParameter{T}(value::T, is_shared::Bool) where T + new(value, is_shared) end - function ScalarModelParameter{T1}(value::T2) where {T1, T2} + function ScalarModelParameter{T1}(value::T2; is_shared::Bool = false) where {T1, T2} try - new(T1(value)) + new(T1(value), is_shared) catch err error("Failed to convert $value::$T2 to $T1") end @@ -25,30 +28,39 @@ end mutable struct ArrayModelParameter{T} <: ModelParameter values::T dim_names::Vector{Symbol} # if empty, we don't have the dimensions' name information + is_shared::Bool - function ArrayModelParameter{T}(values::T, dims::Vector{Symbol}) where T - new(values, dims) + function ArrayModelParameter{T}(values::T, dims::Vector{Symbol}; is_shared::Bool = false) where T + new(values, dims, is_shared) + end + + function ArrayModelParameter{T}(values::T, dims::Vector{Symbol}, is_shared::Bool) where T + new(values, dims, is_shared) end end ScalarModelParameter(value) = ScalarModelParameter{typeof(value)}(value) +ScalarModelParameter(value, is_shared) = ScalarModelParameter{typeof(value)}(value, is_shared) Base.convert(::Type{ScalarModelParameter{T}}, value::Number) where {T} = ScalarModelParameter{T}(T(value)) - Base.convert(::Type{T}, s::ScalarModelParameter{T}) where {T} = T(s.value) ArrayModelParameter(value, dims::Vector{Symbol}) = ArrayModelParameter{typeof(value)}(value, dims) +ArrayModelParameter(value, dims::Vector{Symbol}, is_shared::Bool) = ArrayModelParameter{typeof(value)}(value, dims, is_shared) # Allow values to be obtained from either parameter type using one method name. value(param::ArrayModelParameter) = param.values value(param::ScalarModelParameter) = param.value -Base.copy(obj::ScalarModelParameter{T}) where T = ScalarModelParameter(obj.value) -Base.copy(obj::ArrayModelParameter{T}) where T = ArrayModelParameter(obj.values, obj.dim_names) +Base.copy(obj::ScalarModelParameter{T}) where T = ScalarModelParameter(obj.value, obj.is_shared) +Base.copy(obj::ArrayModelParameter{T}) where T = ArrayModelParameter(obj.values, obj.dim_names, obj.is_shared) dim_names(obj::ArrayModelParameter) = obj.dim_names dim_names(obj::ScalarModelParameter) = [] +is_shared(obj::ArrayModelParameter) = obj.is_shared +is_shared(obj::ScalarModelParameter) = obj.is_shared + abstract type AbstractConnection <: MimiStruct end struct InternalParameterConnection <: AbstractConnection @@ -57,7 +69,7 @@ struct InternalParameterConnection <: AbstractConnection dst_comp_path::ComponentPath dst_par_name::Symbol ignoreunits::Bool - backup::Union{Symbol, Nothing} # a Symbol identifying the external param providing backup data, or nothing + backup::Union{Symbol, Nothing} # a Symbol identifying the model param providing backup data, or nothing backup_offset::Union{Int, Nothing} function InternalParameterConnection(src_path::ComponentPath, src_var::Symbol, @@ -72,13 +84,25 @@ end struct ExternalParameterConnection <: AbstractConnection comp_path::ComponentPath param_name::Symbol # name of the parameter in the component - external_param::Symbol # name of the parameter stored in external_params + model_param_name::Symbol # name of the parameter stored in model_params end # Converts symbol to component path -function ExternalParameterConnection(comp_name::Symbol, param_name::Symbol, external_param::Symbol) - return ExternalParameterConnection(ComponentPath(comp_name), param_name, external_param) +function ExternalParameterConnection(comp_name::Symbol, param_name::Symbol, model_param_name::Symbol) + return ExternalParameterConnection(ComponentPath(comp_name), param_name, model_param_name) end Base.pathof(obj::ExternalParameterConnection) = obj.comp_path Base.nameof(obj::ExternalParameterConnection) = obj.param_name + +## +## DEPRECATIONS - Should move from warning --> error --> removal +## + +function Base.getproperty(epc::ExternalParameterConnection, field::Symbol) + if field == :external_param + @warn "ExternalParameterConnection's `external_param` field is renamed to `model_param_name`, please change code accordingly." + field = :model_param_name + end + return getfield(epc, field) +end diff --git a/src/explorer/explore.jl b/src/explorer/explore.jl index 63a7daa0a..304b46d12 100644 --- a/src/explorer/explore.jl +++ b/src/explorer/explore.jl @@ -67,6 +67,11 @@ function explore(m::Model) end +""" + explore(mi::ModelInstance) + +Produce a UI to explore the parameters and variables of `ModelInstance` `mi` in an independent window. +""" function explore(mi::ModelInstance) m = Model(mi) m.md.dirty = false # we need this to get explorer working, but it's a hack and should be temporary! diff --git a/src/explorer/results.jl b/src/explorer/results.jl index 59aac70c4..52ab65da0 100644 --- a/src/explorer/results.jl +++ b/src/explorer/results.jl @@ -4,8 +4,6 @@ using DataFrames function get_sim_results(sim_inst::SimulationInstance, comp_name::Symbol, datum_name::Symbol; model_index::Int = 1, scen_name::Union{Nothing, String} = nothing) - multiple_results = (length(sim_inst.results) > 1) - key = (comp_name, datum_name) df = (sim_inst.results[model_index])[key] if scen_name !== nothing diff --git a/src/mcs/defmcs.jl b/src/mcs/defmcs.jl index b034aeb9a..4c6622b41 100644 --- a/src/mcs/defmcs.jl +++ b/src/mcs/defmcs.jl @@ -2,11 +2,21 @@ # generated by gensym() don't work for this. global _rvnum = 0 +""" + _make_rvname(name) + +Return a unique name for random variable `rv`. +""" function _make_rvname(name) global _rvnum += 1 return Symbol("$(name)!$(_rvnum)") end +""" + _make_dims(args) + +Return a vector of dimensions from `args`. +""" function _make_dims(args) dims = Vector{Any}() for arg in args @@ -54,7 +64,12 @@ function _make_dims(args) return dims end -macro defsim(expr) +""" + defsim(expr::Expr) + +Define a Mimi `SimulationDef` with the expressions in `expr`. +""" +macro defsim(expr::Expr) let # to make vars local to each macro invocation local _rvs = [] local _transforms = [] @@ -99,7 +114,11 @@ macro defsim(expr) rvname = _make_rvname(extvar) saverv(rvname, distname, distargs) - expr = :(TransformSpec($(QuoteNode(extvar)), :(=), $(QuoteNode(rvname)), [$(dims...)])) + if @capture(extvar, compname_.name_) + expr = :(TransformSpec($(QuoteNode(compname)), $(QuoteNode(name)), :(=), $(QuoteNode(rvname)), [$(dims...)])) + else + expr = :(TransformSpec($(QuoteNode(extvar)), :(=), $(QuoteNode(rvname)), [$(dims...)])) + end push!(_transforms, esc(expr)) end end @@ -113,7 +132,7 @@ macro defsim(expr) @capture(elt, extvar_ *= distname_(distargs__))) # For "anonymous" RVs, e.g., ext_var2[2010:2100, :] *= Uniform(0.8, 1.2), we - # gensym a name based on the external var name and process it as a named RV. + # gensym a name based on the model parameter name and process it as a named RV. if rvname === nothing param_name = @capture(extvar, name_[args__]) ? name : extvar rvname = _make_rvname(param_name) @@ -121,12 +140,11 @@ macro defsim(expr) end op = elt.head - if @capture(extvar, name_[args__]) - # println("Ref: $name, $args") - # Meta.show_sexpr(extvar) - # println("") - - # if extvar.head == :ref, extvar.args must be one of: + # println("Ref: $name, $args") + # Meta.show_sexpr(extvar) + # println("") + # if extvar.head == :ref, extvar.args must be one of the following, + # where the extvar could be paramname or compname.paramname: # - a scalar value, e.g., name[2050] => (:ref, :name, 2050) # convert to tuple of dimension specifiers (:name, 2050) # - a slice expression, e.g., name[2010:2050] => (:ref, :name, (:(:), 2010, 2050)) @@ -135,7 +153,14 @@ macro defsim(expr) # convert to (:name, (:US, :CHI)) # - combinations of these, e.g., name[2010:2050, (US, CHI)] => (:ref, :name, (:(:), 2010, 2050), (:tuple, :US, :CHI)) # convert to (:name, 2010:2050, (:US, :CHI)) - + + if @capture(extvar, compname_.name_[args__]) + dims = _make_dims(args) + expr = :(TransformSpec($(QuoteNode(compname)), $(QuoteNode(name)), $(QuoteNode(op)), $(QuoteNode(rvname)), [$(dims...)])) + elseif @capture(extvar, compname_.name_) + expr = :(TransformSpec($(QuoteNode(compname)), $(QuoteNode(name)), $(QuoteNode(op)), $(QuoteNode(rvname)))) + + elseif @capture(extvar, name_[args__]) dims = _make_dims(args) expr = :(TransformSpec($(QuoteNode(name)), $(QuoteNode(op)), $(QuoteNode(rvname)), [$(dims...)])) else @@ -173,6 +198,7 @@ end # # Simulation Definition update methods # + function _update_nt_type!(sim_def::SimulationDef{T}) where T <: AbstractSimulationData names = (keys(sim_def.rvdict)...,) types = [eltype(fld) for fld in values(sim_def.rvdict)] @@ -273,12 +299,29 @@ end add_transform!(sim_def::SimulationDef, paramname::Symbol, op::Symbol, rvname::Symbol, dims::Vector{T}=[]) where T Create a new TransformSpec based on `paramname`, `op`, `rvname` and `dims` to the -Simulation definition `sim_def`, and update the Simulation's NamedTuple type. The symbol `rvname` must -refer to an existing RV, and `paramname` must refer to an existing parameter. If `dims` are -provided, these must be legal subscripts of `paramname`. Op must be one of :+=, :*=, or :(=). +Simulation definition `sim_def`, and update the Simulation's NamedTuple type. +The symbol `rvname` must refer to an existing random variable, and `paramname` +must refer to an existing shared model parameter that can be accessed by that +name. Use the signature that includes `compname` if your `paramname` +is an unshared model parameter specific to a component. If `dims` are +provided, these must be legal subscripts of `paramname`. Op must be one of :+=, :*=, +or :(=). """ function add_transform!(sim_def::SimulationDef, paramname::Symbol, op::Symbol, rvname::Symbol, dims::Vector{T}=[]) where T - add_transform!(sim_def, TransformSpec(paramname, op, rvname, dims)) + add_transform!(sim_def, TransformSpec(nothing, paramname, op, rvname, dims)) +end + +""" + add_transform!(sim_def::SimulationDef, compname_paramname::Tuple{Symbol, Symbol}, op::Symbol, rvname::Symbol, dims::Vector{T}=[]) where T + +Create a new TransformSpec based on `compname`, `paramname`, `op`, `rvname` and `dims` to the +Simulation definition `sim_def`, and update the Simulation's NamedTuple type. The symbol +`rvname` must refer to an existing RV, and `compname` and `paramname` must refer to +an existing parameter for a given component, parameter pair. If `dims` are provided, +these must be legal subscripts of `paramname`. Op must be one of :+=, :*=, or :(=). +""" +function add_transform!(sim_def::SimulationDef, compname::Symbol, paramname::Symbol, op::Symbol, rvname::Symbol, dims::Vector{T}=[]) where T + add_transform!(sim_def, TransformSpec(compname, paramname, op, rvname, dims)) end """ @@ -291,6 +334,7 @@ longer be saved to a CSV file at the end of the simulation. function delete_save!(sim_def::SimulationDef, key::Tuple{Symbol, Symbol}) pos = findall(isequal(key), sim_def.savelist) isempty(pos) ? @warn("Simulation def doesn't have $key in its save list. Nothing being deleted.") : deleteat!(sim_def.savelist, pos) + _update_nt_type!(sim_def) end """ diff --git a/src/mcs/delta.jl b/src/mcs/delta.jl index 2921e1c65..fc0743d4f 100644 --- a/src/mcs/delta.jl +++ b/src/mcs/delta.jl @@ -35,8 +35,20 @@ function sample!(sim_inst::DeltaSimulationInstance, samplesize::Int) rvdict[name] = RandomVariable(name, SampleStore(values, orig_dist)) end end - -function analyze(sim_inst::DeltaSimulationInstance, model_input::AbstractArray{<:Number, N1}, model_output::AbstractArray{<:Number, N2}; num_resamples::Int = 1_000, conf_level::Number = 0.95, N_override::Union{Nothing, Int}=nothing, progress_meter::Bool = true) where {N1, N2} +""" + analyze(sim_inst::DeltaSimulationInstance, model_input::AbstractArray{<:Number, N1}, + model_output::AbstractArray{<:Number, N2}; num_resamples::Int = 1_000, + conf_level::Number = 0.95, N_override::Union{Nothing, Int}=nothing, + progress_meter::Bool = true) where {N1, N2} + +Analyze the results for `sim_inst` with intput `model_input` and output `model_output` +and return sensitivity analysis metrics as defined by GlobalSensitivityAnalysis package and +type parameterization of the `sim_inst` ie. Delta Method. +""" +function analyze(sim_inst::DeltaSimulationInstance, model_input::AbstractArray{<:Number, N1}, + model_output::AbstractArray{<:Number, N2}; num_resamples::Int = 1_000, + conf_level::Number = 0.95, N_override::Union{Nothing, Int}=nothing, + progress_meter::Bool = true) where {N1, N2} if sim_inst.trials == 0 error("Cannot analyze simulation with 0 trials.") diff --git a/src/mcs/mcs_types.jl b/src/mcs/mcs_types.jl index 5fddbdb4b..e0963a197 100644 --- a/src/mcs/mcs_types.jl +++ b/src/mcs/mcs_types.jl @@ -82,6 +82,7 @@ Base.iterate(ss::SampleStore{T}) where T = iterate(ss.values) Base.iterate(ss::SampleStore{T}, idx) where T = iterate(ss.values, idx) struct TransformSpec + compname::Union{Nothing, Symbol} # if this is not nothing we assume the paramname is a shared model parameter paramname::Symbol op::Symbol rvname::Symbol @@ -91,8 +92,28 @@ struct TransformSpec if ! (op in (:(=), :(+=), :(*=))) error("Valid operators are =, +=, and *= (got $op)") end - - return new(paramname, op, rvname, dims) + return new(nothing, paramname, op, rvname, dims) + end + + function TransformSpec(compname::Union{Nothing, Symbol}, paramname::Symbol, op::Symbol, rvname::Symbol, dims::Vector{T}=[]) where T + if ! (op in (:(=), :(+=), :(*=))) + error("Valid operators are =, +=, and *= (got $op)") + end + return new(compname, paramname, op, rvname, dims) + end +end + +struct TransformSpec_ModelParams + paramnames::Vector{Symbol} + op::Symbol + rvname::Symbol + dims::Vector{Any} + + function TransformSpec_ModelParams(paramnames::Vector{Symbol}, op::Symbol, rvname::Symbol, dims::Vector{T}=[]) where T + if ! (op in (:(=), :(+=), :(*=))) + error("Valid operators are =, +=, and *= (got $op)") + end + return new(paramnames, op, rvname, dims) end end @@ -162,6 +183,7 @@ mutable struct SimulationInstance{T} models::Vector{M} where M <: AbstractModel results::Vector{Dict{Tuple, DataFrame}} payload::Any + translist_modelparams::Vector{TransformSpec_ModelParams} function SimulationInstance{T}(sim_def::SimulationDef{T}) where T <: AbstractSimulationData self = new() @@ -171,6 +193,13 @@ mutable struct SimulationInstance{T} self.sim_def = deepcopy(sim_def) self.payload = deepcopy(self.sim_def.payload) + # This will mirror self.sim_def.translist, but can only be created after + # models are added because it looks for the actual model parameter + # names for unshared parameters used in the statements, and tries to resolve + # ones written as shared parameters but which may in actuality be unshared + # ie. defaults + self.translist_modelparams = Vector{TransformSpec_ModelParams}(undef, 0) + # These are parallel arrays; each model has a corresponding results dict self.models = Vector{AbstractModel}(undef, 0) self.results = [Dict{Tuple, DataFrame}()] @@ -189,7 +218,7 @@ function SimulationDef{T}() where T <: AbstractSimulationData end """ - set_payload!(sim_def::SimulationDef, payload) + set_payload!(sim_def::SimulationDef, payload) Attach a user's `payload` to the `SimulationDef`. A copy of the payload object will be stored in the `SimulationInstance` at run time so it can be diff --git a/src/mcs/montecarlo.jl b/src/mcs/montecarlo.jl index df9d6babb..857aa026e 100644 --- a/src/mcs/montecarlo.jl +++ b/src/mcs/montecarlo.jl @@ -33,6 +33,7 @@ end function Base.show(io::IO, sim_inst::SimulationInstance{T}) where T <: AbstractSimulationData println("SimulationInstance{$T}") + print_nonempty("translist for model params", sim_inst.translist_modelparams) Base.show(io, sim_inst.sim_def) @@ -48,9 +49,17 @@ function Base.show(obj::T) where T <: AbstractSimulationData nothing end -# Store results for a single parameter and return the dataframe for this particular -# trial/scenario -function _store_param_results(m::AbstractModel, datum_key::Tuple{Symbol, Symbol}, trialnum::Int, scen_name::Union{Nothing, String}, results::Dict{Tuple, DataFrame}) +""" + _store_param_results(m::AbstractModel, datum_key::Tuple{Symbol, Symbol}, + trialnum::Int, scen_name::Union{Nothing, String}, + results::Dict{Tuple, DataFrame}) + +Store `results` for a single parameter `datum_key` in model `m` and return the +dataframe for this particular `trial_num`/`scen_name` combination. +""" +function _store_param_results(m::AbstractModel, datum_key::Tuple{Symbol, Symbol}, + trialnum::Int, scen_name::Union{Nothing, String}, + results::Dict{Tuple, DataFrame}) @debug "\nStoring trial results for $datum_key" (comp_name, datum_name) = datum_key @@ -98,7 +107,17 @@ function _store_param_results(m::AbstractModel, datum_key::Tuple{Symbol, Symbol} return trial_df end -function _store_trial_results(sim_inst::SimulationInstance{T}, trialnum::Int, scen_name::Union{Nothing, String}, output_dir::Union{Nothing, String}, streams::Dict{String, CSVFiles.CSVFileSaveStream{IOStream}}) where T <: AbstractSimulationData +""" + _store_trial_results(sim_inst::SimulationInstance{T}, trialnum::Int, + scen_name::Union{Nothing, String}, output_dir::Union{Nothing, String}, + streams::Dict{String, CSVFiles.CSVFileSaveStream{IOStream}}) where T <: AbstractSimulationData + +Save the stored simulation results ` from trial `trialnum` and scenario `scen_name` +to files in the directory `output_dir` +""" +function _store_trial_results(sim_inst::SimulationInstance{T}, trialnum::Int, + scen_name::Union{Nothing, String}, output_dir::Union{Nothing, String}, + streams::Dict{String, CSVFiles.CSVFileSaveStream{IOStream}}) where T <: AbstractSimulationData savelist = sim_inst.sim_def.savelist model_index = 1 @@ -131,9 +150,11 @@ function _store_trial_results(sim_inst::SimulationInstance{T}, trialnum::Int, sc end """ - _save_trial_results(trial_df::DataFrame, datum_name::String, output_dir::String, streams::Dict{String, CSVFiles.CSVFileSaveStream{IOStream}}) + _save_trial_results(trial_df::DataFrame, datum_name::String, output_dir::String, + streams::Dict{String, CSVFiles.CSVFileSaveStream{IOStream}}) -Save the stored simulation results in `trial_df` from trial `trialnum` to files in the directory `output_dir` +Save the stored simulation results in `trial_df` from trial `trialnum` to files +in the directory `output_dir` """ function _save_trial_results(trial_df::DataFrame, datum_name::String, output_dir::AbstractString, streams::Dict{String, CSVFiles.CSVFileSaveStream{IOStream}}) where T <: AbstractSimulationData filename = joinpath(output_dir, "$datum_name.csv") @@ -144,6 +165,11 @@ function _save_trial_results(trial_df::DataFrame, datum_name::String, output_dir end end +""" + save_trial_inputs(sim_inst::SimulationInstance, filename::String) + +Save the trial inputs for `sim_inst` to `filename`. +""" function save_trial_inputs(sim_inst::SimulationInstance, filename::String) mkpath(dirname(filename), mode=0o750) # ensure that the specified path exists save(filename, sim_inst) @@ -227,21 +253,12 @@ is necessary when we are applying distributions by adding or multiplying origina function _copy_sim_params(sim_inst::SimulationInstance{T}) where T <: AbstractSimulationData # If there is a MarginalModel, need to copy the params for both the base and marginal modeldefs separately - flat_model_list = [] - for m in sim_inst.models - if m isa MarginalModel - push!(flat_model_list, m.base) - push!(flat_model_list, m.modified) - else - push!(flat_model_list, m) - end - end - + flat_model_list = _get_flat_model_list(sim_inst) param_vec = Vector{Dict{Symbol, ModelParameter}}(undef, length(flat_model_list)) for (i, m) in enumerate(flat_model_list) md = modelinstance_def(m) - param_vec[i] = Dict{Symbol, ModelParameter}(trans.paramname => copy(external_param(md, trans.paramname)) for trans in sim_inst.sim_def.translist) + param_vec[i] = Dict{Symbol, ModelParameter}(trans.paramnames[i] => copy(model_param(md, trans.paramnames[i])) for trans in sim_inst.translist_modelparams) end return param_vec @@ -249,41 +266,36 @@ end function _restore_sim_params!(sim_inst::SimulationInstance{T}, param_vec::Vector{Dict{Symbol, ModelParameter}}) where T <: AbstractSimulationData + # Need to flatten the list of models so that if there is a MarginalModel, - # both its base and marginal models will have their separate params restored - flat_model_list = [] - for m in sim_inst.models - if m isa MarginalModel - push!(flat_model_list, m.base) - push!(flat_model_list, m.modified) - else - push!(flat_model_list, m) - end - end - for (m, params) in zip(flat_model_list, param_vec) + # both its base and marginal models will have their separate params restored + flat_model_list = _get_flat_model_list(sim_inst) + + for (i, m) in enumerate(flat_model_list) + params = param_vec[i] md = m.mi.md - for trans in sim_inst.sim_def.translist - name = trans.paramname + for trans in sim_inst.translist_modelparams + name = trans.paramnames[i] param = params[name] - _restore_param!(param, name, md, trans) + _restore_param!(param, name, md, i, trans) end end return nothing end -function _restore_param!(param::ScalarModelParameter{T}, name::Symbol, md::ModelDef, trans::TransformSpec) where T - md_param = external_param(md, name) +function _restore_param!(param::ScalarModelParameter{T}, name::Symbol, md::ModelDef, i::Int, trans::TransformSpec_ModelParams) where T + md_param = model_param(md, name) md_param.value = param.value end -function _restore_param!(param::ArrayModelParameter{T}, name::Symbol, md::ModelDef, trans::TransformSpec) where T - md_param = external_param(md, name) - indices = _param_indices(param, md, trans) +function _restore_param!(param::ArrayModelParameter{T}, name::Symbol, md::ModelDef, i::Int, trans::TransformSpec_ModelParams) where T + md_param = model_param(md, name) + indices = _param_indices(param, md, i, trans) md_param.values[indices...] = param.values[indices...] end -function _param_indices(param::ArrayModelParameter{T}, md::ModelDef, trans::TransformSpec) where T +function _param_indices(param::ArrayModelParameter{T}, md::ModelDef, i::Int, trans::TransformSpec_ModelParams) where T pdims = dim_names(param) # returns [] for scalar parameters num_pdims = length(pdims) @@ -297,8 +309,8 @@ function _param_indices(param::ArrayModelParameter{T}, md::ModelDef, trans::Tran end if num_pdims != num_dims - pname = trans.paramname - error("Dimension mismatch: external parameter :$pname has $num_pdims dimensions ($pdims); Sim has $num_dims") + pname = trans.paramnames[i] + error("Dimension mismatch: model parameter :$pname has $num_pdims dimensions ($pdims); Sim has $num_dims") end indices = Vector() @@ -312,7 +324,7 @@ function _param_indices(param::ArrayModelParameter{T}, md::ModelDef, trans::Tran return indices end -function _perturb_param!(param::ScalarModelParameter{T}, md::ModelDef, trans::TransformSpec, rvalue::Number) where T +function _perturb_param!(param::ScalarModelParameter{T}, md::ModelDef, i::Int, trans::TransformSpec_ModelParams, rvalue::Number) where T op = trans.op if op == :(=) @@ -328,12 +340,12 @@ end # rvalue is an Array so we expect the dims to match and don't need to worry about # broadcasting -function _perturb_param!(param::ArrayModelParameter{T}, md::ModelDef, - trans::TransformSpec, rvalue::Array{<: Number, N}) where {T, N} +function _perturb_param!(param::ArrayModelParameter{T}, md::ModelDef, i::Int, + trans::TransformSpec_ModelParams, rvalue::Array{<: Number, N}) where {T, N} op = trans.op pvalue = value(param) - indices = _param_indices(param, md, trans) + indices = _param_indices(param, md, i, trans) if op == :(=) pvalue[indices...] = rvalue @@ -348,11 +360,11 @@ function _perturb_param!(param::ArrayModelParameter{T}, md::ModelDef, end # rvalue is a Number so we might need to deal with broadcasting -function _perturb_param!(param::ArrayModelParameter{T}, md::ModelDef, - trans::TransformSpec, rvalue::Number) where {T, N} +function _perturb_param!(param::ArrayModelParameter{T}, md::ModelDef, i::Int, + trans::TransformSpec_ModelParams, rvalue::Number) where {T, N} op = trans.op pvalue = value(param) - indices = _param_indices(param, md, trans) + indices = _param_indices(param, md, i, trans) if op == :(=) @@ -402,15 +414,13 @@ function _perturb_params!(sim_inst::SimulationInstance{T}, trialnum::Int) where trialdata = get_trial(sim_inst, trialnum) - for m in sim_inst.models - # If it's a MarginalModel, need to perturb the params in both the base and marginal modeldefs - mds = m isa MarginalModel ? [m.base.mi.md, m.modified.mi.md] : [m.mi.md] - for md in mds - for trans in sim_inst.sim_def.translist - param = external_param(md, trans.paramname) - rvalue = getfield(trialdata, trans.rvname) - _perturb_param!(param, md, trans, rvalue) - end + # If it's a MarginalModel, need to perturb the params in both the base and marginal modeldefs + flat_model_list = _get_flat_model_list(sim_inst) + for (i, m) in enumerate(flat_model_list) + for trans in sim_inst.translist_modelparams + param = model_param(m.mi.md, trans.paramnames[i]) + rvalue = getfield(trialdata, trans.rvname) + _perturb_param!(param, m.mi.md, i, trans, rvalue) end end return nothing @@ -445,9 +455,9 @@ function _compute_output_dir(orig_output_dir, tup) end """ - run(sim_def::SimulationDef{T}, - models::Union{Vector{M <: AbstractModel}, AbstractModel}, - samplesize::Int; + Base.run(sim_def::SimulationDef{T}, + models::Union{Vector{M}, AbstractModel}, + samplesize::Int; ntimesteps::Int=typemax(Int), trials_output_filename::Union{Nothing, AbstractString}=nothing, results_output_dir::Union{Nothing, AbstractString}=nothing, @@ -456,7 +466,7 @@ end scenario_func::Union{Nothing, Function}=nothing, scenario_placement::ScenarioLoopPlacement=OUTER, scenario_args=nothing, - results_in_memory::Bool=true) + results_in_memory::Bool=true) where {T <: AbstractSimulationData, M <: AbstractModel} Run the simulation definition `sim_def` for the `models` using `samplesize` samples. @@ -524,6 +534,7 @@ function Base.run(sim_def::SimulationDef{T}, sim_inst = SimulationInstance{typeof(sim_def.data)}(sim_def) set_models!(sim_inst, models) generate_trials!(sim_inst, samplesize; filename=trials_output_filename) + set_translist_modelparams!(sim_inst) # should this use m.md or m.mi.md (after building below)? if (scenario_func === nothing) != (scenario_args === nothing) error("run: scenario_func and scenario_arg must both be nothing or both set to non-nothing values") @@ -532,14 +543,14 @@ function Base.run(sim_def::SimulationDef{T}, for m in sim_inst.models is_built(m) || build!(m) end - + trials = 1:sim_inst.trials # Save the original dir since we modify the output_dir to store scenario results orig_results_output_dir = results_output_dir # booleans vars to simplify the repeated tests in the loop below - has_results_output_dir = (orig_results_output_dir !== nothing) + has_results_output_dir = (orig_results_output_dir !== nothing) has_scenario_func = (scenario_func !== nothing) has_outer_scenario = (has_scenario_func && scenario_placement == OUTER) has_inner_scenario = (has_scenario_func && scenario_placement == INNER) @@ -645,26 +656,139 @@ function Base.run(sim_def::SimulationDef{T}, return sim_inst end +""" + _get_flat_model_list(sim_inst::SimulationInstance{T}) where T <: AbstractSimulationData + +Return a flattened vector of models, splatting out the base and modified models of +a MarginalModel. +""" +function _get_flat_model_list(sim_inst::SimulationInstance{T}) where T <: AbstractSimulationData + + flat_model_list = [] + for m in sim_inst.models + if m isa MarginalModel + push!(flat_model_list, m.base) + push!(flat_model_list, m.modified) + else + push!(flat_model_list, m) + end + end + return flat_model_list +end + +""" + _get_flat_model_list_names(sim_inst::SimulationInstance{T}) where T <: AbstractSimulationData + +Return a vector of names referring to a flattened vector of models, splatting out +the base and modified models of a MarginalModel. +""" +function _get_flat_model_list_names(sim_inst::SimulationInstance{T}) where T <: AbstractSimulationData + + flat_model_list_names = [] # use for errors + for (i, m) in enumerate(sim_inst.models) + if m isa MarginalModel + push!(flat_model_list_names, Symbol("Model$(i)_Base")) + push!(flat_model_list_names, Symbol("Model$(i)_Modified")) + else + push!(flat_model_list_names, Symbol("Model$(i)")) + end + end + return flat_model_list_names + +end + # Set models -""" - set_models!(sim_inst::SimulationInstance{T}, models::Union{Vector{M <: AbstractModel}}) +""" + set_models!(sim_inst::SimulationInstance{T}, models::Vector{M}) where {T <: AbstractSimulationData, M <: AbstractModel} - Set the `models` to be used by the SimulationDef held by `sim_inst`. +Set the `models` to be used by the SimulationDef held by `sim_inst`. """ function set_models!(sim_inst::SimulationInstance{T}, models::Vector{M}) where {T <: AbstractSimulationData, M <: AbstractModel} sim_inst.models = models _reset_results!(sim_inst) # sets results vector to same length end -# Convenience methods for single model and MarginalModel -""" -set_models!(sim_inst::SimulationInstance{T}, m::AbstractModel) +""" + set_models!(sim_inst::SimulationInstance{T}, m::AbstractModel) where T <: AbstractSimulationData - Set the model `m` to be used by the Simulatoin held by `sim_inst`. +Set the model `m` to be used by the Simulation held by `sim_inst`. """ set_models!(sim_inst::SimulationInstance{T}, m::AbstractModel) where T <: AbstractSimulationData = set_models!(sim_inst, [m]) +""" + set_translist_modelparams!(sim_inst::SimulationInstance{T}) + +Create the transform spec list for the simulation instance, finding the matching +model parameter names for each transform spec parameter for each model. +""" +function set_translist_modelparams!(sim_inst::SimulationInstance{T}) where T <: AbstractSimulationData + + # build flat model list that splats out the base and modified models of MarginalModel + flat_model_list = _get_flat_model_list(sim_inst) + flat_model_list_names = _get_flat_model_list_names(sim_inst) + + # allocate simulation instance translist + sim_inst.translist_modelparams = Vector{TransformSpec_ModelParams}(undef, length(sim_inst.sim_def.translist)) + + for (trans_idx, trans) in enumerate(sim_inst.sim_def.translist) + + # initialize the vector of model parameters + model_parameters_vec = Vector{Symbol}(undef, length(flat_model_list)) + + # handling an unshared parameter specific to a component/parameter pair + compname = trans.compname + if !isnothing(compname) + for (model_idx, m) in enumerate(flat_model_list) + + # check for component in the model + compname in keys(components(m.md)) || error("Component $compname does not exist in $(flat_model_list_names[model_idx]).") + + model_parameters_vec[model_idx] = get_model_param_name(m.md, compname, trans.paramname) + end + + # no component, so this should be referring to a shared parameter ... but + # historically might not have done so and been using one set by default etc. + else + paramname = trans.paramname + suggestion_string = "use the `ComponentName.ParameterName` syntax in your SimulationDefinition to explicitly define this transform ie. `ComponentName.$paramname = RandomVariable`" + + for (model_idx, m) in enumerate(flat_model_list) + model_name = flat_model_list_names[model_idx] + # found the shared parameter + if has_parameter(m.md, paramname) + model_parameters_vec[model_idx] = paramname + + # didn't find the shared parameter, will try to resolve + else + @warn "Parameter name $paramname not found in $model_name's shared parameter list, will attempt to resolve." + unshared_paramname = nothing + unshared_compname = nothing + + for (compname, compdef) in components(m.md) + if has_parameter(compdef, paramname) + if isnothing(unshared_paramname) # first time the parameter was found in a component + unshared_paramname = get_model_param_name(m.md, compname, paramname) # NB might not need to use m.mi.md here could be m.md + unshared_compname = compname + else # already found in a previous component + error("Cannot resolve because parameter name $paramname found in more than one component of $model_name, including $unshared_compname and $compname. Please $suggestion_string.") + end + end + end + if isnothing(unshared_paramname) + error("Cannot resolve because $paramname not found in any of the components of $model_name. Please $suggestion_string.") + else + @warn("Found $paramname in $unshared_compname with model parameter name $unshared_paramname. Will use this model parameter, but in the future we suggest you $suggestion_string") + model_parameters_vec[model_idx] = unshared_paramname + end + end + end + end + new_trans = TransformSpec_ModelParams(model_parameters_vec, trans.op, trans.rvname, trans.dims) + sim_inst.translist_modelparams[trans_idx] = new_trans + end +end + # # Iterator functions for Simulation instance directly, and for use as an IterableTable. # diff --git a/src/mcs/sobol.jl b/src/mcs/sobol.jl index abb5f15a4..efd35c0fe 100644 --- a/src/mcs/sobol.jl +++ b/src/mcs/sobol.jl @@ -57,7 +57,18 @@ function sample!(sim_inst::SobolSimulationInstance, samplesize::Int) end end -function analyze(sim_inst::SobolSimulationInstance, model_output::AbstractArray{<:Number, N}; num_resamples::Union{Nothing, Int} = 1_000, conf_level::Union{Nothing, Number} = 0.95, N_override::Union{Nothing, Int}=nothing, progress_meter::Bool = true) where N +""" + analyze(sim_inst::SobolSimulationInstance, model_output::AbstractArray{<:Number, N}; + num_resamples::Union{Nothing, Int} = 1_000, conf_level::Union{Nothing, Number} = 0.95, + N_override::Union{Nothing, Int}=nothing, progress_meter::Bool = true) where N + +Analyze the results for `sim_inst` with intput `model_input` and output `model_output` +and return sensitivity analysis metrics as defined by GlobalSensitivityAnalysis package and +type parameterization of the `sim_inst` ie. Sobol Method. +""" +function analyze(sim_inst::SobolSimulationInstance, model_output::AbstractArray{<:Number, N}; + num_resamples::Union{Nothing, Int} = 1_000, conf_level::Union{Nothing, Number} = 0.95, + N_override::Union{Nothing, Int}=nothing, progress_meter::Bool = true) where N if sim_inst.trials == 0 error("Cannot analyze simulation with 0 trials.") diff --git a/test/mcs/runtests.jl b/test/mcs/runtests.jl index 4e06acf46..bd2b66139 100644 --- a/test/mcs/runtests.jl +++ b/test/mcs/runtests.jl @@ -1,7 +1,7 @@ using Mimi using Test -@testset "Mimi-SA" begin +@testset "Mimi-MCS" begin @info("test_empirical.jl") include("test_empirical.jl") @@ -19,11 +19,14 @@ using Test include("test_defmcs_delta.jl") @info("test_reshaping.jl") - include("test_reshaping.jl") + include("test_reshaping.jl") @info("test_payload.jl") include("test_payload.jl") @info("test_marginalmodel.jl") include("test_marginalmodel.jl") + + @info("test_translist.jl") + include("test_translist.jl") end diff --git a/test/mcs/test-model-2/main.jl b/test/mcs/test-model-2/main.jl deleted file mode 100644 index ff7de8f0b..000000000 --- a/test/mcs/test-model-2/main.jl +++ /dev/null @@ -1,10 +0,0 @@ -using Mimi - -include("multi-region-model.jl") -using .MyModel -model = construct_MyModel() - -run(model) - -# show results -getdataframe(model, :emissions, :E_Global) diff --git a/test/mcs/test-model-2/multi-region-model.jl b/test/mcs/test-model-2/multi-region-model.jl index 2864678c4..a71f94ae3 100644 --- a/test/mcs/test-model-2/multi-region-model.jl +++ b/test/mcs/test-model-2/multi-region-model.jl @@ -18,15 +18,16 @@ function construct_MyModel() add_comp!(m, grosseconomy) add_comp!(m, emissions) - set_param!(m, :grosseconomy, :l, l) - set_param!(m, :grosseconomy, :tfp, tfp) - set_param!(m, :grosseconomy, :s, s) - set_param!(m, :grosseconomy, :depk,depk) - set_param!(m, :grosseconomy, :k0, k0) - set_param!(m, :grosseconomy, :share, 0.3) - - # set parameters for emissions component - set_param!(m, :emissions, :sigma, sigma) + # set parameters for grosseconomy component + update_param!(m, :grosseconomy, :l, l) + update_param!(m, :grosseconomy, :tfp, tfp) + update_param!(m, :grosseconomy, :s, s) + update_param!(m, :grosseconomy, :depk,depk) + update_param!(m, :grosseconomy, :k0, k0) + update_param!(m, :grosseconomy, :share, 0.3) + + # set and connect parameters for emissions component + update_param!(m, :emissions, :sigma, sigma) connect_param!(m, :emissions, :YGROSS, :grosseconomy, :YGROSS) return m diff --git a/test/mcs/test-model/main.jl b/test/mcs/test-model/main.jl deleted file mode 100644 index ff7de8f0b..000000000 --- a/test/mcs/test-model/main.jl +++ /dev/null @@ -1,10 +0,0 @@ -using Mimi - -include("multi-region-model.jl") -using .MyModel -model = construct_MyModel() - -run(model) - -# show results -getdataframe(model, :emissions, :E_Global) diff --git a/test/mcs/test-model/test-model.jl b/test/mcs/test-model/test-model.jl index 32d0fd6dd..4ea45099b 100644 --- a/test/mcs/test-model/test-model.jl +++ b/test/mcs/test-model/test-model.jl @@ -18,18 +18,18 @@ function create_model() add_comp!(m, grosseconomy) add_comp!(m, emissions) - set_param!(m, :grosseconomy, :l, l) - set_param!(m, :grosseconomy, :tfp, tfp) - set_param!(m, :grosseconomy, :s, s) - set_param!(m, :grosseconomy, :depk,depk) - set_param!(m, :grosseconomy, :k0, k0) - set_param!(m, :grosseconomy, :share, 0.3) - - set_param!(m, :grosseconomy, :tester, zeros(Mimi.dim_count(m.md, :time), + # set parameters for grosseconomy component + update_param!(m, :grosseconomy, :l, l) + update_param!(m, :grosseconomy, :tfp, tfp) + update_param!(m, :grosseconomy, :s, s) + update_param!(m, :grosseconomy, :depk,depk) + update_param!(m, :grosseconomy, :k0, k0) + update_param!(m, :grosseconomy, :share, 0.3) + update_param!(m, :grosseconomy, :tester, zeros(Mimi.dim_count(m.md, :time), Mimi.dim_count(m.md, :regions))) - # set parameters for emissions component - set_param!(m, :emissions, :sigma, sigma) + # set and connect parameters for emissions component + update_param!(m, :emissions, :sigma, sigma) connect_param!(m, :emissions, :YGROSS, :grosseconomy, :YGROSS) return m diff --git a/test/mcs/test_defmcs.jl b/test/mcs/test_defmcs.jl index 0c908c395..0eadea5a7 100644 --- a/test/mcs/test_defmcs.jl +++ b/test/mcs/test_defmcs.jl @@ -12,16 +12,81 @@ using Mimi: modelinstance, compinstance, get_var_value, OUTER, INNER, ReshapedDi using CSVFiles: load +# Toy @defsim + +@defcomp test begin + regions = Index() + + p_shared1 = Parameter() + p_shared2 = Parameter(index = [time]) + p_shared3 = Parameter(index=[time, regions]) + p_shared4 = Parameter(index = [regions]) + + p_unshared1 = Parameter(default = 5.0) + p_unshared2 = Parameter(index = [time], default = collect(1:20)) + p_unshared3 = Parameter(index=[time, regions], default = fill(10,20,3)) + p_unshared4 = Parameter(index = [regions], default = collect(1:3)) + + function run_timestep(p, v, d, t) + end +end + +sd_toy = @defsim begin + + rv(name1) = Normal(1, 0.2) + rv(name2) = Uniform(0.75, 1.25) + rv(name3) = LogNormal(20, 4) + + # shared parameters + p_shared1 = name1 + p_shared2[2015] *= name2 + p_shared3[2020:5:2050, (Region2, Region3)] *= Uniform(0.8, 1.2) + p_shared4 = [Region1 => Uniform(0.08, 0.14), + Region2 => Uniform(0.10, 1.50), + Region3 => Uniform(0.10, 0.20)] + + # unshared parameters + test.p_unshared1 = name1 + test.p_unshared2[2015] *= name2 + test.p_unshared3[2020:5:2050, (Region2, Region3)] *= Uniform(0.8, 1.2) + test.p_unshared4 = [Region1 => Uniform(0.08, 0.14), + Region2 => Uniform(0.10, 1.50), + Region3 => Uniform(0.10, 0.20)] + +end + +m = Model() +set_dimension!(m, :time, 2015:5:2110) +set_dimension!(m, :regions, [:Region1, :Region2, :Region3]) +add_comp!(m, test) + +add_shared_param!(m, :p_shared1, 5) +connect_param!(m, :test, :p_shared1, :p_shared1) + +@test_throws ErrorException add_shared_param!(m, :p_shared2, collect(1:20)) # need dimensions +add_shared_param!(m, :p_shared2, collect(1:20), dims = [:time]) +connect_param!(m, :test, :p_shared2, :p_shared2) + +@test_throws ErrorException add_shared_param!(m, :p_shared3, fill(10,20,3), dims = [:time]) # need 2 dimensions +add_shared_param!(m, :p_shared3, fill(10,20,3), dims = [:time, :regions]) +connect_param!(m, :test, :p_shared3, :p_shared3) + +add_shared_param!(m, :p_shared4, collect(1:3), dims = [:regions]) +connect_param!(m, :test, :p_shared4, :p_shared4) + +run(sd_toy, m, 10) + +# More Complex/Realistic @defsim + include("test-model-2/multi-region-model.jl") using .MyModel m = construct_MyModel() - N = 100 sd = @defsim begin # Define random variables. The rv() is required to disambiguate an # RV definition name = Dist(args...) from application of a distribution - # to an external parameter. This makes the (less common) naming of an + # to an model parameter. This makes the (less common) naming of an # RV slightly more burdensome, but it's only required when defining # correlations or sharing an RV across parameters. rv(name1) = Normal(1, 0.2) @@ -29,14 +94,14 @@ sd = @defsim begin rv(name3) = LogNormal(20, 4) # assign RVs to model Parameters - share = Uniform(0.2, 0.8) - sigma[:, Region1] *= name2 - sigma[2020:5:2050, (Region2, Region3)] *= Uniform(0.8, 1.2) + grosseconomy.share = Uniform(0.2, 0.8) + emissions.sigma[:, Region1] *= name2 + emissions.sigma[2020:5:2050, (Region2, Region3)] *= Uniform(0.8, 1.2) - depk = [Region1 => Uniform(0.08, 0.14), + grosseconomy.depk = [Region1 => Uniform(0.08, 0.14), Region2 => Uniform(0.10, 1.50), Region3 => Uniform(0.10, 0.20)] - + sampling(LHSData, corrlist=[(:name1, :name2, 0.7), (:name1, :name3, 0.5)]) # indicate which parameters to save for each model run. Specify @@ -238,7 +303,7 @@ trial2 = copy(si2.sim_def.rvdict[:name1].dist.values) sd2 = @defsim begin # Define random variables. The rv() is required to disambiguate an # RV definition name = Dist(args...) from application of a distribution - # to an external parameter. This makes the (less common) naming of an + # to an model parameter. This makes the (less common) naming of an # RV slightly more burdensome, but it's only required when defining # correlations or sharing an RV across parameters. rv(name1) = Normal(1, 0.2) @@ -246,11 +311,11 @@ sd2 = @defsim begin rv(name3) = LogNormal(20, 4) # assign RVs to model Parameters - share = Uniform(0.2, 0.8) - sigma[:, Region1] *= name2 - sigma[2020:5:2050, (Region2, Region3)] *= Uniform(0.8, 1.2) + grosseconomy.share = Uniform(0.2, 0.8) + emissions.sigma[:, Region1] *= name2 + emissions.sigma[2020:5:2050, (Region2, Region3)] *= Uniform(0.8, 1.2) - depk = [Region1 => Uniform(0.08, 0.14), + grosseconomy.depk = [Region1 => Uniform(0.08, 0.14), Region2 => Uniform(0.10, 1.50), Region3 => Uniform(0.10, 0.20)] @@ -274,13 +339,13 @@ trial2 = copy(si2.sim_def.rvdict[:name1].dist.values) sd3 = @defsim begin # 1 dimension - depk[:] = Uniform(0.1, 0.2) - k0[(Region2, Region3)] = Uniform(20, 30) + grosseconomy.depk[:] = Uniform(0.1, 0.2) + grosseconomy.k0[(Region2, Region3)] = Uniform(20, 30) # 2 dimensions - tfp[:, Region1] = Uniform(0.75, 1.25) - sigma[2020:5:2050, (Region2, Region3)] = Uniform(0.8, 1.2) - s[2020, Region1] = Uniform(0.2, 0.3) + grosseconomy.tfp[:, Region1] = Uniform(0.75, 1.25) + emissions.sigma[2020:5:2050, (Region2, Region3)] = Uniform(0.8, 1.2) + grosseconomy.s[2020, Region1] = Uniform(0.2, 0.3) end diff --git a/test/mcs/test_defmcs_delta.jl b/test/mcs/test_defmcs_delta.jl index 3872a93d3..faf19e701 100644 --- a/test/mcs/test_defmcs_delta.jl +++ b/test/mcs/test_defmcs_delta.jl @@ -16,7 +16,7 @@ N = 100 sd = @defsim begin # Define random variables. The rv() is required to disambiguate an # RV definition name = Dist(args...) from application of a distribution - # to an external parameter. This makes the (less common) naming of an + # to an model parameter. This makes the (less common) naming of an # RV slightly more burdensome, but it's only required when defining # correlations or sharing an RV across parameters. rv(name1) = Normal(1, 0.2) @@ -24,11 +24,11 @@ sd = @defsim begin rv(name3) = LogNormal(20, 4) # assign RVs to model Parameters - share = Uniform(0.2, 0.8) - sigma[:, Region1] *= name2 - sigma[2020:5:2050, (Region2, Region3)] *= Uniform(0.8, 1.2) + grosseconomy.share = Uniform(0.2, 0.8) + emissions.sigma[:, Region1] *= name2 + emissions.sigma[2020:5:2050, (Region2, Region3)] *= Uniform(0.8, 1.2) - depk = [Region1 => Uniform(0.08, 0.14), + grosseconomy.depk = [Region1 => Uniform(0.08, 0.14), Region2 => Uniform(0.10, 1.50), Region3 => Uniform(0.10, 0.20)] diff --git a/test/mcs/test_defmcs_modifications.jl b/test/mcs/test_defmcs_modifications.jl index f870557b8..9b117440a 100644 --- a/test/mcs/test_defmcs_modifications.jl +++ b/test/mcs/test_defmcs_modifications.jl @@ -18,11 +18,11 @@ sd = @defsim begin rv(name2) = Uniform(0.75, 1.25) rv(name3) = LogNormal(20, 4) - share = Uniform(0.2, 0.8) - sigma[:, Region1] *= name2 - sigma[2020:5:2050, (Region2, Region3)] *= Uniform(0.8, 1.2) + grosseconomy.share = Uniform(0.2, 0.8) + emissions.sigma[:, Region1] *= name2 + emissions.sigma[2020:5:2050, (Region2, Region3)] *= Uniform(0.8, 1.2) - depk = [Region1 => Uniform(0.08, 0.14), + grosseconomy.depk = [Region1 => Uniform(0.08, 0.14), Region2 => Uniform(0.10, 1.50), Region3 => Uniform(0.10, 0.20)] @@ -35,7 +35,7 @@ run(sd, m, N; trials_output_filename = joinpath(output_dir, "trialdata.csv"), re # test modification functions -# add_RV! (calls add_transform!) +# add_RV! @test_throws ErrorException add_RV!(sd, :name1, Normal(1,0)) add_RV!(sd, :new_RV, Normal(0, 1)) @test sd.rvdict[:new_RV].dist == Normal(0, 1) @@ -66,6 +66,20 @@ pos = findall(isequal((:grosseconomy, :K)), sd.savelist) @test length(pos) == 1 run(sd, m, N; trials_output_filename = joinpath(output_dir, "trialdata.csv"), results_output_dir=output_dir) +# add_transform! +rvs = get_simdef_rvnames(sd, :share) +delete_RV!(sd, rvs[1]) +add_RV!(sd, :new_RV, Uniform(0.2, 0.8)) +add_transform!(sd, :grosseconomy, :share, :(=), :new_RV) +@test :new_RV in map(i->i.rvname, sd.translist) +run(sd, m, N; trials_output_filename = joinpath(output_dir, "trialdata.csv"), results_output_dir=output_dir) + +delete_RV!(sd, :new_RV) +add_RV!(sd, :new_RV, Uniform(0.2, 0.8)) +add_transform!(sd, :grosseconomy, :share, :(=), :new_RV) # should work with the component name too even though it is shared +@test :new_RV in map(i->i.rvname, sd.translist) +run(sd, m, N; trials_output_filename = joinpath(output_dir, "trialdata.csv"), results_output_dir=output_dir) + # get_simdef_rvnames rvs = get_simdef_rvnames(sd, :depk) @test length(rvs) == 3 diff --git a/test/mcs/test_defmcs_sobol.jl b/test/mcs/test_defmcs_sobol.jl index 4cb4a039b..e2d220682 100644 --- a/test/mcs/test_defmcs_sobol.jl +++ b/test/mcs/test_defmcs_sobol.jl @@ -16,7 +16,7 @@ N = 100 sd = @defsim begin # Define random variables. The rv() is required to disambiguate an # RV definition name = Dist(args...) from application of a distribution - # to an external parameter. This makes the (less common) naming of an + # to an model parameter. This makes the (less common) naming of an # RV slightly more burdensome, but it's only required when defining # correlations or sharing an RV across parameters. rv(name1) = Normal(1, 0.2) @@ -24,11 +24,11 @@ sd = @defsim begin rv(name3) = LogNormal(20, 4) # assign RVs to model Parameters - share = Uniform(0.2, 0.8) - sigma[:, Region1] *= name2 - sigma[2020:5:2050, (Region2, Region3)] *= Uniform(0.8, 1.2) + grosseconomy.share = Uniform(0.2, 0.8) + emissions.sigma[:, Region1] *= name2 + emissions.sigma[2020:5:2050, (Region2, Region3)] *= Uniform(0.8, 1.2) - depk = [Region1 => Uniform(0.08, 0.14), + grosseconomy.depk = [Region1 => Uniform(0.08, 0.14), Region2 => Uniform(0.10, 1.50), Region3 => Uniform(0.10, 0.20)] diff --git a/test/mcs/test_empirical.jl b/test/mcs/test_empirical.jl index c6c5fdd3d..d86bfc8c1 100644 --- a/test/mcs/test_empirical.jl +++ b/test/mcs/test_empirical.jl @@ -6,23 +6,6 @@ using Test include("../../wip/load_empirical_dist.jl") -# function load_vector(path, range, header=false) -# tups = collect(load(path, range, header=header)) -# name = fieldnames(tups[1])[1] # field name of first item in NamedTuple -# map(obj -> getfield(obj, name), tups) -# end - -# function load_empirical_dist(path::AbstractString, -# values_range::AbstractString, -# probs_range::AbstractString="") -# println("Reading from '$path'") -# values = load_vector(path, values_range) -# probs = probs_range == "" ? nothing : load_vector(path, probs_range) -# d = Mimi.EmpiricalDistribution(values, probs) -# println("returning distribution $(typeof(d))") -# return d -# end - filename = joinpath(@__DIR__, "RB-ECS-distribution.xls") d = load_empirical_dist(filename, "Sheet1!A2:A1001", "Sheet1!B2:B1001") @@ -52,7 +35,7 @@ _probs = 0.1 * ones(10) sd = @defsim begin sampling(LHSData) - p = EmpiricalDistribution(_values, _probs) + test.p = EmpiricalDistribution(_values, _probs) end @defcomp test begin @@ -84,7 +67,7 @@ si = run(sd, m, num_trials; scenario_args = scenario_args, scenario_func = scenario_func, post_trial_func = post_trial_func - ) +) for rv in values(si.sim_def.rvdict) @test rv.dist isa Mimi.SampleStore diff --git a/test/mcs/test_marginalmodel.jl b/test/mcs/test_marginalmodel.jl index 8f4719d6f..b90646277 100644 --- a/test/mcs/test_marginalmodel.jl +++ b/test/mcs/test_marginalmodel.jl @@ -10,7 +10,7 @@ mm1 = create_marginal_model(create_model()) mm2 = create_marginal_model(create_model()) simdef = @defsim begin - share = Uniform(0, 1) + grosseconomy.share = Uniform(0, 1) save(emissions.E_Global) end diff --git a/test/mcs/test_reshaping.jl b/test/mcs/test_reshaping.jl index 5b1371fd4..73ae6bf18 100644 --- a/test/mcs/test_reshaping.jl +++ b/test/mcs/test_reshaping.jl @@ -18,7 +18,7 @@ N = 100 sd = @defsim begin # Define random variables. The rv() is required to disambiguate an # RV definition name = Dist(args...) from application of a distribution - # to an external parameter. This makes the (less common) naming of an + # to an model parameter. This makes the (less common) naming of an # RV slightly more burdensome, but it's only required when defining # correlations or sharing an RV across parameters. rv(name1) = Normal(1, 0.2) @@ -26,13 +26,13 @@ sd = @defsim begin rv(name3) = LogNormal(20, 4) # assign RVs to model Parameters - share = Uniform(0.2, 0.8) - sigma[:, Region1] *= name2 - sigma[2020:5:2050, (Region2, Region3)] *= Uniform(0.8, 1.2) + grosseconomy.share = Uniform(0.2, 0.8) + emissions.sigma[:, Region1] *= name2 + emissions.sigma[2020:5:2050, (Region2, Region3)] *= Uniform(0.8, 1.2) - tester = ReshapedDistribution([20, 3], Dirichlet(20*3, 1)) + grosseconomy.tester = ReshapedDistribution([20, 3], Dirichlet(20*3, 1)) - depk = [Region1 => Uniform(0.08, 0.14), + grosseconomy.depk = [Region1 => Uniform(0.08, 0.14), Region2 => Uniform(0.10, 1.50), Region3 => Uniform(0.10, 0.20)] diff --git a/test/mcs/test_translist.jl b/test/mcs/test_translist.jl new file mode 100644 index 000000000..1f6b3b0d9 --- /dev/null +++ b/test/mcs/test_translist.jl @@ -0,0 +1,154 @@ +using Mimi +using Distributions +using Test + +@defcomp test1 begin + p = Parameter(default = 5) + function run_timestep(p, v, d, t) end +end + +@defcomp test2 begin + p = Parameter(default = 5) + function run_timestep(p, v, d, t) end +end + +@defcomp test3 begin + a = Parameter(default = 5) + function run_timestep(p, v, d, t) end +end + +## +## Tests for set_translist_modelparams +## + +sd = @defsim begin + sampling(LHSData) + p = Normal(0, 1) +end + +#------------------------------------------------------------------------------ +# Test a failure to find the unshared parameter in any components +m = Model() +set_dimension!(m, :time, 2000:10:2050) +add_comp!(m, test3) + +fail_expr1 = :( + run(sd, m, 100) +) + +err1 = try eval(fail_expr1) catch err err end +@test occursin("Cannot resolve because p not found in any of the components of Model1.", sprint(showerror, err1)) + +#------------------------------------------------------------------------------ +# Test a failure due to finding the unshared parameter in more than one component +m = Model() +set_dimension!(m, :time, 2000:10:2050) +add_comp!(m, test1) +add_comp!(m, test2) + +fail_expr2 = :( + run(sd, m, 100) +) + +err2 = try eval(fail_expr2) catch err err end +@test occursin("Cannot resolve because parameter name p found in more than one component of Model1", sprint(showerror, err2)) + +#------------------------------------------------------------------ +# Test a failure due to finding an unshared parameter in one model, but not +# the other + +m1 = Model() +set_dimension!(m1, :time, 2000:10:2050) +add_comp!(m1, test1) + +m2 = Model() +set_dimension!(m2, :time, 2000:10:2050) +add_comp!(m2, test3) + +fail_expr3 = :( + run(sd, [m1, m2], 100) +) + +err3 = try eval(fail_expr3) catch err err end +@test occursin("Cannot resolve because p not found in any of the components of Model2", sprint(showerror, err3)) + +#------------------------------------------------------------------------------ +# Test success cases + +# unshared parameter set by default +m = Model() +set_dimension!(m, :time, 2000:10:2050) +add_comp!(m, test1) +run(sd, m, 100) + +# shared parameter +m1 = Model() +set_dimension!(m1, :time, 2000:10:2050) +add_comp!(m1, test1) +add_comp!(m1, test2) +add_shared_param!(m1, :model_p, 5) +connect_param!(m1, :test1, :p, :model_p) +connect_param!(m1, :test2, :p, :model_p) +run(sd, m, 100) + +# unshared parameter in both models with different names +m1 = Model() +set_dimension!(m1, :time, 2000:10:2050) +add_comp!(m1, test1) + +m2 = Model() +set_dimension!(m2, :time, 2000:10:2050) +add_comp!(m2, test2) + +run(sd, [m1, m2], 100) + +## +## Tests for set_translist_modelparams with a default (not shared) +## + +sd = @defsim begin + sampling(LHSData) + test1.p = Normal(0, 1) +end + +# simple case +m = Model() +set_dimension!(m, :time, 2000:10:2050) +add_comp!(m, test1) +run(sd, m, 100) + +# component not found +m = Model() +set_dimension!(m, :time, 2000:10:2050) +add_comp!(m, test2) +fail_expr = :(run(sd, m, 100)) +err4 = try eval(fail_expr) catch err err end +@test occursin("Component test1 does not exist in Model1.", sprint(showerror, err4)) + +m1 = Model() +set_dimension!(m1, :time, 2000:10:2050) +add_comp!(m1, test2) +m2 = Model() +set_dimension!(m2, :time, 2000:10:2050) +add_comp!(m2, test1) +fail_expr = :(run(sd, [m1, m2], 100)) +err5 = try eval(fail_expr) catch err err end +@test occursin("Component test1 does not exist in Model1.", sprint(showerror, err5)) + +# transform used only for one of the component's parameters p +m = Model() +set_dimension!(m, :time, 2000:10:2050) +add_comp!(m, test1) +add_comp!(m, test2) # no transform used +run(sd, m, 100) + +# two models, both with component parameter pair +m1 = Model() +set_dimension!(m1, :time, 2000:10:2050) +add_comp!(m1, test1) + +m2 = Model() +set_dimension!(m2, :time, 2000:10:2050) +add_comp!(m2, test1) + +run(sd, [m1, m2], 100) diff --git a/test/runtests.jl b/test/runtests.jl index 7a0b09dfe..ab34a2f01 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -62,6 +62,9 @@ Electron.prep_test_env() @info("test_parametertypes.jl") @time include("test_parametertypes.jl") + @info("test_new_parameterAPI.jl") + @time include("test_new_paramAPI.jl") + @info("test_defaults.jl") @time include("test_defaults.jl") @@ -113,7 +116,7 @@ Electron.prep_test_env() @info("test_firstlast.jl") @time include("test_firstlast.jl") - @info("test_explorer_model.jl") + @info("test_explorer_model.jl") @time include("test_explorer_model.jl") @info("test_explorer_sim.jl") diff --git a/test/test_adder.jl b/test/test_adder.jl index 1744e5977..2864a5d86 100644 --- a/test/test_adder.jl +++ b/test/test_adder.jl @@ -14,8 +14,8 @@ add_comp!(model1, Mimi.adder) x = collect(1:10) y = collect(2:2:20) -set_param!(model1, :adder, :input, x) -set_param!(model1, :adder, :add, y) +update_param!(model1, :adder, :input, x) +update_param!(model1, :adder, :add, y) run(model1) @@ -30,8 +30,8 @@ end model2 = Model() set_dimension!(model2, :time, 1:10) add_comp!(model2, Mimi.adder, :compA) -set_param!(model2, :compA, :input, x) -set_param!(model2, :compA, :add, y) +update_param!(model2, :compA, :input, x) +update_param!(model2, :compA, :add, y) run(model2) for i in 1:10 diff --git a/test/test_components.jl b/test/test_components.jl index 7f2df64af..ccfd451a2 100644 --- a/test/test_components.jl +++ b/test/test_components.jl @@ -119,7 +119,7 @@ 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)) +update_param!(m, :C, :par1, zeros(5)) Mimi.build!(m) # Build the model ci = compinstance(m, :C) # Get the component instance @test ci.first == 2001 && ci.last == 2005 # no change @@ -128,7 +128,7 @@ 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(21)) +update_param!(m, :C, :par1, zeros(21)) Mimi.build!(m) # Build the model ci = compinstance(m, :C) # Get the component instance @test ci.first == 2001 && ci.last == 2005 # no change @@ -147,14 +147,14 @@ 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, 1950:2090) -set_param!(m, :C, :par1, zeros(141)) +update_param!(m, :C, :par1, zeros(141)) Mimi.build!(m) # Build the model ci = compinstance(m, :C) # Get the component instance @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, 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 +update_param!(m, :C, :par1, zeros(261)) # Have to reset the parameter to have the same width as the model time dimension 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 diff --git a/test/test_composite.jl b/test/test_composite.jl index 69933387b..a9770172b 100644 --- a/test/test_composite.jl +++ b/test/test_composite.jl @@ -122,16 +122,15 @@ c2 = md[:top][:A][:Comp2] c3 = find_comp(md, "/top/B/Comp3") @test c3.comp_id == Comp3.comp_id -set_param!(m, :fooA1, 1) -set_param!(m, :fooA2, 2) +add_shared_param!(m, :model_fooA1, 1) +connect_param!(m, :top, :fooA1, :model_fooA1) -# TBD: default values set in @defcomp are not working... -# Also, external_parameters are stored in the parent, so both of the -# following set parameter :foo in "/top/B", with 2nd overwriting 1st. -set_param!(m, :foo3, 10) -set_param!(m, :foo4, 20) +add_shared_param!(m, :model_fooA2, 2) +connect_param!(m, :top, :fooA2, :model_fooA2) -set_param!(m, :par_1_1, collect(1:length(time_labels(md)))) +@test_throws ErrorException add_shared_param!(m, :model_par_1_1, collect(1:length(time_labels(md)))) # need to give index +add_shared_param!(m, :model_par_1_1, collect(1:length(time_labels(md))), dims = [:time]) +connect_param!(m, :top, :par_1_1, :model_par_1_1) Mimi.build!(m) @@ -159,7 +158,7 @@ end @test mi["/top/B/Comp4", :par_4_1] == collect(6.0:6:96.0) @test m[:top, :fooA1] == 1 -@test m[:top, :foo3] == 10 +@test m[:top, :foo3] == 30. @test m[:top, :var_3_1] == collect(6.0:6:96.0) # test ways to drill down into composites to get information diff --git a/test/test_composite_parameters.jl b/test/test_composite_parameters.jl index 8b0f40868..64498b98e 100644 --- a/test/test_composite_parameters.jl +++ b/test/test_composite_parameters.jl @@ -3,6 +3,8 @@ module TestCompositeParameters using Mimi using Test +import Mimi: model_params + @defcomp A begin p1 = Parameter(unit = "\$", default=3) p2 = Parameter() @@ -68,7 +70,7 @@ err3 = try eval(fail_expr3) catch err err end #------------------------------------------------------------------------------ -# Test a failure to auto-import a paramter because it's name has already been used +# Test a failure to auto-import a parameter because it's name has already been used fail_expr4 = :( @defcomposite TestFailComposite begin @@ -119,36 +121,188 @@ err6 = try set_param!(m1, :p1, 5) catch err err end set_param!(m1, :p1, 5, ignoreunits=true) err7 = try run(m1) catch err err end -@test occursin("Cannot build model; the following parameters are not set", sprint(showerror, err7)) +@test occursin("Cannot build model; the following parameters still have values of `nothing` and need to be updated:", sprint(showerror, err7)) # Set separate values for p1 in A and B m2 = get_model() -set_param!(m2, :A, :p1, 1) # Set the value only for component A -@test length(m2.md.external_param_conns) == 1 # test that only one connection has been made -@test Mimi.UnnamedReference(:B, :p1) in Mimi.unconnected_params(m2.md) # and that B.p1 is still unconnected - -err8 = try set_param!(m2, :B, :p1, 2) catch err err end -@test occursin("the model already has an external parameter with this name", sprint(showerror, err8)) - -set_param!(m2, :B, :p1, :B_p1, 2) # Use a unique name to set B.p1 -@test length(m2.md.external_param_conns) == 2 -@test Set(keys(m2.md.external_params)) == Set([:p1, :B_p1]) - -# Test defaults being set properly: +set_param!(m2, :A, :p1, 2) # Set the value only for component A + +# test that the proper connection has been made for :p1 in :A +@test Mimi.model_param(m2, :p1).value == 2 +@test Mimi.model_param(m2, :p1).is_shared +# and that B.p1 is still the default value and unshared +sym = Mimi.get_model_param_name(m2, :B, :p1) +@test Mimi.model_param(m2, sym).value == 3 +@test Mimi.model_param(m2, :B, :p1).value == 3 +@test !Mimi.model_param(m2, :B, :p1).is_shared + +# test defaults m3 = get_model() set_param!(m3, :p1, 1, ignoreunits=true) # Need to set parameter values for all except :p5, which has a default set_param!(m3, :p2, 2) set_param!(m3, :p3, 3) set_param!(m3, :p4, 1:10) run(m3) -@test length(keys(m3.md.external_params)) == 4 # The default value was not added to the original md's list -@test length(keys(m3.mi.md.external_params)) == 5 # Only added to the model instance's definition + +err8 = try set_param!(m3, :B, :p1, 2) catch err err end +@test occursin("the model already has a parameter with this name", sprint(showerror, err8)) + +set_param!(m3, :B, :p1, :B_p1, 2) # Use a unique name to set B.p1 +@test Mimi.model_param(m3, :B_p1).value == 2 +@test Mimi.model_param(m3, :B_p1).is_shared +@test issubset(Set([:p1, :B_p1]), Set(keys(m3.md.model_params))) + + +#------------------------------------------------------------------------------ +# Test update_param! with unit collision + +function get_model() + m = Model() + set_dimension!(m, :time, 10) + add_comp!(m, A) + add_comp!(m, B) + return m +end + +m1 = get_model() +add_shared_param!(m1, :p1, 5) +connect_param!(m1, :A, :p1, :p1) # no conflict +err9 = try connect_param!(m1, :B, :p1, :p1) catch err err end +@test occursin("Cannot connect B:p1 to shared model parameter", sprint(showerror, err9)) + +# use ignoreunits flag +connect_param!(m1, :B, :p1, :p1, ignoreunits=true) + +err10 = try run(m1) catch err err end +@test occursin("Cannot build model; the following parameters still have values of `nothing` and need to be updated:", sprint(showerror, err10)) + +# Set separate values for p1 in A and B +m2 = get_model() +add_shared_param!(m2, :p1, 2) +connect_param!(m2, :A, :p1, :p1) # Set the value only for component A + +# test that the proper connection has been made for :p1 in :A +@test Mimi.model_param(m2.md, :p1).value == 2 +@test Mimi.model_param(m2.md, :p1).is_shared +# and that B.p1 is still the default value and unshared +sym = Mimi.get_model_param_name(m2, :B, :p1) +@test Mimi.model_param(m2, sym).value == 3 +@test Mimi.model_param(m2, :B, :p1).value == 3 +@test !Mimi.model_param(m2, :B, :p1).is_shared + +# test defaults - # Need to set parameter values for all except :p5, which has a default +m3 = get_model() +add_shared_param!(m3, :p1, 1) +connect_param!(m3, :A, :p1, :p1, ignoreunits = true) +connect_param!(m3, :B, :p1, :p1, ignoreunits = true) +update_param!(m3, :A, :p2, 2) +update_param!(m3, :B, :p3, 3) +update_param!(m3, :B, :p4, 1:10) +run(m3) + +err11 = try add_shared_param!(m3, :p1, 2) catch err err end +@test occursin("the model already has a shared parameter with this name", sprint(showerror, err11)) + +add_shared_param!(m3, :B_p1, 2) # Use a unique name to set B.p1 +connect_param!(m3, :B, :p1, :B_p1) +@test Mimi.model_param(m3, :B_p1).value == 2 +@test Mimi.model_param(m3, :B_p1).is_shared +@test issubset(Set([:p1, :B_p1]), Set(keys(m3.md.model_params))) + +#------------------------------------------------------------------------------ +# Unit tests on default behavior + +# different default and override +@defcomp A begin + p1 = Parameter(default=3) +end +@defcomp B begin + p1 = Parameter(default=2) +end + +@defcomposite top begin + Component(A) + Component(B) + superp1 = Parameter(A.p1, B.p1, default = nothing) # override default collision with nothing +end + +m = Model() +set_dimension!(m, :time, 2000:2005) +add_comp!(m, top); +@test length(model_params(m)) == 1 +model_param_name = Mimi.get_model_param_name(m.md, :top, :superp1) +@test Mimi.is_nothing_param(model_params(m)[model_param_name]) +@test !model_params(m)[model_param_name].is_shared + +@defcomposite top begin + Component(A) + Component(B) + superp1 = Parameter(A.p1, B.p1, default = 8.0) # override default collision with value +end + +m = Model() +set_dimension!(m, :time, 2000:2005) +add_comp!(m, top); +@test length(model_params(m)) == 1 +model_param_name = Mimi.get_model_param_name(m.md, :top, :superp1) +@test model_params(m)[model_param_name].value == 8.0 +@test !model_params(m)[model_param_name].is_shared + +# same default and no override +@defcomp A begin + p1 = Parameter(default=2) +end +@defcomp B begin + p1 = Parameter(default=2) +end + +@defcomposite top begin + Component(A) + Component(B) + superp1 = Parameter(A.p1, B.p1) +end + +m = Model() +set_dimension!(m, :time, 2000:2005) +add_comp!(m, top); +@test length(model_params(m)) == 1 +model_param_name = Mimi.get_model_param_name(m.md, :top, :superp1) +@test model_params(m)[model_param_name].value == 2 +@test !model_params(m)[model_param_name].is_shared + +# simple case with no super parameter +@defcomp A begin + p1 = Parameter(default=2) +end +@defcomp B begin + p2 = Parameter(default=3) +end + +@defcomposite top begin + Component(A) + Component(B) +end + +m = Model() +set_dimension!(m, :time, 2000:2005) +add_comp!(m, top); +@test length(model_params(m)) == 2 +model_param_name = Mimi.get_model_param_name(m.md, :top, :p1) +@test model_params(m)[model_param_name].value == 2 +@test !model_params(m)[model_param_name].is_shared +model_param_name = Mimi.get_model_param_name(m.md, :top, :p2) +@test model_params(m)[model_param_name].value == 3 +@test !model_params(m)[model_param_name].is_shared #------------------------------------------------------------------------------ # Test set_param! for parameter that exists in neither model definition nor any subcomponent m1 = get_model() -err8 = try set_param!(m1, :pDNE, 42) catch err err end -@test occursin("not found in ModelDef or children", sprint(showerror, err8)) +err12 = try set_param!(m1, :pDNE, 42) catch err err end +@test occursin("not found in ModelDef or children", sprint(showerror, err12)) + +# Test update_param! for parameter that exists in neither model definition nor any subcomponent +err13 = try update_param!(m1, :pDNE, 42) catch err err end +@test occursin("not found in composite's model parameters", sprint(showerror, err13)) end #module diff --git a/test/test_composite_simple.jl b/test/test_composite_simple.jl index d1e1df23d..713ecfdda 100644 --- a/test/test_composite_simple.jl +++ b/test/test_composite_simple.jl @@ -46,15 +46,19 @@ end end m = Model() -md = m.md - set_dimension!(m, :time, 2005:2020) - add_comp!(m, Top) +update_param!(m, :Top, :fooA1, 10) +update_param!(m, :Top, :par_1_1, 1:16) # unshared +run(m) -set_param!(m, :Top, :fooA1, 10) -set_param!(m, :par_1_1, 1:16) - +m = Model() +set_dimension!(m, :time, 2005:2020) +add_comp!(m, Top) +update_param!(m, :Top, :fooA1, 10) +@test_throws ErrorException add_shared_param!(m, :par_1_1, 1:16) # need to give indices +add_shared_param!(m, :par_1_1, 1:16, dims = [:time]) # shared +connect_param!(m, :Top, :par_1_1, :par_1_1) run(m) end diff --git a/test/test_connectorcomp.jl b/test/test_connectorcomp.jl index 38f2186ea..db5798097 100644 --- a/test/test_connectorcomp.jl +++ b/test/test_connectorcomp.jl @@ -35,7 +35,7 @@ model1 = Model() set_dimension!(model1, :time, years) add_comp!(model1, Short, first=late_start) add_comp!(model1, Long) -set_param!(model1, :Short, :a, 2.) +update_param!(model1, :Short, :a, 2.) connect_param!(model1, :Long, :x, :Short, :b, zeros(length(years))) run(model1) @@ -88,7 +88,7 @@ model2 = Model() set_dimension!(model2, :time, years_variable) add_comp!(model2, Short; last=early_last) add_comp!(model2, Long) -set_param!(model2, :Short, :a, 2.) +update_param!(model2, :Short, :a, 2.) connect_param!(model2, :Long, :x, :Short, :b, zeros(length(years_variable))) run(model2) @@ -142,7 +142,7 @@ set_dimension!(model3, :time, years) set_dimension!(model3, :regions, regions) add_comp!(model3, Short_multi, first=late_start) add_comp!(model3, Long_multi) -set_param!(model3, :Short_multi, :a, [1,2]) +update_param!(model3, :Short_multi, :a, [1,2]) connect_param!(model3, :Long_multi, :x, :Short_multi, :b, zeros(length(years), length(regions))) run(model3) @@ -180,7 +180,7 @@ set_dimension!(model4, :regions, regions) add_comp!(model4, Short_multi; first=first, last=last) add_comp!(model4, Long_multi) -set_param!(model4, :Short_multi, :a, [1,2]) +update_param!(model4, :Short_multi, :a, [1,2]) connect_param!(model4, :Long_multi=>:x, :Short_multi=>:b, zeros(length(years), length(regions))) run(model4) @@ -217,7 +217,7 @@ model5 = Model() set_dimension!(model5, :time, years) add_comp!(model5, Short; first = late_start) add_comp!(model5, Long; first = late_start_long) -set_param!(model5, :Short, :a, 2) +update_param!(model5, :Short, :a, 2) # A. test wrong size (needs to be length of model, not length of component) @test_throws ErrorException connect_param!(model5, :Long=>:x, :Short=>:b, zeros(getspan(model5, :Long))) @@ -246,7 +246,7 @@ add_comp!(model6, foo, :Long; rename=[:var => :long_foo]) add_comp!(model6, foo, :Short; rename=[:var => :short_foo],first=late_start) @test_throws ErrorException connect_param!(model6, :Short => :par, :Long => :var, backup_offset = 1) # can't use backup_offset without backup connect_param!(model6, :Short => :par, :Long => :var) -set_param!(model6, :Long, :par, years) +update_param!(model6, :Long, :par, years) run(model6) diff --git a/test/test_defaults.jl b/test/test_defaults.jl index 8704e2b78..289f823ba 100644 --- a/test/test_defaults.jl +++ b/test/test_defaults.jl @@ -3,43 +3,59 @@ module TestDefaults using Mimi using Test +import Mimi: model_params @defcomp A begin p1 = Parameter(default = 1) - p2 = Parameter() + p2 = Parameter{Symbol}() end m = Model() set_dimension!(m, :time, 1:10) add_comp!(m, A) -set_param!(m, :p2, 2) + +add_shared_param!(m, :p2, :hello) +connect_param!(m, :A, :p2, :p2) # So far only :p2 is in the model definition's dictionary -@test length(m.md.external_params) == 1 +@test :p2 in keys(model_params(m)) +@test length(model_params(m)) == 2 run(m) -# During build, :p1's value is set to it's default +# :p1's value is it's default @test m[:A, :p1] == 1 -# But the original model definition does not have :p1 in it's external parameters -@test length(m.md.external_params) == 1 -@test length(m.mi.md.external_params) == 2 # But the model instance's md is where the default value was set -@test ! (:p1 in keys(m.md.external_params)) -@test :p1 in keys(m.mi.md.external_params) +# This errors because p1 is unshared +@test_throws ErrorException update_param!(m, :p1, 10) +update_param!(m, :A, :p1, 10) + +# :p1 still not in the dictionary because unshared +@test !(:p1 in keys(model_params(m))) -# This errors because p1 isn't in the model definition's external params -@test_throws ErrorException update_param!(m, :p1, 10) +# now add it as a shared parameter +add_shared_param!(m, :model_p1, 20) +connect_param!(m, :A, :p1, :model_p1) -# Need to use set_param! instead -set_param!(m, :p1, 10) +# Now there is a :model_p1 in the model definition's dictionary but not :p1 +@test !(:p1 in keys(model_params(m))) +@test :model_p1 in keys(model_params(m)) -# Now there is a :p1 in the model definition's dictionary -@test :p1 in keys(m.md.external_params) +run(m) +@test m[:A, :p1] == 20 +# Now we can use update_param! but only for the model parameter name and exclusively as a shared parameter +@test_throws ErrorException update_param!(m, :p1, 11) +update_param!(m, :model_p1, 30) run(m) -@test m[:A, :p1] == 10 -update_param!(m, :p1, 11) # Now we can use update_param! +@test m[:A, :p1] == 30 +# convert explicitly back to being unshared +@test_throws ErrorException update_param!(m, :A, :p1, 40) +disconnect_param!(m, :A, :p1) +update_param!(m, :A, :p1, 40) +run(m) +@test m[:A, :p1] == 40 +@test Mimi.model_param(m, :model_p1).value == 30 end \ No newline at end of file diff --git a/test/test_delete.jl b/test/test_delete.jl index e68dd6524..f5dbd1d63 100644 --- a/test/test_delete.jl +++ b/test/test_delete.jl @@ -11,13 +11,22 @@ using Test end function _get_model() + m = Model() set_dimension!(m, :time, 1:2) add_comp!(m, A, :A1) add_comp!(m, A, :A2) - set_param!(m, :p1, 1) - set_param!(m, :A1, :p2, :p2_A1, 21) - set_param!(m, :A2, :p2, :p2_A2, 22) + + add_shared_param!(m, :p1, 1) + connect_param!(m, :A1, :p1, :p1) + connect_param!(m, :A2, :p1, :p1) + + add_shared_param!(m, :p2_A1, 21) + connect_param!(m, :A1, :p2, :p2_A1) + + add_shared_param!(m, :p2_A2, 22) + connect_param!(m, :A2, :p2, :p2_A2) + return m end @@ -26,27 +35,28 @@ m1 = _get_model() run(m1) @test length(Mimi.components(m1)) == 2 @test length(m1.md.external_param_conns) == 4 # two components with two connections each -@test length(m1.md.external_params) == 3 # three total external params +@test length(m1.md.model_params) == 3 + delete!(m1, :A1) run(m1) # run before and after to test that `delete!` properly "dirties" the model, and builds a new instance on the next run @test length(Mimi.components(m1)) == 1 @test length(m1.md.external_param_conns) == 2 # Component A1 deleted, so only two connections left -@test length(m1.md.external_params) == 3 # but all three external params remain -@test :p2_A1 in keys(m1.md.external_params) +@test length(m1.md.model_params) == 3 +@test :p2_A1 in keys(m1.md.model_params) # Test component deletion that removes unbound component parameters m2 = _get_model() delete!(m2, :A1, deep = true) @test length(Mimi.components(m2.md)) == 1 -@test length(m2.md.external_params) == 2 # :p2_A1 has been removed -@test !(:p2_A1 in keys(m2.md.external_params)) +@test length(m2.md.model_params) == 2 # :p2_A1 has been removed +@test !(:p2_A1 in keys(m2.md.model_params)) run(m2) # Test the `delete_param! function on its own m3 = _get_model() run(m3) delete_param!(m3, :p1) -@test_throws ErrorException run(m3) # will not be able to run because p1 in both components aren't connected to anything -@test length(m3.md.external_params) == 2 +@test_throws KeyError run(m3) # will not be able to run because p1 in both components can't find it's key @test length(m3.md.external_param_conns) == 2 # The external param connections to p1 have also been removed + end \ No newline at end of file diff --git a/test/test_dimensions.jl b/test/test_dimensions.jl index 90b9314fe..553b0915a 100644 --- a/test/test_dimensions.jl +++ b/test/test_dimensions.jl @@ -5,15 +5,15 @@ using Test import Mimi: compdef, AbstractDimension, RangeDimension, Dimension, key_type, first_period, last_period, - ComponentReference, ComponentPath, ComponentDef, time_labels + ComponentReference, ComponentPath, ComponentDef, time_labels, model_params ## ## Constants ## dim_varargs = Dimension(:foo, :bar, :baz) # varargs -dim_vec = Dimension([:foo, :bar, :baz]) # Vector -dim_range = Dimension(2010:2100) # AbstractRange +dim_vec = Dimension([:foo, :bar, :baz]) # Vector +dim_range = Dimension(2010:2100) # AbstractRange rangedim = RangeDimension(2010:2100) # RangeDimension type dim_vals = Dimension(4) # Same as 1:4 @@ -144,9 +144,11 @@ my_foo2 = compdef(foo2_ref1) # 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) +@test_throws ErrorException update_param!(m, :foo2, :x, 1990:2200) # too long +@test_throws ErrorException update_param!(m, :foo2, :x, 2005:2095) # too short + +add_shared_param!(m, :x, original_x_vals, dims = [:time]) +connect_param!(m, :foo2, :x, :x) run(m) @@ -161,7 +163,7 @@ set_dimension!(m, :time, 1990:2050) @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 +new_x_vals = model_params(m)[: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]) @@ -170,7 +172,7 @@ 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 +new_x_vals = model_params(m)[: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] diff --git a/test/test_explorer_model.jl b/test/test_explorer_model.jl index 1d0e09d48..b03597fcf 100644 --- a/test/test_explorer_model.jl +++ b/test/test_explorer_model.jl @@ -35,12 +35,12 @@ set_dimension!(m, :regions, 3) set_dimension!(m, :four, 4) add_comp!(m, MyComp) -set_param!(m, :MyComp, :a, ones(101,3)) -set_param!(m, :MyComp, :b, 1:101) -set_param!(m, :MyComp, :c, [4,5,6]) -set_param!(m, :MyComp, :d, .5) -set_param!(m, :MyComp, :e, [1,2,3,4]) -set_param!(m, :MyComp, :f, [1.0 2.0; 3.0 4.0]) +update_param!(m, :MyComp, :a, ones(101,3)) +update_param!(m, :MyComp, :b, 1:101) +update_param!(m, :MyComp, :c, [4,5,6]) +update_param!(m, :MyComp, :d, .5) +update_param!(m, :MyComp, :e, [1,2,3,4]) +update_param!(m, :MyComp, :f, [1.0 2.0; 3.0 4.0]) run(m) @@ -98,7 +98,7 @@ set_dimension!(m2, :regions, 3) set_dimension!(m2, :four, 4) add_comp!(m2, MyComp2) -set_param!(m2, :MyComp2, :a, ones(101, 3, 4)) +update_param!(m2, :MyComp2, :a, ones(101, 3, 4)) run(m2) @@ -140,8 +140,8 @@ set_dimension!(m, :time, time_index) set_dimension!(m, :regions, regions) set_dimension!(m, :foo, 3) add_comp!(m, gdp) -set_param!(m, :gdp, :gdp0, [3; 7] .* ones(length(regions), 3, 2)) -set_param!(m, :gdp, :growth, [0.02; 0.03] .* ones(length(regions), 3, nsteps, 2)) +update_param!(m, :gdp, :gdp0, [3; 7] .* ones(length(regions), 3, 2)) +update_param!(m, :gdp, :growth, [0.02; 0.03] .* ones(length(regions), 3, nsteps, 2)) set_leftover_params!(m, Dict{String, Any}([ "pgrowth" => ones(length(regions), 3, nsteps), "mat" => rand(length(regions), nsteps) @@ -176,14 +176,14 @@ set_dimension!(m, :baz, [:A, :B, :C]) add_comp!(m, example) -set_param!(m, :example, :p0, 1:10) -set_param!(m, :example, :p1, 6:10) -set_param!(m, :example, :p2, 4:6) +update_param!(m, :example, :p0, 1:10) +update_param!(m, :example, :p1, 6:10) +update_param!(m, :example, :p2, 4:6) -set_param!(m, :example, :p3, reshape(1:15, 5, 3)) -set_param!(m, :example, :p4, reshape(1:15, 3, 5)) -set_param!(m, :example, :p5, reshape(1:30, 10, 3)) -set_param!(m, :example, :p6, reshape(1:30, 10, 3)) +update_param!(m, :example, :p3, reshape(1:15, 5, 3)) +update_param!(m, :example, :p4, reshape(1:15, 3, 5)) +update_param!(m, :example, :p5, reshape(1:30, 10, 3)) +update_param!(m, :example, :p6, reshape(1:30, 10, 3)) run(m) explore(m) diff --git a/test/test_explorer_sim.jl b/test/test_explorer_sim.jl index b4400b8d2..7187b942d 100644 --- a/test/test_explorer_sim.jl +++ b/test/test_explorer_sim.jl @@ -25,7 +25,7 @@ N = 100 sd = @defsim begin # Define random variables. The rv() is required to disambiguate an # RV definition name = Dist(args...) from application of a distribution - # to an external parameter. This makes the (less common) naming of an + # to an model parameter. This makes the (less common) naming of an # RV slightly more burdensome, but it's only required when defining # correlations or sharing an RV across parameters. rv(name1) = Normal(1, 0.2) @@ -33,11 +33,11 @@ sd = @defsim begin rv(name3) = LogNormal(20, 4) # assign RVs to model Parameters - share = Uniform(0.2, 0.8) - sigma[:, Region1] *= name2 - sigma[2020:5:2050, (Region2, Region3)] *= Uniform(0.8, 1.2) + grosseconomy.share = Uniform(0.2, 0.8) + emissions.sigma[:, Region1] *= name2 + emissions.sigma[2020:5:2050, (Region2, Region3)] *= Uniform(0.8, 1.2) - depk = [Region1 => Uniform(0.08, 0.14), + grosseconomy.depk = [Region1 => Uniform(0.08, 0.14), Region2 => Uniform(0.10, 1.50), Region3 => Uniform(0.10, 0.20)] diff --git a/test/test_firstlast.jl b/test/test_firstlast.jl index 2491e61a9..12e91a1fd 100644 --- a/test/test_firstlast.jl +++ b/test/test_firstlast.jl @@ -56,17 +56,17 @@ add_comp!(m, emissions, first = 2020, last = 2105) @test m.md.namespace[:emissions].first == 2020 @test m.md.namespace[:emissions].last == 2105 -# Set parameters for the grosseconomy component -set_param!(m, :grosseconomy, :l, [(1. + 0.015)^t *6404 for t in 1:20]) -set_param!(m, :grosseconomy, :tfp, [(1 + 0.065)^t * 3.57 for t in 1:20]) -set_param!(m, :grosseconomy, :s, ones(20).* 0.22) -set_param!(m, :grosseconomy, :depk, 0.1) -set_param!(m, :grosseconomy, :k0, 130.) -set_param!(m, :grosseconomy, :share, 0.3) - -# Set parameters for the emissions component -@test_throws ErrorException set_param!(m, :emissions, :sigma, [(1. - 0.05)^t *0.58 for t in 1:19]) # the parameter needs to be length of model -set_param!(m, :emissions, :sigma, [(1. - 0.05)^t *0.58 for t in 1:20]) +# update parameters for the grosseconomy component +update_param!(m, :grosseconomy, :l, [(1. + 0.015)^t *6404 for t in 1:20]) +update_param!(m, :grosseconomy, :tfp, [(1 + 0.065)^t * 3.57 for t in 1:20]) +update_param!(m, :grosseconomy, :s, ones(20).* 0.22) +update_param!(m, :grosseconomy, :depk, 0.1) +update_param!(m, :grosseconomy, :k0, 130.) +update_param!(m, :grosseconomy, :share, 0.3) + +# update and connect parameters for the emissions component +@test_throws ErrorException update_param!(m, :emissions, :sigma, [(1. - 0.05)^t *0.58 for t in 1:19]) # the parameter needs to be length of model +update_param!(m, :emissions, :sigma, [(1. - 0.05)^t *0.58 for t in 1:20]) connect_param!(m, :emissions, :YGROSS, :grosseconomy, :YGROSS) run(m) @@ -90,10 +90,10 @@ set_dimension!(m, :time, collect(2015:5:2115)) @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]) -update_param!(m, :tfp, [(1 + 0.065)^t * 3.57 for t in 1:21]) -update_param!(m, :s, ones(21).* 0.22) -update_param!(m, :sigma, [(1. - 0.05)^t *0.58 for t in 1:21]) +update_param!(m, :grosseconomy, :l, [(1. + 0.015)^t *6404 for t in 1:21]) +update_param!(m, :grosseconomy, :tfp, [(1 + 0.065)^t * 3.57 for t in 1:21]) +update_param!(m, :grosseconomy, :s, ones(21).* 0.22) +update_param!(m, :emissions, :sigma, [(1. - 0.05)^t *0.58 for t in 1:21]) run(m) @@ -113,16 +113,16 @@ set_dimension!(m, :time, collect(2015:5:2110)) # 20 timesteps add_comp!(m, grosseconomy, first = 2020) add_comp!(m, emissions, first = 2020) -# Set parameters for the grosseconomy component -set_param!(m, :grosseconomy, :l, [(1. + 0.015)^t *6404 for t in 1:20]) -set_param!(m, :grosseconomy, :tfp, [(1 + 0.065)^t * 3.57 for t in 1:20]) -set_param!(m, :grosseconomy, :s, ones(20).* 0.22) -set_param!(m, :grosseconomy, :depk, 0.1) -set_param!(m, :grosseconomy, :k0, 130.) -set_param!(m, :grosseconomy, :share, 0.3) +# update parameters for the grosseconomy component +update_param!(m, :grosseconomy, :l, [(1. + 0.015)^t *6404 for t in 1:20]) +update_param!(m, :grosseconomy, :tfp, [(1 + 0.065)^t * 3.57 for t in 1:20]) +update_param!(m, :grosseconomy, :s, ones(20).* 0.22) +update_param!(m, :grosseconomy, :depk, 0.1) +update_param!(m, :grosseconomy, :k0, 130.) +update_param!(m, :grosseconomy, :share, 0.3) -# Set parameters for the emissions component -set_param!(m, :emissions, :sigma, [(1. - 0.05)^t *0.58 for t in 1:20]) +# update and connect the parameters for the emissions component +update_param!(m, :emissions, :sigma, [(1. - 0.05)^t *0.58 for t in 1:20]) connect_param!(m, :emissions, :YGROSS, :grosseconomy, :YGROSS) run(m) @@ -140,16 +140,16 @@ set_dimension!(m, :time, collect(2015:5:2110)) # 20 timesteps add_comp!(m, grosseconomy, last = 2105) add_comp!(m, emissions, last = 2105) -# Set parameters for the grosseconomy component -set_param!(m, :grosseconomy, :l, [(1. + 0.015)^t *6404 for t in 1:20]) -set_param!(m, :grosseconomy, :tfp, [(1 + 0.065)^t * 3.57 for t in 1:20]) -set_param!(m, :grosseconomy, :s, ones(20).* 0.22) -set_param!(m, :grosseconomy, :depk, 0.1) -set_param!(m, :grosseconomy, :k0, 130.) -set_param!(m, :grosseconomy, :share, 0.3) +# update parameters for the grosseconomy component +update_param!(m, :grosseconomy, :l, [(1. + 0.015)^t *6404 for t in 1:20]) +update_param!(m, :grosseconomy, :tfp, [(1 + 0.065)^t * 3.57 for t in 1:20]) +update_param!(m, :grosseconomy, :s, ones(20).* 0.22) +update_param!(m, :grosseconomy, :depk, 0.1) +update_param!(m, :grosseconomy, :k0, 130.) +update_param!(m, :grosseconomy, :share, 0.3) -# Set parameters for the emissions component -set_param!(m, :emissions, :sigma, [(1. - 0.05)^t *0.58 for t in 1:20]) +# update and connect parameters for the emissions component +update_param!(m, :emissions, :sigma, [(1. - 0.05)^t *0.58 for t in 1:20]) connect_param!(m, :emissions, :YGROSS, :grosseconomy, :YGROSS) run(m) @@ -407,11 +407,11 @@ m = Model() set_dimension!(m, :time, 2005:2020) add_comp!(m, top, nameof(top)) -set_param!(m, :fooA1, 1) -set_param!(m, :fooA2, 2) -set_param!(m, :foo3, 10) -set_param!(m, :foo4, 20) -set_param!(m, :par_1_1, collect(1:length(time_labels(m)))) +update_param!(m, :top, :fooA1, 1) +update_param!(m, :top, :fooA2, 2) +update_param!(m, :top, :foo3, 10) +update_param!(m, :top, :foo4, 20) +update_param!(m, :top, :par_1_1, collect(1:length(time_labels(m)))) run(m) diff --git a/test/test_getdataframe.jl b/test/test_getdataframe.jl index 812b42a4c..b6f505c0b 100644 --- a/test/test_getdataframe.jl +++ b/test/test_getdataframe.jl @@ -37,14 +37,14 @@ years = collect(2015:5:2110) set_dimension!(model1, :time, years) add_comp!(model1, testcomp1) -set_param!(model1, :testcomp1, :par1, years) -set_param!(model1, :testcomp1, :par_scalar, 5.) +update_param!(model1, :testcomp1, :par1, years) +update_param!(model1, :testcomp1, :par_scalar, 5.) add_comp!(model1, testcomp2) -@test_throws ErrorException set_param!(model1, :testcomp2, :par2, late_first:5:early_last) -@test ! (:par2 in keys(model1.md.external_params)) # Test that after the previous error, the :par2 didn't stay in the model's parameter list -set_param!(model1, :testcomp2, :par2, years) +@test_throws ErrorException update_param!(model1, :testcomp2, :par2, late_first:5:early_last) +@test ! (:par2 in keys(model1.md.model_params)) # Test that after the previous error, the :par2 didn't stay in the model's parameter list +update_param!(model1, :testcomp2, :par2, years) # Test running before model built @test_throws ErrorException df = getdataframe(model1, :testcomp1, :var1) @@ -101,7 +101,7 @@ data = Array{Int}(undef, nyears, nregions, nrates) data[:] = 1:(nyears * nregions * nrates) add_comp!(model2, testcomp3) -set_param!(model2, :testcomp3, :par3, data) +update_param!(model2, :testcomp3, :par3, data) run(model2) @@ -136,7 +136,7 @@ par3 = Array{Union{Missing,Float64}}(undef, nyears, nregions, nrates) par3[:] .= missing par3[valid_indices, :, :] = 1:(nindices * nregions * nrates) -set_param!(model3, :testcomp3, :par3, par3) +update_param!(model3, :testcomp3, :par3, par3) run(model3) df3 = getdataframe(model3, :testcomp3 => :par3) diff --git a/test/test_getindex.jl b/test/test_getindex.jl index 18a0a1747..8b9e738b1 100644 --- a/test/test_getindex.jl +++ b/test/test_getindex.jl @@ -39,7 +39,7 @@ add_comp!(my_model, testcomp1) par = collect(2015:5:2110) -set_param!(my_model, :testcomp1, :par1, par) +update_param!(my_model, :testcomp1, :par1, par) run(my_model) # Regular get index diff --git a/test/test_getindex_variabletimestep.jl b/test/test_getindex_variabletimestep.jl index 3fd3e5ec1..e7b8e1cae 100644 --- a/test/test_getindex_variabletimestep.jl +++ b/test/test_getindex_variabletimestep.jl @@ -39,7 +39,7 @@ add_comp!(my_model, testcomp1) par = collect([2000:1:2014; 2015:5:2110]) -set_param!(my_model, :testcomp1, :par1, par) +update_param!(my_model, :testcomp1, :par1, par) run(my_model) # Regular get index diff --git a/test/test_main.jl b/test/test_main.jl index ef79ef62d..d17c1145b 100644 --- a/test/test_main.jl +++ b/test/test_main.jl @@ -5,7 +5,7 @@ using Mimi import Mimi: reset_variables, - variable, variable_names, external_param, + variable, variable_names, model_param, compdefs, dimension, compinstance @defcomp foo1 begin @@ -31,20 +31,21 @@ set_dimension!(x1, :time, 2010:10:2030) set_dimension!(x1, :idx3, 1:3) set_dimension!(x1, :idx4, 1:4) add_comp!(x1, foo1) -set_param!(x1, :foo1, :par1, 5.0) +update_param!(x1, :foo1, :par1, 5.0) @test length(dimension(x1.md, :index1)) == 3 -par1 = external_param(x1, :par1) +@test_throws ErrorException par1 = model_param(x1, :par1) # not shared +par1 = model_param(x1, :foo1, :par1) @test par1.value == 5.0 -update_param!(x1, :par1, 6.0) -par1 = external_param(x1, :par1) +@test_throws ErrorException update_param!(x1, :par1, 6.0) # not shared +update_param!(x1, :foo1, :par1, 6.0) +par1 = model_param(x1, :foo1, :par1) @test par1.value == 6.0 -set_param!(x1, :foo1, :par2, [true true false; true false false; true true true]) - -set_param!(x1, :foo1, :par3, [1.0, 2.0, 3.0]) +update_param!(x1, :foo1, :par2, [true true false; true false false; true true true]) +update_param!(x1, :foo1, :par3, [1.0, 2.0, 3.0]) Mimi.build!(x1) @@ -67,23 +68,44 @@ set_dimension!(m, :time, 2010:10:2030) set_dimension!(m, :idx3, 1:3) set_dimension!(m, :idx4, 1:4) add_comp!(m, foo1) -set_param!(m, :par1, 6.0) -set_param!(m, :par2, [true true false; true false false; true true true]) -set_param!(m, :par3, [1.0, 2.0, 3.0]) + +update_param!(m, :foo1, :par1, 6.0) +update_param!(m, :foo1, :par2, [true true false; true false false; true true true]) +update_param!(m, :foo1, :par3, [1.0, 2.0, 3.0]) run(m) @test m.md.dirty == false -update_param!(m, :par1, 7.0) +update_param!(m, :foo1, :par1, 7.0) @test m.md.dirty == true # should dirty the model run(m) mi = Mimi.build(m) + par1 = 6.0 par2 = [false false false; false false false; false false false] par3 = [3.0, 2.0, 1.0]; -update_param!(mi, :par1, par1) -update_param!(mi, :par2, par2) -update_param!(mi, :par3, par3) + +@test_throws KeyError update_param!(mi, :par1, par1) # not shared +@test_throws KeyError update_param!(mi, :par2, par2) # not shared +@test_throws KeyError update_param!(mi, :par3, par3) # not shared + +update_param!(mi, Mimi.get_model_param_name(m, :foo1, :par1), par1) +update_param!(mi, Mimi.get_model_param_name(m, :foo1, :par2), par2) +update_param!(mi, Mimi.get_model_param_name(m, :foo1, :par3), par3) + +@test mi[:foo1, :par1] == par1 +@test mi[:foo1, :par2] == par2 +@test mi[:foo1, :par3] == par3 +@test m.md.dirty == false # should not dirty the model + +par1 = 7.0 +par2 = [true false false; true false false; true false false] +par3 = [1.0, 2.0, 3.0]; + +update_param!(mi, :foo1, :par1, par1) +update_param!(mi, :foo1, :par2, par2) +update_param!(mi, :foo1, :par3, par3) + @test mi[:foo1, :par1] == par1 @test mi[:foo1, :par2] == par2 @test mi[:foo1, :par3] == par3 diff --git a/test/test_main_variabletimestep.jl b/test/test_main_variabletimestep.jl index a389611f5..fe93a713a 100644 --- a/test/test_main_variabletimestep.jl +++ b/test/test_main_variabletimestep.jl @@ -5,7 +5,7 @@ using Mimi import Mimi: reset_variables, - variable, variable_names, external_param, + variable, variable_names, model_param, compdef, compdefs, dimension, compinstance @defcomp foo1 begin @@ -31,20 +31,21 @@ set_dimension!(x1, :time, [2010, 2015, 2030]) set_dimension!(x1, :idx3, 1:3) set_dimension!(x1, :idx4, 1:4) add_comp!(x1, foo1) -set_param!(x1, :foo1, :par1, 5.0) +update_param!(x1, :foo1, :par1, 5.0) @test length(dimension(x1.md, :index1)) == 3 -par1 = external_param(x1, :par1) +@test_throws ErrorException par1 = model_param(x1, :par1) # unshared +par1 = model_param(x1, :foo1, :par1) @test par1.value == 5.0 -update_param!(x1, :par1, 6.0) -par1 = external_param(x1, :par1) +update_param!(x1, :foo1, :par1, 6.0) +par1 = model_param(x1, :foo1, :par1) @test par1.value == 6.0 -set_param!(x1, :foo1, :par2, [true true false; true false false; true true true]) +update_param!(x1, :foo1, :par2, [true true false; true false false; true true true]) -set_param!(x1, :foo1, :par3, [1.0, 2.0, 3.0]) +update_param!(x1, :foo1, :par3, [1.0, 2.0, 3.0]) Mimi.build!(x1) diff --git a/test/test_marginal_models.jl b/test/test_marginal_models.jl index 0d9d423e7..ab879f568 100644 --- a/test/test_marginal_models.jl +++ b/test/test_marginal_models.jl @@ -18,12 +18,13 @@ x2 = collect(2:2:20) model1 = Model() set_dimension!(model1, :time, collect(1:10)) add_comp!(model1, compA) -set_param!(model1, :compA, :parA, x1) +update_param!(model1, :compA, :parA, x1) mm = MarginalModel(model1, .5) model2 = mm.modified -update_param!(model2, :parA, x2) +add_shared_param!(model2, :parA, x2; dims = [:time]) +connect_param!(model2, :compA, :parA, :parA) run(mm) @@ -35,7 +36,7 @@ mm2 = create_marginal_model(model1, 0.5) @test_throws ErrorException mm2_modified = mm2.marginal # test that trying to access by the old field name, "marginal", now errors mm2_modified = mm2.modified -update_param!(mm2_modified, :parA, x2) +update_param!(mm2_modified, :compA, :parA, x2) run(mm2) diff --git a/test/test_model_structure.jl b/test/test_model_structure.jl index 1c26c7ea0..67136c1a4 100644 --- a/test/test_model_structure.jl +++ b/test/test_model_structure.jl @@ -8,7 +8,8 @@ using Mimi import Mimi: connect_param!, unconnected_params, set_dimension!, get_connections, internal_param_conns, dim_count, dim_names, - modeldef, modelinstance, compdef, getproperty, setproperty!, dimension, compdefs + modeldef, modelinstance, compdef, getproperty, setproperty!, dimension, + nothing_params, compdefs @defcomp A begin varA = Variable{Int}(index=[time]) @@ -56,11 +57,15 @@ add_comp!(m, C, after=:B) connect_param!(m, :A, :parA, :C, :varC) unconns = unconnected_params(m) -@test length(unconns) == 1 +@test length(unconns) == 0 + +nothingparams = nothing_params(m) +@test length(nothingparams) == 1 + c = compdef(m, :C) -uconn = unconns[1] -@test uconn.comp_name == :C -@test uconn.datum_name == :parC +nothingparam = nothingparams[1] +@test nothingparam.comp_name == :C +@test nothingparam.datum_name == :parC connect_param!(m, :C => :parC, :B => :varB) @@ -76,8 +81,7 @@ c = compdef(m, :C) @test get_connections(m, :B, :outgoing)[1].dst_comp_path == c.comp_path @test length(get_connections(m, :A, :all)) == 1 - -@test length(unconnected_params(m)) == 0 +@test length(nothing_params(m)) == 0 run(m) @@ -162,8 +166,8 @@ m = Model() set_dimension!(m, :time, 2015:5:2100) add_comp!(m, E) -set_param!(m, :E, :parE1, 1) -set_param!(m, :E, :parE2, 10) +update_param!(m, :E, :parE1, 1) +update_param!(m, :E, :parE2, 10) run(m) @test m[:E, :varE] == 10 diff --git a/test/test_model_structure_variabletimestep.jl b/test/test_model_structure_variabletimestep.jl index b662e662d..2b3bc7300 100644 --- a/test/test_model_structure_variabletimestep.jl +++ b/test/test_model_structure_variabletimestep.jl @@ -8,7 +8,8 @@ using Mimi import Mimi: connect_param!, unconnected_params, set_dimension!, has_comp, get_connections, internal_param_conns, dim_count, - dim_names, compdef, getproperty, setproperty!, dimension, compdefs + dim_names, compdef, getproperty, setproperty!, dimension, compdefs, + nothing_params @defcomp A begin varA = Variable{Int}(index=[time]) @@ -57,13 +58,16 @@ add_comp!(m, C, after=:B) # test a later first than model # Component order is B -> C -> A. connect_param!(m, :A, :parA, :C, :varC) - unconns = unconnected_params(m) -@test length(unconns) == 1 +@test length(unconns) == 0 + +nothingparams = nothing_params(m) +@test length(nothingparams) == 1 + c = compdef(m, :C) -uconn = unconns[1] -@test uconn.comp_name == :C -@test uconn.datum_name == :parC +nothingparam = nothingparams[1] +@test nothingparam.comp_name == :C +@test nothingparam.datum_name == :parC connect_param!(m, :C => :parC, :B => :varB) @@ -81,7 +85,7 @@ c = compdef(m, :C) @test length(get_connections(m, :A, :all)) == 1 -@test length(unconnected_params(m)) == 0 +@test length(nothing_params(m)) == 0 ############################################# # Tests for connecting scalar parameters # diff --git a/test/test_mult_getdataframe.jl b/test/test_mult_getdataframe.jl index d794f92ac..e46631d3a 100644 --- a/test/test_mult_getdataframe.jl +++ b/test/test_mult_getdataframe.jl @@ -103,15 +103,15 @@ function run_my_model() add_comp!(my_model, grosseconomy) add_comp!(my_model, emissions) - set_param!(my_model, :grosseconomy, :l, l) - set_param!(my_model, :grosseconomy, :tfp, tfp) - set_param!(my_model, :grosseconomy, :s, s) - set_param!(my_model, :grosseconomy, :depk,depk) - set_param!(my_model, :grosseconomy, :k0, k0) - set_param!(my_model, :grosseconomy, :share, 0.3) + update_param!(my_model, :grosseconomy, :l, l) + update_param!(my_model, :grosseconomy, :tfp, tfp) + update_param!(my_model, :grosseconomy, :s, s) + update_param!(my_model, :grosseconomy, :depk,depk) + update_param!(my_model, :grosseconomy, :k0, k0) + update_param!(my_model, :grosseconomy, :share, 0.3) #set parameters for emissions component - set_param!(my_model, :emissions, :sigma, sigma2) + update_param!(my_model, :emissions, :sigma, sigma2) connect_param!(my_model, :emissions, :YGROSS, :grosseconomy, :YGROSS) run(my_model) @@ -186,7 +186,7 @@ end par = collect(2015:5:2110) add_comp!(my_model, testcomp1) -set_param!(my_model, :testcomp1, :par1, par) +update_param!(my_model, :testcomp1, :par1, par) run(my_model) #Regular getdataframe diff --git a/test/test_multiplier.jl b/test/test_multiplier.jl index 32075bae1..6272c8dff 100644 --- a/test/test_multiplier.jl +++ b/test/test_multiplier.jl @@ -14,8 +14,8 @@ add_comp!(model1, Mimi.multiplier) x = collect(1:10) y = collect(2:2:20) -set_param!(model1, :multiplier, :input, x) -set_param!(model1, :multiplier, :multiply, y) +update_param!(model1, :multiplier, :input, x) +update_param!(model1, :multiplier, :multiply, y) run(model1) @@ -28,8 +28,8 @@ run(model1) model2 = Model() set_dimension!(model2, :time, 1:10) add_comp!(model2, Mimi.multiplier, :compA) -set_param!(model2, :compA, :input, x) -set_param!(model2, :compA, :multiply, y) +update_param!(model2, :compA, :input, x) +update_param!(model2, :compA, :multiply, y) run(model2) @test model2[:compA, :output] == x.*y diff --git a/test/test_new_paramAPI.jl b/test/test_new_paramAPI.jl new file mode 100644 index 000000000..02a86b465 --- /dev/null +++ b/test/test_new_paramAPI.jl @@ -0,0 +1,354 @@ +## Testing the New Parameter API + +import Mimi: model_param, is_shared + +# +# Section 1. update_param!, add_shared_param! and connect_param! +# + +@defcomp A begin + + p1 = Parameter{Symbol}() + p2 = Parameter(default = 2) + p3 = Parameter() + p4 = Parameter(unit = "dollars") + p5 = Parameter(unit = "\$") + p6 = Parameter(index = [time]) + p7 = Parameter(index = [regions, time]) + + function run_timestep(p,v,d,t) + end +end + +function _get_model() + m = Model() + set_dimension!(m, :time, 1:5); + set_dimension!(m, :regions, [:R1, :R2, :R3]) + add_comp!(m, A) + return m +end + +# General Functionality +m = _get_model() + +@test_throws MethodError update_param!(m, :A, :p1, 3) # can't convert +update_param!(m, :A, :p1, :hello) +@test model_param(m, :A, :p1).value == :hello +add_shared_param!(m, :p1_fail, 3) +@test_throws ErrorException connect_param!(m, :A, :p1, :p1_fail) # we throw specific error here +add_shared_param!(m, :p1, :goodbye) +connect_param!(m, :A, :p1, :p1) +@test model_param(m, :A, :p1).value == :goodbye +@test_throws ErrorException update_param!(m, :A, :p1, :foo) # can't call this method on a shared parameter +update_param!(m, :p1, :foo) +@test model_param(m, :A, :p1).value == :foo +disconnect_param!(m, :A, :p1) +update_param!(m, :A, :p1, :foo) # now we can update :p1 with this syntax since it was disconnected +update_param!(m, :p1, :bar) # this is the shared parameter named :p1 +@test model_param(m, :A, :p1).value == :foo +@test model_param(m, :p1).value == :bar + +m = _get_model() + +add_shared_param!(m, :shared_param, 100) +connect_param!(m, :A, :p2, :shared_param) +connect_param!(m, :A, :p3, :shared_param) +@test model_param(m, :A, :p2).value == model_param(m, :A, :p3).value == 100 + +# still error because they are connected to a shared parameter +@test_throws ErrorException update_param!(m, :A, :p2, 1) +@test_throws ErrorException update_param!(m, :A, :p3, 2) + +disconnect_param!(m, :A, :p2) +update_param!(m, :A, :p2, 1) +model_param(m, :A, :p2).value == 1 +model_param(m, :A, :p3).value == model_param(m, :shared_param).value == 100 + +# Defaults +m = _get_model() + +@test model_param(m, :A, :p2).value == 2 +@test !(is_shared(model_param(m, :A, :p2))) +update_param!(m, :A, :p2, 100) +@test !(is_shared(model_param(m, :A, :p2))) + +# Dimensions +m = _get_model() + +@test_throws ErrorException add_shared_param!(m, :x, [1:10]) # need dimensions to be specified +@test_throws ErrorException add_shared_param!(m, :x, [1:10], dims = [:time]) # wrong dimensions +add_shared_param!(m, :x, 1:5, dims = [:time]) + +@test_throws ErrorException add_shared_param!(m, :y, fill(1, 3, 5)) # need dimensions to be specified +@test_throws ErrorException add_shared_param!(m, :y, fill(1, 3, 5), dims = [:time, :regions]) # need dimensions to be specified +add_shared_param!(m, :y, fill(1, 5, 3), dims = [:time, :regions]) + +@test_throws ErrorException connect_param!(m, :A, :p7, :y) # wrong dimensions, flipped around + +# Units and Datatypes +m = _get_model() + +add_shared_param!(m, :myparam, 100) +connect_param!(m, :A, :p3, :myparam) +@test_throws ErrorException connect_param!(m, :A, :p4, :myparam) # units error +connect_param!(m, :A, :p4, :myparam; ignoreunits = true) +@test model_param(m, :A, :p3).value == model_param(m, :A, :p4).value == 100 +@test_throws ErrorException update_param!(m, :myparam, :boo) # cannot convert +update_param!(m, :myparam, 200) +@test model_param(m, :A, :p3).value == model_param(m, :A, :p4).value == 200 +@test_throws ErrorException connect_param!(m, :A, :p3, :myparam) # units error + +# +# Section 2. add_shared_param! defaults +# + +@defcomp A begin + pA1 = Parameter{Symbol}() # type will by Symbol + pA2 = Parameter() # type will be Number + function run_timestep(p,v,d,t) + end +end + +@defcomp B begin + pB1 = Parameter{Number}() # type will be Number + pB2 = Parameter{Int64}() # type will be Int64 + function run_timestep(p,v,d,t) + end +end + +function _get_model() + m = Model() + set_dimension!(m, :time, 1:5); + add_comp!(m, A) + add_comp!(m, B) + return m +end + +# typical behavior +m = _get_model() +add_shared_param!(m, :myparam, 5) +@test model_param(m, :myparam) isa Mimi.ScalarModelParameter{Float64} # by default same as model, which defaults to number_type(m) == Float64 + +exp = :(connect_param!(m, :A, :pA1, :myparam)) # pA1 should have a specified parameter type of Symbol and !(Float64 <: Symbol) +myerr1 = try eval(exp) catch err err end +@test occursin("Mismatched datatype of parameter connection", sprint(showerror, myerr1)) + +connect_param!(m, :A, :pA2, :myparam) # pA2 should have a parameter type of Number by default and Float64 <: Number +connect_param!(m, :B, :pB1, :myparam) # pB1 should have a specified parameter type of Number and Float64 <: Number + +exp = :(connect_param!(m, :B, :pB2, :myparam)) # pB2 should have a specified parameter type of Int64 and !(Float64 <: Int64) +myerr2 = try eval(exp) catch err err end +@test occursin("Mismatched datatype of parameter connection", sprint(showerror, myerr2)) + +# try data_type keyword argument +m = _get_model() # number_type(m) == Float64 + +exp = :(add_shared_param!(m, :myparam, :foo; data_type = Int64)) # !(:foo isa Int64) +myerr3 = try eval(exp) catch err err end +@test occursin("Mismatched datatypes:", sprint(showerror, myerr3)) + +add_shared_param!(m, :myparam, 5; data_type = Int64) +@test model_param(m, :myparam) isa Mimi.ScalarModelParameter{Int64} # 5 is convertible to Int64 + +connect_param!(m, :B, :pB2, :myparam) # pB2 should have a specified parameter type of Int64 and Int64 <: Int64 +connect_param!(m, :A, :pA2, :myparam) # we allow pB2 and pA2 types to conflict as long as they both passed compatibilty with the model parameter + +# +# Section 2. update_leftover_params! and set_leftover_params! +# + +@defcomp A begin + + p1 = Parameter{Symbol}() + p2 = Parameter(default = 100) + p3 = Parameter() + + function run_timestep(p,v,d,t) + end +end + +@defcomp B begin + + p1 = Parameter{Symbol}() + p2 = Parameter() + p3 = Parameter() + p4 = Parameter() + function run_timestep(p,v,d,t) + end +end + +function _get_model() + m = Model() + set_dimension!(m, :time, 1:5); + add_comp!(m, A) + add_comp!(m, B) + return m +end + +# +# set_leftover_params! +# + +m = _get_model() + +# wrong type (p1 must be a Symbol) +m = _get_model() +parameters = Dict("p1" => 1, "p2" => 2, "p3" => 3, "p4" => 4) +fail_expr1 = :(set_leftover_params!(m, parameters)) +err1 = try eval(fail_expr1) catch err err end +@test occursin("Cannot `convert`", sprint(showerror, err1)) + +# missing entry (missing p4) +m = _get_model() +parameters = Dict("p1" => :foo, "p2" => 2, "p3" => 3) +fail_expr2 = :(set_leftover_params!(m, parameters)) +err2 = try eval(fail_expr2) catch err err end +@test occursin("not found in provided dictionary", sprint(showerror, err2)) + +# successful calls +m = _get_model() +parameters = Dict(:p1 => :foo, "p2" => 2, :p3 => 3, "p4" => 4) # keys can be Symbols or Strings +set_leftover_params!(m, parameters) +run(m) +@test m[:A, :p1] == m[:B, :p1] == :foo +@test model_param(m, :p1).is_shared + +@test m[:A, :p2] == 100 # remained default value +@test !model_param(m, :A, :p2).is_shared # remains its default so is not shared + +@test m[:B, :p2] == 2 # took on shared value +@test model_param(m, :p2).is_shared + +@test m[:A, :p3] == m[:B, :p3] == 3 +@test model_param(m, :p3).is_shared + +@test m[:B, :p4] == 4 +@test model_param(m, :p4).is_shared + + +# +# update_leftover_params! +# + +# wrong type (p1 must be a Symbol) +m = _get_model() +parameters = Dict( (:A, :p1) => 1, (:B, :p1) => 10, + (:B, :p2) => 20, + (:A, :p3) => 3, (:B, :p3) => 30, + (:A, :p4) => 4, (:B, :p4) => 40 + ) +fail_expr3 = :(update_leftover_params!(m, parameters)) +err3 = try eval(fail_expr3) catch err err end +@test occursin("Cannot `convert`", sprint(showerror, err3)) + +# missing entry (missing B's p4) +m = _get_model() +parameters = Dict( (:A, :p1) => :foo, (:B, :p1) => :bar, + (:B, :p2) => 20, + (:A, :p3) => 3, (:B, :p3) => 30, + (:A, :p4) => 4 + ) +fail_expr4 = :(update_leftover_params!(m, parameters)) +err4 = try eval(fail_expr4) catch err err end +@test occursin("not found in provided dictionary", sprint(showerror, err4)) + +# successful calls +m = _get_model() +parameters = Dict( (:A, :p1) => :foo, (:B, "p1") => :bar, + (:B, :p2) => 20, + (:A, :p3) => 3, (:B, :p3) => 30, + (:A, "p4") => 4, (:B, :p4) => 40 + ) +update_leftover_params!(m, parameters) +run(m) +@test m[:A, :p1] == :foo && m[:B, :p1] == :bar +@test !model_param(m, :A, :p1).is_shared && !model_param(m, :B, :p1).is_shared +@test isnothing(model_param(m, :p1, missing_ok = true)) # no shared model parameter created + +@test m[:A, :p2] == 100 # remained default value +@test !model_param(m, :A, :p2).is_shared # remains its default so is not shared + +@test m[:B, :p2] == 20 # took on shared value +@test !model_param(m, :B, :p2).is_shared + +@test isnothing(model_param(m, :p2, missing_ok = true)) # no shared model parameter created + +@test m[:A, :p3] == 3 && m[:B, :p3] == 30 +@test !model_param(m, :A, :p3).is_shared && !model_param(m, :B, :p3).is_shared +@test isnothing(model_param(m, :p3, missing_ok = true)) # no shared model parameter created + +@test m[:B, :p4] == 40 +@test !model_param(m, :B, :p4).is_shared +@test isnothing(model_param(m, :p4, missing_ok = true)) # no shared model parameter created + +# +# Section 3. update_params! +# + +@defcomp A begin + + p1 = Parameter(default = 0) + p2 = Parameter(default = 0) + p3 = Parameter() + p4 = Parameter() + p5 = Parameter() + p6 = Parameter() + + function run_timestep(p,v,d,t) + end +end + +function _get_model() + m = Model() + set_dimension!(m, :time, 1:5); + add_comp!(m, A) + + add_shared_param!(m, :shared_param, 0) + connect_param!(m, :A, :p3, :shared_param) + connect_param!(m, :A, :p4, :shared_param) + + return m +end + +# update the shared parameters and unshared parameters separately +m = _get_model() + +shared_dict = Dict(:shared_param => 1) +update_params!(m, shared_dict) + +unshared_dict = Dict((:A, :p5) => 2, (:A, :p6) => 3) +update_params!(m, unshared_dict) + +run(m) +@test m[:A, :p3] == m[:A, :p4] == 1 +@test m[:A, :p5] == 2 +@test m[:A, :p6] == 3 + +# update both at the same time +m = _get_model() + +dict = Dict(:shared_param => 1, (:A, :p5) => 2, (:A, :p6) => 3) +update_params!(m, dict) + +run(m) +@test m[:A, :p3] == m[:A, :p4] == 1 +@test m[:A, :p5] == 2 +@test m[:A, :p6] == 3 + +# test failures + +m = _get_model() + +shared_dict = Dict(:shared_param => :foo) +@test_throws ErrorException update_params!(m, shared_dict) # units failure +shared_dict = Dict(:p3 => 3) +@test_throws ErrorException update_params!(m, shared_dict) # can't find parameter + +unshared_dict = Dict((:A, :p5) => :foo, (:A, :p6) => 3) +@test_throws MethodError update_params!(m, unshared_dict) # units failure +unshared_dict = Dict((:B, :p5) => 5) +@test_throws ErrorException update_params!(m, unshared_dict) # can't find component +unshared_dict = Dict((:B, :missing) => 5) +@test_throws ErrorException update_params!(m, unshared_dict) # can't find parameter + +nothing diff --git a/test/test_parameter_labels.jl b/test/test_parameter_labels.jl index 71b597e8e..7e3c7b530 100644 --- a/test/test_parameter_labels.jl +++ b/test/test_parameter_labels.jl @@ -43,13 +43,13 @@ model1 = Model() set_dimension!(model1, :time, time_labels) set_dimension!(model1, :regions, region_labels) add_comp!(model1, compA) -set_param!(model1, :compA, :x, x) +update_param!(model1, :compA, :x, x) model2 = Model() set_dimension!(model2, :time, time_labels) set_dimension!(model2, :regions, region_labels) add_comp!(model2, compA) -set_param!(model2, :compA, :x, x2) # should perform parameter dimension check +update_param!(model2, :compA, :x, x2) # should perform parameter dimension check run(model1) run(model2) @@ -161,15 +161,15 @@ function run_my_model() add_comp!(my_model, grosseconomy) add_comp!(my_model, emissions) - set_param!(my_model, :grosseconomy, :l, l) - set_param!(my_model, :grosseconomy, :tfp, tfp) - set_param!(my_model, :grosseconomy, :s, s) - set_param!(my_model, :grosseconomy, :depk, depk) - set_param!(my_model, :grosseconomy, :k0, k0) - set_param!(my_model, :grosseconomy, :share, 0.3) + update_param!(my_model, :grosseconomy, :l, l) + update_param!(my_model, :grosseconomy, :tfp, tfp) + update_param!(my_model, :grosseconomy, :s, s) + update_param!(my_model, :grosseconomy, :depk, depk) + update_param!(my_model, :grosseconomy, :k0, k0) + update_param!(my_model, :grosseconomy, :share, 0.3) #set parameters for emissions component - set_param!(my_model, :emissions, :sigma, sigma2) + update_param!(my_model, :emissions, :sigma, sigma2) connect_param!(my_model, :emissions, :YGROSS, :grosseconomy, :YGROSS) run(my_model) @@ -224,15 +224,15 @@ function run_my_model2() add_comp!(my_model2, grosseconomy) add_comp!(my_model2, emissions) - set_param!(my_model2, :grosseconomy, :l, l2) - set_param!(my_model2, :grosseconomy, :tfp, tfp2) - set_param!(my_model2, :grosseconomy, :s, s2) - set_param!(my_model2, :grosseconomy, :depk,depk2) - set_param!(my_model2, :grosseconomy, :k0, k02) - set_param!(my_model2, :grosseconomy, :share, 0.3) + update_param!(my_model2, :grosseconomy, :l, l2) + update_param!(my_model2, :grosseconomy, :tfp, tfp2) + update_param!(my_model2, :grosseconomy, :s, s2) + update_param!(my_model2, :grosseconomy, :depk,depk2) + update_param!(my_model2, :grosseconomy, :k0, k02) + update_param!(my_model2, :grosseconomy, :share, 0.3) #set parameters for emissions component - set_param!(my_model2, :emissions, :sigma, sigma2) + update_param!(my_model2, :emissions, :sigma, sigma2) connect_param!(my_model2, :emissions, :YGROSS, :grosseconomy, :YGROSS) run(my_model2) @@ -274,4 +274,23 @@ for t in 1:length(time_labels) end end + +###################################################### +# update_param! option with list of dimension names # +###################################################### + +model3 = Model() +set_dimension!(model3, :time, collect(2015:5:2110)) +set_dimension!(model3, :regions, ["Region1", "Region2", "Region3"]) +add_comp!(model3, compA) +add_shared_param!(model3, :x, x, dims = [:time, :regions]) +connect_param!(model3, :compA, :x, :x) +run(model3) + +for t in 1:length(time_labels) + for r in 1:length(region_labels) + @test(model1[:compA, :y][t, r] == model3[:compA, :y][t, r]) + end +end + end #module diff --git a/test/test_parametertypes.jl b/test/test_parametertypes.jl index 2156b4950..7ec7737c9 100644 --- a/test/test_parametertypes.jl +++ b/test/test_parametertypes.jl @@ -4,10 +4,28 @@ using Mimi using Test import Mimi: - external_params, external_param, TimestepMatrix, TimestepVector, + model_params, model_param, TimestepMatrix, TimestepVector, ArrayModelParameter, ScalarModelParameter, FixedTimestep, import_params!, set_first_last!, _get_param_times +# +# Test that simple constructors don't error +# + +values = [1,2,3] +dim_names = [:time] +shared = true +p1 = ArrayModelParameter(values, dim_names, shared) +p2 = ArrayModelParameter(values, dim_names) +@test p1.values == p2.values == values +@test p1.dim_names == p2.dim_names == dim_names +@test Mimi.is_shared(p1) && !Mimi.is_shared(p2) + +p3 = ScalarModelParameter(3, shared) +p4 = ScalarModelParameter(3) +@test p3.value == p4.value == 3 +@test Mimi.is_shared(p3) && !Mimi.is_shared(p4) + # # Test that parameter type mismatches are caught # @@ -68,55 +86,63 @@ set_dimension!(m, :regions, 3) set_dimension!(m, :four, 4) add_comp!(m, MyComp) -set_param!(m, :MyComp, :c, [4,5,6]) -set_param!(m, :MyComp, :d, 0.5) # 32-bit float constant -set_param!(m, :MyComp, :e, [1,2,3,4]) -set_param!(m, :MyComp, :f, reshape(1:16, 4, 4)) -set_param!(m, :MyComp, :j, [1,2,3]) - -Mimi.build!(m) # applies defaults, creating external params in the model instance's copied definition -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) -@test isa(extpars[:e], ArrayModelParameter) -@test isa(extpars[:f], ScalarModelParameter) # note that :f is stored as a scalar parameter even though its values are an array - -@test typeof(extpars[:a].values) == TimestepMatrix{FixedTimestep{2000, 1, 2100}, arrtype, 1, Array{arrtype, 2}} -@test typeof(extpars[:b].values) == TimestepVector{FixedTimestep{2000, 1, 2100}, arrtype, Array{arrtype, 1}} - -@test typeof(extpars[:c].values) == Array{arrtype, 1} -@test typeof(extpars[:d].value) == numtype -@test typeof(extpars[:e].values) == Array{arrtype, 1} -@test typeof(extpars[:f].value) == Array{Float64, 2} -@test typeof(extpars[:g].value) <: Int -@test typeof(extpars[:h].value) == numtype +update_param!(m, :MyComp, :c, [4,5,6]) +update_param!(m, :MyComp, :d, 0.5) # 32-bit float constant +update_param!(m, :MyComp, :e, [1,2,3,4]) +update_param!(m, :MyComp, :f, reshape(1:16, 4, 4)) +update_param!(m, :MyComp, :j, [1,2,3]) + +Mimi.build!(m) +extpars = model_params(m.mi.md) + +a_sym = Mimi.get_model_param_name(m.mi.md, :MyComp, :a) +b_sym = Mimi.get_model_param_name(m.mi.md, :MyComp, :b) +c_sym = Mimi.get_model_param_name(m.mi.md, :MyComp, :c) +d_sym = Mimi.get_model_param_name(m.mi.md, :MyComp, :d) +e_sym = Mimi.get_model_param_name(m.mi.md, :MyComp, :e) +f_sym = Mimi.get_model_param_name(m.mi.md, :MyComp, :f) +g_sym = Mimi.get_model_param_name(m.mi.md, :MyComp, :g) +h_sym = Mimi.get_model_param_name(m.mi.md, :MyComp, :h) + +@test isa(extpars[a_sym], ArrayModelParameter) +@test isa(extpars[b_sym], ArrayModelParameter) +@test _get_param_times(extpars[a_sym]) == _get_param_times(extpars[b_sym]) == 2000:2100 + +@test isa(extpars[c_sym], ArrayModelParameter) +@test isa(extpars[d_sym], ScalarModelParameter) +@test isa(extpars[e_sym], ArrayModelParameter) +@test isa(extpars[f_sym], ScalarModelParameter) # note that :f is stored as a scalar parameter even though its values are an array + +@test typeof(extpars[a_sym].values) == TimestepMatrix{FixedTimestep{2000, 1, 2100}, arrtype, 1, Array{arrtype, 2}} +@test typeof(extpars[b_sym].values) == TimestepVector{FixedTimestep{2000, 1, 2100}, arrtype, Array{arrtype, 1}} + +@test typeof(extpars[c_sym].values) == Array{arrtype, 1} +@test typeof(extpars[d_sym].value) == numtype +@test typeof(extpars[e_sym].values) == Array{arrtype, 1} +@test typeof(extpars[f_sym].value) == Array{Float64, 2} +@test typeof(extpars[g_sym].value) <: Int +@test typeof(extpars[h_sym].value) == numtype # test updating parameters @test_throws ErrorException update_param!(m, :a, 5) # expects an array @test_throws ErrorException update_param!(m, :a, ones(101)) # wrong size @test_throws ErrorException update_param!(m, :a, fill("hi", 101, 3)) # wrong type -set_param!(m, :a, Array{Int,2}(zeros(101, 3))) # should be able to convert from Int to Float -@test_throws ErrorException update_param!(m, :d, ones(5)) # wrong type; should be scalar -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 dictionary has the updated value +update_param!(m, :MyComp, :a, Array{Int,2}(zeros(101, 3))) # should be able to convert from Int to Float +@test_throws ErrorException update_param!(m, :MyComp, :d, ones(5)) # wrong type; should be scalar +update_param!(m, :MyComp, :d, 5) # should work, will convert to float +new_extpars = model_params(m) # Since there are changes since the last build, need to access the updated dictionary in the model definition +@test extpars[d_sym].value == 0.5 # The original dictionary still has the old value +@test new_extpars[d_sym].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]) +update_param!(m, :MyComp, :e, [4,5,6,7]) -@test length(extpars) == 9 # The old dictionary has the default values that were added during build, so it has more entries -@test length(new_extpars) == 6 -@test typeof(new_extpars[:a].values) == TimestepMatrix{FixedTimestep{2000, 1, 2100}, arrtype, 1, Array{arrtype, 2}} +@test length(extpars) == length(new_extpars) == 9 # we replaced the unshared default for :a with a shared for :a +@test typeof(new_extpars[a_sym].values) == TimestepMatrix{FixedTimestep{2000, 1, 2100}, arrtype, 1, Array{arrtype, 2}} -@test typeof(new_extpars[:d].value) == numtype -@test typeof(new_extpars[:e].values) == Array{arrtype, 1} +@test typeof(new_extpars[d_sym].value) == numtype +@test typeof(new_extpars[e_sym].values) == Array{arrtype, 1} #------------------------------------------------------------------------------ @@ -136,7 +162,7 @@ end 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]) +update_param!(m, :MyComp2, :x, [1, 2, 3, 4, 5]) # Year x Model MyComp2 # 2000 1 first # 2001 2 first @@ -144,9 +170,9 @@ set_param!(m, :MyComp2, :x, [1, 2, 3, 4, 5]) # 2003 4 last # 2004 5 last -update_param!(m, :x, [2.,3.,4.,5.,6.]) -update_param!(m, :x, zeros(5)) -update_param!(m, :x, [1,2,3,4,5]) +update_param!(m, :MyComp2, :x, [2.,3.,4.,5.,6.]) +update_param!(m, :MyComp2, :x, zeros(5)) +update_param!(m, :MyComp2, :x, [1,2,3,4,5]) set_dimension!(m, :time, 1999:2001) # Year x Model MyComp2 @@ -154,19 +180,19 @@ set_dimension!(m, :time, 1999:2001) # 2000 1 # 2001 2 last first, last -x = external_param(m.md, :x) +x = model_param(m, :MyComp2, :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 +update_param!(m, :MyComp2, :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) +x = model_param(m, :MyComp2, :x) @test x.values isa Mimi.TimestepArray{Mimi.FixedTimestep{1999, 1, 2001}, Union{Missing,Float64}, 1} @test x.values.data == [2., 3., 4.] run(m) @@ -188,7 +214,7 @@ run(m) m = Model() set_dimension!(m, :time, [2000, 2005, 2020]) add_comp!(m, MyComp2) -set_param!(m, :MyComp2, :x, [1, 2, 3]) +update_param!(m, :MyComp2, :x, [1, 2, 3]) # Year x Model MyComp2 # 2000 1 first first # 2005 2 @@ -201,18 +227,18 @@ set_dimension!(m, :time, [2000, 2005, 2020, 2100]) # 2020 3 last # 2100 missing last -x = external_param(m.md, :x) +x = model_param(m, :MyComp2, :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 +update_param!(m, :MyComp2, :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 -x = external_param(m.md, :x) +x = model_param(m, :MyComp2, :x) @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) @@ -236,12 +262,12 @@ run(m) m = Model() set_dimension!(m, :time, [2000, 2005, 2020]) add_comp!(m, MyComp2) -set_param!(m, :MyComp2, :x, [1, 2, 3]) +update_param!(m, :MyComp2, :x, [1, 2, 3]) set_dimension!(m, :time, [2000, 2005, 2020, 2100]) -update_params!(m, Dict(:x=>[2, 3, 4, 5])) -x = external_param(m.md, :x) +update_params!(m, Dict((:MyComp2, :x)=>[2, 3, 4, 5])) +x = model_param(m, :MyComp2, :x) @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) @@ -256,14 +282,14 @@ run(m) m = Model() set_dimension!(m, :time, 2000:2002) # length 3 add_comp!(m, MyComp2) -set_param!(m, :MyComp2, :x, [1, 2, 3]) +update_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]) +update_param!(m, :MyComp2, :x, [2, 3, 4, 5, 6]) # Year x Model MyComp2 # 1999 2 first # 2000 3 first @@ -271,7 +297,7 @@ update_param!(m, :x, [2, 3, 4, 5, 6]) # 2002 5 last # 2003 6 last -x = external_param(m.md, :x) +x = model_param(m, :MyComp2, :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.] @@ -306,25 +332,25 @@ m = Model() # Build the model set_dimension!(m, :time, 2000:2002) # Set the time dimension set_dimension!(m, :regions, [:A, :B]) add_comp!(m, MyComp3) -set_param!(m, :MyComp3, :x, [1, 2, 3]) -set_param!(m, :MyComp3, :y, [10, 20]) -set_param!(m, :MyComp3, :z, 0) +update_param!(m, :MyComp3, :x, [1, 2, 3]) +update_param!(m, :MyComp3, :y, [10, 20]) +update_param!(m, :MyComp3, :z, 0) @test_throws ErrorException update_param!(m, :x, [1, 2, 3, 4]) # Will throw an error because size -update_param!(m, :y, [10, 15]) -@test external_param(m.md, :y).values == [10., 15.] -update_param!(m, :z, 1) -@test external_param(m.md, :z).value == 1 +update_param!(m, :MyComp3, :y, [10, 15]) +@test model_param(m, :MyComp3, :y).values == [10., 15.] +update_param!(m, :MyComp3, :z, 1) +@test model_param(m, :MyComp3, :z).value == 1 # Reset the time dimensions 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 +update_params!(m, Dict((:MyComp3, :x) =>[3,4,5], (:MyComp3, :y) =>[10,20], (:MyComp3, :z) =>0)) # Won't error when updating from a dictionary -@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 model_param(m, :MyComp3, :x).values isa Mimi.TimestepArray{Mimi.FixedTimestep{1999,1, 2001},Union{Missing,Float64},1} +@test model_param(m, :MyComp3, :x).values.data == [3.,4.,5.] +@test model_param(m, :MyComp3, :y).values == [10.,20.] +@test model_param(m, :MyComp3, :z).value == 0 #------------------------------------------------------------------------------ # Test the three different set_param! methods for a Symbol type parameter @@ -365,6 +391,43 @@ set_param!(m, :A, :p1, :A_p1, :foo) run(m) @test m[:A, :p1] == :foo + +#------------------------------------------------------------------------------ +# Test a few different update_param! methods for a Symbol type parameter +#------------------------------------------------------------------------------ + +@defcomp A begin + p1 = Parameter{Symbol}() +end + +function _get_model() + m = Model() + set_dimension!(m, :time, 10) + add_comp!(m, A) + return m +end + +# Test the 3-argument version of update_param! +m = _get_model() + +add_shared_param!(m, :p1_fail, 3) +@test_throws ErrorException connect_param!(m, :A, :p1, :p1_fail) # Can't connect it to an Int + +add_shared_param!(m, :p1, :foo) +connect_param!(m, :A, :p1, :p1) # connect it to a Symbol + +run(m) +@test m[:A, :p1] == :foo + +# Test the 4-argument version of update_param! +m = _get_model() +@test_throws MethodError update_param!(m, :A, :p1, 3) # wrong type +@test_throws MethodError update_param!(m, :A, :p1, [1,2,3]) # wrong type + +update_param!(m, :A, :p1, :foo) +run(m) +@test m[:A, :p1] == :foo + #------------------------------------------------------------------------------ # Test that if set_param! errors in the connection step, # the created param doesn't remain in the model's list of params @@ -384,6 +447,6 @@ add_comp!(m, A) add_comp!(m, B) @test_throws ErrorException set_param!(m, :p1, 1:5) # this will error because the provided data is the wrong size -@test isempty(m.md.external_params) # But it should not be added to the model's dictionary +@test !(:p1 in keys(model_params(m))) # But it should not be added to the model's dictionary end #module diff --git a/test/test_references.jl b/test/test_references.jl index 352904e70..1d0dc7035 100644 --- a/test/test_references.jl +++ b/test/test_references.jl @@ -3,6 +3,8 @@ module TestReferences using Test using Mimi +import Mimi: model_params + @defcomp A begin p1 = Parameter() v1 = Variable(index = [time]) @@ -22,13 +24,13 @@ refA = add_comp!(m, A, :foo) refB = add_comp!(m, B) refA[:p1] = 3 # creates a parameter specific to this component, with name "foo_p1" -@test length(m.md.external_param_conns) == 1 -@test Mimi.UnnamedReference(:B, :p1) in Mimi.unconnected_params(m.md) -@test :foo_p1 in keys(m.md.external_params) +@test Mimi.get_model_param_name(m.md, :foo, :p1) == :foo_p1 +@test :foo_p1 in keys(model_params(m)) +@test Mimi.UnnamedReference(:B, :p1) in Mimi.nothing_params(m.md) refB[:p1] = 5 -@test length(m.md.external_param_conns) == 2 -@test :B_p1 in keys(m.md.external_params) +@test Mimi.get_model_param_name(m.md, :B, :p1) == :B_p1 +@test :B_p1 in keys(model_params(m)) # Use the ComponentReferences to make an internal connection refB[:p2] = refA[:v1] diff --git a/test/test_replace_comp.jl b/test/test_replace_comp.jl index 7e967ea8e..764629a1a 100644 --- a/test/test_replace_comp.jl +++ b/test/test_replace_comp.jl @@ -3,7 +3,7 @@ module TestReplaceComp using Test using Mimi import Mimi: - compdefs, compname, compdef, components, comp_id, external_param_conns, external_params + compdefs, compname, compdef, components, comp_id, external_param_conns, model_params @defcomp X begin x = Parameter(index = [time]) @@ -21,6 +21,18 @@ end end end +@defcomp X_repl_extraparams begin + x = Parameter(index = [time]) + y = Variable(index = [time]) + + a = Parameter(default = 10) + b = Parameter() + + function run_timestep(p, v, d, t) + v.y[t] = 2 + end +end + @defcomp bad1 begin x = Parameter() # parameter has same name but different dimensions y = Variable(index = [time]) @@ -46,7 +58,7 @@ end m = Model() set_dimension!(m, :time, 2000:2005) add_comp!(m, X) # Original component X -set_param!(m, :X, :x, zeros(6)) +update_param!(m, :X, :x, zeros(6)) @test_throws ErrorException replace_comp!(m, X_repl, :X) # test that the old function name now errors replace!(m, :X => X_repl) # Replace X with X_repl run(m) @@ -81,12 +93,12 @@ first = compdef(m, :first) @test first.comp_id.comp_name == :bad2 # Successfully replaced -# 4. Test bad external parameter name +# 4. Test bad model parameter name m = Model() set_dimension!(m, :time, 2000:2005) add_comp!(m, X) -set_param!(m, :X, :x, zeros(6)) # Set external parameter for :x +update_param!(m, :X, :x, zeros(6)) # Set model parameter for :x # Replaces with bad3, but warns that there is no parameter by the same name :x @test_logs( @@ -95,26 +107,26 @@ set_param!(m, :X, :x, zeros(6)) # Set external parameter for ) @test compname(compdef(m, :X)) == :bad3 # The replacement was still successful -@test length(external_param_conns(m)) == 0 # The external parameter connection was removed -@test length(external_params(m)) == 1 # The external parameter still exists +@test length(external_param_conns(m)) == 1 # The external parameter connection was removed, so just :z is there +@test length(model_params(m)) == 2 # The model parameter still exists for both :x and :z -# 5. Test bad external parameter dimensions +# 5. Test bad model parameter dimensions m = Model() set_dimension!(m, :time, 2000:2005) add_comp!(m, X) -set_param!(m, :X, :x, zeros(6)) # Set external parameter for :x -@test_throws ErrorException replace!(m, :X => bad1) # Cannot reconnect external parameter, :x in bad1 has different dimensions +update_param!(m, :X, :x, zeros(6)) # Set model parameter for :x +@test_throws ErrorException replace!(m, :X => bad1) # Cannot reconnect model parameter, :x in bad1 has different dimensions -# 6. Test bad external parameter datatype +# 6. Test bad model parameter datatype m = Model() set_dimension!(m, :time, 2000:2005) add_comp!(m, X) -set_param!(m, :X, :x, zeros(6)) # Set external parameter for :x -@test_throws ErrorException replace!(m, :X => bad4) # Cannot reconnect external parameter, :x in bad4 has different datatype +update_param!(m, :X, :x, zeros(6)) # Set model parameter for :x +@test_throws ErrorException replace!(m, :X => bad4) # Cannot reconnect model parameter, :x in bad4 has different datatype # 7. Test component name that doesn't exist @@ -153,10 +165,22 @@ end m = Model() set_dimension!(m, :time, 10) add_comp!(m, A) -set_param!(m, :A, :p1, 3) +update_param!(m, :A, :p1, 3) replace!(m, :A => B) run(m) @test m[:A, :p1] == 3 +# 10. Test when the new component has extra parameters not in the original one + +m = Model() +set_dimension!(m, :time, 2000:2005) +add_comp!(m, X) # Original component X +update_param!(m, :X, :x, zeros(6)) +replace!(m, :X => X_repl_extraparams) # Replace X with X_repl_extraparams +@test length(model_params(m)) == 3 # should have two new parameters in the model parameters list +update_param!(m, :X, :b, 8.0) # need to set b since it doesn't have a default, a will have a default +run(m) +@test length(components(m)) == 1 # Only one component exists in the model +@test m[:X, :y] == 2 * ones(6) # Successfully ran the run_timestep function from X_repl end # module diff --git a/test/test_show.jl b/test/test_show.jl index 30d4fb075..ae5fd4306 100644 --- a/test/test_show.jl +++ b/test/test_show.jl @@ -48,7 +48,7 @@ p = ParameterDef(:v1, Float64, [:time], "", "", nothing) m = Model() set_dimension!(m, :time, 2000:2005) add_comp!(m, X) # Original component X -set_param!(m, :X, :x, zeros(6)) +update_param!(m, :X, :x, zeros(6)) expected = """ Model @@ -79,8 +79,8 @@ Model 1: ExternalParameterConnection comp_name: :X param_name: :x - external_param: :x - external_params: Dict{Symbol,ModelParameter} + model_param_name: :x + model_params: Dict{Symbol,ModelParameter} x => ArrayModelParameter{TimestepArray{FixedTimestep{2000,1,2005},Float64,1}} values: TimestepArray{FixedTimestep{2000,1,2005},Float64,1} 1: 0.0 diff --git a/test/test_timesteparrays.jl b/test/test_timesteparrays.jl index 2f1672f07..d3162a759 100644 --- a/test/test_timesteparrays.jl +++ b/test/test_timesteparrays.jl @@ -664,7 +664,7 @@ set_dimension!(m, :time, years) add_comp!(m, foo, :first) add_comp!(m, bar, :second) connect_param!(m, :second => :par2, :first => :var1) -set_param!(m, :first, :par1, 1:length(years)) +update_param!(m, :first, :par1, 1:length(years)) @test_throws MissingException run(m) @@ -704,8 +704,8 @@ set_dimension!(m, :time, time_index) set_dimension!(m, :regions, regions) set_dimension!(m, :foo, 3) add_comp!(m, gdp) -set_param!(m, :gdp, :gdp0, [3; 7] .* ones(length(regions), 3, 2)) -set_param!(m, :gdp, :growth, [0.02; 0.03] .* ones(length(regions), 3, nsteps, 2)) +update_param!(m, :gdp, :gdp0, [3; 7] .* ones(length(regions), 3, 2)) +update_param!(m, :gdp, :growth, [0.02; 0.03] .* ones(length(regions), 3, nsteps, 2)) set_leftover_params!(m, Dict{String, Any}([ "pgrowth" => ones(length(regions), 3, nsteps), "mat" => rand(length(regions), nsteps) diff --git a/test/test_timesteps.jl b/test/test_timesteps.jl index 7e225ddfd..c56368165 100644 --- a/test/test_timesteps.jl +++ b/test/test_timesteps.jl @@ -145,8 +145,8 @@ set_dimension!(m, :time, years) foo = add_comp!(m, Foo, first=first_foo) bar = add_comp!(m, Bar) -set_param!(m, :Foo, :inputF, 5.) -set_param!(m, :Bar, :inputB, collect(1:length(years))) +update_param!(m, :Foo, :inputF, 5.) +update_param!(m, :Bar, :inputB, collect(1:length(years))) run(m) @@ -217,7 +217,7 @@ set_dimension!(m2, :time, years) bar = add_comp!(m2, Bar) foo2 = add_comp!(m2, Foo2, first = first_foo) -set_param!(m2, :Bar, :inputB, collect(1:length(years))) +update_param!(m2, :Bar, :inputB, collect(1:length(years))) connect_param!(m2, :Foo2, :inputF, :Bar, :output) run(m2) @@ -248,7 +248,7 @@ set_dimension!(m3, :time, years) add_comp!(m3, Foo, first=2005) add_comp!(m3, Bar2) -set_param!(m3, :Foo, :inputF, 5.) +update_param!(m3, :Foo, :inputF, 5.) connect_param!(m3, :Bar2, :inputB, :Foo, :output, zeros(length(years))) run(m3) diff --git a/test/test_variables_model_instance.jl b/test/test_variables_model_instance.jl index 3a9729448..f2de986ef 100644 --- a/test/test_variables_model_instance.jl +++ b/test/test_variables_model_instance.jl @@ -27,7 +27,7 @@ set_dimension!(my_model, :time, 2015:5:2110) @test_throws ErrorException run(my_model) #no components added yet add_comp!(my_model, testcomp1) -set_param!(my_model, :testcomp1, :par1, par) +update_param!(my_model, :testcomp1, :par1, par) run(my_model) #NOTE: this variables function does NOT take in Nullable instances @test (variable_names(my_model, :testcomp1) == [:var1, :var2]) diff --git a/wip/create_composite.jl b/wip/create_composite.jl index 4572ec778..50c5329ae 100644 --- a/wip/create_composite.jl +++ b/wip/create_composite.jl @@ -95,9 +95,9 @@ end end add_comp!(m, top, nameof(top)) -set_param!(m, :fooA1, 1) -set_param!(m, :fooA2, 2) -set_param!(m, :foo3, 10) -set_param!(m, :foo4, 20) -set_param!(m, :par_1_1, collect(1:length(Mimi.time_labels(m.md)))) +set_param!(m, :top, :fooA1, 1) +set_param!(m, :top, 2) +set_param!(m, :top, 10) +set_param!(m, :top, 20) +set_param!(m, :top, :par_1_1, collect(1:length(Mimi.time_labels(m.md)))) run(m) \ No newline at end of file