diff --git a/docs/src/howto/howto_3.md b/docs/src/howto/howto_3.md index 52bf3b4c9..a55015c9d 100644 --- a/docs/src/howto/howto_3.md +++ b/docs/src/howto/howto_3.md @@ -18,7 +18,7 @@ The rest of this document will be organized as follows: 2. The `run` function 3. The `analyze` function 4. Plotting and the Explorer UI -5. Simulation Modification Functions +5. Other Useful Functions 6. Examples *We will refer separately to two types, `SimulationDef` and `SimulationInstance`. They are referred to as `sim_def` and `sim_inst` respectively as function arguments, and `sd` and `si` respectively as local variables.* @@ -62,9 +62,30 @@ If using Latin Hypercube Sampling (LHS) is used, the following function must als - `quantile(dist, quantiles::Vector{Float64})` which returns values for the given `quantiles` of the distribution. -In addition to the distributions available in the `Distributions` package, Mimi provides: +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. +- `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: + + ```julia + using CSVFiles + using DataFrames + using Mimi + import Mimi: EmpiricalDistribution # not currently exported so you just need to grab it + + # read in your values + values = load("path_to_values_file"; header_exists = false) |> DataFrame + # read in your probabilities (optional, if none are provided we assume all equal) + # note that the probabilities need to be Float type and should roughly add to 1 + probs = load("path_to_probabilities_file"; header_exists = false) |> DataFrame + + # create your simulation + @defsim begin + ... + trc_transientresponse = EmpiricalDistribution(values, probs) + ... + end + ``` + Note there are many ways to load values, we use DataFrames and CSVFiles above but there might be an easier way depending on what packages you like - `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. @@ -85,7 +106,7 @@ The macro next defines how to apply the values generated by each RV to model par - `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. -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. +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. #### Apply RVs to model parameters: Assigning to array slices @@ -338,16 +359,70 @@ plot(sim_inst::SimulationInstance, comp_name::Symbol, datum_name::Symbol; intera ![Plot Simulation Example](../figs/plot_sim_example.png) -## 5. Simulation Modification Functions +## 5. Other Useful Functions + +### Simulation Modification Functions A small set of unexported functions are available to modify an existing `SimulationDefinition`. The functions include: -* `delete_RV!` -* `add_RV!` -* `replace_RV!` -* `delete_transform!` -* `add_transform!` -* `delete_save!` -* `add_save!` + +* `delete_RV!(sim_def::SimulationDef, name::Symbol)` - deletes the random variable with name `name` from the Simulation Definition `sim_def`, along with all transformations using that random variable +* `add_RV!(sim_def::SimulationDef, name::Symbol, dist::Distribution)` - add the random variable with the name `name` from the Simulation Definition `sim_def` +* `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 :(=). + +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)`. + +Case 1: The user wants this random variable to draw from a new distribution, say `Normal(0,1)`, which will affect all parameters with transforms attached to this random variable. +``` +using Distributions +using Mimi +replace_RV!(MySimDef, MyRV, Normal(0,1)) +``` +Case 2: The user parameter `MyParameter` to to take on the value of a random draw from a `Normal(2,0.1)` distribution. We assume this requires a new random variable, because no random variable has this distribution yet. +``` +using Distributions +using Mimi +add_RV!(MySimDef, :NewRV, Normal(2, 0.1)) +add_transform!(MySimDef, :MyParameter, :=, :NewRV) +``` + +* `add_save!(sim_def::SimulationDef, comp_name::Symbol, datum_name::Symbol)` - Add to Simulation Definition`sim_def` a "save" instruction for component `comp_name` and parameter or variable `datum_name`. This result will be saved to a CSV file at the end of the simulation. +* `delete_save!(sim_def::SimulationDef, comp_name::Symbol, datum_name::Symbol)` - Delete from Simulation Definition `sim_def` a "save" instruction for component `comp_name` and parameter nor variable `datum_name`. This result will no longer be saved to a CSV file at the end of the simulation. + +### Helper Functions + +``` +""" + get_simdef_rvnames(sim_def::SimulationDef, name::Union{String, Symbol}) + +A helper function to support finding the keys in a Simulation Definition `sim_def`'s +rvdict that contain a given `name`. This can be particularly helpful if the random +variable was set via the shortcut syntax ie. my_parameter = Normal(0,1) and thus the +`sim_def` has automatically created a key in the format `:my_parameter!x`. In this +case this function is useful to get the key and carry out modification functions +such as `replace_rv!`. +""" + +function get_simdef_rvnames(sim_def::SimulationDef, name::Union{String, Symbol}) + names = String.([keys(sim_def.rvdict)...]) + matches = Symbol.(filter(x -> occursin(String(name), x), names)) +end +``` +As shown in the examples below, and described above, parameters can be assigned unique random variables under the hood without explicitly declaring the RV. Fore example, instead of pairing +``` +rv(name) = Uniform(0.2, 0.8) +share = name1 +``` +we can write +``` +share = Uniform(0.2, 0.8) +``` +When this is done, Mimi will create a new unique RV with a unique name `share!x` where `x` is an integer determined by internal processes that gaurantee it to be unique in this `sim_def`. This syntax is therefore not recommended if the user expects to want to reference that random variable using the aforementioned modification functions. That said, if the user does need to do so we have added a helper function `get_simdef_rvnames(sim_def::SimulationDef, name::Union{String, Symbol})` which will return the unique names of the random variables that start with `name`. In the case above, for example, `get_simdef_rvnames(sim_def, :share)` would return `[share!x]`. In a case where share had multiple dimensions, like three regions, it would return `[share!x1, share!x2, share!x3]`. + +### Payload Functions + * `set_payload!` * `payload` diff --git a/docs/src/tutorials/tutorial_5.md b/docs/src/tutorials/tutorial_5.md index cdacb8ffc..2a29fdaf1 100644 --- a/docs/src/tutorials/tutorial_5.md +++ b/docs/src/tutorials/tutorial_5.md @@ -366,8 +366,10 @@ scc_results = Mimi.payload(si)[2] # Recall that the SCC array was the second o ``` -#### Simulation Modification Functions -A small set of unexported functions are available to modify an existing `SimulationDef`. The functions include: +#### Other Helpful Functions + +A small set of unexported functions are available to modify an existing `SimulationDef`. Please refer to How-to Guide 3: Conduct Monte Carlo Simulations and Sensitivity Analysis for an in depth description of their use cases. The functions include the following: + * `delete_RV!` * `add_RV!` * `replace_RV!` @@ -375,12 +377,13 @@ A small set of unexported functions are available to modify an existing `Simulat * `add_transform!` * `delete_save!` * `add_save!` +* `get_simdef_rvnames` * `set_payload!` * `payload` #### Full list of keyword options for running a simulation -View How-to Guide 3: Conduct Sensitivity Analysis for **critical and useful details on the full signature of this function**, as well as more details and optionality for more advanced use cases. +View How-to Guide 3: Conduct Monte Carlo Simulations and Sensitivity Analysis for **critical and useful details on the full signature of this function**, as well as more details and optionality for more advanced use cases. ```julia function Base.run(sim_def::SimulationDef{T}, models::Union{Vector{Model}, Model}, samplesize::Int; diff --git a/src/mcs/defmcs.jl b/src/mcs/defmcs.jl index d2461744b..b034aeb9a 100644 --- a/src/mcs/defmcs.jl +++ b/src/mcs/defmcs.jl @@ -183,20 +183,24 @@ end """ delete_RV!(sim_def::SimulationDef, name::Symbol) -Delete the random variable `name` from the Simulation definition `sim`. +Delete the random variable with name `name` from the Simulation definition `sim_def`. Transformations using this RV are deleted, and the Simulation's NamedTuple type is updated to reflect the dropped RV. """ function delete_RV!(sim_def::SimulationDef, name::Symbol) - delete_transform!(sim_def, name) - delete!(sim_def.rvdict, name) - _update_nt_type!(sim_def) + if !haskey(sim_def.rvdict, name) + @warn("Simulation def does not have RV :$name. Nothing being deleted.") + else + delete_transform!(sim_def, name) + delete!(sim_def.rvdict, name) + _update_nt_type!(sim_def) + end end """ add_RV!(sim_def::SimulationDef, rv::RandomVariable) -Add random variable definition `rv` to Simulation definition `sim`. The +Add random variable definition `rv` to Simulation definition `sim_def`. The Simulation's NamedTuple type is updated to include the RV. """ function add_RV!(sim_def::SimulationDef, rv::RandomVariable) @@ -209,18 +213,20 @@ end """ add_RV!(sim_def::SimulationDef, name::Symbol, dist::Distribution) -Add random variable definition `rv` to Simulation definition `sim`. The -Simulation's NamedTuple type is updated to include the RV. +Add a random variable with name `name` and distribution `dist` to Simulation definition +`sim_def`. The Simulation's NamedTuple type is updated to include the RV. """ add_RV!(sim_def::SimulationDef, name::Symbol, dist::Distribution) = add_RV!(sim_def, RandomVariable(name, dist)) """ replace_RV!(sim_def::SimulationDef, rv::RandomVariable) -Replace the RV with the given `rv`s name in the Simulation definition Sim with +Replace the RV with the given `rv`s name in the Simulation definition `sim_def` with `rv` and update the Simulation's NamedTuple type accordingly. """ function replace_RV!(sim_def::SimulationDef, rv::RandomVariable) + name = rv.name + haskey(sim_def.rvdict, name) || error("Simulation def does not have has RV :$name. Use add_RV! to add it.") sim_def.rvdict[rv.name] = rv _update_nt_type!(sim_def) end @@ -228,7 +234,7 @@ end """ replace_RV!(sim_def::SimulationDef, name::Symbol, dist::Distribution) -Replace the with name `name` in the Simulation definition Sim with a new RV +Replace the rv with name `name` in the Simulation definition `sim_def` with a new RV with `name` and distribution `dist`. Update the Simulation's NamedTuple type accordingly. """ @@ -243,14 +249,18 @@ Simulation definition's NamedTuple type accordingly. """ function delete_transform!(sim_def::SimulationDef, name::Symbol) pos = findall(t -> t.rvname == name, sim_def.translist) - deleteat!(sim_def.translist, pos) - _update_nt_type!(sim_def) + if isempty(pos) + @warn("Simulation def does not have and transformations using RV :$name. Nothing being deleted.") + else + deleteat!(sim_def.translist, pos) + _update_nt_type!(sim_def) + end end """ add_transform!(sim_def::SimulationDef, t::TransformSpec) -Add the data transformation `t` to the Simulation definition `sim`, and update the +Add the data transformation `t` to the Simulation definition `sim_def`, and update the Simulation's NamedTuple type. The TransformSpec `t` must refer to an existing RV. """ @@ -263,7 +273,7 @@ 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 definitino `sim`, and update the Simulation's NamedTuple type. The symbol `rvname` must +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 :(=). """ @@ -274,19 +284,19 @@ end """ delete_save!(sim_def::SimulationDef, key::Tuple{Symbol, Symbol}) -Delete from Simulation definition `sim` a "save" instruction for the given `key`, comprising +Delete from Simulation definition `sim_def` a "save" instruction for the given `key`, comprising component `comp_name` and parameter or variable `datum_name`. This result will no 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) - deleteat!(sim_def.savelist, pos) + isempty(pos) ? @warn("Simulation def doesn't have $key in its save list. Nothing being deleted.") : deleteat!(sim_def.savelist, pos) end """ delete_save!(sim_def::SimulationDef, comp_name::Symbol, datum_name::Symbol) -Delete from Simulation definition `sim` a "save" instruction for component `comp_name` and parameter +Delete from Simulation definition `sim_def` a "save" instruction for component `comp_name` and parameter or variable `datum_name`. This result will no longer be saved to a CSV file at the end of the simulation. """ @@ -295,20 +305,34 @@ delete_save!(sim_def::SimulationDef, comp_name::Symbol, datum_name::Symbol) = de """ add_save!(sim_def::SimulationDef, key::Tuple{Symbol, Symbol}) -Add to Simulation definition `sim` a "save" instruction for the given `key`, comprising +Add to Simulation definition `sim_def` a "save" instruction for the given `key`, comprising component `comp_name` and parameter or variable `datum_name`. This result will be saved to a CSV file at the end of the simulation. """ function add_save!(sim_def::SimulationDef, key::Tuple{Symbol, Symbol}) - delete_save!(sim_def, key) - push!(sim_def.savelist, key) - nothing + pos = findall(isequal(key), sim_def.savelist) + !isempty(pos) ? @warn("Simulation def already has $key in its save list. Nothing being added.") : push!(sim_def.savelist, key) end """ add_save!(sim_def::SimulationDef, comp_name::Symbol, datum_name::Symbol) -Add to Simulation definition`sim` a "save" instruction for component `comp_name` and parameter or +Add to Simulation definition`sim_def` a "save" instruction for component `comp_name` and parameter or variable `datum_name`. This result will be saved to a CSV file at the end of the simulation. """ add_save!(sim_def::SimulationDef, comp_name::Symbol, datum_name::Symbol) = add_save!(sim_def, (comp_name, datum_name)) + +""" + get_simdef_rvnames(sim_def::SimulationDef, name::Union{String, Symbol}) + +A helper function to support finding the keys in a Simulation Definition `sim_def`'s +rvdict that contain a given `name`. This can be particularly helpful if the random +variable was set via the shortcut syntax ie. my_parameter = Normal(0,1) and thus the +`sim_def` has automatically created a key in the format `:my_parameter!xx`. In this +case this function is useful to get the key and carry out modification functions +such as `replace_rv!`. +""" +function get_simdef_rvnames(sim_def::SimulationDef, name::Union{String, Symbol}) + names = String.([keys(sim_def.rvdict)...]) + matches = Symbol.(filter(x -> occursin(String(name), x), names)) +end diff --git a/test/mcs/runtests.jl b/test/mcs/runtests.jl index e91508837..4e06acf46 100644 --- a/test/mcs/runtests.jl +++ b/test/mcs/runtests.jl @@ -9,6 +9,9 @@ using Test @info("test_defmcs.jl") include("test_defmcs.jl") + @info("test_defmcs_modifications.jl") + include("test_defmcs_modifications.jl") + @info("test_defmcs_sobol.jl") include("test_defmcs_sobol.jl") diff --git a/test/mcs/test_defmcs_modifications.jl b/test/mcs/test_defmcs_modifications.jl new file mode 100644 index 000000000..f870557b8 --- /dev/null +++ b/test/mcs/test_defmcs_modifications.jl @@ -0,0 +1,71 @@ +using Mimi +using Distributions +using Test +using Mimi: delete_RV!, delete_transform!, add_RV!, add_transform!, replace_RV!, delete_save!, add_save!, get_simdef_rvnames + +# construct a mcs + +include("test-model-2/multi-region-model.jl") +using .MyModel + +m = construct_MyModel() +N = 10 +output_dir = joinpath(tempdir(), "sim") + +sd = @defsim begin + + rv(name1) = Normal(1, 0.2) + 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) + + 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)]) + + save(grosseconomy.K, grosseconomy.YGROSS, emissions.E, emissions.E_Global, grosseconomy.share_var, grosseconomy.depk_var) +end + +run(sd, m, N; trials_output_filename = joinpath(output_dir, "trialdata.csv"), results_output_dir=output_dir) + +# test modification functions + +# add_RV! (calls add_transform!) +@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) +run(sd, m, N; trials_output_filename = joinpath(output_dir, "trialdata.csv"), results_output_dir=output_dir) + +# replace_RV! +@test_throws ErrorException replace_RV!(sd, :missing_RV, Uniform(0, 1)) +replace_RV!(sd, :new_RV, Uniform(0, 1)) +@test sd.rvdict[:new_RV].dist == Uniform(0, 1) +run(sd, m, N; trials_output_filename = joinpath(output_dir, "trialdata.csv"), results_output_dir=output_dir) + +# delete_RV! (calls delete_transform!) +@test_logs (:warn, "Simulation def does not have RV :missing_RV. Nothing being deleted.") delete_RV!(sd, :missing_RV) +delete_RV!(sd, :new_RV) +@test !haskey(sd.rvdict, :new_RV) +run(sd, m, N; trials_output_filename = joinpath(output_dir, "trialdata.csv"), results_output_dir=output_dir) + +# delete_save! and add_save! +@test_logs (:warn, "Simulation def doesn't have (:comp, :param) in its save list. Nothing being deleted.") delete_save!(sd, :comp, :param) +delete_save!(sd, :grosseconomy, :K) +pos = pos = findall(isequal((:grosseconomy, :K)), sd.savelist) +@test isempty(pos) +run(sd, m, N; trials_output_filename = joinpath(output_dir, "trialdata.csv"), results_output_dir=output_dir) + +@test_logs (:warn, "Simulation def already has (:emissions, :E) in its save list. Nothing being added.") add_save!(sd, :emissions, :E) +add_save!(sd, :grosseconomy, :K) +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) + +# get_simdef_rvnames +rvs = get_simdef_rvnames(sd, :depk) +@test length(rvs) == 3