From 323d69f45f8c865bc141f04fbdf830ffbc8a2f1e Mon Sep 17 00:00:00 2001 From: lrennels Date: Thu, 6 May 2021 13:44:42 -0700 Subject: [PATCH 01/47] Add descriptions of path forward --- docs/src/wip/Mimi Meeting_5_26_2021.ipynb | 145 ++++++++++++++++++++++ docs/src/wip/Mimi Meetings - 20210506.png | Bin 0 -> 422760 bytes 2 files changed, 145 insertions(+) create mode 100644 docs/src/wip/Mimi Meeting_5_26_2021.ipynb create mode 100644 docs/src/wip/Mimi Meetings - 20210506.png diff --git a/docs/src/wip/Mimi Meeting_5_26_2021.ipynb b/docs/src/wip/Mimi Meeting_5_26_2021.ipynb new file mode 100644 index 000000000..12e76122f --- /dev/null +++ b/docs/src/wip/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/wip/Mimi Meetings - 20210506.png b/docs/src/wip/Mimi Meetings - 20210506.png new file mode 100644 index 0000000000000000000000000000000000000000..4144983823eea99027d0de7df3617db91021be99 GIT binary patch literal 422760 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

9e}p7IT@o)5hVQW>BB7XGJoWuMzdo$$D>HF2 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

8+Daq-E4G0H{+vwb(6Xru?}m~3e+LY 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 Date: Mon, 10 May 2021 21:42:06 -0700 Subject: [PATCH 02/47] Add shared attribute to model parameter types and implement instantiation --- docs/src/howto/howto_6.md | 2 +- src/core/build.jl | 21 ------ src/core/connections.jl | 59 +++++++++------- src/core/defs.jl | 143 +++++++++++++++++++++++++++++++------- src/core/types/params.jl | 34 ++++++--- 5 files changed, 174 insertions(+), 85 deletions(-) diff --git a/docs/src/howto/howto_6.md b/docs/src/howto/howto_6.md index edef2aab8..2530ffa63 100644 --- a/docs/src/howto/howto_6.md +++ b/docs/src/howto/howto_6.md @@ -94,7 +94,7 @@ The full API: #### Updating an external parameter -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: +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: * `update_params!(md::ModelDef, parameters::Dict; update_timesteps = false)` diff --git a/src/core/build.jl b/src/core/build.jl index 88a8dbae9..61749cd2d 100644 --- a/src/core/build.jl +++ b/src/core/build.jl @@ -318,25 +318,6 @@ function _get_parameters(comp_def::AbstractCompositeComponentDef) return parameters end -""" - _set_defaults!(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. -""" -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)" @@ -369,8 +350,6 @@ end 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 diff --git a/src/core/connections.jl b/src/core/connections.jl index 3aab02ab6..091381e3d 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -121,7 +121,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 +183,7 @@ function _connect_param!(obj::AbstractCompositeComponentDef, end - set_external_array_param!(obj, dst_par_name, values, dst_dims) + set_external_array_param!(obj, dst_par_name, values, dst_dims) backup_param_name = dst_par_name else @@ -395,22 +395,21 @@ function add_external_param_conn!(obj::ModelDef, conn::ExternalParameterConnecti 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" - # end obj.external_params[name] = value dirty!(obj) 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) + param_dims::Union{Nothing,Array{Symbol}} = nothing, + shared::Bool = false) + set_external_scalar_param!(obj, name, value, shared) end function set_external_param!(obj::ModelDef, name::Symbol, value::Union{AbstractArray, AbstractRange, Tuple}; - param_dims::Union{Nothing,Array{Symbol}} = nothing) + param_dims::Union{Nothing,Array{Symbol}} = nothing, + shared::Bool = false) ti = get_time_index_position(param_dims) if ti != nothing value = convert(Array{number_type(obj)}, value) @@ -420,54 +419,64 @@ function set_external_param!(obj::ModelDef, name::Symbol, values = value end - set_external_array_param!(obj, name, values, param_dims) + set_external_array_param!(obj, name, values, param_dims, shared = shared) end """ set_external_array_param!(obj::ModelDef, - name::Symbol, value::TimestepVector, dims) + name::Symbol, value::TimestepVector, + dims; 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 composite `obj`. The `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 + name::Symbol, value::TimestepVector, + dims; shared::Bool = false) + param = ArrayModelParameter(value, [:time], shared) # must be :time set_external_param!(obj, name, param) end """ set_external_array_param!(obj::ModelDef, - name::Symbol, value::TimestepMatrix, dims) + name::Symbol, value::TimestepMatrix, dims; + 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 composite `obj`. The `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::TimestepArray, dims) - param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims) + name::Symbol, value::TimestepArray, dims; + shared::Bool = false) + param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims, shared) set_external_param!(obj, name, param) end """ set_external_array_param!(obj::ModelDef, - name::Symbol, value::AbstractArray, dims) + name::Symbol, value::AbstractArray, dims; + 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 +composite `obj`. The `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) + name::Symbol, value::AbstractArray, dims; + shared::Bool = false) + param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims, shared) set_external_param!(obj, name, param) end """ - set_external_scalar_param!(obj::ModelDef, name::Symbol, value::Any) + set_external_scalar_param!(obj::ModelDef, name::Symbol, value::Any; shared::Bool = false) Add a scalar type parameter `name` with the value `value` to the composite `obj`. """ -function set_external_scalar_param!(obj::ModelDef, name::Symbol, value::Any) - param = ScalarModelParameter(value) +function set_external_scalar_param!(obj::ModelDef, name::Symbol, value::Any; shared::Bool = false) + param = ScalarModelParameter(value, shared) set_external_param!(obj, name, param) end @@ -559,7 +568,7 @@ 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))) + set_external_param!(obj, name, ArrayModelParameter(new_timestep_array, dim_names(param), shared = param.shared)) else copyto!(param.values.data, value) end diff --git a/src/core/defs.jl b/src/core/defs.jl index 14d1d14f1..c2e8e8480 100644 --- a/src/core/defs.jl +++ b/src/core/defs.jl @@ -520,35 +520,28 @@ function set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing, ignor if ti !== nothing # there is a time dimension T = eltype(value) - - if num_dims == 0 - values = value + + # 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) + last = last_period(md) + + if isuniform(md) + stepsize = step_size(md) + values = TimestepArray{FixedTimestep{first, stepsize, last}, T, num_dims, ti}(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 + times = time_labels(md) + first_index = findfirst(isequal(first), times) + values = TimestepArray{VariableTimestep{(times[first_index:end]...,)}, T, num_dims, ti}(value) 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 + param = ArrayModelParameter(values, param_dims, true) + + # Need to check the dimensions of the parameter data against each component before adding it to the model's external parameters for comp in comps _check_labels(md, comp, param_name, param) end @@ -557,13 +550,13 @@ function set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing, ignor else # scalar parameter case value = convert(dtype, value) - set_external_scalar_param!(md, ext_param_name, value) + set_external_scalar_param!(md, ext_param_name, value, shared = true) end # connect_param! calls dirty! so we don't have to 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_labels = false because we already checked above before setting the param + connect_param!(md, comp, param_name, ext_param_name, check_labels = false) end nothing end @@ -764,6 +757,99 @@ function _insert_comp!(obj::AbstractCompositeComponentDef, comp_def::AbstractCom nothing end +""" + _initialize_parameters!(md::ModelDef, comp_def::AbstractComponentDef) + +Add an unshared external parameters to `md` for each parameter in `comp_def`. +""" +function _initialize_parameters!(md::ModelDef, comp_def::AbstractComponentDef) + for param_def in parameters(comp_def) + + # gather info + param_name = nameof(param_def) + 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)} + + # create the unshared external parameter + ext_param_name = gensym() # unique name + value = param_def.default # if the default is nothing then value is nothing + + # no default + if isnothing(value) + if num_dims > 0 + param = ArrayModelParameter(value, param_dims, false) + else + param = ScalarModelParameter(value, false) + end + set_external_param!(md, ext_param_name, param) + connect_param!(md, comp_def, param_name, ext_param_name, check_labels = false) # don't check the labels because our values are nothing + + # default + else + if num_dims > 0 # array parameter case + + # 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 for an _initialize_parameters call: 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) + + # 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) + last = last_period(md) + + 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 + + else + values = value + end + + param = ArrayModelParameter(values, param_dims, false) + + # Need to check the dimensions of the parameter data against component before adding it to the model's external parameters + _check_labels(md, comp_def, param_name, param) + + else # scalar parameter case + value = convert(dtype, value) + param = ScalarModelParameter(value, false) + end + + set_external_param!(md, ext_param_name, param) + connect_param!(md, comp_def, param_name, ext_param_name) + end + end +end + """ _propagate_first_last!(obj::AbstractComponentDef; first::NothingInt=nothing, last::NothingInt=nothing) @@ -913,7 +999,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 +1019,9 @@ function add_comp!(obj::AbstractCompositeComponentDef, _add_anonymous_dims!(obj, comp_def) _insert_comp!(obj, comp_def, before=before, after=after) + # Create an unshared external 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 diff --git a/src/core/types/params.jl b/src/core/types/params.jl index 9769a8eb0..f6ee334d1 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 + shared::Bool + + function ScalarModelParameter{T}(value::T; shared::Bool = false) where T + new(value, shared) + end - function ScalarModelParameter{T}(value::T) where T - new(value) + function ScalarModelParameter{T}(value::T, shared::Bool) where T + new(value, shared) end - function ScalarModelParameter{T1}(value::T2) where {T1, T2} + function ScalarModelParameter{T1}(value::T2; shared::Bool = false) where {T1, T2} try - new(T1(value)) + new(T1(value), 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 + shared::Bool - function ArrayModelParameter{T}(values::T, dims::Vector{Symbol}) where T - new(values, dims) + function ArrayModelParameter{T}(values::T, dims::Vector{Symbol}; shared::Bool = false) where T + new(values, dims, shared) + end + + function ArrayModelParameter{T}(values::T, dims::Vector{Symbol}, shared::Bool) where T + new(values, dims, shared) end end ScalarModelParameter(value) = ScalarModelParameter{typeof(value)}(value) +ScalarModelParameter(value, shared) = ScalarModelParameter{typeof(value)}(value, 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}, shared::Bool) = ArrayModelParameter{typeof(value)}(value, dims, 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.shared) +Base.copy(obj::ArrayModelParameter{T}) where T = ArrayModelParameter(obj.values, obj.dim_names, obj.shared) dim_names(obj::ArrayModelParameter) = obj.dim_names dim_names(obj::ScalarModelParameter) = [] +is_shared(obj::ArrayModelParameter) = obj.shared +is_shared(obj::ScalarModelParameter) = obj.shared + abstract type AbstractConnection <: MimiStruct end struct InternalParameterConnection <: AbstractConnection From 8f4374ddaf127a92f8a2d12364cb6c89ef0b5ab1 Mon Sep 17 00:00:00 2001 From: lrennels Date: Tue, 11 May 2021 14:24:11 -0700 Subject: [PATCH 03/47] Add helper functions and update tests --- src/core/build.jl | 8 +- src/core/connections.jl | 73 +++++++++++++++---- src/core/model.jl | 1 + test/mcs/runtests.jl | 2 +- test/runtests.jl | 2 +- test/test_composite_parameters.jl | 24 +++--- test/test_defaults.jl | 11 +-- test/test_delete.jl | 24 ++++-- test/test_model_structure.jl | 18 +++-- test/test_model_structure_variabletimestep.jl | 18 +++-- test/test_parametertypes.jl | 27 ++++--- test/test_references.jl | 6 +- 12 files changed, 142 insertions(+), 72 deletions(-) diff --git a/src/core/build.jl b/src/core/build.jl index 61749cd2d..b60594c04 100644 --- a/src/core/build.jl +++ b/src/core/build.jl @@ -324,11 +324,11 @@ function _build(md::ModelDef) add_connector_comps!(md) # check if all parameters are set - not_set = unconnected_params(md) + nothingparams = nothing_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") + if ! isempty(nothingparams) + params = join([p.datum_name for p in nothingparams], "\n ") + error("Cannot build model; the following parameters do not have non-nothing values:\n $params") end vdict = _instantiate_vars(md) diff --git a/src/core/connections.jl b/src/core/connections.jl index 091381e3d..ab9a70aef 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -311,6 +311,46 @@ function connection_refs(obj::ModelDef) return refs end +""" + nothing_params(obj::AbstractCompositeComponentDef) + +Return a list of UnnamedReference's to parameters that have a value of nothing +and thus need to be assigned a value. This function replaces the use case of +`unconnected_params` since there is no notion of unconnected parameters +now that everything is connected upon the `add_comp!` call. +""" +function nothing_params(obj::AbstractCompositeComponentDef) + + # we only need to look at external parameters here since those are what is + # initialized during `add_comp!` with nothing as the value (unless there is a + # default) + refs = UnnamedReference[] + for conn in obj.external_param_conns + value = external_param(obj, conn.external_param) + if _is_nothing_param(value) + push!(refs, UnnamedReference(conn.comp_path.names[end], conn.param_name)) + end + end + return refs +end + +function _is_nothing_param(param::ScalarModelParameter) + return isnothing(param.value) +end + +function _is_nothing_param(param::ArrayModelParameter) + return isnothing(param.values) +end + +function _get_externalparam_name(obj::AbstractCompositeComponentDef, comp::Symbol, param_name::Symbol) + for conn in obj.external_param_conns + if comp == conn.comp_path.names[end] && conn.param_name == param_name + return conn.external_param + end + end + error("Cannot find an external parameter connection for component $comp's parameter $param_name in external parameter connections vector.") +end + """ unconnected_params(obj::AbstractCompositeComponentDef) @@ -329,27 +369,24 @@ to some other component to a value from a dictionary `parameters`. This method a the dictionary keys are strings that match the names of unset parameters in the model. """ function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T - for param_ref in unconnected_params(md) + 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 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 - connect_param!(md, comp_name, param_name, param_name) end + connect_param!(md, comp_name, param_name, param_name) end nothing end @@ -568,7 +605,7 @@ 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), shared = param.shared)) + set_external_param!(obj, name, ArrayModelParameter(new_timestep_array, dim_names(param), param.shared)) else copyto!(param.values.data, value) end @@ -631,6 +668,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, @@ -674,7 +715,7 @@ 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) + 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) diff --git a/src/core/model.jl b/src/core/model.jl index d71b178c5..56c6c8425 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -31,6 +31,7 @@ is_built(mm::MarginalModel) = (is_built(mm.base) && is_built(mm.modified)) @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 diff --git a/test/mcs/runtests.jl b/test/mcs/runtests.jl index 4e06acf46..5facafdc0 100644 --- a/test/mcs/runtests.jl +++ b/test/mcs/runtests.jl @@ -3,7 +3,7 @@ using Test @testset "Mimi-SA" begin - @info("test_empirical.jl") + @info("test_empirical.jl") # broken include("test_empirical.jl") @info("test_defmcs.jl") diff --git a/test/runtests.jl b/test/runtests.jl index 7a0b09dfe..1f1b88fc3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -50,7 +50,7 @@ Electron.prep_test_env() @info("test_delete.jl") @time include("test_delete.jl") - @info("test_replace_comp.jl") + @info("test_replace_comp.jl") # broken @time include("test_replace_comp.jl") @info("test_tools.jl") diff --git a/test/test_composite_parameters.jl b/test/test_composite_parameters.jl index 8b0f40868..192df93bb 100644 --- a/test/test_composite_parameters.jl +++ b/test/test_composite_parameters.jl @@ -68,7 +68,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,20 +119,28 @@ 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 do not have non-nothing values", 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 +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.external_param(m2.md, :p1).value == 2 +@test Mimi.external_param(m2.md, :p1).shared +# and that B.p1 is still the default value and unshared +sym = Mimi._get_externalparam_name(m2.md, :B, :p1) +@test Mimi.external_param(m2.md, sym).value == 3 +@test !(Mimi.external_param(m2.md, sym).shared) + 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 Mimi.external_param(m2.md, :B_p1).value == 2 +@test Mimi.external_param(m2.md, :B_p1).shared +@test issubset(Set([:p1, :B_p1]), Set(keys(m2.md.external_params))) # Test defaults being set properly: m3 = get_model() @@ -141,8 +149,6 @@ 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 #------------------------------------------------------------------------------ # Test set_param! for parameter that exists in neither model definition nor any subcomponent diff --git a/test/test_defaults.jl b/test/test_defaults.jl index 8704e2b78..f13e4bc2f 100644 --- a/test/test_defaults.jl +++ b/test/test_defaults.jl @@ -15,19 +15,14 @@ add_comp!(m, A) set_param!(m, :p2, 2) # So far only :p2 is in the model definition's dictionary -@test length(m.md.external_params) == 1 +@test :p2 in keys(m.md.external_params) +@test length(m.md.external_params) == 3 # two initial unshared defaults + shared :p2 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 isn't in the model definition's external params @test_throws ErrorException update_param!(m, :p1, 10) diff --git a/test/test_delete.jl b/test/test_delete.jl index e68dd6524..47a13fba7 100644 --- a/test/test_delete.jl +++ b/test/test_delete.jl @@ -5,6 +5,16 @@ module TestDelete using Mimi using Test +function count_nonnothing_external_params(md) + num_params = 0 + for param in values(md.external_params) + if !Mimi._is_nothing_param(param) + num_params += 1 + end + end + return num_params +end + @defcomp A begin p1 = Parameter() p2 = Parameter() @@ -26,19 +36,23 @@ 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.external_params) == 7 +@test count_nonnothing_external_params(m1.md) == 3 # three total non-nothing external params + 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 length(m1.md.external_params) == 7 +@test count_nonnothing_external_params(m1.md) == 3 # but all three external params remain @test :p2_A1 in keys(m1.md.external_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 length(m2.md.external_params) == 6 +@test count_nonnothing_external_params(m2.md) == 2 # :p2_A1 has been removed @test !(:p2_A1 in keys(m2.md.external_params)) run(m2) @@ -46,7 +60,7 @@ run(m2) 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_model_structure.jl b/test/test_model_structure.jl index 1c26c7ea0..cb7199299 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) 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_parametertypes.jl b/test/test_parametertypes.jl index 2156b4950..224588564 100644 --- a/test/test_parametertypes.jl +++ b/test/test_parametertypes.jl @@ -74,27 +74,32 @@ 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 +Mimi.build!(m) 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 +a_sym = Mimi._get_externalparam_name(m.mi.md, :MyComp, :a) +b_sym = Mimi._get_externalparam_name(m.mi.md, :MyComp, :b) +g_sym = Mimi._get_externalparam_name(m.mi.md, :MyComp, :g) +h_sym = Mimi._get_externalparam_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], 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[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].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 +@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 @@ -111,8 +116,8 @@ new_extpars = external_params(m) # Since there are changes since the last bui @test_throws ErrorException update_param!(m, :e, ones(10)) # wrong size update_param!(m, :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 length(extpars) == 14 +@test length(new_extpars) == 15 # adds another parameter for :a, so there is one old unshared defualt one and one new udpated one @test typeof(new_extpars[:a].values) == TimestepMatrix{FixedTimestep{2000, 1, 2100}, arrtype, 1, Array{arrtype, 2}} @test typeof(new_extpars[:d].value) == numtype @@ -384,6 +389,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(m.md.external_params)) # 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..91a5a76de 100644 --- a/test/test_references.jl +++ b/test/test_references.jl @@ -22,12 +22,12 @@ 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 Mimi._get_externalparam_name(m.md, :foo, :p1) == :foo_p1 @test :foo_p1 in keys(m.md.external_params) +@test Mimi.UnnamedReference(:B, :p1) in Mimi.nothing_params(m.md) refB[:p1] = 5 -@test length(m.md.external_param_conns) == 2 +@test Mimi._get_externalparam_name(m.md, :B, :p1) == :B_p1 @test :B_p1 in keys(m.md.external_params) # Use the ComponentReferences to make an internal connection From 10af90917ef3bb163d9fea54ced8b3ba59830204 Mon Sep 17 00:00:00 2001 From: lrennels Date: Tue, 11 May 2021 18:00:24 -0700 Subject: [PATCH 04/47] Remove unshared parameters when replaced with shared; simply functions --- src/core/connections.jl | 129 +++++++++++++++---- src/core/defs.jl | 203 +++++++----------------------- src/core/types/params.jl | 36 +++--- test/runtests.jl | 2 +- test/test_composite_parameters.jl | 6 +- test/test_defaults.jl | 2 +- test/test_delete.jl | 19 +-- test/test_parametertypes.jl | 3 +- 8 files changed, 182 insertions(+), 218 deletions(-) diff --git a/src/core/connections.jl b/src/core/connections.jl index ab9a70aef..93787fbfd 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 we are disconnecting an unshared parameter, remove it's unique Symbol + # parameter definition from the ModelDef's list of external parameters as well + ext_param_name = _get_externalparam_name(obj, nameof(comp_def), param_name) + if !isnothing(ext_param_name) && !(external_param(obj, ext_param_name).is_shared) + delete!(obj.external_params, ext_param_name); + end + + # filter the external parameter connections filter!(x -> !(x.comp_path == path && x.param_name == param_name), obj.external_param_conns) end dirty!(obj) @@ -342,13 +351,14 @@ function _is_nothing_param(param::ArrayModelParameter) return isnothing(param.values) end -function _get_externalparam_name(obj::AbstractCompositeComponentDef, comp::Symbol, param_name::Symbol) +function _get_externalparam_name(obj::AbstractCompositeComponentDef, comp::Symbol, param_name::Symbol; error_if_not_found = false) for conn in obj.external_param_conns if comp == conn.comp_path.names[end] && conn.param_name == param_name return conn.external_param end end - error("Cannot find an external parameter connection for component $comp's parameter $param_name in external parameter connections vector.") + error_if_not_found && error("Cannot find an external parameter connection for component $comp's parameter $param_name in external parameter connections vector.") + return nothing end """ @@ -439,14 +449,14 @@ end function set_external_param!(obj::ModelDef, name::Symbol, value::Number; param_dims::Union{Nothing,Array{Symbol}} = nothing, - shared::Bool = false) - set_external_scalar_param!(obj, name, value, shared) + is_shared::Bool = false) + set_external_scalar_param!(obj, name, value, is_shared) end function set_external_param!(obj::ModelDef, name::Symbol, value::Union{AbstractArray, AbstractRange, Tuple}; param_dims::Union{Nothing,Array{Symbol}} = nothing, - shared::Bool = false) + is_shared::Bool = false) ti = get_time_index_position(param_dims) if ti != nothing value = convert(Array{number_type(obj)}, value) @@ -456,64 +466,64 @@ function set_external_param!(obj::ModelDef, name::Symbol, values = value end - set_external_array_param!(obj, name, values, param_dims, shared = shared) + set_external_array_param!(obj, name, values, param_dims, is_shared = is_shared) end """ set_external_array_param!(obj::ModelDef, name::Symbol, value::TimestepVector, - dims; shared::Bool = false) + dims; is_shared::Bool = false) Add a one dimensional time-indexed array parameter indicated by `name` and -`value` to the composite `obj`. The `shared` attribute of the ArrayModelParameter +`value` to the composite `obj`. 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; shared::Bool = false) - param = ArrayModelParameter(value, [:time], shared) # must be :time + dims; is_shared::Bool = false) + param = ArrayModelParameter(value, [:time], is_shared) # must be :time set_external_param!(obj, name, param) end """ set_external_array_param!(obj::ModelDef, name::Symbol, value::TimestepMatrix, dims; - shared::Bool = false) + is_shared::Bool = false) Add a multi-dimensional time-indexed array parameter `name` with value -`value` to the composite `obj`. The `shared` attribute of the ArrayModelParameter +`value` to the composite `obj`. 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::TimestepArray, dims; - shared::Bool = false) - param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims, shared) + is_shared::Bool = false) + param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims, is_shared) set_external_param!(obj, name, param) end """ set_external_array_param!(obj::ModelDef, name::Symbol, value::AbstractArray, dims; - shared::Bool = false) + is_shared::Bool = false) Add an array type parameter `name` with value `value` and `dims` dimensions to the -composite `obj`. The `shared` attribute of the ArrayModelParameter will default to +composite `obj`. The `is_shared` attribute of the ArrayModelParameter will default to false. """ function set_external_array_param!(obj::ModelDef, name::Symbol, value::AbstractArray, dims; - shared::Bool = false) - param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims, shared) + is_shared::Bool = false) + param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims, is_shared) set_external_param!(obj, name, param) end """ - set_external_scalar_param!(obj::ModelDef, name::Symbol, value::Any; shared::Bool = false) + set_external_scalar_param!(obj::ModelDef, name::Symbol, value::Any; is_shared::Bool = false) Add a scalar type parameter `name` with the value `value` to the composite `obj`. """ -function set_external_scalar_param!(obj::ModelDef, name::Symbol, value::Any; shared::Bool = false) - param = ScalarModelParameter(value, shared) +function set_external_scalar_param!(obj::ModelDef, name::Symbol, value::Any; is_shared::Bool = false) + param = ScalarModelParameter(value, is_shared) set_external_param!(obj, name, param) end @@ -605,7 +615,7 @@ 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), param.shared)) + set_external_param!(obj, name, ArrayModelParameter(new_timestep_array, dim_names(param), param.is_shared)) else copyto!(param.values.data, value) end @@ -796,3 +806,78 @@ 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 + +function create_external_param(md::ModelDef, param_def::AbstractParameterDef, value::Any; is_shared::Bool = false) + + # gather info + param_name = nameof(param_def) + 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)} + + # create a sentinal unshared parameter + if isnothing(value) + if num_dims > 0 + param = ArrayModelParameter(value, param_dims, is_shared) + else + param = ScalarModelParameter(value, is_shared) + end + + # have a value - in the initiliazation of parameters case this is a default + else + if num_dims > 0 # array parameter case + + # 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 for an _initialize_parameters call: 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) + + # 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) + last = last_period(md) + + 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 + + else + values = value + end + + param = ArrayModelParameter(values, param_dims, is_shared) + + else # scalar parameter case + value = convert(dtype, value) + param = ScalarModelParameter(value, is_shared) + end + end + return param +end diff --git a/src/core/defs.jl b/src/core/defs.jl index c2e8e8480..29cbd90f1 100644 --- a/src/core/defs.jl +++ b/src/core/defs.jl @@ -457,21 +457,18 @@ of the dimension names of the provided data, and will be used to check that they model's index labels. """ function set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing, ignoreunits::Bool=false, comps=nothing, ext_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 + isempty(comps) && error("Cannot set parameter :$param_name; not found in ModelDef or children") - 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 - + + # 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 @@ -484,78 +481,33 @@ function set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing, ignor end end - if value isa NamedArray - dims = dimnames(value) - end - - if dims !== nothing - 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 + # check dimensions + dims !== nothing && check_parameter_dimensions(md, value, dims, param_name) - # 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) - - # 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) - last = last_period(md) - - 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 - else - values = value - end - - param = ArrayModelParameter(values, param_dims, true) - - # Need to check the dimensions of the parameter data against each component before adding it to the model's external parameters + # create shared external 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_external_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 external parameters + if param isa ArrayModelParameter for comp in comps _check_labels(md, comp, param_name, param) end - set_external_param!(md, ext_param_name, param) - + end - else # scalar parameter case - value = convert(dtype, value) - set_external_scalar_param!(md, ext_param_name, value, shared = true) + # add the shared external parameter to the model def + if ext_param_name === nothing + ext_param_name = param_name end + set_external_param!(md, ext_param_name, param) + # connect # connect_param! calls dirty! so we don't have to for comp in comps - # Set check_labels = false because we already checked above before setting the param + # Set check_labels = false because we already checked above connect_param!(md, comp, param_name, ext_param_name, check_labels = false) end nothing @@ -765,89 +717,27 @@ Add an unshared external parameters to `md` for each parameter in `comp_def`. function _initialize_parameters!(md::ModelDef, comp_def::AbstractComponentDef) for param_def in parameters(comp_def) - # gather info + ext_param_name = gensym() param_name = nameof(param_def) - 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)} - - # create the unshared external parameter - ext_param_name = gensym() # unique name - value = param_def.default # if the default is nothing then value is nothing - - # no default - if isnothing(value) - if num_dims > 0 - param = ArrayModelParameter(value, param_dims, false) - else - param = ScalarModelParameter(value, false) - end - set_external_param!(md, ext_param_name, param) - connect_param!(md, comp_def, param_name, ext_param_name, check_labels = false) # don't check the labels because our values are nothing - - # default - else - if num_dims > 0 # array parameter case - - # 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 for an _initialize_parameters call: 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) + value = param_def.default - # 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) - last = last_period(md) + # create the unshared external parameter with a value of param_def.default, + # which will be nothing if it not set explicitly + param = create_external_param(md, param_def, value) - 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 - - else - values = value - end - - param = ArrayModelParameter(values, param_dims, false) - - # Need to check the dimensions of the parameter data against component before adding it to the model's external parameters - _check_labels(md, comp_def, param_name, param) - - else # scalar parameter case - value = convert(dtype, value) - param = ScalarModelParameter(value, false) - end - - set_external_param!(md, ext_param_name, param) - connect_param!(md, comp_def, param_name, ext_param_name) + # Need to check the dimensions of the parameter data against component + # before adding it to the model's external parameters + if param isa ArrayModelParameter && !isnothing(value) + _check_labels(md, comp_def, param_name, param) end + + # add the unshared external parameter to the model def + set_external_param!(md, ext_param_name, param) + + # connect - don't need to check labels since did it above + connect_param!(md, comp_def, param_name, ext_param_name; check_labels = false) end + nothing end """ @@ -984,6 +874,7 @@ function add_comp!(obj::AbstractCompositeComponentDef, last::NothingInt=nothing, before::NothingSymbol=nothing, after::NothingSymbol=nothing, + initialize_params::Bool = true, rename::NothingPairList=nothing) # TBD: rename is not yet implemented # Check if component being added already exists @@ -1020,7 +911,9 @@ function add_comp!(obj::AbstractCompositeComponentDef, _insert_comp!(obj, comp_def, before=before, after=after) # Create an unshared external parameter for each of the new component's parameters - isa(obj, ModelDef) && _initialize_parameters!(obj, comp_def) + if isa(obj, ModelDef) && initialize_params + _initialize_parameters!(obj, comp_def) + end # Return the comp since it's a copy of what was passed in return comp_def @@ -1147,13 +1040,13 @@ 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 and not initializing new connections in the add_comp! phase delete!(obj.namespace, comp_name) + return add_comp!(obj, comp_id, comp_name; before=before, after=after, initialize_params = false) else # Delete the old component and all its internal and external parameter connections delete!(obj, comp_name) + return add_comp!(obj, comp_id, comp_name; before=before, after=after) end - - # Re-add - return add_comp!(obj, comp_id, comp_name; before=before, after=after) end diff --git a/src/core/types/params.jl b/src/core/types/params.jl index f6ee334d1..7363d9453 100644 --- a/src/core/types/params.jl +++ b/src/core/types/params.jl @@ -6,19 +6,19 @@ abstract type ModelParameter <: MimiStruct end mutable struct ScalarModelParameter{T} <: ModelParameter value::T - shared::Bool + is_shared::Bool - function ScalarModelParameter{T}(value::T; shared::Bool = false) where T - new(value, shared) + function ScalarModelParameter{T}(value::T; is_shared::Bool = false) where T + new(value, is_shared) end - function ScalarModelParameter{T}(value::T, shared::Bool) where T - new(value, shared) + function ScalarModelParameter{T}(value::T, is_shared::Bool) where T + new(value, is_shared) end - function ScalarModelParameter{T1}(value::T2; shared::Bool = false) where {T1, T2} + function ScalarModelParameter{T1}(value::T2; is_shared::Bool = false) where {T1, T2} try - new(T1(value), shared) + new(T1(value), is_shared) catch err error("Failed to convert $value::$T2 to $T1") end @@ -28,38 +28,38 @@ end mutable struct ArrayModelParameter{T} <: ModelParameter values::T dim_names::Vector{Symbol} # if empty, we don't have the dimensions' name information - shared::Bool + is_shared::Bool - function ArrayModelParameter{T}(values::T, dims::Vector{Symbol}; shared::Bool = false) where T - new(values, dims, shared) + 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}, shared::Bool) where T - new(values, dims, shared) + 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, shared) = ScalarModelParameter{typeof(value)}(value, shared) +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}, shared::Bool) = ArrayModelParameter{typeof(value)}(value, dims, shared) +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, obj.shared) -Base.copy(obj::ArrayModelParameter{T}) where T = ArrayModelParameter(obj.values, obj.dim_names, obj.shared) +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.shared -is_shared(obj::ScalarModelParameter) = obj.shared +is_is_shared(obj::ArrayModelParameter) = obj.is_shared +is_is_shared(obj::ScalarModelParameter) = obj.is_shared abstract type AbstractConnection <: MimiStruct end diff --git a/test/runtests.jl b/test/runtests.jl index 1f1b88fc3..7a0b09dfe 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -50,7 +50,7 @@ Electron.prep_test_env() @info("test_delete.jl") @time include("test_delete.jl") - @info("test_replace_comp.jl") # broken + @info("test_replace_comp.jl") @time include("test_replace_comp.jl") @info("test_tools.jl") diff --git a/test/test_composite_parameters.jl b/test/test_composite_parameters.jl index 192df93bb..9eab668a4 100644 --- a/test/test_composite_parameters.jl +++ b/test/test_composite_parameters.jl @@ -127,11 +127,11 @@ 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.external_param(m2.md, :p1).value == 2 -@test Mimi.external_param(m2.md, :p1).shared +@test Mimi.external_param(m2.md, :p1).is_shared # and that B.p1 is still the default value and unshared sym = Mimi._get_externalparam_name(m2.md, :B, :p1) @test Mimi.external_param(m2.md, sym).value == 3 -@test !(Mimi.external_param(m2.md, sym).shared) +@test !(Mimi.external_param(m2.md, sym).is_shared) err8 = try set_param!(m2, :B, :p1, 2) catch err err end @@ -139,7 +139,7 @@ err8 = try set_param!(m2, :B, :p1, 2) catch err err end set_param!(m2, :B, :p1, :B_p1, 2) # Use a unique name to set B.p1 @test Mimi.external_param(m2.md, :B_p1).value == 2 -@test Mimi.external_param(m2.md, :B_p1).shared +@test Mimi.external_param(m2.md, :B_p1).is_shared @test issubset(Set([:p1, :B_p1]), Set(keys(m2.md.external_params))) # Test defaults being set properly: diff --git a/test/test_defaults.jl b/test/test_defaults.jl index f13e4bc2f..1a0c6ac95 100644 --- a/test/test_defaults.jl +++ b/test/test_defaults.jl @@ -16,7 +16,7 @@ set_param!(m, :p2, 2) # So far only :p2 is in the model definition's dictionary @test :p2 in keys(m.md.external_params) -@test length(m.md.external_params) == 3 # two initial unshared defaults + shared :p2 +@test length(m.md.external_params) == 2 run(m) diff --git a/test/test_delete.jl b/test/test_delete.jl index 47a13fba7..3428c5ecd 100644 --- a/test/test_delete.jl +++ b/test/test_delete.jl @@ -5,16 +5,6 @@ module TestDelete using Mimi using Test -function count_nonnothing_external_params(md) - num_params = 0 - for param in values(md.external_params) - if !Mimi._is_nothing_param(param) - num_params += 1 - end - end - return num_params -end - @defcomp A begin p1 = Parameter() p2 = Parameter() @@ -36,23 +26,20 @@ 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) == 7 -@test count_nonnothing_external_params(m1.md) == 3 # three total non-nothing external params +@test length(m1.md.external_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) == 7 -@test count_nonnothing_external_params(m1.md) == 3 # but all three external params remain +@test length(m1.md.external_params) == 3 @test :p2_A1 in keys(m1.md.external_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) == 6 -@test count_nonnothing_external_params(m2.md) == 2 # :p2_A1 has been removed +@test length(m2.md.external_params) == 2 # :p2_A1 has been removed @test !(:p2_A1 in keys(m2.md.external_params)) run(m2) diff --git a/test/test_parametertypes.jl b/test/test_parametertypes.jl index 224588564..5d9b975a3 100644 --- a/test/test_parametertypes.jl +++ b/test/test_parametertypes.jl @@ -116,8 +116,7 @@ new_extpars = external_params(m) # Since there are changes since the last bui @test_throws ErrorException update_param!(m, :e, ones(10)) # wrong size update_param!(m, :e, [4,5,6,7]) -@test length(extpars) == 14 -@test length(new_extpars) == 15 # adds another parameter for :a, so there is one old unshared defualt one and one new udpated one +@test length(extpars) == length(new_extpars) == 9 # we replaced the unshared default for :a with a shared for :a @test typeof(new_extpars[:a].values) == TimestepMatrix{FixedTimestep{2000, 1, 2100}, arrtype, 1, Array{arrtype, 2}} @test typeof(new_extpars[:d].value) == numtype From d7831f4653fc9141326e98333c1f2d102074e7aa Mon Sep 17 00:00:00 2001 From: lrennels Date: Tue, 11 May 2021 23:04:07 -0700 Subject: [PATCH 05/47] Clean up code --- src/core/build.jl | 6 +-- src/core/connections.jl | 87 +++++++++++++++++++++---------- src/core/defs.jl | 21 +++++--- test/mcs/runtests.jl | 2 +- test/test_composite_parameters.jl | 4 +- test/test_parametertypes.jl | 8 +-- test/test_references.jl | 4 +- 7 files changed, 85 insertions(+), 47 deletions(-) diff --git a/src/core/build.jl b/src/core/build.jl index b60594c04..0bd0f1eca 100644 --- a/src/core/build.jl +++ b/src/core/build.jl @@ -323,12 +323,12 @@ function _build(md::ModelDef) # @info "_build(md)" add_connector_comps!(md) - # check if all parameters are set + # 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 do not have non-nothing values:\n $params") + error("Cannot build model; the following parameters still have values of nothing and need to be updated or set:\n $params") end vdict = _instantiate_vars(md) diff --git a/src/core/connections.jl b/src/core/connections.jl index 93787fbfd..eed54e655 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -21,14 +21,14 @@ function disconnect_param!(obj::AbstractCompositeComponentDef, comp_def::Abstrac if obj isa ModelDef - # if we are disconnecting an unshared parameter, remove it's unique Symbol - # parameter definition from the ModelDef's list of external parameters as well - ext_param_name = _get_externalparam_name(obj, nameof(comp_def), param_name) + # 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 external parameters + ext_param_name = get_external_param_name(obj, nameof(comp_def), param_name; missing_ok = true) if !isnothing(ext_param_name) && !(external_param(obj, ext_param_name).is_shared) delete!(obj.external_params, ext_param_name); end - # filter the external parameter connections filter!(x -> !(x.comp_path == path && x.param_name == param_name), obj.external_param_conns) end dirty!(obj) @@ -192,7 +192,7 @@ function _connect_param!(obj::AbstractCompositeComponentDef, end - set_external_array_param!(obj, dst_par_name, values, dst_dims) + set_external_array_param!(obj, dst_par_name, values, dst_dims) backup_param_name = dst_par_name else @@ -323,44 +323,40 @@ end """ nothing_params(obj::AbstractCompositeComponentDef) -Return a list of UnnamedReference's to parameters that have a value of nothing -and thus need to be assigned a value. This function replaces the use case of -`unconnected_params` since there is no notion of unconnected parameters -now that everything is connected upon the `add_comp!` call. +Return a list of UnnamedReference's to parameters that are connected to a an +external parameter with a value of nothing. """ function nothing_params(obj::AbstractCompositeComponentDef) - # we only need to look at external parameters here since those are what is - # initialized during `add_comp!` with nothing as the value (unless there is a - # default) refs = UnnamedReference[] + for conn in obj.external_param_conns value = external_param(obj, conn.external_param) - if _is_nothing_param(value) + if is_nothing_param(value) push!(refs, UnnamedReference(conn.comp_path.names[end], conn.param_name)) end end return refs end -function _is_nothing_param(param::ScalarModelParameter) +""" + 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 -function _is_nothing_param(param::ArrayModelParameter) +""" + 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 -function _get_externalparam_name(obj::AbstractCompositeComponentDef, comp::Symbol, param_name::Symbol; error_if_not_found = false) - for conn in obj.external_param_conns - if comp == conn.comp_path.names[end] && conn.param_name == param_name - return conn.external_param - end - end - error_if_not_found && error("Cannot find an external parameter connection for component $comp's parameter $param_name in external parameter connections vector.") - return nothing -end - """ unconnected_params(obj::AbstractCompositeComponentDef) @@ -393,7 +389,7 @@ function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T 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.") + error("Cannot set parameter :$param_name, not found in provided dictionary.") end end connect_param!(md, comp_name, param_name, param_name) @@ -436,12 +432,35 @@ function external_param(obj::ModelDef, name::Symbol; missing_ok=false) error("$name not found in external parameter list") end +""" + get_external_param_name(obj::ModelDef, comp_name::Symbol, param_name::Symbol; missing_ok=false) + +Get the external 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_external_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.external_param + end + end + + missing_ok && return nothing + + error("External parameter connected to $comp's parameter $param_name not found in external parameter connections list.") +end + function add_external_param_conn!(obj::ModelDef, conn::ExternalParameterConnection) push!(obj.external_param_conns, conn) 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" + # end obj.external_params[name] = value dirty!(obj) return value @@ -450,7 +469,7 @@ end function set_external_param!(obj::ModelDef, name::Symbol, value::Number; param_dims::Union{Nothing,Array{Symbol}} = nothing, is_shared::Bool = false) - set_external_scalar_param!(obj, name, value, is_shared) + set_external_scalar_param!(obj, name, value, is_shared = is_shared) end function set_external_param!(obj::ModelDef, name::Symbol, @@ -725,7 +744,11 @@ 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) && !_is_nothing_param(param) + # 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) @@ -807,6 +830,14 @@ function _get_param_times(param::ArrayModelParameter{TimestepArray{VariableTimes return [TIMES...] end +""" + create_external_param(md::ModelDef, param_def::AbstractParameterDef, value::Any; is_shared::Bool = false) + +Create a new external 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_external_param(md::ModelDef, param_def::AbstractParameterDef, value::Any; is_shared::Bool = false) # gather info diff --git a/src/core/defs.jl b/src/core/defs.jl index 29cbd90f1..52e912d25 100644 --- a/src/core/defs.jl +++ b/src/core/defs.jl @@ -464,11 +464,10 @@ function set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing, ignor comps = [comp for (compname, comp) in components(md) if has_parameter(comp, param_name)] 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 @@ -482,7 +481,13 @@ function set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing, ignor end # check dimensions - dims !== nothing && check_parameter_dimensions(md, value, dims, param_name) + if value isa NamedArray + dims = dimnames(value) + end + + if dims !== nothing + check_parameter_dimensions(md, value, dims, param_name) + end # create shared external parameter - since we alread checked that the found # comps have no conflicting fields in their parameter definitions, we can @@ -505,9 +510,9 @@ function set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing, ignor set_external_param!(md, ext_param_name, param) # connect - # connect_param! calls dirty! so we don't have to for comp in comps # Set check_labels = false because we already checked above + # connect_param! calls dirty! so we don't have to connect_param!(md, comp, param_name, ext_param_name, check_labels = false) end nothing @@ -712,7 +717,8 @@ end """ _initialize_parameters!(md::ModelDef, comp_def::AbstractComponentDef) -Add an unshared external parameters to `md` for each parameter in `comp_def`. +Add and connect an unshared external parameter to `md` for each parameter in +`comp_def`. """ function _initialize_parameters!(md::ModelDef, comp_def::AbstractComponentDef) for param_def in parameters(comp_def) @@ -865,7 +871,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, @@ -1041,7 +1047,8 @@ function _replace!(obj::AbstractCompositeComponentDef, filter!(epc -> !(epc in remove), external_param_conns(obj)) # Delete the old component from composite's namespace only, leaving parameter - # connections and not initializing new connections in the add_comp! phase + # connections and not initializing new connections in the add_comp! phase, + # which allows the existing connections to persist. delete!(obj.namespace, comp_name) return add_comp!(obj, comp_id, comp_name; before=before, after=after, initialize_params = false) else diff --git a/test/mcs/runtests.jl b/test/mcs/runtests.jl index 5facafdc0..4e06acf46 100644 --- a/test/mcs/runtests.jl +++ b/test/mcs/runtests.jl @@ -3,7 +3,7 @@ using Test @testset "Mimi-SA" begin - @info("test_empirical.jl") # broken + @info("test_empirical.jl") include("test_empirical.jl") @info("test_defmcs.jl") diff --git a/test/test_composite_parameters.jl b/test/test_composite_parameters.jl index 9eab668a4..2ced3118e 100644 --- a/test/test_composite_parameters.jl +++ b/test/test_composite_parameters.jl @@ -119,7 +119,7 @@ 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 do not have non-nothing values", sprint(showerror, err7)) +@test occursin("Cannot build model; the following parameters still have values of nothing and need to be updated or set:", sprint(showerror, err7)) # Set separate values for p1 in A and B m2 = get_model() @@ -129,7 +129,7 @@ set_param!(m2, :A, :p1, 2) # Set the value only for component A @test Mimi.external_param(m2.md, :p1).value == 2 @test Mimi.external_param(m2.md, :p1).is_shared # and that B.p1 is still the default value and unshared -sym = Mimi._get_externalparam_name(m2.md, :B, :p1) +sym = Mimi.get_external_param_name(m2.md, :B, :p1) @test Mimi.external_param(m2.md, sym).value == 3 @test !(Mimi.external_param(m2.md, sym).is_shared) diff --git a/test/test_parametertypes.jl b/test/test_parametertypes.jl index 5d9b975a3..238b32f7e 100644 --- a/test/test_parametertypes.jl +++ b/test/test_parametertypes.jl @@ -77,10 +77,10 @@ set_param!(m, :MyComp, :j, [1,2,3]) Mimi.build!(m) extpars = external_params(m.mi.md) -a_sym = Mimi._get_externalparam_name(m.mi.md, :MyComp, :a) -b_sym = Mimi._get_externalparam_name(m.mi.md, :MyComp, :b) -g_sym = Mimi._get_externalparam_name(m.mi.md, :MyComp, :g) -h_sym = Mimi._get_externalparam_name(m.mi.md, :MyComp, :h) +a_sym = Mimi.get_external_param_name(m.mi.md, :MyComp, :a) +b_sym = Mimi.get_external_param_name(m.mi.md, :MyComp, :b) +g_sym = Mimi.get_external_param_name(m.mi.md, :MyComp, :g) +h_sym = Mimi.get_external_param_name(m.mi.md, :MyComp, :h) @test isa(extpars[a_sym], ArrayModelParameter) @test isa(extpars[b_sym], ArrayModelParameter) diff --git a/test/test_references.jl b/test/test_references.jl index 91a5a76de..71e32c777 100644 --- a/test/test_references.jl +++ b/test/test_references.jl @@ -22,12 +22,12 @@ 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 Mimi._get_externalparam_name(m.md, :foo, :p1) == :foo_p1 +@test Mimi.get_external_param_name(m.md, :foo, :p1) == :foo_p1 @test :foo_p1 in keys(m.md.external_params) @test Mimi.UnnamedReference(:B, :p1) in Mimi.nothing_params(m.md) refB[:p1] = 5 -@test Mimi._get_externalparam_name(m.md, :B, :p1) == :B_p1 +@test Mimi.get_external_param_name(m.md, :B, :p1) == :B_p1 @test :B_p1 in keys(m.md.external_params) # Use the ComponentReferences to make an internal connection From aea3a897212b18c6d8d8ffabc8cd5288aaa2fb1d Mon Sep 17 00:00:00 2001 From: lrennels Date: Thu, 13 May 2021 11:47:09 -0700 Subject: [PATCH 06/47] Alter replace comp behavior and add tests for default composites --- src/core/connections.jl | 18 +++++- src/core/defs.jl | 52 ++++++++-------- test/test_composite_parameters.jl | 100 +++++++++++++++++++++++++++--- test/test_defaults.jl | 7 ++- test/test_dimensions.jl | 6 +- test/test_parametertypes.jl | 2 +- test/test_references.jl | 6 +- test/test_replace_comp.jl | 28 ++++++++- 8 files changed, 173 insertions(+), 46 deletions(-) diff --git a/src/core/connections.jl b/src/core/connections.jl index eed54e655..cd867c8d0 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -435,8 +435,8 @@ end """ get_external_param_name(obj::ModelDef, comp_name::Symbol, param_name::Symbol; missing_ok=false) -Get the external parameter name for the exernal parameter conneceted to $comp_name's -parameter $param_name. The keyword argument `missing_ok` defaults to false so +Get the external 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`. """ @@ -449,7 +449,19 @@ function get_external_param_name(obj::ModelDef, comp_name::Symbol, param_name::S missing_ok && return nothing - error("External parameter connected to $comp's parameter $param_name not found in external parameter connections list.") + error("External parameter connected to $comp_name's parameter $param_name not found in external parameter connections list.") +end + +""" + get_external_param_name(obj::Model, comp_name::Symbol, param_name::Symbol; missing_ok=false) + +Get the external 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_external_param_name(obj::Model, comp_name::Symbol, param_name::Symbol; missing_ok=false) + get_external_param_name(obj.md, comp_name, param_name; missing_ok = missing_ok) end function add_external_param_conn!(obj::ModelDef, conn::ExternalParameterConnection) diff --git a/src/core/defs.jl b/src/core/defs.jl index 52e912d25..e0e2583cd 100644 --- a/src/core/defs.jl +++ b/src/core/defs.jl @@ -723,25 +723,31 @@ Add and connect an unshared external parameter to `md` for each parameter in function _initialize_parameters!(md::ModelDef, comp_def::AbstractComponentDef) for param_def in parameters(comp_def) - ext_param_name = gensym() - param_name = nameof(param_def) - value = param_def.default + # check if the parameter is already created and connected, which may be + # the case if we are using replace! with the default reconnect = true + curr_ext_param_name = get_external_param_name(md, nameof(comp_def), nameof(param_def); missing_ok = true) + + if isnothing(curr_ext_param_name) + ext_param_name = gensym() + param_name = nameof(param_def) + value = param_def.default + + # create the unshared external parameter with a value of param_def.default, + # which will be nothing if it not set explicitly + param = create_external_param(md, param_def, value) + + # Need to check the dimensions of the parameter data against component + # before adding it to the model's external parameters + if param isa ArrayModelParameter && !isnothing(value) + _check_labels(md, comp_def, param_name, param) + end + + # add the unshared external parameter to the model def + set_external_param!(md, ext_param_name, param) - # create the unshared external parameter with a value of param_def.default, - # which will be nothing if it not set explicitly - param = create_external_param(md, param_def, value) - - # Need to check the dimensions of the parameter data against component - # before adding it to the model's external parameters - if param isa ArrayModelParameter && !isnothing(value) - _check_labels(md, comp_def, param_name, param) + # connect - don't need to check labels since did it above + connect_param!(md, comp_def, param_name, ext_param_name; check_labels = false) end - - # add the unshared external parameter to the model def - set_external_param!(md, ext_param_name, param) - - # connect - don't need to check labels since did it above - connect_param!(md, comp_def, param_name, ext_param_name; check_labels = false) end nothing end @@ -880,7 +886,6 @@ function add_comp!(obj::AbstractCompositeComponentDef, last::NothingInt=nothing, before::NothingSymbol=nothing, after::NothingSymbol=nothing, - initialize_params::Bool = true, rename::NothingPairList=nothing) # TBD: rename is not yet implemented # Check if component being added already exists @@ -917,9 +922,7 @@ function add_comp!(obj::AbstractCompositeComponentDef, _insert_comp!(obj, comp_def, before=before, after=after) # Create an unshared external parameter for each of the new component's parameters - if isa(obj, ModelDef) && initialize_params - _initialize_parameters!(obj, comp_def) - end + isa(obj, ModelDef) && _initialize_parameters!(obj, comp_def) # Return the comp since it's a copy of what was passed in return comp_def @@ -1047,13 +1050,12 @@ function _replace!(obj::AbstractCompositeComponentDef, filter!(epc -> !(epc in remove), external_param_conns(obj)) # Delete the old component from composite's namespace only, leaving parameter - # connections and not initializing new connections in the add_comp! phase, - # which allows the existing connections to persist. + # connections delete!(obj.namespace, comp_name) - return add_comp!(obj, comp_id, comp_name; before=before, after=after, initialize_params = false) else # Delete the old component and all its internal and external parameter connections delete!(obj, comp_name) - return add_comp!(obj, comp_id, comp_name; before=before, after=after) end + return add_comp!(obj, comp_id, comp_name; before=before, after=after) + end diff --git a/test/test_composite_parameters.jl b/test/test_composite_parameters.jl index 2ced3118e..1302207e7 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: external_params + @defcomp A begin p1 = Parameter(unit = "\$", default=3) p2 = Parameter() @@ -133,6 +135,13 @@ sym = Mimi.get_external_param_name(m2.md, :B, :p1) @test Mimi.external_param(m2.md, sym).value == 3 @test !(Mimi.external_param(m2.md, sym).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) 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)) @@ -142,13 +151,90 @@ set_param!(m2, :B, :p1, :B_p1, 2) # Use a unique name to set B.p1 @test Mimi.external_param(m2.md, :B_p1).is_shared @test issubset(Set([:p1, :B_p1]), Set(keys(m2.md.external_params))) -# Test defaults being set properly: -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) +#------------------------------------------------------------------------------ +# 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(external_params(m)) == 1 +ext_param_name = Mimi.get_external_param_name(m.md, :top, :superp1) +@test Mimi.is_nothing_param(external_params(m)[ext_param_name]) +@test !external_params(m)[ext_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(external_params(m)) == 1 +ext_param_name = Mimi.get_external_param_name(m.md, :top, :superp1) +@test external_params(m)[ext_param_name].value == 8.0 +@test !external_params(m)[ext_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(external_params(m)) == 1 +ext_param_name = Mimi.get_external_param_name(m.md, :top, :superp1) +@test external_params(m)[ext_param_name].value == 2 +@test !external_params(m)[ext_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(external_params(m)) == 2 +ext_param_name = Mimi.get_external_param_name(m.md, :top, :p1) +@test external_params(m)[ext_param_name].value == 2 +@test !external_params(m)[ext_param_name].is_shared +ext_param_name = Mimi.get_external_param_name(m.md, :top, :p2) +@test external_params(m)[ext_param_name].value == 3 +@test !external_params(m)[ext_param_name].is_shared #------------------------------------------------------------------------------ # Test set_param! for parameter that exists in neither model definition nor any subcomponent diff --git a/test/test_defaults.jl b/test/test_defaults.jl index 1a0c6ac95..3e910d698 100644 --- a/test/test_defaults.jl +++ b/test/test_defaults.jl @@ -3,6 +3,7 @@ module TestDefaults using Mimi using Test +import Mimi: external_params @defcomp A begin p1 = Parameter(default = 1) @@ -15,8 +16,8 @@ add_comp!(m, A) set_param!(m, :p2, 2) # So far only :p2 is in the model definition's dictionary -@test :p2 in keys(m.md.external_params) -@test length(m.md.external_params) == 2 +@test :p2 in keys(external_params(m)) +@test length(external_params(m)) == 2 run(m) @@ -30,7 +31,7 @@ run(m) set_param!(m, :p1, 10) # Now there is a :p1 in the model definition's dictionary -@test :p1 in keys(m.md.external_params) +@test :p1 in keys(external_params(m)) run(m) @test m[:A, :p1] == 10 diff --git a/test/test_dimensions.jl b/test/test_dimensions.jl index 90b9314fe..c50b908fe 100644 --- a/test/test_dimensions.jl +++ b/test/test_dimensions.jl @@ -5,7 +5,7 @@ using Test import Mimi: compdef, AbstractDimension, RangeDimension, Dimension, key_type, first_period, last_period, - ComponentReference, ComponentPath, ComponentDef, time_labels + ComponentReference, ComponentPath, ComponentDef, time_labels, external_params ## ## Constants @@ -161,7 +161,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 = external_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 +170,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 = external_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_parametertypes.jl b/test/test_parametertypes.jl index 238b32f7e..530e6d97f 100644 --- a/test/test_parametertypes.jl +++ b/test/test_parametertypes.jl @@ -388,6 +388,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 !(:p1 in keys(m.md.external_params)) # But it should not be added to the model's dictionary +@test !(:p1 in keys(external_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 71e32c777..5d189373b 100644 --- a/test/test_references.jl +++ b/test/test_references.jl @@ -3,6 +3,8 @@ module TestReferences using Test using Mimi +import Mimi: external_params + @defcomp A begin p1 = Parameter() v1 = Variable(index = [time]) @@ -23,12 +25,12 @@ refB = add_comp!(m, B) refA[:p1] = 3 # creates a parameter specific to this component, with name "foo_p1" @test Mimi.get_external_param_name(m.md, :foo, :p1) == :foo_p1 -@test :foo_p1 in keys(m.md.external_params) +@test :foo_p1 in keys(external_params(m)) @test Mimi.UnnamedReference(:B, :p1) in Mimi.nothing_params(m.md) refB[:p1] = 5 @test Mimi.get_external_param_name(m.md, :B, :p1) == :B_p1 -@test :B_p1 in keys(m.md.external_params) +@test :B_p1 in keys(external_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..709a177c7 100644 --- a/test/test_replace_comp.jl +++ b/test/test_replace_comp.jl @@ -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]) @@ -95,8 +107,8 @@ 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(external_params(m)) == 2 # The external parameter still exists for both :x and :z # 5. Test bad external parameter dimensions @@ -158,5 +170,17 @@ 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 +set_param!(m, :X, :x, zeros(6)) +replace!(m, :X => X_repl_extraparams) # Replace X with X_repl_extraparams +@test length(external_params(m)) == 3 # should have two new parameters in the external parameters list +set_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 From 97a016e6061418dca1f787c670962926a3e99c6c Mon Sep 17 00:00:00 2001 From: lrennels Date: Wed, 19 May 2021 15:38:08 -0700 Subject: [PATCH 07/47] Add check for unshared model parameters for simulation instance runs --- src/mcs/montecarlo.jl | 75 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/mcs/montecarlo.jl b/src/mcs/montecarlo.jl index df9d6babb..b6a3e4190 100644 --- a/src/mcs/montecarlo.jl +++ b/src/mcs/montecarlo.jl @@ -525,6 +525,11 @@ function Base.run(sim_def::SimulationDef{T}, set_models!(sim_inst, models) generate_trials!(sim_inst, samplesize; filename=trials_output_filename) + # Check that each named external parameter in the translist exists in each + # of the models, and if it doesn't try to resolve this by checking if it + # has an unnamed external parameter connected to it (ex. was set by default) + _resolve_translist_extparams!(sim_inst) + 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") end @@ -664,6 +669,76 @@ set_models!(sim_inst::SimulationInstance{T}, m::AbstractModel) """ set_models!(sim_inst::SimulationInstance{T}, m::AbstractModel) where T <: AbstractSimulationData = set_models!(sim_inst, [m]) +""" + _resolve_translist_extparams!(sim_inst::SimulationInstance{T}) +Check that each named external parameter in the translist exists in each of the +models, and if it doesn't try to resolve this by walking through components to find +the parameter name and an external parameter name for the connection. If a +resolution can be assumed, update the `translist` of `sim_inst`. +""" +function _resolve_translist_extparams!(sim_inst) where T <: AbstractSimulationData + + for (trans_idx, trans) in enumerate(sim_inst.sim_def.translist) + + paramname = trans.paramname + suggestion_string = "use the `ComponentName.ParameterName` syntax in your SimulationDefinition to explicitly define this transform ie. `ComponentName.$paramname = RandomVariable`" + + unshared_paramnames = [] # name of the unshared model parameters + unshared_compnames = [] # name of the component connected to the unshared model parameters + unshared_modelidxs = [] # idxs of models where paramname not found + + for (model_idx, m) in enumerate(sim_inst.models) + + if !has_parameter(m.md, paramname) + unshared_paramname = nothing + unshared_compname = nothing + + # warn about attempt to resolve missing paramter + @warn "Parameter name $paramname not found in model $model_idx's shared parameter list, will attempt to resolve." + + 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_external_param_name(m, compname, paramname) + unshared_compname = compname + else + # error because parameter found in more than one component + error("Cannot resolve because parameter name $paramname found in more than one component of model $model_idx, including $unshared_compname and $compname. Please $suggestion_string.") + end + end + + if isnothing(unshared_paramname) + # error because parameter not found in any of teh model components + error("Cannot resolve because $paramname not found in any of the components of model $model_idx. Please $suggestion_string.") + else + push!(unshared_paramnames, unshared_paramname) + push!(unshared_compnames, unshared_compname) + push!(unshared_modelidxs, model_idx) + end + end + end + end + + # return if found in all models + if isempty(unshared_modelidxs) + return + + # error because found in some models but not others, so the names of the model parameters will not match + elseif length(unshared_modelidxs) !== length(sim_inst.models) + error("Cannot resolve because $paramname is not a shared parameter in model $([unshared_modelidxs...]), but is a shared parameter in the other models in sim_inst.models list. Please $suggestion_string.") + + # error because the parameter name has different model parameter names in different models + elseif !all(unshared_paramnames[1] .== unshared_paramnames) + error("Cannot resolve because model parameter connected to $paramname has different names in different models, with the names $([unshared_paramnames...]) for models in idxs $([unshared_modelidxs...]) of the sim_inst.models respectively. Please $suggestion_string.") + + # can safely rename + else + new_paramname = unshared_paramnames[1] # just use the first one + @warn("Parameter name $paramname found in model components $([unshared_compnames...]) for models in idxs $([unshared_modelidxs...]) of sim_inst.models respectively. We will assume these were the intended parameters for transformation assignment. In the future we suggest you $suggestion_string.") + sim_inst.sim_def.translist[trans_idx] = TransformSpec(new_paramname, trans.op, trans.rvname, trans.dims) + end + end +end # # Iterator functions for Simulation instance directly, and for use as an IterableTable. From 7ceef5f8645ac5125caf813cbc9e0e67063841fd Mon Sep 17 00:00:00 2001 From: lrennels Date: Wed, 19 May 2021 17:11:50 -0700 Subject: [PATCH 08/47] Add testing for parameter resolution --- src/mcs/montecarlo.jl | 46 +++++-- test/mcs/runtests.jl | 3 + test/mcs/test_resolve_translist_extparams.jl | 131 +++++++++++++++++++ 3 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 test/mcs/test_resolve_translist_extparams.jl diff --git a/src/mcs/montecarlo.jl b/src/mcs/montecarlo.jl index b6a3e4190..d3c3ce475 100644 --- a/src/mcs/montecarlo.jl +++ b/src/mcs/montecarlo.jl @@ -678,23 +678,41 @@ resolution can be assumed, update the `translist` of `sim_inst`. """ function _resolve_translist_extparams!(sim_inst) where T <: AbstractSimulationData + flat_model_list = [] + flat_model_list_names = [] + + for (i, m) in enumerate(sim_inst.models) + if m isa MarginalModel + push!(flat_model_list, m.base) + push!(flat_model_list_names, Symbol("Model$(i)_Base")) + push!(flat_model_list, m.modified) + push!(flat_model_list_names, Symbol("Model$(i)_Modified")) + + else + push!(flat_model_list, m) + push!(flat_model_list_names, Symbol("Model$(i)")) + end + end + for (trans_idx, trans) in enumerate(sim_inst.sim_def.translist) paramname = trans.paramname suggestion_string = "use the `ComponentName.ParameterName` syntax in your SimulationDefinition to explicitly define this transform ie. `ComponentName.$paramname = RandomVariable`" - unshared_paramnames = [] # name of the unshared model parameters - unshared_compnames = [] # name of the component connected to the unshared model parameters - unshared_modelidxs = [] # idxs of models where paramname not found + unshared_paramnames = [] # name of the unshared model parameters + unshared_compnames = [] # name of the component connected to the unshared model parameters + unshared_modelnames = [] # names of models where paramname not found - for (model_idx, m) in enumerate(sim_inst.models) + for (model_idx, m) in enumerate(flat_model_list) + + model_name = flat_model_list_names[model_idx] if !has_parameter(m.md, paramname) unshared_paramname = nothing unshared_compname = nothing # warn about attempt to resolve missing paramter - @warn "Parameter name $paramname not found in model $model_idx's shared parameter list, will attempt to resolve." + @warn "Parameter name $paramname not found in $model_name's shared parameter list, will attempt to resolve." for (compname, compdef) in components(m.md) if has_parameter(compdef, paramname) @@ -703,38 +721,38 @@ function _resolve_translist_extparams!(sim_inst) where T <: AbstractSimulationDa unshared_compname = compname else # error because parameter found in more than one component - error("Cannot resolve because parameter name $paramname found in more than one component of model $model_idx, including $unshared_compname and $compname. Please $suggestion_string.") + 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 if isnothing(unshared_paramname) - # error because parameter not found in any of teh model components - error("Cannot resolve because $paramname not found in any of the components of model $model_idx. Please $suggestion_string.") + # error because parameter not found in any of the model components + error("Cannot resolve because $paramname not found in any of the components of $model_name. Please $suggestion_string.") else push!(unshared_paramnames, unshared_paramname) push!(unshared_compnames, unshared_compname) - push!(unshared_modelidxs, model_idx) + push!(unshared_modelnames, model_name) end end end end # return if found in all models - if isempty(unshared_modelidxs) + if isempty(unshared_modelnames) return # error because found in some models but not others, so the names of the model parameters will not match - elseif length(unshared_modelidxs) !== length(sim_inst.models) - error("Cannot resolve because $paramname is not a shared parameter in model $([unshared_modelidxs...]), but is a shared parameter in the other models in sim_inst.models list. Please $suggestion_string.") + elseif length(unshared_modelnames) !== length(sim_inst.models) + error("Cannot resolve because $paramname is not a shared parameter in models $unshared_modelnames, but is a shared parameter in the other models in sim_inst.models list. Please $suggestion_string.") # error because the parameter name has different model parameter names in different models elseif !all(unshared_paramnames[1] .== unshared_paramnames) - error("Cannot resolve because model parameter connected to $paramname has different names in different models, with the names $([unshared_paramnames...]) for models in idxs $([unshared_modelidxs...]) of the sim_inst.models respectively. Please $suggestion_string.") + error("Cannot resolve because model parameter connected to $paramname has different names in different models, with the names $([unshared_paramnames...]) for models $unshared_modelnames respectively. Please $suggestion_string.") # can safely rename else new_paramname = unshared_paramnames[1] # just use the first one - @warn("Parameter name $paramname found in model components $([unshared_compnames...]) for models in idxs $([unshared_modelidxs...]) of sim_inst.models respectively. We will assume these were the intended parameters for transformation assignment. In the future we suggest you $suggestion_string.") + @warn("Parameter name $paramname found in model components $unshared_compnames for models $unshared_modelnames respectively. We will assume these were the intended parameters for transformation assignment. In the future we suggest you $suggestion_string.") sim_inst.sim_def.translist[trans_idx] = TransformSpec(new_paramname, trans.op, trans.rvname, trans.dims) end end diff --git a/test/mcs/runtests.jl b/test/mcs/runtests.jl index 4e06acf46..5aab33c9a 100644 --- a/test/mcs/runtests.jl +++ b/test/mcs/runtests.jl @@ -26,4 +26,7 @@ using Test @info("test_marginalmodel.jl") include("test_marginalmodel.jl") + + @info("test_resolve_translist_extparams.jl") + include("test_resolve_translist_extparams.jl") end diff --git a/test/mcs/test_resolve_translist_extparams.jl b/test/mcs/test_resolve_translist_extparams.jl new file mode 100644 index 000000000..e1be412bd --- /dev/null +++ b/test/mcs/test_resolve_translist_extparams.jl @@ -0,0 +1,131 @@ +using Mimi +using Distributions +using Test + +sd = @defsim begin + sampling(LHSData) + p = Normal(0, 1) +end + +@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 + +#------------------------------------------------------------------------------ +# 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 component", 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", sprint(showerror, err2)) + +#------------------------------------------------------------------------------ +# 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_expr3 = :( + run(sd, m, 100) +) + +err3 = try eval(fail_expr3) catch err err end +@test occursin("Cannot resolve because parameter name p found in more than one component", sprint(showerror, err3)) + +#------------------------------------------------------------------------------ +# Test a failure due to finding an unshared parameter in both components but 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) + +fail_expr4 = :( + run(sd, [m1, m2], 100) +) + +err4 = try eval(fail_expr4) catch err err end +@test occursin("Cannot resolve because model parameter connected to p has different names in different models", sprint(showerror, err4)) + +#------------------------------------------------------------------------------ +# Test a failure due to finding an unshared parameter in one model, but it is shared +# in the other (and thus has a different name) +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) +set_param!(m2, :p, 5) + +fail_expr5 = :( + run(sd, [m1, m2], 100) +) + +err5 = try eval(fail_expr5) catch err err end +@test occursin("Cannot resolve because p is not a shared parameter in models Any[:Model1], but is a shared parameter in the other models in sim_inst.models list", sprint(showerror, err5)) + +#------------------------------------------------------------------------------ +# 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, test2) + +fail_expr6 = :( + run(sd, [m1, m2], 100) +) + +err6 = try eval(fail_expr6) catch err err end +@test occursin("Cannot resolve because model parameter connected to p has different names in different models", sprint(showerror, err6)) + +#------------------------------------------------------------------------------ +# Test success cases + +m = Model() +set_dimension!(m, :time, 2000:10:2050) +add_comp!(m, test1) +run(sd, m, 100) + +add_comp!(m, test2) +set_param!(m, :p, 5) +run(sd, m, 100) From 51829e864aa276577783613064cd97b762b903e3 Mon Sep 17 00:00:00 2001 From: lrennels Date: Wed, 19 May 2021 18:51:27 -0700 Subject: [PATCH 09/47] Add sharedparamname to TransformSpec --- docs/src/howto/howto_3.md | 3 +- src/mcs/defmcs.jl | 31 +++++++++-- src/mcs/mcs_types.jl | 14 ++++- src/mcs/montecarlo.jl | 109 ++++++++++++++++++++------------------ 4 files changed, 97 insertions(+), 60 deletions(-) diff --git a/docs/src/howto/howto_3.md b/docs/src/howto/howto_3.md index a55015c9d..a5a244ece 100644 --- a/docs/src/howto/howto_3.md +++ b/docs/src/howto/howto_3.md @@ -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)`. diff --git a/src/mcs/defmcs.jl b/src/mcs/defmcs.jl index b034aeb9a..619d98471 100644 --- a/src/mcs/defmcs.jl +++ b/src/mcs/defmcs.jl @@ -121,7 +121,12 @@ macro defsim(expr) end op = elt.head - if @capture(extvar, name_[args__]) + if @capture(extvar, comp_.datum_[args__]) + dims = _make_dims(args) + expr = :(TransformSpec($(QuoteNode(comp)), $(QuoteNode(datum)), $(QuoteNode(op)), $(QuoteNode(rvname)), [$(dims...)])) + elseif @capture(extvar, comp_.datum_) + expr = :(TransformSpec($(QuoteNode(comp)), $(QuoteNode(datum)), $(QuoteNode(op)), $(QuoteNode(rvname)))) + elseif @capture(extvar, name_[args__]) # println("Ref: $name, $args") # Meta.show_sexpr(extvar) # println("") @@ -273,14 +278,32 @@ 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 thus be accessed by that +name. Use the signature with the `compname_paramname` pair argument 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)) 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 holding an +existing component and parameter. 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 + """ delete_save!(sim_def::SimulationDef, key::Tuple{Symbol, Symbol}) diff --git a/src/mcs/mcs_types.jl b/src/mcs/mcs_types.jl index 5fddbdb4b..6cb0a282c 100644 --- a/src/mcs/mcs_types.jl +++ b/src/mcs/mcs_types.jl @@ -82,17 +82,27 @@ 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 given we assume the paramname is a shared model parameter paramname::Symbol op::Symbol rvname::Symbol dims::Vector{Any} + sharedparamname::Union{Nothing, Symbol} # when there is no component given, and thus we assume a shared model parameter, we set this to paramname, otherwise we set it to nothing - function TransformSpec(paramname::Symbol, op::Symbol, rvname::Symbol, dims::Vector{T}=[]) where T + function TransformSpec(paramname::Symbol, op::Symbol, rvname::Symbol, dims::Vector{T}=[], sharedparamname::Union{Nothing, Symbol}=paramname) where T if ! (op in (:(=), :(+=), :(*=))) error("Valid operators are =, +=, and *= (got $op)") end - return new(paramname, op, rvname, dims) + return new(nothing, paramname, op, rvname, dims, sharedparamname) + end + + function TransformSpec(compname::Symbol, paramname::Symbol, op::Symbol, rvname::Symbol, dims::Vector{T}=[], sharedparamname::Union{Nothing, Symbol}=nothing) where T + if ! (op in (:(=), :(+=), :(*=))) + error("Valid operators are =, +=, and *= (got $op)") + end + + return new(compname, paramname, op, rvname, dims, sharedparamname) end end diff --git a/src/mcs/montecarlo.jl b/src/mcs/montecarlo.jl index d3c3ce475..a399f56a7 100644 --- a/src/mcs/montecarlo.jl +++ b/src/mcs/montecarlo.jl @@ -241,7 +241,7 @@ function _copy_sim_params(sim_inst::SimulationInstance{T}) where T <: AbstractSi 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.paramname => copy(external_param(md, trans.paramname)) for trans in sim_inst.sim_def.translist) # HERE! end return param_vec @@ -263,7 +263,7 @@ function _restore_sim_params!(sim_inst::SimulationInstance{T}, for (m, params) in zip(flat_model_list, param_vec) md = m.mi.md for trans in sim_inst.sim_def.translist - name = trans.paramname + name = trans.paramname # HERE! param = params[name] _restore_param!(param, name, md, trans) end @@ -297,7 +297,7 @@ function _param_indices(param::ArrayModelParameter{T}, md::ModelDef, trans::Tran end if num_pdims != num_dims - pname = trans.paramname + pname = trans.paramname # HERE! error("Dimension mismatch: external parameter :$pname has $num_pdims dimensions ($pdims); Sim has $num_dims") end @@ -407,7 +407,7 @@ function _perturb_params!(sim_inst::SimulationInstance{T}, trialnum::Int) where 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) + param = external_param(md, trans.paramname) # HERE! rvalue = getfield(trialdata, trans.rvname) _perturb_param!(param, md, trans, rvalue) end @@ -695,65 +695,68 @@ function _resolve_translist_extparams!(sim_inst) where T <: AbstractSimulationDa end for (trans_idx, trans) in enumerate(sim_inst.sim_def.translist) - - paramname = trans.paramname - suggestion_string = "use the `ComponentName.ParameterName` syntax in your SimulationDefinition to explicitly define this transform ie. `ComponentName.$paramname = RandomVariable`" + # 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. + if isnothing(trans.compname) + paramname = trans.paramname + suggestion_string = "use the `ComponentName.ParameterName` syntax in your SimulationDefinition to explicitly define this transform ie. `ComponentName.$paramname = RandomVariable`" + + unshared_paramnames = [] # name of the unshared model parameters + unshared_compnames = [] # name of the component connected to the unshared model parameters + unshared_modelnames = [] # names of models where paramname not found + + for (model_idx, m) in enumerate(flat_model_list) - unshared_paramnames = [] # name of the unshared model parameters - unshared_compnames = [] # name of the component connected to the unshared model parameters - unshared_modelnames = [] # names of models where paramname not found - - for (model_idx, m) in enumerate(flat_model_list) + model_name = flat_model_list_names[model_idx] - model_name = flat_model_list_names[model_idx] + if !has_parameter(m.md, paramname) + unshared_paramname = nothing + unshared_compname = nothing + + # warn about attempt to resolve missing paramter + @warn "Parameter name $paramname not found in $model_name's shared parameter list, will attempt to resolve." + + 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_external_param_name(m, compname, paramname) + unshared_compname = compname + else + # error because parameter found in more than one 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 - if !has_parameter(m.md, paramname) - unshared_paramname = nothing - unshared_compname = nothing - - # warn about attempt to resolve missing paramter - @warn "Parameter name $paramname not found in $model_name's shared parameter list, will attempt to resolve." - - 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_external_param_name(m, compname, paramname) - unshared_compname = compname + if isnothing(unshared_paramname) + # error because parameter not found in any of the model components + error("Cannot resolve because $paramname not found in any of the components of $model_name. Please $suggestion_string.") else - # error because parameter found in more than one 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.") + push!(unshared_paramnames, unshared_paramname) + push!(unshared_compnames, unshared_compname) + push!(unshared_modelnames, model_name) end end - - if isnothing(unshared_paramname) - # error because parameter not found in any of the model components - error("Cannot resolve because $paramname not found in any of the components of $model_name. Please $suggestion_string.") - else - push!(unshared_paramnames, unshared_paramname) - push!(unshared_compnames, unshared_compname) - push!(unshared_modelnames, model_name) - end end end - end - - # return if found in all models - if isempty(unshared_modelnames) - return - # error because found in some models but not others, so the names of the model parameters will not match - elseif length(unshared_modelnames) !== length(sim_inst.models) - error("Cannot resolve because $paramname is not a shared parameter in models $unshared_modelnames, but is a shared parameter in the other models in sim_inst.models list. Please $suggestion_string.") - - # error because the parameter name has different model parameter names in different models - elseif !all(unshared_paramnames[1] .== unshared_paramnames) - error("Cannot resolve because model parameter connected to $paramname has different names in different models, with the names $([unshared_paramnames...]) for models $unshared_modelnames respectively. Please $suggestion_string.") + # return if found in all models + if isempty(unshared_modelnames) + return - # can safely rename - else - new_paramname = unshared_paramnames[1] # just use the first one - @warn("Parameter name $paramname found in model components $unshared_compnames for models $unshared_modelnames respectively. We will assume these were the intended parameters for transformation assignment. In the future we suggest you $suggestion_string.") - sim_inst.sim_def.translist[trans_idx] = TransformSpec(new_paramname, trans.op, trans.rvname, trans.dims) + # error because found in some models but not others, so the names of the model parameters will not match + elseif length(unshared_modelnames) !== length(sim_inst.models) + error("Cannot resolve because $paramname is not a shared parameter in models $unshared_modelnames, but is a shared parameter in the other models in sim_inst.models list. Please $suggestion_string.") + + # error because the parameter name has different model parameter names in different models + elseif !all(unshared_paramnames[1] .== unshared_paramnames) + error("Cannot resolve because model parameter connected to $paramname has different names in different models, with the names $([unshared_paramnames...]) for models $unshared_modelnames respectively. Please $suggestion_string.") + + # can safely alter the sharedparamname symbol + else + sharedparamname = unshared_paramnames[1] # just use the first one + @warn("Parameter name $paramname found in model components $unshared_compnames for models $unshared_modelnames respectively. We will assume these were the intended parameters for transformation assignment. In the future we suggest you $suggestion_string.") + sim_inst.sim_def.translist[trans_idx] = TransformSpec(trans.compname, trans.paramname, trans.op, trans.rvname, trans.dims, sharedparamname) + end end end end From 0875669bd5791a6195820cc5fc2b292ffa0b1ca9 Mon Sep 17 00:00:00 2001 From: lrennels Date: Thu, 20 May 2021 00:00:29 -0700 Subject: [PATCH 10/47] Continue work on simulation --- docs/src/howto/howto_3.md | 2 +- src/core/model.jl | 2 +- src/mcs/defmcs.jl | 9 +- src/mcs/mcs.jl | 2 +- src/mcs/mcs_types.jl | 29 ++- src/mcs/montecarlo.jl | 241 +++++++++--------- test/mcs/runtests.jl | 22 +- test/mcs/{test_defmcs.jl => test_defsim.jl} | 0 ...t_defmcs_delta.jl => test_defsim_delta.jl} | 0 ...ations.jl => test_defsim_modifications.jl} | 0 ...t_defmcs_sobol.jl => test_defsim_sobol.jl} | 0 test/mcs/test_empirical.jl | 2 +- ...anslist_extparams.jl => test_translist.jl} | 125 +++++---- 13 files changed, 234 insertions(+), 200 deletions(-) rename test/mcs/{test_defmcs.jl => test_defsim.jl} (100%) rename test/mcs/{test_defmcs_delta.jl => test_defsim_delta.jl} (100%) rename test/mcs/{test_defmcs_modifications.jl => test_defsim_modifications.jl} (100%) rename test/mcs/{test_defmcs_sobol.jl => test_defsim_sobol.jl} (100%) rename test/mcs/{test_resolve_translist_extparams.jl => test_translist.jl} (56%) diff --git a/docs/src/howto/howto_3.md b/docs/src/howto/howto_3.md index a5a244ece..6b6ea7614 100644 --- a/docs/src/howto/howto_3.md +++ b/docs/src/howto/howto_3.md @@ -429,7 +429,7 @@ When this is done, Mimi will create a new unique RV with a unique name `share!x` ## 6. Examples -The following example is derived from `"Mimi.jl/test/mcs/test_defmcs.jl"`. +The following example is derived from `"Mimi.jl/test/mcs/test_defsim.jl"`. ```julia using Mimi diff --git a/src/core/model.jl b/src/core/model.jl index 56c6c8425..5a4440757 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -106,7 +106,7 @@ end """ 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 external parameter in model `m`, referenced by `name`. The update_timesteps keyword argument is deprecated, we keep it here just to provide warnings. """ diff --git a/src/mcs/defmcs.jl b/src/mcs/defmcs.jl index 619d98471..aa276b4d4 100644 --- a/src/mcs/defmcs.jl +++ b/src/mcs/defmcs.jl @@ -279,16 +279,15 @@ end 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 random variable, and `paramname` -must refer to an existing shared model parameter that can thus be accessed by that -name. Use the signature with the `compname_paramname` pair argument if your `paramname` -is an unshared model parameter specific to a component. If `dims` are +must refer to an existing shared external parameter that can thus be accessed by that +name. Use the signature that includes `compname` if your `paramname` +is an unshared external 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 """ diff --git a/src/mcs/mcs.jl b/src/mcs/mcs.jl index 915a079bc..77942cac6 100644 --- a/src/mcs/mcs.jl +++ b/src/mcs/mcs.jl @@ -12,7 +12,7 @@ include("EmpiricalDistribution.jl") include("montecarlo.jl") include("lhs.jl") include("sobol.jl") -include("defmcs.jl") +include("defsim.jl") include("delta.jl") export diff --git a/src/mcs/mcs_types.jl b/src/mcs/mcs_types.jl index 6cb0a282c..716a00830 100644 --- a/src/mcs/mcs_types.jl +++ b/src/mcs/mcs_types.jl @@ -82,27 +82,38 @@ 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 given we assume the paramname is a shared model parameter + compname::Union{Nothing, Symbol} # if this is not nothing we assume the paramname is a shared model parameter paramname::Symbol op::Symbol rvname::Symbol dims::Vector{Any} - sharedparamname::Union{Nothing, Symbol} # when there is no component given, and thus we assume a shared model parameter, we set this to paramname, otherwise we set it to nothing - function TransformSpec(paramname::Symbol, op::Symbol, rvname::Symbol, dims::Vector{T}=[], sharedparamname::Union{Nothing, Symbol}=paramname) where T + function TransformSpec(paramname::Symbol, op::Symbol, rvname::Symbol, dims::Vector{T}=[]) where T if ! (op in (:(=), :(+=), :(*=))) error("Valid operators are =, +=, and *= (got $op)") end - - return new(nothing, paramname, op, rvname, dims, sharedparamname) + return new(nothing, paramname, op, rvname, dims) end - function TransformSpec(compname::Symbol, paramname::Symbol, op::Symbol, rvname::Symbol, dims::Vector{T}=[], sharedparamname::Union{Nothing, Symbol}=nothing) where T + 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, sharedparamname) + return new(compname, paramname, op, rvname, dims) + end +end + +struct TransformSpec_ExternalParams + paramnames::Vector{Symbol} + op::Symbol + rvname::Symbol + dims::Vector{Any} + + function TransformSpec_ExternalParams(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 @@ -172,6 +183,7 @@ mutable struct SimulationInstance{T} models::Vector{M} where M <: AbstractModel results::Vector{Dict{Tuple, DataFrame}} payload::Any + translist_externalparams::Vector{TransformSpec_ExternalParams} function SimulationInstance{T}(sim_def::SimulationDef{T}) where T <: AbstractSimulationData self = new() @@ -180,6 +192,7 @@ mutable struct SimulationInstance{T} self.current_data = nothing self.sim_def = deepcopy(sim_def) self.payload = deepcopy(self.sim_def.payload) + self.translist_externalparams = Vector{TransformSpec_ExternalParams}(undef, 0) # These are parallel arrays; each model has a corresponding results dict self.models = Vector{AbstractModel}(undef, 0) diff --git a/src/mcs/montecarlo.jl b/src/mcs/montecarlo.jl index a399f56a7..f5ea74be4 100644 --- a/src/mcs/montecarlo.jl +++ b/src/mcs/montecarlo.jl @@ -227,21 +227,13 @@ 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) # HERE! + param_vec[i] = Dict{Symbol, ModelParameter}(trans.paramnames[i] => copy(external_param(md, trans.paramnames[i])) for trans in sim_inst.translist_externalparams) end return param_vec @@ -250,40 +242,34 @@ 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 # HERE! + for trans in sim_inst.translist_externalparams + 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 +function _restore_param!(param::ScalarModelParameter{T}, name::Symbol, md::ModelDef, i::Int, trans::TransformSpec_ExternalParams) where T md_param = external_param(md, name) md_param.value = param.value end -function _restore_param!(param::ArrayModelParameter{T}, name::Symbol, md::ModelDef, trans::TransformSpec) where T +function _restore_param!(param::ArrayModelParameter{T}, name::Symbol, md::ModelDef, i::Int, trans::TransformSpec_ExternalParams) where T md_param = external_param(md, name) - indices = _param_indices(param, md, trans) + 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_ExternalParams) where T pdims = dim_names(param) # returns [] for scalar parameters num_pdims = length(pdims) @@ -297,7 +283,7 @@ function _param_indices(param::ArrayModelParameter{T}, md::ModelDef, trans::Tran end if num_pdims != num_dims - pname = trans.paramname # HERE! + pname = trans.paramnames[i] error("Dimension mismatch: external parameter :$pname has $num_pdims dimensions ($pdims); Sim has $num_dims") end @@ -312,7 +298,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_ExternalParams, rvalue::Number) where T op = trans.op if op == :(=) @@ -328,12 +314,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_ExternalParams, 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 +334,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_ExternalParams, 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 +388,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) # HERE! - 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_externalparams + param = external_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 @@ -524,11 +508,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) - - # Check that each named external parameter in the translist exists in each - # of the models, and if it doesn't try to resolve this by checking if it - # has an unnamed external parameter connected to it (ex. was set by default) - _resolve_translist_extparams!(sim_inst) + set_translist_externalparams!(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") @@ -537,14 +517,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) @@ -650,117 +630,138 @@ 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(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::Union{Vector{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) - 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]) """ - _resolve_translist_extparams!(sim_inst::SimulationInstance{T}) -Check that each named external parameter in the translist exists in each of the -models, and if it doesn't try to resolve this by walking through components to find -the parameter name and an external parameter name for the connection. If a -resolution can be assumed, update the `translist` of `sim_inst`. + set_translist_externalparams!(sim_inst::SimulationInstance{T}) + +Create the transform spec list for the simulation instance, finding the matching +external parameter names for each transform spec parameter for each model. """ -function _resolve_translist_extparams!(sim_inst) where T <: AbstractSimulationData +function set_translist_externalparams!(sim_inst::SimulationInstance{T}) where T <: AbstractSimulationData - flat_model_list = [] - flat_model_list_names = [] + # 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) - for (i, m) in enumerate(sim_inst.models) - if m isa MarginalModel - push!(flat_model_list, m.base) - push!(flat_model_list_names, Symbol("Model$(i)_Base")) - push!(flat_model_list, m.modified) - push!(flat_model_list_names, Symbol("Model$(i)_Modified")) - - else - push!(flat_model_list, m) - push!(flat_model_list_names, Symbol("Model$(i)")) - end - end + # allocate simulation instance translist + sim_inst.translist_externalparams = Vector{TransformSpec_ExternalParams}(undef, length(sim_inst.sim_def.translist)) for (trans_idx, trans) in enumerate(sim_inst.sim_def.translist) + + # initialize the vector of external parameters + external_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]).") + + external_parameters_vec[model_idx] = get_external_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. - if isnothing(trans.compname) + else paramname = trans.paramname suggestion_string = "use the `ComponentName.ParameterName` syntax in your SimulationDefinition to explicitly define this transform ie. `ComponentName.$paramname = RandomVariable`" - - unshared_paramnames = [] # name of the unshared model parameters - unshared_compnames = [] # name of the component connected to the unshared model parameters - unshared_modelnames = [] # names of models where paramname not found for (model_idx, m) in enumerate(flat_model_list) - model_name = flat_model_list_names[model_idx] - if !has_parameter(m.md, paramname) + # found the shared parameter + if has_parameter(m.md, paramname) + external_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 - - # warn about attempt to resolve missing paramter - @warn "Parameter name $paramname not found in $model_name's shared parameter list, will attempt to resolve." - + 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_external_param_name(m, compname, paramname) + unshared_paramname = get_external_param_name(m.md, compname, paramname) # NB might not need to use m.mi.md here could be m.md unshared_compname = compname - else - # error because parameter found in more than one component + 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 - - if isnothing(unshared_paramname) - # error because parameter not found in any of the model components - error("Cannot resolve because $paramname not found in any of the components of $model_name. Please $suggestion_string.") - else - push!(unshared_paramnames, unshared_paramname) - push!(unshared_compnames, unshared_compname) - push!(unshared_modelnames, model_name) - 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 + external_parameters_vec[model_idx] = unshared_paramname end end end - - # return if found in all models - if isempty(unshared_modelnames) - return - - # error because found in some models but not others, so the names of the model parameters will not match - elseif length(unshared_modelnames) !== length(sim_inst.models) - error("Cannot resolve because $paramname is not a shared parameter in models $unshared_modelnames, but is a shared parameter in the other models in sim_inst.models list. Please $suggestion_string.") - - # error because the parameter name has different model parameter names in different models - elseif !all(unshared_paramnames[1] .== unshared_paramnames) - error("Cannot resolve because model parameter connected to $paramname has different names in different models, with the names $([unshared_paramnames...]) for models $unshared_modelnames respectively. Please $suggestion_string.") - - # can safely alter the sharedparamname symbol - else - sharedparamname = unshared_paramnames[1] # just use the first one - @warn("Parameter name $paramname found in model components $unshared_compnames for models $unshared_modelnames respectively. We will assume these were the intended parameters for transformation assignment. In the future we suggest you $suggestion_string.") - sim_inst.sim_def.translist[trans_idx] = TransformSpec(trans.compname, trans.paramname, trans.op, trans.rvname, trans.dims, sharedparamname) - end end + new_trans = TransformSpec_ExternalParams(external_parameters_vec, trans.op, trans.rvname, trans.dims) + sim_inst.translist_externalparams[trans_idx] = new_trans end end - + # # Iterator functions for Simulation instance directly, and for use as an IterableTable. # diff --git a/test/mcs/runtests.jl b/test/mcs/runtests.jl index 5aab33c9a..9f73c9405 100644 --- a/test/mcs/runtests.jl +++ b/test/mcs/runtests.jl @@ -6,20 +6,20 @@ using Test @info("test_empirical.jl") include("test_empirical.jl") - @info("test_defmcs.jl") - include("test_defmcs.jl") + @info("test_defsim.jl") + include("test_defsim.jl") - @info("test_defmcs_modifications.jl") - include("test_defmcs_modifications.jl") + @info("test_defsim_modifications.jl") + include("test_defsim_modifications.jl") - @info("test_defmcs_sobol.jl") - include("test_defmcs_sobol.jl") + @info("test_defsim_sobol.jl") + include("test_defsim_sobol.jl") - @info("test_defmcs_delta.jl") - include("test_defmcs_delta.jl") + @info("test_defsim_delta.jl") + include("test_defsim_delta.jl") @info("test_reshaping.jl") - include("test_reshaping.jl") + include("test_reshaping.jl") @info("test_payload.jl") include("test_payload.jl") @@ -27,6 +27,6 @@ using Test @info("test_marginalmodel.jl") include("test_marginalmodel.jl") - @info("test_resolve_translist_extparams.jl") - include("test_resolve_translist_extparams.jl") + @info("test_translist.jl") + include("test_translist.jl") end diff --git a/test/mcs/test_defmcs.jl b/test/mcs/test_defsim.jl similarity index 100% rename from test/mcs/test_defmcs.jl rename to test/mcs/test_defsim.jl diff --git a/test/mcs/test_defmcs_delta.jl b/test/mcs/test_defsim_delta.jl similarity index 100% rename from test/mcs/test_defmcs_delta.jl rename to test/mcs/test_defsim_delta.jl diff --git a/test/mcs/test_defmcs_modifications.jl b/test/mcs/test_defsim_modifications.jl similarity index 100% rename from test/mcs/test_defmcs_modifications.jl rename to test/mcs/test_defsim_modifications.jl diff --git a/test/mcs/test_defmcs_sobol.jl b/test/mcs/test_defsim_sobol.jl similarity index 100% rename from test/mcs/test_defmcs_sobol.jl rename to test/mcs/test_defsim_sobol.jl diff --git a/test/mcs/test_empirical.jl b/test/mcs/test_empirical.jl index c6c5fdd3d..a22af832f 100644 --- a/test/mcs/test_empirical.jl +++ b/test/mcs/test_empirical.jl @@ -52,7 +52,7 @@ _probs = 0.1 * ones(10) sd = @defsim begin sampling(LHSData) - p = EmpiricalDistribution(_values, _probs) + test.p = EmpiricalDistribution(_values, _probs) end @defcomp test begin diff --git a/test/mcs/test_resolve_translist_extparams.jl b/test/mcs/test_translist.jl similarity index 56% rename from test/mcs/test_resolve_translist_extparams.jl rename to test/mcs/test_translist.jl index e1be412bd..cc89f4ad6 100644 --- a/test/mcs/test_resolve_translist_extparams.jl +++ b/test/mcs/test_translist.jl @@ -2,11 +2,6 @@ using Mimi using Distributions using Test -sd = @defsim begin - sampling(LHSData) - p = Normal(0, 1) -end - @defcomp test1 begin p = Parameter(default = 5) function run_timestep(p, v, d, t) end @@ -22,6 +17,15 @@ end function run_timestep(p, v, d, t) end end +## +## Tests for set_translist_externalparams +## + +sd = @defsim begin + sampling(LHSData) + p = Normal(0, 1) # should be shared, but was set with default so have to find it +end + #------------------------------------------------------------------------------ # Test a failure to find the unshared parameter in any components m = Model() @@ -33,7 +37,7 @@ fail_expr1 = :( ) err1 = try eval(fail_expr1) catch err err end -@test occursin("Cannot resolve because p not found in any of the component", sprint(showerror, err1)) +@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 @@ -47,62 +51,45 @@ fail_expr2 = :( ) err2 = try eval(fail_expr2) catch err err end -@test occursin("Cannot resolve because parameter name p found in more than one component", sprint(showerror, err2)) - -#------------------------------------------------------------------------------ -# 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_expr3 = :( - run(sd, m, 100) -) +@test occursin("Cannot resolve because parameter name p found in more than one component of Model1", sprint(showerror, err2)) -err3 = try eval(fail_expr3) catch err err end -@test occursin("Cannot resolve because parameter name p found in more than one component", sprint(showerror, err3)) +#------------------------------------------------------------------ +# Test a failure due to finding an unshared parameter in one model, but not +# the other -#------------------------------------------------------------------------------ -# Test a failure due to finding an unshared parameter in both components but 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) +add_comp!(m2, test3) -fail_expr4 = :( +fail_expr3 = :( run(sd, [m1, m2], 100) ) -err4 = try eval(fail_expr4) catch err err end -@test occursin("Cannot resolve because model parameter connected to p has different names in different models", sprint(showerror, err4)) +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 a failure due to finding an unshared parameter in one model, but it is shared -# in the other (and thus has a different name) +# 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) +set_param!(m1, :p, 5) +run(sd, m, 100) -m2 = Model() -set_dimension!(m2, :time, 2000:10:2050) -add_comp!(m2, test2) -set_param!(m2, :p, 5) - -fail_expr5 = :( - run(sd, [m1, m2], 100) -) - -err5 = try eval(fail_expr5) catch err err end -@test occursin("Cannot resolve because p is not a shared parameter in models Any[:Model1], but is a shared parameter in the other models in sim_inst.models list", sprint(showerror, err5)) - -#------------------------------------------------------------------------------ -# Test a failure due to finding an unshared parameter in one model, but not -# the other +# unshared parameter in both models with different names m1 = Model() set_dimension!(m1, :time, 2000:10:2050) add_comp!(m1, test1) @@ -111,21 +98,55 @@ m2 = Model() set_dimension!(m2, :time, 2000:10:2050) add_comp!(m2, test2) -fail_expr6 = :( - run(sd, [m1, m2], 100) -) +run(sd, [m1, m2], 100) -err6 = try eval(fail_expr6) catch err err end -@test occursin("Cannot resolve because model parameter connected to p has different names in different models", sprint(showerror, err6)) +## +## Tests for set_translist_externalparams with a default (not shared) +## -#------------------------------------------------------------------------------ -# Test success cases +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) -set_param!(m, :p, 5) +fail_expr = :(run(sd, m, 100)) +err = try eval(fail_expr) catch err err end +@test occursin("Component test1 does not exist in Model1.", sprint(showerror, err)) + +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)) +err = try eval(fail_expr) catch err err end +@test occursin("Component test1 does not exist in Model1.", sprint(showerror, err)) + +# 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) From 3a59f7cc875b5bdddcec9e0e49ba839ee5482edc Mon Sep 17 00:00:00 2001 From: lrennels Date: Thu, 20 May 2021 11:54:14 -0700 Subject: [PATCH 11/47] Fix up docs and clean up --- docs/src/howto/howto_3.md | 10 +- docs/src/ref/ref_main.md | 1 + docs/src/tutorials/tutorial_5.md | 2 +- ecs_sample.csv | 101 ++++++++++++++++++ src/mcs/defmcs.jl | 34 +++--- src/mcs/mcs.jl | 2 +- src/mcs/mcs_types.jl | 8 +- src/mcs/montecarlo.jl | 5 +- test/mcs/runtests.jl | 8 +- test/mcs/{test_defsim.jl => test_defmcs.jl} | 0 ...t_defsim_delta.jl => test_defmcs_delta.jl} | 0 ...ations.jl => test_defmcs_modifications.jl} | 0 ...t_defsim_sobol.jl => test_defmcs_sobol.jl} | 0 test/mcs/test_empirical.jl | 2 +- 14 files changed, 142 insertions(+), 31 deletions(-) create mode 100644 ecs_sample.csv rename test/mcs/{test_defsim.jl => test_defmcs.jl} (100%) rename test/mcs/{test_defsim_delta.jl => test_defmcs_delta.jl} (100%) rename test/mcs/{test_defsim_modifications.jl => test_defmcs_modifications.jl} (100%) rename test/mcs/{test_defsim_sobol.jl => test_defmcs_sobol.jl} (100%) diff --git a/docs/src/howto/howto_3.md b/docs/src/howto/howto_3.md index 6b6ea7614..a695c5d3c 100644 --- a/docs/src/howto/howto_3.md +++ b/docs/src/howto/howto_3.md @@ -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 ``` @@ -100,11 +100,11 @@ 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 external parameter, or `comp.param` which refers to an unshared external 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. 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/tutorials/tutorial_5.md b/docs/src/tutorials/tutorial_5.md index 50408b693..f2ea35153 100644 --- a/docs/src/tutorials/tutorial_5.md +++ b/docs/src/tutorials/tutorial_5.md @@ -182,7 +182,7 @@ rv(rv1) = Normal(0, 0.8) # create a random variable called "rv1" with the spe param1 = rv1 # then assign this random variable "rv1" to the parameter "param1" in the model ``` -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 external parameter `param1`. ```julia param1 = Normal(0, 0.8) ``` diff --git a/ecs_sample.csv b/ecs_sample.csv new file mode 100644 index 000000000..a67ed4e9a --- /dev/null +++ b/ecs_sample.csv @@ -0,0 +1,101 @@ +"t2xco2!1" +3.484903149444094 +3.0038351531583354 +3.5054377675945645 +1.848733202274411 +2.837787371417789 +4.220991076015284 +3.1808684968736443 +7.0177892272483104 +3.0015198762681607 +2.153210668197504 +5.003792634771766 +2.243878210719045 +1.5476822554699063 +2.697212983019759 +4.159358676522619 +3.5016900718325608 +4.331837165754642 +2.647266660175863 +4.958158984816713 +3.305244815012257 +3.703150087053597 +3.325970973573943 +2.5868017419287845 +2.807836619537109 +2.7276821102756363 +4.332489156147987 +4.693136962395447 +3.5316646040295376 +3.038813479746818 +2.9697902027767107 +2.8453059657176474 +3.8658003759689965 +2.2921524942613622 +3.112616488171833 +2.033766217382601 +1.5263326918516695 +3.1900704126117985 +2.376856629817983 +2.774522535129677 +3.9370441518248245 +6.211569377730636 +5.761141807863151 +2.673008080866706 +3.1177191889566873 +4.223657481672159 +4.611787702348182 +4.175466619781566 +2.31636155056812 +2.3828476782620305 +2.3380265827835127 +6.775226596112925 +6.6471897207724 +2.373677826651272 +6.612476538771226 +2.4326713785102507 +4.248731916808614 +3.0033304163625516 +2.504867695598893 +6.6479034322824 +1.8631282225858148 +2.8789926957035656 +3.9388301301954467 +2.102540362881828 +4.502851147029185 +1.396008848272069 +3.8608455152219205 +3.7224713425454627 +3.1964748700085592 +1.577675351339017 +1.6579353458418271 +3.9814655530877436 +2.5019554383546088 +1.9439213724434483 +3.7043844518774467 +2.843160374569818 +4.3678861382422145 +4.114264275546777 +3.131173965476299 +3.9757460514393372 +3.028883140241852 +3.8116682230626733 +7.025435582059167 +3.014005998002994 +3.489914023822767 +3.8548689732029175 +3.3473535151953944 +2.026760284998585 +3.477231543132124 +1.457967595793264 +2.657997694513455 +2.151904636844561 +2.412621247012041 +3.3259416390416505 +3.817568724097634 +5.056588953264081 +2.241439298216346 +3.1321671538398888 +2.7284894581318406 +4.2044034733881155 +3.1002645293850413 diff --git a/src/mcs/defmcs.jl b/src/mcs/defmcs.jl index aa276b4d4..c77f35a4e 100644 --- a/src/mcs/defmcs.jl +++ b/src/mcs/defmcs.jl @@ -121,17 +121,11 @@ macro defsim(expr) end op = elt.head - if @capture(extvar, comp_.datum_[args__]) - dims = _make_dims(args) - expr = :(TransformSpec($(QuoteNode(comp)), $(QuoteNode(datum)), $(QuoteNode(op)), $(QuoteNode(rvname)), [$(dims...)])) - elseif @capture(extvar, comp_.datum_) - expr = :(TransformSpec($(QuoteNode(comp)), $(QuoteNode(datum)), $(QuoteNode(op)), $(QuoteNode(rvname)))) - elseif @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)) @@ -140,7 +134,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 @@ -178,6 +179,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)] @@ -280,7 +282,7 @@ end 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 random variable, and `paramname` -must refer to an existing shared external parameter that can thus be accessed by that +must refer to an existing shared external parameter that can be accessed by that name. Use the signature that includes `compname` if your `paramname` is an unshared external parameter specific to a component. If `dims` are provided, these must be legal subscripts of `paramname`. Op must be one of :+=, :*=, @@ -295,9 +297,9 @@ end 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 :(=). +`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)) diff --git a/src/mcs/mcs.jl b/src/mcs/mcs.jl index 77942cac6..915a079bc 100644 --- a/src/mcs/mcs.jl +++ b/src/mcs/mcs.jl @@ -12,7 +12,7 @@ include("EmpiricalDistribution.jl") include("montecarlo.jl") include("lhs.jl") include("sobol.jl") -include("defsim.jl") +include("defmcs.jl") include("delta.jl") export diff --git a/src/mcs/mcs_types.jl b/src/mcs/mcs_types.jl index 716a00830..b3382863e 100644 --- a/src/mcs/mcs_types.jl +++ b/src/mcs/mcs_types.jl @@ -183,7 +183,7 @@ mutable struct SimulationInstance{T} models::Vector{M} where M <: AbstractModel results::Vector{Dict{Tuple, DataFrame}} payload::Any - translist_externalparams::Vector{TransformSpec_ExternalParams} + translist_externalparams::Vector{TransformSpec_ExternalParams} function SimulationInstance{T}(sim_def::SimulationDef{T}) where T <: AbstractSimulationData self = new() @@ -192,6 +192,12 @@ mutable struct SimulationInstance{T} self.current_data = nothing 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 external 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_externalparams = Vector{TransformSpec_ExternalParams}(undef, 0) # These are parallel arrays; each model has a corresponding results dict diff --git a/src/mcs/montecarlo.jl b/src/mcs/montecarlo.jl index f5ea74be4..653cf990c 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 external_params", sim_inst.translist_externalparams) Base.show(io, sim_inst.sim_def) @@ -228,7 +229,6 @@ function _copy_sim_params(sim_inst::SimulationInstance{T}) where T <: AbstractSi # If there is a MarginalModel, need to copy the params for both the base and marginal modeldefs separately 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) @@ -241,6 +241,7 @@ 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 = _get_flat_model_list(sim_inst) @@ -651,7 +652,7 @@ function _get_flat_model_list(sim_inst::SimulationInstance{T}) where T <: Abstra end """ - _get_flat_model_list(sim_inst::SimulationInstance{T}) where T <: AbstractSimulationData + _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. diff --git a/test/mcs/runtests.jl b/test/mcs/runtests.jl index 9f73c9405..0d5a73f3c 100644 --- a/test/mcs/runtests.jl +++ b/test/mcs/runtests.jl @@ -7,16 +7,16 @@ using Test include("test_empirical.jl") @info("test_defsim.jl") - include("test_defsim.jl") + include("test_defmcs.jl") @info("test_defsim_modifications.jl") - include("test_defsim_modifications.jl") + include("test_defmcs_modifications.jl") @info("test_defsim_sobol.jl") - include("test_defsim_sobol.jl") + include("test_defmcs_sobol.jl") @info("test_defsim_delta.jl") - include("test_defsim_delta.jl") + include("test_defmcs_delta.jl") @info("test_reshaping.jl") include("test_reshaping.jl") diff --git a/test/mcs/test_defsim.jl b/test/mcs/test_defmcs.jl similarity index 100% rename from test/mcs/test_defsim.jl rename to test/mcs/test_defmcs.jl diff --git a/test/mcs/test_defsim_delta.jl b/test/mcs/test_defmcs_delta.jl similarity index 100% rename from test/mcs/test_defsim_delta.jl rename to test/mcs/test_defmcs_delta.jl diff --git a/test/mcs/test_defsim_modifications.jl b/test/mcs/test_defmcs_modifications.jl similarity index 100% rename from test/mcs/test_defsim_modifications.jl rename to test/mcs/test_defmcs_modifications.jl diff --git a/test/mcs/test_defsim_sobol.jl b/test/mcs/test_defmcs_sobol.jl similarity index 100% rename from test/mcs/test_defsim_sobol.jl rename to test/mcs/test_defmcs_sobol.jl diff --git a/test/mcs/test_empirical.jl b/test/mcs/test_empirical.jl index a22af832f..59ba6b425 100644 --- a/test/mcs/test_empirical.jl +++ b/test/mcs/test_empirical.jl @@ -84,7 +84,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 From 334d86b3cb30a74d317978b6811e1e75a4631761 Mon Sep 17 00:00:00 2001 From: lrennels Date: Thu, 20 May 2021 11:57:49 -0700 Subject: [PATCH 12/47] Fix names and remove extra files --- docs/src/howto/howto_3.md | 2 +- ecs_sample.csv | 101 -------------------------------------- test/mcs/runtests.jl | 8 +-- 3 files changed, 5 insertions(+), 106 deletions(-) delete mode 100644 ecs_sample.csv diff --git a/docs/src/howto/howto_3.md b/docs/src/howto/howto_3.md index a695c5d3c..7a678d5d0 100644 --- a/docs/src/howto/howto_3.md +++ b/docs/src/howto/howto_3.md @@ -429,7 +429,7 @@ When this is done, Mimi will create a new unique RV with a unique name `share!x` ## 6. Examples -The following example is derived from `"Mimi.jl/test/mcs/test_defsim.jl"`. +The following example is derived from `"Mimi.jl/test/mcs/test_defmcs.jl"`. ```julia using Mimi diff --git a/ecs_sample.csv b/ecs_sample.csv deleted file mode 100644 index a67ed4e9a..000000000 --- a/ecs_sample.csv +++ /dev/null @@ -1,101 +0,0 @@ -"t2xco2!1" -3.484903149444094 -3.0038351531583354 -3.5054377675945645 -1.848733202274411 -2.837787371417789 -4.220991076015284 -3.1808684968736443 -7.0177892272483104 -3.0015198762681607 -2.153210668197504 -5.003792634771766 -2.243878210719045 -1.5476822554699063 -2.697212983019759 -4.159358676522619 -3.5016900718325608 -4.331837165754642 -2.647266660175863 -4.958158984816713 -3.305244815012257 -3.703150087053597 -3.325970973573943 -2.5868017419287845 -2.807836619537109 -2.7276821102756363 -4.332489156147987 -4.693136962395447 -3.5316646040295376 -3.038813479746818 -2.9697902027767107 -2.8453059657176474 -3.8658003759689965 -2.2921524942613622 -3.112616488171833 -2.033766217382601 -1.5263326918516695 -3.1900704126117985 -2.376856629817983 -2.774522535129677 -3.9370441518248245 -6.211569377730636 -5.761141807863151 -2.673008080866706 -3.1177191889566873 -4.223657481672159 -4.611787702348182 -4.175466619781566 -2.31636155056812 -2.3828476782620305 -2.3380265827835127 -6.775226596112925 -6.6471897207724 -2.373677826651272 -6.612476538771226 -2.4326713785102507 -4.248731916808614 -3.0033304163625516 -2.504867695598893 -6.6479034322824 -1.8631282225858148 -2.8789926957035656 -3.9388301301954467 -2.102540362881828 -4.502851147029185 -1.396008848272069 -3.8608455152219205 -3.7224713425454627 -3.1964748700085592 -1.577675351339017 -1.6579353458418271 -3.9814655530877436 -2.5019554383546088 -1.9439213724434483 -3.7043844518774467 -2.843160374569818 -4.3678861382422145 -4.114264275546777 -3.131173965476299 -3.9757460514393372 -3.028883140241852 -3.8116682230626733 -7.025435582059167 -3.014005998002994 -3.489914023822767 -3.8548689732029175 -3.3473535151953944 -2.026760284998585 -3.477231543132124 -1.457967595793264 -2.657997694513455 -2.151904636844561 -2.412621247012041 -3.3259416390416505 -3.817568724097634 -5.056588953264081 -2.241439298216346 -3.1321671538398888 -2.7284894581318406 -4.2044034733881155 -3.1002645293850413 diff --git a/test/mcs/runtests.jl b/test/mcs/runtests.jl index 0d5a73f3c..243f32834 100644 --- a/test/mcs/runtests.jl +++ b/test/mcs/runtests.jl @@ -6,16 +6,16 @@ using Test @info("test_empirical.jl") include("test_empirical.jl") - @info("test_defsim.jl") + @info("test_defmcs.jl") include("test_defmcs.jl") - @info("test_defsim_modifications.jl") + @info("test_defmcs_modifications.jl") include("test_defmcs_modifications.jl") - @info("test_defsim_sobol.jl") + @info("test_defmcs_sobol.jl") include("test_defmcs_sobol.jl") - @info("test_defsim_delta.jl") + @info("test_defmcs_delta.jl") include("test_defmcs_delta.jl") @info("test_reshaping.jl") From 3a237aa7fcdee52f72f08a2b18f1e872a688f835 Mon Sep 17 00:00:00 2001 From: lrennels Date: Thu, 20 May 2021 13:20:30 -0700 Subject: [PATCH 13/47] Fix typo --- src/core/types/params.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/types/params.jl b/src/core/types/params.jl index 7363d9453..5127ef734 100644 --- a/src/core/types/params.jl +++ b/src/core/types/params.jl @@ -58,8 +58,8 @@ Base.copy(obj::ArrayModelParameter{T}) where T = ArrayModelParameter(obj.values, dim_names(obj::ArrayModelParameter) = obj.dim_names dim_names(obj::ScalarModelParameter) = [] -is_is_shared(obj::ArrayModelParameter) = obj.is_shared -is_is_shared(obj::ScalarModelParameter) = obj.is_shared +is_shared(obj::ArrayModelParameter) = obj.is_shared +is_shared(obj::ScalarModelParameter) = obj.is_shared abstract type AbstractConnection <: MimiStruct end From 4f3eb13bd6020b7214a0986a2aaa8ad1cd70c02a Mon Sep 17 00:00:00 2001 From: lrennels Date: Thu, 20 May 2021 17:34:55 -0700 Subject: [PATCH 14/47] Update testing and error messages --- contrib/test_all_models.jl | 6 +++++- src/mcs/montecarlo.jl | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/contrib/test_all_models.jl b/contrib/test_all_models.jl index 96b10d2a5..b3196c9c7 100644 --- a/contrib/test_all_models.jl +++ b/contrib/test_all_models.jl @@ -10,14 +10,18 @@ # julia --color=yes test_all_models.jl # +# should locally also test +# - MimiIWG (can't pass on CI because of local registry) +# - MimiDICE2016R2 (not all tests pass, but check for new failures) packages_to_test = [ "MimiDICE2010" => ("https://github.com/anthofflab/MimiDICE2010.jl", "master"), "MimiDICE2013" => ("https://github.com/anthofflab/MimiDICE2013.jl", "master"), + "MimiDICE2016" => ("https://github.com/AlexandrePavlov/MimiDICE2016.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"), - "MimiDICE2016" => ("https://github.com/AlexandrePavlov/MimiDICE2016.jl", "master"), + "MimiPAGE2020" => ("https://github.com/anthofflab/MimiPAGE2020.jl", "master"), "MimiSNEASY" => ("https://github.com/anthofflab/MimiSNEASY.jl", "master"), "MimiFAIR" => ("https://github.com/anthofflab/MimiFAIR.jl", "master"), "MimiMAGICC" => ("https://github.com/anthofflab/MimiMAGICC.jl", "master"), diff --git a/src/mcs/montecarlo.jl b/src/mcs/montecarlo.jl index 653cf990c..1c80f7ccd 100644 --- a/src/mcs/montecarlo.jl +++ b/src/mcs/montecarlo.jl @@ -753,6 +753,7 @@ function set_translist_externalparams!(sim_inst::SimulationInstance{T}) where T 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 external parameter name $unshared_paramname. Will use this external parameter, but in the future we suggest you $suggestion_string") external_parameters_vec[model_idx] = unshared_paramname end end From c4b658350364f184f81a600ad463b55d37845d67 Mon Sep 17 00:00:00 2001 From: lrennels Date: Thu, 20 May 2021 20:16:00 -0700 Subject: [PATCH 15/47] Handle vectors in defsim --- src/mcs/defmcs.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mcs/defmcs.jl b/src/mcs/defmcs.jl index c77f35a4e..63ef4305b 100644 --- a/src/mcs/defmcs.jl +++ b/src/mcs/defmcs.jl @@ -99,7 +99,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 From 598b740a651c2273027095e08eee1f9f1efd10dd Mon Sep 17 00:00:00 2001 From: lrennels Date: Thu, 20 May 2021 23:01:38 -0700 Subject: [PATCH 16/47] Add tests --- test/mcs/test_defmcs.jl | 37 ++++++++++++++++++++++++++- test/mcs/test_defmcs_modifications.jl | 2 +- test/test_parametertypes.jl | 20 +++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/test/mcs/test_defmcs.jl b/test/mcs/test_defmcs.jl index 0c908c395..0758c45ed 100644 --- a/test/mcs/test_defmcs.jl +++ b/test/mcs/test_defmcs.jl @@ -18,7 +18,9 @@ m = construct_MyModel() N = 100 -sd = @defsim begin +# just use this for the check that the sd can be defined, then use simpler version +# below +sd_complex = @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 @@ -37,6 +39,39 @@ sd = @defsim begin Region2 => Uniform(0.10, 1.50), Region3 => Uniform(0.10, 0.20)] + grosseconomy.tfp[2020, Region1] = Uniform(3.39, 3.40) + + grosseconomy.k0 = [Region1 => Uniform(50.5, 50.6), + Region2 => Uniform(22., 23.), + Region3 => Uniform(33.5, 33.6)] + + sampling(LHSData, corrlist=[(:name1, :name2, 0.7), (:name1, :name3, 0.5)]) + + # indicate which parameters to save for each model run. Specify + # a parameter name or [later] some slice of its data, similar to the + # assignment of RVs, above. + save(grosseconomy.K, grosseconomy.YGROSS, emissions.E, emissions.E_Global, grosseconomy.share_var, grosseconomy.depk_var) +end + +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 + # RV slightly more burdensome, but it's only required when defining + # correlations or sharing an RV across parameters. + rv(name1) = Normal(1, 0.2) + rv(name2) = Uniform(0.75, 1.25) + 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) + + 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 diff --git a/test/mcs/test_defmcs_modifications.jl b/test/mcs/test_defmcs_modifications.jl index f870557b8..61eba97d2 100644 --- a/test/mcs/test_defmcs_modifications.jl +++ b/test/mcs/test_defmcs_modifications.jl @@ -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) diff --git a/test/test_parametertypes.jl b/test/test_parametertypes.jl index 530e6d97f..8fe40b8a3 100644 --- a/test/test_parametertypes.jl +++ b/test/test_parametertypes.jl @@ -8,6 +8,26 @@ import Mimi: 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 p1.is_shared && !p2.is_shared +@test Mimi.is_shared(p1) && Mimi.is_shared(p2) + +p3 = ScalarModelParameter(3, shared) +p4 = ScalarModelParameter(3) +@test p3.value == p4.value == 3 +@test p3.is_shared && !p4.shared +@test Mimi.is_shared(p3) && !Mimi.is_shared(p4) + # # Test that parameter type mismatches are caught # From 3a670af50ca0b5871a2955eeca0b4f5cbb3e006e Mon Sep 17 00:00:00 2001 From: lrennels Date: Fri, 21 May 2021 10:23:26 -0700 Subject: [PATCH 17/47] Add more tests --- test/mcs/test_defmcs.jl | 81 +++++++++++++++++++++++-------------- test/mcs/test_translist.jl | 8 ++-- test/test_parametertypes.jl | 4 +- 3 files changed, 55 insertions(+), 38 deletions(-) diff --git a/test/mcs/test_defmcs.jl b/test/mcs/test_defmcs.jl index 0758c45ed..0796546f1 100644 --- a/test/mcs/test_defmcs.jl +++ b/test/mcs/test_defmcs.jl @@ -12,46 +12,65 @@ using Mimi: modelinstance, compinstance, get_var_value, OUTER, INNER, ReshapedDi using CSVFiles: load -include("test-model-2/multi-region-model.jl") -using .MyModel -m = construct_MyModel() +# Toy @defsim -N = 100 +@defcomp test begin + regions = Index() -# just use this for the check that the sd can be defined, then use simpler version -# below -sd_complex = @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 - # RV slightly more burdensome, but it's only required when defining - # correlations or sharing an RV across parameters. + 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) - # 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) + # 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)] - depk = [Region1 => Uniform(0.08, 0.14), - Region2 => Uniform(0.10, 1.50), - Region3 => Uniform(0.10, 0.20)] +end - grosseconomy.tfp[2020, Region1] = Uniform(3.39, 3.40) +m = Model() +set_dimension!(m, :time, 2015:5:2110) +set_dimension!(m, :regions, [:Region1, :Region2, :Region3]) +add_comp!(m, test) +set_param!(m, :p_shared1, -5) +set_param!(m, :p_shared2, -collect(1:20)) +set_param!(m, :p_shared3, -fill(10,20,3)) +set_param!(m, :p_shared4, -collect(1:3)) +run(sd_toy, m, 10) - grosseconomy.k0 = [Region1 => Uniform(50.5, 50.6), - Region2 => Uniform(22., 23.), - Region3 => Uniform(33.5, 33.6)] - - sampling(LHSData, corrlist=[(:name1, :name2, 0.7), (:name1, :name3, 0.5)]) - - # indicate which parameters to save for each model run. Specify - # a parameter name or [later] some slice of its data, similar to the - # assignment of RVs, above. - save(grosseconomy.K, grosseconomy.YGROSS, emissions.E, emissions.E_Global, grosseconomy.share_var, grosseconomy.depk_var) -end +# 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 diff --git a/test/mcs/test_translist.jl b/test/mcs/test_translist.jl index cc89f4ad6..d02bc2ebc 100644 --- a/test/mcs/test_translist.jl +++ b/test/mcs/test_translist.jl @@ -120,8 +120,8 @@ m = Model() set_dimension!(m, :time, 2000:10:2050) add_comp!(m, test2) fail_expr = :(run(sd, m, 100)) -err = try eval(fail_expr) catch err err end -@test occursin("Component test1 does not exist in Model1.", sprint(showerror, err)) +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) @@ -130,8 +130,8 @@ m2 = Model() set_dimension!(m2, :time, 2000:10:2050) add_comp!(m2, test1) fail_expr = :(run(sd, [m1, m2], 100)) -err = try eval(fail_expr) catch err err end -@test occursin("Component test1 does not exist in Model1.", sprint(showerror, err)) +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() diff --git a/test/test_parametertypes.jl b/test/test_parametertypes.jl index 8fe40b8a3..d8925df7b 100644 --- a/test/test_parametertypes.jl +++ b/test/test_parametertypes.jl @@ -19,13 +19,11 @@ 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 p1.is_shared && !p2.is_shared -@test Mimi.is_shared(p1) && Mimi.is_shared(p2) +@test Mimi.is_shared(p1) && !Mimi.is_shared(p2) p3 = ScalarModelParameter(3, shared) p4 = ScalarModelParameter(3) @test p3.value == p4.value == 3 -@test p3.is_shared && !p4.shared @test Mimi.is_shared(p3) && !Mimi.is_shared(p4) # From a1abeccc32222133aafd5b6f79ffa9355f8fdf7d Mon Sep 17 00:00:00 2001 From: lrennels Date: Fri, 21 May 2021 13:51:11 -0700 Subject: [PATCH 18/47] Add transform tests --- test/mcs/test_defmcs_modifications.jl | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/mcs/test_defmcs_modifications.jl b/test/mcs/test_defmcs_modifications.jl index 61eba97d2..61114d3cb 100644 --- a/test/mcs/test_defmcs_modifications.jl +++ b/test/mcs/test_defmcs_modifications.jl @@ -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, :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 From f84a9e2f28bc9d67dd158e359a711b6120a443d1 Mon Sep 17 00:00:00 2001 From: lrennels Date: Tue, 25 May 2021 16:38:34 -0700 Subject: [PATCH 19/47] Add to testing; change search for existing connections in add_comp --- contrib/test_all_models.jl | 12 ++++++------ src/core/defs.jl | 25 +++++++++++++++++++------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/contrib/test_all_models.jl b/contrib/test_all_models.jl index b3196c9c7..f5a5b25b3 100644 --- a/contrib/test_all_models.jl +++ b/contrib/test_all_models.jl @@ -15,13 +15,13 @@ # - MimiDICE2016R2 (not all tests pass, but check for new failures) packages_to_test = [ - "MimiDICE2010" => ("https://github.com/anthofflab/MimiDICE2010.jl", "master"), - "MimiDICE2013" => ("https://github.com/anthofflab/MimiDICE2013.jl", "master"), + "MimiDICE2010" => ("https://github.com/anthofflab/MimiDICE2010.jl", "master"), # fails because need to use new DataFrames syngax + "MimiDICE2013" => ("https://github.com/anthofflab/MimiDICE2013.jl", "master"), # fails because need to use new DataFrames syngax "MimiDICE2016" => ("https://github.com/AlexandrePavlov/MimiDICE2016.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"), - "MimiPAGE2020" => ("https://github.com/anthofflab/MimiPAGE2020.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"), diff --git a/src/core/defs.jl b/src/core/defs.jl index e0e2583cd..189e71068 100644 --- a/src/core/defs.jl +++ b/src/core/defs.jl @@ -723,13 +723,23 @@ Add and connect an unshared external parameter to `md` for each parameter in function _initialize_parameters!(md::ModelDef, comp_def::AbstractComponentDef) for param_def in parameters(comp_def) - # check if the parameter is already created and connected, which may be - # the case if we are using replace! with the default reconnect = true - curr_ext_param_name = get_external_param_name(md, nameof(comp_def), nameof(param_def); missing_ok = true) + param_name = nameof(param_def) + comp_name = nameof(comp_def) - if isnothing(curr_ext_param_name) + # Make some checks to see if the parameter needs to be created, specifically + # tends to be the case if we are using replace! with the default reconnect + # = true. The parameter could be: + + # (1) externally created and connected, as checked with unconnected_params + # or alternatively by checking !isnothing(get_external_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) + + if !connected ext_param_name = gensym() - param_name = nameof(param_def) value = param_def.default # create the unshared external parameter with a value of param_def.default, @@ -1056,6 +1066,9 @@ function _replace!(obj::AbstractCompositeComponentDef, # Delete the old component and all its internal and external parameter connections delete!(obj, comp_name) end - 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 From 5f5d39048bf140d50ac4c6dcdec0a33016f225b9 Mon Sep 17 00:00:00 2001 From: lrennels Date: Tue, 25 May 2021 17:40:05 -0700 Subject: [PATCH 20/47] Make set_leftover_params default to making shared parameters --- docs/src/howto/howto_4.md | 2 +- src/core/connections.jl | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/src/howto/howto_4.md b/docs/src/howto/howto_4.md index 2733c6af4..8ae3d7295 100644 --- a/docs/src/howto/howto_4.md +++ b/docs/src/howto/howto_4.md @@ -116,7 +116,7 @@ In larger models it can be beneficial to set some of the external parameters usi 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. +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, and all resulting new external parameters will be shared parameters. ## Using NamedArrays for setting parameters diff --git a/src/core/connections.jl b/src/core/connections.jl index cd867c8d0..f0dd09575 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -372,7 +372,8 @@ end 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 that match the names of unset parameters in the model. +the dictionary keys are strings that match the names of unset parameters in the model, +and all resulting new external parameters will be shared parameters. """ function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T for param_ref in nothing_params(md) @@ -387,7 +388,7 @@ function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T 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) + set_external_param!(md, param_name, value; param_dims = param_dims, is_shared = true) else error("Cannot set parameter :$param_name, not found in provided dictionary.") end From 58b0e558f069617e8983139607f42f142934c318 Mon Sep 17 00:00:00 2001 From: lrennels Date: Wed, 26 May 2021 12:59:32 -0700 Subject: [PATCH 21/47] Add MimiIWG to contrib testing of all models --- contrib/test_all_models.jl | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/contrib/test_all_models.jl b/contrib/test_all_models.jl index f5a5b25b3..e4116e8f4 100644 --- a/contrib/test_all_models.jl +++ b/contrib/test_all_models.jl @@ -10,14 +10,11 @@ # julia --color=yes test_all_models.jl # -# should locally also test -# - MimiIWG (can't pass on CI because of local registry) -# - MimiDICE2016R2 (not all tests pass, but check for new failures) - packages_to_test = [ "MimiDICE2010" => ("https://github.com/anthofflab/MimiDICE2010.jl", "master"), # fails because need to use new DataFrames syngax "MimiDICE2013" => ("https://github.com/anthofflab/MimiDICE2013.jl", "master"), # fails because need to use new DataFrames syngax "MimiDICE2016" => ("https://github.com/AlexandrePavlov/MimiDICE2016.jl", "master"), + ## "MimiDICE2016R2" => ("https://github.com/AlexandrePavlov/MimiDICE2016R2.jl", "master"), # doesn't pass in repo, just look for new failures "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 @@ -25,7 +22,8 @@ packages_to_test = [ "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"), + "MimiIWG" => ("https://github.com/rffscghg/MimiIWG.jl", "mcs") # note here we use the mcs branch, we can only testing this one if we have the Mimi registry in our current environment ] using Pkg From b417389f27aec0fde6e10b4e891a21d959a6d603 Mon Sep 17 00:00:00 2001 From: lrennels Date: Thu, 27 May 2021 17:01:34 -0700 Subject: [PATCH 22/47] Add update_param and rename some functions --- src/core/connections.jl | 193 ++++++++++++++++++++++++------ src/core/defs.jl | 29 +++-- src/core/model.jl | 31 ++++- src/mcs/montecarlo.jl | 4 +- test/mcs/runtests.jl | 2 +- test/runtests.jl | 4 +- test/test_composite_parameters.jl | 24 ++-- test/test_parametertypes.jl | 8 +- test/test_references.jl | 4 +- 9 files changed, 227 insertions(+), 72 deletions(-) diff --git a/src/core/connections.jl b/src/core/connections.jl index f0dd09575..a38ab6e08 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -24,7 +24,7 @@ function disconnect_param!(obj::AbstractCompositeComponentDef, comp_def::Abstrac # 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 external parameters - ext_param_name = get_external_param_name(obj, nameof(comp_def), param_name; missing_ok = true) + ext_param_name = get_model_param_name(obj, nameof(comp_def), param_name; missing_ok = true) if !isnothing(ext_param_name) && !(external_param(obj, ext_param_name).is_shared) delete!(obj.external_params, ext_param_name); end @@ -192,7 +192,7 @@ function _connect_param!(obj::AbstractCompositeComponentDef, end - set_external_array_param!(obj, dst_par_name, values, dst_dims) + add_model_array_param!(obj, dst_par_name, values, dst_dims) backup_param_name = dst_par_name else @@ -382,13 +382,13 @@ function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T comp_def = find_comp(md, comp_name) param_def = comp_def[param_name] - # check whether we need to create the external parameter + # check whether we need to add the external parameter to the ModelDef 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, is_shared = true) + add_model_param!(md, param_name, value; param_dims = param_dims, is_shared = true) else error("Cannot set parameter :$param_name, not found in provided dictionary.") end @@ -434,14 +434,14 @@ function external_param(obj::ModelDef, name::Symbol; missing_ok=false) end """ - get_external_param_name(obj::ModelDef, comp_name::Symbol, param_name::Symbol; missing_ok=false) + get_model_param_name(obj::ModelDef, comp_name::Symbol, param_name::Symbol; missing_ok=false) Get the external 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_external_param_name(obj::ModelDef, comp_name::Symbol, param_name::Symbol; missing_ok=false) +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.external_param @@ -454,15 +454,15 @@ function get_external_param_name(obj::ModelDef, comp_name::Symbol, param_name::S end """ - get_external_param_name(obj::Model, comp_name::Symbol, param_name::Symbol; missing_ok=false) + get_model_param_name(obj::Model, comp_name::Symbol, param_name::Symbol; missing_ok=false) Get the external 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_external_param_name(obj::Model, comp_name::Symbol, param_name::Symbol; missing_ok=false) - get_external_param_name(obj.md, comp_name, param_name; missing_ok = missing_ok) +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) @@ -470,93 +470,165 @@ function add_external_param_conn!(obj::ModelDef, conn::ExternalParameterConnecti dirty!(obj) end -function set_external_param!(obj::ModelDef, name::Symbol, value::ModelParameter) +""" + add_model_param!(md::ModelDef, name::Symbol, value::ModelParameter) + +Add an external parameter with name `name` and Model Parameter `value` to ModelDef `md`. +""" +function add_model_param!(md::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" # end - obj.external_params[name] = value - dirty!(obj) + md.external_params[name] = value + dirty!(md) return value end +# deprecated version of above +function set_external_param!(obj::ModelDef, name::Symbol, value::ModelParameter) + @warn "`set_external_param! is deprecated and will be removed in the future, please use `add_external_param` with the same arguments." + add_model_param!(obj, name, value) +end +""" + add_model_param!(md::ModelDef, name::Symbol, value::Number; + param_dims::Union{Nothing,Array{Symbol}} = nothing, + is_shared::Bool = false) + +Create and add an external 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::Number; + param_dims::Union{Nothing,Array{Symbol}} = nothing, + is_shared::Bool = false) + add_model_scalar_param!(md, name, value, is_shared = is_shared) +end +# deprecated version of above function set_external_param!(obj::ModelDef, name::Symbol, value::Number; param_dims::Union{Nothing,Array{Symbol}} = nothing, is_shared::Bool = false) - set_external_scalar_param!(obj, name, value, is_shared = is_shared) + @warn "`set_external_param! is deprecated and will be removed in the future, please use `add_external_param` with the same arguments." + add_model_param!(obj, name, value; param_dims = param_dims, 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 an external 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, is_shared::Bool = false) + ti = get_time_index_position(param_dims) if ti != nothing - value = convert(Array{number_type(obj)}, value) + 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, is_shared = is_shared) + add_model_array_param!(md, name, values, param_dims, is_shared = is_shared) +end +# deprecated version of above +function set_external_param!(obj::ModelDef, name::Symbol, + value::Union{AbstractArray, AbstractRange, Tuple}; + param_dims::Union{Nothing,Array{Symbol}} = nothing, + is_shared::Bool = false) + @warn "`set_external_param! is deprecated and will be removed in the future, please use `add_external_param` with the same arguments." + add_model_param!(obj, name, value; param_dims, is_shared = is_shared) end """ - set_external_array_param!(obj::ModelDef, + 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`. The `is_shared` attribute of the ArrayModelParameter +`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; is_shared::Bool = false) +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 - set_external_param!(obj, name, param) + add_model_param!(md, name, param) +end +# deprecated version of above +function set_external_array_param!(obj::ModelDef, + name::Symbol, value::TimestepVector, + dims; is_shared::Bool = false) + @warn "`set_external_array_param! is deprecated and will be removed in the future, please use `add_external_array_param` with the same arguments." + add_model_array_param!(obj, name, value, dims; is_shared = is_shared) end """ - set_external_array_param!(obj::ModelDef, + 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`. The `is_shared` attribute of the ArrayModelParameter +`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 add_model_array_param!(md::ModelDef, + name::Symbol, value::TimestepArray, dims; + is_shared::Bool = false) + param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims, is_shared) + add_model_param!(md, name, param) +end +# deprecated version of above function set_external_array_param!(obj::ModelDef, name::Symbol, value::TimestepArray, dims; is_shared::Bool = false) - param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims, is_shared) - set_external_param!(obj, name, param) + @warn "`set_external_array_param! is deprecated and will be removed in the future, please use `add_external_array_param` with the same arguments." + add_external_array_param(obj, name, value, dims; is_shared = is_shared) end """ - set_external_array_param!(obj::ModelDef, + 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`. The `is_shared` attribute of the ArrayModelParameter will default to +Model Def `md`. The `is_shared` attribute of the ArrayModelParameter will default to false. """ -function set_external_array_param!(obj::ModelDef, +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) - set_external_param!(obj, name, param) + add_model_param!(md, name, param) +end +# deprecated version of above +function set_external_array_param!(obj::ModelDef, + name::Symbol, value::AbstractArray, dims; + is_shared::Bool = false) + @warn "`set_external_array_param! is deprecated and will be removed in the future, please use `add_external_array_param` with the same arguments." + add_external_array_param(obj, name, value, dims; is_shared = is_shared) end """ - set_external_scalar_param!(obj::ModelDef, name::Symbol, value::Any; is_shared::Bool = false) + 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; is_shared::Bool = false) +function add_model_scalar_param!(md::ModelDef, name::Symbol, value::Any; is_shared::Bool = false) param = ScalarModelParameter(value, is_shared) - set_external_param!(obj, name, param) + add_model_param!(md, name, param) +end +# deprecated version of above +function set_external_scalar_param!(obj::ModelDef, name::Symbol, value::Any; is_shared::Bool = false) + @warn "`set_external_scalar_param! is deprecated and will be removed in the future, please use `add_external_scalar_param` with the same arguments." + add_external_scalar_param(obj, name, value; is_shared = is_shared) end """ @@ -585,6 +657,38 @@ function update_param!(mi::ModelInstance, name::Symbol, value) 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) + + # first check if we need to create an unshared model parameter, which may happen + # in the case of a previously unshared parameter being connected internally + model_param_name = get_model_param_name(md, comp_name, param_name; missing_ok = true) + + # create an unshared parameter + if isnothing(model_param_name) + comp_def = find_comp(md, comp_name) + param_def = comp_def[param_name] + param = create_external_param(md, param_def, value; is_shared = false) + add_model_param!(md, model_param_name, param) + name = get_model_param_name + + # make sure the model parameter is unshared + elseif external_param(md, name).is_shared + error("Parameter $param_name is a shared model parameter, to safely update", + "please call `update_param!(m, param_name, value)` to explicitly update", + "a shared parameter that may be connected to several components") + end + + # update the parameter + _update_param!(md, model_param_name, value) + +end + function _update_param!(obj::AbstractCompositeComponentDef, name::Symbol, value) param = external_param(obj, name, missing_ok=true) @@ -647,7 +751,7 @@ 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), param.is_shared)) + add_model_param!(obj, name, ArrayModelParameter(new_timestep_array, dim_names(param), param.is_shared)) else copyto!(param.values.data, value) end @@ -843,6 +947,23 @@ function _get_param_times(param::ArrayModelParameter{TimestepArray{VariableTimes return [TIMES...] end +""" + add_shared_parameter(md::ModelDef, name::Symbol, value::Any; + param_dims::Union{Nothing,Array{Symbol}} = nothing) + +User-facing API function to add a shared parameter to Model Def `md` with name +`name` and value `value`, and optional dimensions `param_dims`. The `is_shared` +attribute of the added Model Parameter will be `true`. +""" +function add_shared_parameter(md::ModelDef, name::Symbol, value::Any; + param_dims::Union{Nothing,Array{Symbol}} = nothing) + + has_parameter(md, name) && error("Cannot set parameter :$name, the model already has a shared parameter with this name.") + + # check to make sure the parameter doesn't already exist + add_model_param!(md, name, value; param_dims = param_dims, is_shared = true) +end + """ create_external_param(md::ModelDef, param_def::AbstractParameterDef, value::Any; is_shared::Bool = false) diff --git a/src/core/defs.jl b/src/core/defs.jl index 189e71068..6ece10cbc 100644 --- a/src/core/defs.jl +++ b/src/core/defs.jl @@ -110,7 +110,7 @@ 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) else - error("Cannot delete $external_param_name, not found in external parameter list.") + error("Cannot delete $external_param_name, not found in model's parameter list.") end # Remove external parameter connections @@ -438,9 +438,14 @@ function set_param!(md::ModelDef, comp_def::AbstractComponentDef, param_name::Sy 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("Cannot set parameter :$ext_param_name, the model already has a parameter with this name.", + " IF you wish to change the name 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 :$ext_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 ", + "`set_param!(m, comp_name, param_name, unique_param_name, value)`.") end set_param!(md, param_name, value, dims = dims, comps = [comp_def], ext_param_name = ext_param_name) @@ -471,12 +476,14 @@ function set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing, ignor 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 @@ -507,7 +514,7 @@ function set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing, ignor if ext_param_name === nothing ext_param_name = param_name end - set_external_param!(md, ext_param_name, param) + add_model_param!(md, ext_param_name, param) # connect for comp in comps @@ -731,7 +738,7 @@ function _initialize_parameters!(md::ModelDef, comp_def::AbstractComponentDef) # = true. The parameter could be: # (1) externally created and connected, as checked with unconnected_params - # or alternatively by checking !isnothing(get_external_param_name(md, nameof(comp_def), nameof(param_def); missing_ok = true)) + # 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 @@ -753,7 +760,7 @@ function _initialize_parameters!(md::ModelDef, comp_def::AbstractComponentDef) end # add the unshared external parameter to the model def - set_external_param!(md, ext_param_name, param) + add_model_param!(md, ext_param_name, param) # connect - don't need to check labels since did it above connect_param!(md, comp_def, param_name, ext_param_name; check_labels = false) diff --git a/src/core/model.jl b/src/core/model.jl index 5a4440757..cea2ffe4a 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -96,6 +96,12 @@ Remove any parameter connections for a given parameter `param_name` in a given c value::Union{Number, AbstractArray, AbstractRange, Tuple}; param_dims::Union{Nothing,Array{Symbol}} = nothing) => md +@delegate add_model_param!(m::Model, name::Symbol, value::ModelParameter) => md + +@delegate add_model_param!(m::Model, name::Symbol, + value::Union{Number, AbstractArray, AbstractRange, Tuple}; + param_dims::Union{Nothing,Array{Symbol}} = nothing) => md + @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... @@ -112,6 +118,14 @@ just to provide warnings. """ @delegate update_param!(m::Model, name::Symbol, value; update_timesteps = nothing) => md +""" + 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{T, Any}; update_timesteps = nothing) where T @@ -377,18 +391,31 @@ 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_parameter(m::Model, name::Symbol, value::Any; + param_dims::Union{Nothing,Array{Symbol}} = nothing) + +User-facing API function to add a shared parameter to Model `m`'s ModelDef` with name +`name` and value `value`, and optional dimensions `param_dims`. The `is_shared` +attribute of the added Model Parameter will be `true`. +""" +@delegate add_shared_parameter(m::Model, name::Symbol, value::Any; + param_dims::Union{Nothing,Array{Symbol}} = 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 add_model_array_param!(m::Model, name::Symbol, value::Union{AbstractArray, TimestepArray}, dims) => md @delegate set_external_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 add_model_scalar_param!(m::Model, name::Symbol, value::Any) => md @delegate set_external_scalar_param!(m::Model, name::Symbol, value::Any) => md """ diff --git a/src/mcs/montecarlo.jl b/src/mcs/montecarlo.jl index 1c80f7ccd..beb9d9c9c 100644 --- a/src/mcs/montecarlo.jl +++ b/src/mcs/montecarlo.jl @@ -718,7 +718,7 @@ function set_translist_externalparams!(sim_inst::SimulationInstance{T}) where T # 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]).") - external_parameters_vec[model_idx] = get_external_param_name(m.md, compname, trans.paramname) + external_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 @@ -743,7 +743,7 @@ function set_translist_externalparams!(sim_inst::SimulationInstance{T}) where T 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_external_param_name(m.md, compname, paramname) # NB might not need to use m.mi.md here could be m.md + 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.") diff --git a/test/mcs/runtests.jl b/test/mcs/runtests.jl index 243f32834..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") diff --git a/test/runtests.jl b/test/runtests.jl index 7a0b09dfe..411ef3071 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -53,8 +53,8 @@ Electron.prep_test_env() @info("test_replace_comp.jl") @time include("test_replace_comp.jl") - @info("test_tools.jl") - @time include("test_tools.jl") + # @info("test_tools.jl") + # @time include("test_tools.jl") @info("test_parameter_labels.jl") @time include("test_parameter_labels.jl") diff --git a/test/test_composite_parameters.jl b/test/test_composite_parameters.jl index 1302207e7..48d0c2d05 100644 --- a/test/test_composite_parameters.jl +++ b/test/test_composite_parameters.jl @@ -131,7 +131,7 @@ set_param!(m2, :A, :p1, 2) # Set the value only for component A @test Mimi.external_param(m2.md, :p1).value == 2 @test Mimi.external_param(m2.md, :p1).is_shared # and that B.p1 is still the default value and unshared -sym = Mimi.get_external_param_name(m2.md, :B, :p1) +sym = Mimi.get_model_param_name(m2.md, :B, :p1) @test Mimi.external_param(m2.md, sym).value == 3 @test !(Mimi.external_param(m2.md, sym).is_shared) @@ -143,13 +143,13 @@ set_param!(m3, :p3, 3) set_param!(m3, :p4, 1:10) run(m3) -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)) +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!(m2, :B, :p1, :B_p1, 2) # Use a unique name to set B.p1 -@test Mimi.external_param(m2.md, :B_p1).value == 2 -@test Mimi.external_param(m2.md, :B_p1).is_shared -@test issubset(Set([:p1, :B_p1]), Set(keys(m2.md.external_params))) +set_param!(m3, :B, :p1, :B_p1, 2) # Use a unique name to set B.p1 +@test Mimi.external_param(m3.md, :B_p1).value == 2 +@test Mimi.external_param(m3.md, :B_p1).is_shared +@test issubset(Set([:p1, :B_p1]), Set(keys(m3.md.external_params))) #------------------------------------------------------------------------------ # Unit tests on default behavior @@ -172,7 +172,7 @@ m = Model() set_dimension!(m, :time, 2000:2005) add_comp!(m, top); @test length(external_params(m)) == 1 -ext_param_name = Mimi.get_external_param_name(m.md, :top, :superp1) +ext_param_name = Mimi.get_model_param_name(m.md, :top, :superp1) @test Mimi.is_nothing_param(external_params(m)[ext_param_name]) @test !external_params(m)[ext_param_name].is_shared @@ -186,7 +186,7 @@ m = Model() set_dimension!(m, :time, 2000:2005) add_comp!(m, top); @test length(external_params(m)) == 1 -ext_param_name = Mimi.get_external_param_name(m.md, :top, :superp1) +ext_param_name = Mimi.get_model_param_name(m.md, :top, :superp1) @test external_params(m)[ext_param_name].value == 8.0 @test !external_params(m)[ext_param_name].is_shared @@ -208,7 +208,7 @@ m = Model() set_dimension!(m, :time, 2000:2005) add_comp!(m, top); @test length(external_params(m)) == 1 -ext_param_name = Mimi.get_external_param_name(m.md, :top, :superp1) +ext_param_name = Mimi.get_model_param_name(m.md, :top, :superp1) @test external_params(m)[ext_param_name].value == 2 @test !external_params(m)[ext_param_name].is_shared @@ -229,10 +229,10 @@ m = Model() set_dimension!(m, :time, 2000:2005) add_comp!(m, top); @test length(external_params(m)) == 2 -ext_param_name = Mimi.get_external_param_name(m.md, :top, :p1) +ext_param_name = Mimi.get_model_param_name(m.md, :top, :p1) @test external_params(m)[ext_param_name].value == 2 @test !external_params(m)[ext_param_name].is_shared -ext_param_name = Mimi.get_external_param_name(m.md, :top, :p2) +ext_param_name = Mimi.get_model_param_name(m.md, :top, :p2) @test external_params(m)[ext_param_name].value == 3 @test !external_params(m)[ext_param_name].is_shared diff --git a/test/test_parametertypes.jl b/test/test_parametertypes.jl index d8925df7b..f1ab320da 100644 --- a/test/test_parametertypes.jl +++ b/test/test_parametertypes.jl @@ -95,10 +95,10 @@ set_param!(m, :MyComp, :j, [1,2,3]) Mimi.build!(m) extpars = external_params(m.mi.md) -a_sym = Mimi.get_external_param_name(m.mi.md, :MyComp, :a) -b_sym = Mimi.get_external_param_name(m.mi.md, :MyComp, :b) -g_sym = Mimi.get_external_param_name(m.mi.md, :MyComp, :g) -h_sym = Mimi.get_external_param_name(m.mi.md, :MyComp, :h) +a_sym = Mimi.get_model_param_name(m.mi.md, :MyComp, :a) +b_sym = Mimi.get_model_param_name(m.mi.md, :MyComp, :b) +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) diff --git a/test/test_references.jl b/test/test_references.jl index 5d189373b..41a9a3d64 100644 --- a/test/test_references.jl +++ b/test/test_references.jl @@ -24,12 +24,12 @@ 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 Mimi.get_external_param_name(m.md, :foo, :p1) == :foo_p1 +@test Mimi.get_model_param_name(m.md, :foo, :p1) == :foo_p1 @test :foo_p1 in keys(external_params(m)) @test Mimi.UnnamedReference(:B, :p1) in Mimi.nothing_params(m.md) refB[:p1] = 5 -@test Mimi.get_external_param_name(m.md, :B, :p1) == :B_p1 +@test Mimi.get_model_param_name(m.md, :B, :p1) == :B_p1 @test :B_p1 in keys(external_params(m)) # Use the ComponentReferences to make an internal connection From 4eeddf44c32ee511a8e4c4f777a8590383c1e5eb Mon Sep 17 00:00:00 2001 From: lrennels Date: Thu, 27 May 2021 17:36:56 -0700 Subject: [PATCH 23/47] Fix typo --- src/core/connections.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/connections.jl b/src/core/connections.jl index a38ab6e08..f5351062a 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -544,7 +544,7 @@ function set_external_param!(obj::ModelDef, name::Symbol, param_dims::Union{Nothing,Array{Symbol}} = nothing, is_shared::Bool = false) @warn "`set_external_param! is deprecated and will be removed in the future, please use `add_external_param` with the same arguments." - add_model_param!(obj, name, value; param_dims, is_shared = is_shared) + add_model_param!(obj, name, value; param_dims = param_dims, is_shared = is_shared) end """ From baac99392db940bcc608ba4dc6b901bfdb26e2ca Mon Sep 17 00:00:00 2001 From: lrennels Date: Thu, 27 May 2021 22:45:03 -0700 Subject: [PATCH 24/47] PMove to use of term model paramter over external parameter --- docs/src/howto/howto_1.md | 2 +- docs/src/howto/howto_3.md | 6 +- docs/src/howto/howto_4.md | 8 +- docs/src/howto/howto_5.md | 6 +- docs/src/internals/proposals.md | 2 +- docs/src/ref/ref_structures_definitions.md | 15 +-- docs/src/ref/ref_structures_instances.md | 2 +- docs/src/tutorials/tutorial_3.md | 6 +- docs/src/tutorials/tutorial_5.md | 4 +- src/core/build.jl | 8 +- src/core/connections.jl | 134 ++++++++++----------- src/core/defcomposite.jl | 37 ------ src/core/defs.jl | 84 ++++++------- src/core/model.jl | 34 +++--- src/core/references.jl | 6 +- src/core/types/defs.jl | 10 +- src/core/types/params.jl | 8 +- src/mcs/defmcs.jl | 6 +- src/mcs/mcs_types.jl | 10 +- src/mcs/montecarlo.jl | 54 ++++----- test/mcs/test_defmcs.jl | 4 +- test/mcs/test_defmcs_delta.jl | 2 +- test/mcs/test_defmcs_sobol.jl | 2 +- test/mcs/test_reshaping.jl | 2 +- test/mcs/test_translist.jl | 4 +- test/runtests.jl | 4 +- test/test_composite_parameters.jl | 54 ++++----- test/test_defaults.jl | 10 +- test/test_delete.jl | 10 +- test/test_dimensions.jl | 6 +- test/test_explorer_sim.jl | 2 +- test/test_getdataframe.jl | 2 +- test/test_main.jl | 6 +- test/test_main_variabletimestep.jl | 6 +- test/test_parametertypes.jl | 32 ++--- test/test_references.jl | 6 +- test/test_replace_comp.jl | 22 ++-- test/test_show.jl | 4 +- 38 files changed, 290 insertions(+), 330 deletions(-) diff --git a/docs/src/howto/howto_1.md b/docs/src/howto/howto_1.md index bf933b865..4c19c6273 100644 --- a/docs/src/howto/howto_1.md +++ b/docs/src/howto/howto_1.md @@ -105,7 +105,7 @@ 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. -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 diff --git a/docs/src/howto/howto_3.md b/docs/src/howto/howto_3.md index 7a678d5d0..bfebb01f0 100644 --- a/docs/src/howto/howto_3.md +++ b/docs/src/howto/howto_3.md @@ -100,7 +100,7 @@ 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 left hand side of these assignments can be either a `param`, which must refer to a shared external parameter, or `comp.param` which refers to an unshared external parameter specific to a component. +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` 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. @@ -190,7 +190,7 @@ Certain sampling strategies support (or necessitate) further customization. Thes ## 2. The `run` 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` 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 @@ -440,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 8ae3d7295..09c30ac1b 100644 --- a/docs/src/howto/howto_4.md +++ b/docs/src/howto/howto_4.md @@ -98,9 +98,9 @@ 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 +## Updating a model 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. +When `set_param!` is called, it creates a shared model 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) @@ -110,13 +110,13 @@ Note here that `newvalues` must be the same type (or be able to convert to the t #### 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: +In larger models it can be beneficial to set some of the shared model 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, and all resulting new external parameters will be shared 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, noting that all resulting model parameters will be shared parameters. ## Using NamedArrays for setting parameters diff --git a/docs/src/howto/howto_5.md b/docs/src/howto/howto_5.md index 590e937a6..d3749a51b 100644 --- a/docs/src/howto/howto_5.md +++ b/docs/src/howto/howto_5.md @@ -53,9 +53,9 @@ add_comp!(m, FAIR_component) # will run from 1765 to 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. +- 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.external_params(m)[:currtaxn2o].values.data # get param values for use in next run (ignore messy internals syntax) + 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 @@ -66,4 +66,4 @@ add_comp!(m, FAIR_component) # will run from 1765 to 1950 #### The following options are now available for further modifcations if this end state is not desireable: - If you want to update a component's run period, you may use the function `Mimi.set_first_last!(m, :ComponentName, first = new_first, last = new_last)` to specify when you want the component to run. -- You can update external parameters to have values in place of the assumed `missing`s using the `update_param!(m, :ParameterName, values)` function +- You can update shared model parameters to have values in place of the assumed `missing`s using the `update_param!(m, :ParameterName, values)` function 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_structures_definitions.md b/docs/src/ref/ref_structures_definitions.md index a8cfec2d4..b84efbf91 100644 --- a/docs/src/ref/ref_structures_definitions.md +++ b/docs/src/ref/ref_structures_definitions.md @@ -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,7 +99,7 @@ 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 +Values that are exogenous to the model are defined in model 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!()`. @@ -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_3.md b/docs/src/tutorials/tutorial_3.md index 97c9bd21a..bde32e152 100644 --- a/docs/src/tutorials/tutorial_3.md +++ b/docs/src/tutorials/tutorial_3.md @@ -22,7 +22,7 @@ Possible modifications range in complexity, from simply altering parameter value 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. -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. +When the original model calls [`set_param!`](@ref), Mimi creates an shared model 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 shared model parameter. ```julia update_param!(mymodel, :parametername, newvalues) @@ -94,9 +94,9 @@ 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!` 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: +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}() diff --git a/docs/src/tutorials/tutorial_5.md b/docs/src/tutorials/tutorial_5.md index f2ea35153..ed5dead7e 100644 --- a/docs/src/tutorials/tutorial_5.md +++ b/docs/src/tutorials/tutorial_5.md @@ -182,7 +182,7 @@ rv(rv1) = Normal(0, 0.8) # create a random variable called "rv1" with the spe param1 = rv1 # then assign this random variable "rv1" to the parameter "param1" in the model ``` -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 external parameter `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`. ```julia param1 = Normal(0, 0.8) ``` @@ -237,7 +237,7 @@ end 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**. -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` 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 diff --git a/src/core/build.jl b/src/core/build.jl index 0bd0f1eca..ebe9bcbfb 100644 --- a/src/core/build.jl +++ b/src/core/build.jl @@ -194,13 +194,13 @@ 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 @@ -228,7 +228,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,7 +244,7 @@ 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 diff --git a/src/core/connections.jl b/src/core/connections.jl index f5351062a..9c476b600 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -23,10 +23,10 @@ function disconnect_param!(obj::AbstractCompositeComponentDef, comp_def::Abstrac # 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 external parameters - ext_param_name = get_model_param_name(obj, nameof(comp_def), param_name; missing_ok = true) - if !isnothing(ext_param_name) && !(external_param(obj, ext_param_name).is_shared) - delete!(obj.external_params, ext_param_name); + # 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) @@ -50,17 +50,17 @@ end verify_units(unit1::AbstractString, unit2::AbstractString) = (unit1 == unit2) function _check_labels(obj::AbstractCompositeComponentDef, - comp_def::AbstractComponentDef, param_name::Symbol, ext_param::ArrayModelParameter) + comp_def::AbstractComponentDef, param_name::Symbol, mod_param::ArrayModelParameter) 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)") end comp_dims = dim_names(param_def) - param_dims = dim_names(ext_param) + param_dims = dim_names(mod_param) if ! isempty(param_dims) && size(param_dims) != size(comp_dims) d1 = size(comp_dims) @@ -77,42 +77,42 @@ function _check_labels(obj::AbstractCompositeComponentDef, for (i, dim) in enumerate(comp_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 $(comp_def.comp_id) has $comp_length elements; model parameter :$param_name has $param_length elements.") end end end end """ - connect_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, param_name::Symbol, ext_param_name::Symbol; + connect_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, param_name::Symbol, model_param_name::Symbol; check_labels::Bool=true) 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; + param_name::Symbol, model_param_name::Symbol; check_labels::Bool=true) 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_labels=check_labels) 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) + param_name::Symbol, model_param_name::Symbol; check_labels::Bool=true) + mod_param = model_param(obj, model_param_name) - if ext_param isa ArrayModelParameter && check_labels - _check_labels(obj, comp_def, param_name, ext_param) + if mod_param isa ArrayModelParameter && check_labels + _check_labels(obj, comp_def, param_name, mod_param) 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) - add_external_param_conn!(obj, conn) + conn = ExternalParameterConnection(comp_path, param_name, model_param_name) + add_model_param_conn!(obj, conn) return nothing end @@ -304,7 +304,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[] @@ -323,15 +323,15 @@ end """ nothing_params(obj::AbstractCompositeComponentDef) -Return a list of UnnamedReference's to parameters that are connected to a an -external parameter with a value of nothing. +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 - value = external_param(obj, conn.external_param) + value = model_param(obj, conn.model_param_name) if is_nothing_param(value) push!(refs, UnnamedReference(conn.comp_path.names[end], conn.param_name)) end @@ -373,7 +373,7 @@ end 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 that match the names of unset parameters in the model, -and all resulting new external parameters will be shared parameters. +and all resulting new model parameters will be shared parameters. """ function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T for param_ref in nothing_params(md) @@ -382,8 +382,8 @@ function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T comp_def = find_comp(md, comp_name) param_def = comp_def[param_name] - # check whether we need to add the external parameter to the ModelDef - if external_param(md, param_name, missing_ok=true) === nothing + # check whether we need to add the model parameter to the ModelDef + if model_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) @@ -425,18 +425,18 @@ 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] +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 external parameter list") + error("$name not found in model parameter list") end """ get_model_param_name(obj::ModelDef, comp_name::Symbol, param_name::Symbol; missing_ok=false) -Get the external parameter name for the exernal parameter conneceted to comp_name's +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`. @@ -444,19 +444,19 @@ 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.external_param + return conn.model_param_name end end missing_ok && return nothing - error("External parameter connected to $comp_name's parameter $param_name not found in external parameter connections 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 external parameter name for the exernal parameter conneceted to comp_name's +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`. @@ -465,7 +465,7 @@ function get_model_param_name(obj::Model, comp_name::Symbol, param_name::Symbol; get_model_param_name(obj.md, comp_name, param_name; missing_ok = missing_ok) end -function add_external_param_conn!(obj::ModelDef, conn::ExternalParameterConnection) +function add_model_param_conn!(obj::ModelDef, conn::ExternalParameterConnection) push!(obj.external_param_conns, conn) dirty!(obj) end @@ -473,19 +473,19 @@ end """ add_model_param!(md::ModelDef, name::Symbol, value::ModelParameter) -Add an external parameter with name `name` and Model Parameter `value` to ModelDef `md`. +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.external_params, name) - # @warn "Redefining external param :$name in $(obj.comp_path) from $(obj.external_params[name]) to $value" + # if haskey(obj.model_params, name) + # @warn "Redefining model param :$name in $(obj.comp_path) from $(obj.model_params[name]) to $value" # end - md.external_params[name] = value + md.model_params[name] = value dirty!(md) return value end # deprecated version of above function set_external_param!(obj::ModelDef, name::Symbol, value::ModelParameter) - @warn "`set_external_param! is deprecated and will be removed in the future, please use `add_external_param` with the same arguments." + @warn "`set_external_param! is deprecated and will be removed in the future, please use `add_model_param` with the same arguments." add_model_param!(obj, name, value) end @@ -494,7 +494,7 @@ end param_dims::Union{Nothing,Array{Symbol}} = nothing, is_shared::Bool = false) -Create and add an external parameter with name `name` and Model Parameter `value` +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. @@ -508,7 +508,7 @@ end function set_external_param!(obj::ModelDef, name::Symbol, value::Number; param_dims::Union{Nothing,Array{Symbol}} = nothing, is_shared::Bool = false) - @warn "`set_external_param! is deprecated and will be removed in the future, please use `add_external_param` with the same arguments." + @warn "`set_external_param! is deprecated and will be removed in the future, please use `add_model_param` with the same arguments." add_model_param!(obj, name, value; param_dims = param_dims, is_shared = is_shared) end @@ -517,7 +517,7 @@ end param_dims::Union{Nothing,Array{Symbol}} = nothing, is_shared::Bool = false) -Create and add an external parameter with name `name` and Model Parameter `value` +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. @@ -543,7 +543,7 @@ function set_external_param!(obj::ModelDef, name::Symbol, value::Union{AbstractArray, AbstractRange, Tuple}; param_dims::Union{Nothing,Array{Symbol}} = nothing, is_shared::Bool = false) - @warn "`set_external_param! is deprecated and will be removed in the future, please use `add_external_param` with the same arguments." + @warn "`set_external_param! is deprecated and will be removed in the future, please use `add_model_param` with the same arguments." add_model_param!(obj, name, value; param_dims = param_dims, is_shared = is_shared) end @@ -566,7 +566,7 @@ end function set_external_array_param!(obj::ModelDef, name::Symbol, value::TimestepVector, dims; is_shared::Bool = false) - @warn "`set_external_array_param! is deprecated and will be removed in the future, please use `add_external_array_param` with the same arguments." + @warn "`set_external_array_param! is deprecated and will be removed in the future, please use `add_model_array_param` with the same arguments." add_model_array_param!(obj, name, value, dims; is_shared = is_shared) end @@ -589,8 +589,8 @@ end function set_external_array_param!(obj::ModelDef, name::Symbol, value::TimestepArray, dims; is_shared::Bool = false) - @warn "`set_external_array_param! is deprecated and will be removed in the future, please use `add_external_array_param` with the same arguments." - add_external_array_param(obj, name, value, dims; is_shared = is_shared) + @warn "`set_external_array_param! is deprecated and will be removed in the future, please use `add_model_array_param` with the same arguments." + add_model_array_param(obj, name, value, dims; is_shared = is_shared) end """ @@ -612,8 +612,8 @@ end function set_external_array_param!(obj::ModelDef, name::Symbol, value::AbstractArray, dims; is_shared::Bool = false) - @warn "`set_external_array_param! is deprecated and will be removed in the future, please use `add_external_array_param` with the same arguments." - add_external_array_param(obj, name, value, dims; is_shared = is_shared) + @warn "`set_external_array_param! is deprecated and will be removed in the future, please use `add_model_array_param` with the same arguments." + add_model_array_param(obj, name, value, dims; is_shared = is_shared) end """ @@ -627,14 +627,14 @@ function add_model_scalar_param!(md::ModelDef, name::Symbol, value::Any; is_shar end # deprecated version of above function set_external_scalar_param!(obj::ModelDef, name::Symbol, value::Any; is_shared::Bool = false) - @warn "`set_external_scalar_param! is deprecated and will be removed in the future, please use `add_external_scalar_param` with the same arguments." - add_external_scalar_param(obj, name, value; is_shared = is_shared) + @warn "`set_external_scalar_param! is deprecated and will be removed in the future, please use `add_model_scalar_param` with the same arguments." + add_model_scalar_param(obj, name, value; is_shared = is_shared) 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. """ @@ -644,7 +644,7 @@ function update_param!(obj::AbstractCompositeComponentDef, name::Symbol, value; end 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 @@ -673,27 +673,27 @@ function update_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, valu if isnothing(model_param_name) comp_def = find_comp(md, comp_name) param_def = comp_def[param_name] - param = create_external_param(md, param_def, value; is_shared = false) + param = create_model_param(md, param_def, value; is_shared = false) add_model_param!(md, model_param_name, param) name = get_model_param_name # make sure the model parameter is unshared - elseif external_param(md, name).is_shared + elseif model_param(md, name).is_shared error("Parameter $param_name is a shared model parameter, to safely update", "please call `update_param!(m, param_name, value)` to explicitly update", "a shared parameter that may be connected to several components") end # update the parameter - _update_param!(md, model_param_name, value) + update_param!(md, model_param_name, value) end function _update_param!(obj::AbstractCompositeComponentDef, name::Symbol, value) - param = external_param(obj, name, missing_ok=true) + param = model_param(obj, name, missing_ok=true) if param === nothing - error("Cannot update parameter; $name not found in composite's external parameters.") + error("Cannot update parameter; $name not found in composite's model parameters.") end if param isa ScalarModelParameter @@ -720,7 +720,7 @@ end 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) @@ -767,8 +767,8 @@ end update_params!(obj::AbstractCompositeComponentDef, parameters::Dict{T, Any}; update_timesteps = nothing) where T 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 +is called to update the model parameter by name k to value v. Each key k must be a symbol or convert to a +symbol matching the name of an model parameter that already exists in the component definition. """ function update_params!(obj::AbstractCompositeComponentDef, parameters::Dict; update_timesteps = nothing) @@ -797,7 +797,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.") @@ -829,7 +829,7 @@ function add_connector_comps!(obj::AbstractCompositeComponentDef) conn.ignoreunits)) # add a connection between ConnectorComp and the external backup data - add_external_param_conn!(obj, ExternalParameterConnection(conn_path, :input2, conn.backup)) + add_model_param_conn!(obj, ExternalParameterConnection(conn_path, :input2, conn.backup)) # set the first and last parameters for WITHIN the component which # decide when backup is used and when connection is used @@ -851,7 +851,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. @@ -860,7 +860,7 @@ function _pad_parameters!(obj::ModelDef) model_times = time_labels(obj) - for (name, param) in obj.external_params + 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 @@ -965,14 +965,14 @@ function add_shared_parameter(md::ModelDef, name::Symbol, value::Any; end """ - create_external_param(md::ModelDef, param_def::AbstractParameterDef, value::Any; is_shared::Bool = false) + create_model_param(md::ModelDef, param_def::AbstractParameterDef, value::Any; is_shared::Bool = false) -Create a new external parameter to be added to Model Def `md` with specifications +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_external_param(md::ModelDef, param_def::AbstractParameterDef, value::Any; is_shared::Bool = false) +function create_model_param(md::ModelDef, param_def::AbstractParameterDef, value::Any; is_shared::Bool = false) # gather info param_name = nameof(param_def) diff --git a/src/core/defcomposite.jl b/src/core/defcomposite.jl index dd328302c..8ae3672ee 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] diff --git a/src/core/defs.jl b/src/core/defs.jl index 6ece10cbc..7694a83c7 100644 --- a/src/core/defs.jl +++ b/src/core/defs.jl @@ -61,7 +61,7 @@ find_last_period(comp_def::AbstractComponentDef) = @or(last_period(comp_def), la 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) @@ -81,13 +81,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,20 +101,20 @@ 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 model's 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) @@ -291,7 +291,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) @@ -427,28 +427,28 @@ function set_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, value; 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) +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) +function set_param!(md::ModelDef, comp_def::AbstractComponentDef, param_name::Symbol, model_param_name::Symbol, value; dims=nothing) has_parameter(comp_def, param_name) || error("Cannot find parameter :$param_name in component $(pathof(comp_def))") - if has_parameter(md, ext_param_name) + if has_parameter(md, model_param_name) - error("Cannot set parameter :$ext_param_name, the model already has a parameter with this name.", + error("Cannot set parameter :$model_param_name, the model already has a parameter with this name.", " IF you wish to change the name 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 :$ext_param_name, ", + " 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 ", "`set_param!(m, comp_name, param_name, unique_param_name, value)`.") 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 """ @@ -461,7 +461,7 @@ The `value` can by a scalar, an array, or a NamedAray. Optional keyword argument 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 @@ -496,31 +496,31 @@ function set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing, ignor check_parameter_dimensions(md, value, dims, param_name) end - # create shared external parameter - since we alread checked that the found + # 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_external_param(md, param_def, value; is_shared = true) + 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 external parameters + # before adding it to the model's model parameters if param isa ArrayModelParameter for comp in comps _check_labels(md, comp, param_name, param) end end - # add the shared external parameter to the model def - if ext_param_name === nothing - ext_param_name = param_name + # add the shared model parameter to the model def + if model_param_name === nothing + model_param_name = param_name end - add_model_param!(md, ext_param_name, param) + add_model_param!(md, model_param_name, param) # connect for comp in comps # Set check_labels = false because we already checked above # connect_param! calls dirty! so we don't have to - connect_param!(md, comp, param_name, ext_param_name, check_labels = false) + connect_param!(md, comp, param_name, model_param_name, check_labels = false) end nothing end @@ -724,7 +724,7 @@ end """ _initialize_parameters!(md::ModelDef, comp_def::AbstractComponentDef) -Add and connect an unshared external parameter to `md` for each parameter in +Add and connect an unshared model parameter to `md` for each parameter in `comp_def`. """ function _initialize_parameters!(md::ModelDef, comp_def::AbstractComponentDef) @@ -746,24 +746,24 @@ function _initialize_parameters!(md::ModelDef, comp_def::AbstractComponentDef) connected = UnnamedReference(comp_name, param_name) in connection_refs(md) if !connected - ext_param_name = gensym() + model_param_name = gensym() value = param_def.default - # create the unshared external parameter with a value of 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_external_param(md, param_def, value) + 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 external parameters + # before adding it to the model's parameter list if param isa ArrayModelParameter && !isnothing(value) _check_labels(md, comp_def, param_name, param) end - # add the unshared external parameter to the model def - add_model_param!(md, ext_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 labels since did it above - connect_param!(md, comp_def, param_name, ext_param_name; check_labels = false) + connect_param!(md, comp_def, param_name, model_param_name; check_labels = false) end end nothing @@ -938,7 +938,7 @@ function add_comp!(obj::AbstractCompositeComponentDef, _add_anonymous_dims!(obj, comp_def) _insert_comp!(obj, comp_def, before=before, after=after) - # Create an unshared external parameter for each of the new component's parameters + # 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 @@ -1046,7 +1046,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 diff --git a/src/core/model.jl b/src/core/model.jl index cea2ffe4a..d4ecedd81 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -26,8 +26,8 @@ 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, name::Symbol; missing_ok=false) => md @delegate connected_params(m::Model) => md @delegate unconnected_params(m::Model) => md @@ -55,12 +55,12 @@ data for the second timestep and beyond. ignoreunits::Bool=false, backup_offset::Union{Int, Nothing} = 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) -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. +Bind the parameter `param_name` in the component `comp_name` of model `m` to the model parameter +`model_param_name` already present in the model's list of model parameters. """ -@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) => md """ connect_param!(m::Model, dst::Pair{Symbol, Symbol}, src::Pair{Symbol, Symbol}, backup::Array; ignoreunits::Bool=false) @@ -112,7 +112,7 @@ end """ update_param!(m::Model, name::Symbol, value; update_timesteps = nothing) -Update the `value` of an external 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. """ @@ -130,8 +130,8 @@ to component `comp_name`'s parameter `param_name`. update_params!(m::Model, parameters::Dict{T, Any}; update_timesteps = nothing) where T 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 +is called to update the model parameter by name k to value v. Each key k +must be a symbol or convert to a symbol matching the name of an model 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. """ @@ -422,18 +422,18 @@ Add a scalar type parameter `name` with value `value` to the model `m`. 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) @@ -446,15 +446,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 """ 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/types/defs.jl b/src/core/types/defs.jl index efb029e2b..7cb7ce1a5 100644 --- a/src/core/types/defs.jl +++ b/src/core/types/defs.jl @@ -156,7 +156,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 +206,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 +216,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. diff --git a/src/core/types/params.jl b/src/core/types/params.jl index 5127ef734..05f3ed5ee 100644 --- a/src/core/types/params.jl +++ b/src/core/types/params.jl @@ -69,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, @@ -84,12 +84,12 @@ 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 diff --git a/src/mcs/defmcs.jl b/src/mcs/defmcs.jl index 63ef4305b..c7677070b 100644 --- a/src/mcs/defmcs.jl +++ b/src/mcs/defmcs.jl @@ -117,7 +117,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) @@ -286,9 +286,9 @@ end 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 random variable, and `paramname` -must refer to an existing shared external parameter that can be accessed by that +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 external parameter specific to a component. If `dims` are +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 :(=). """ diff --git a/src/mcs/mcs_types.jl b/src/mcs/mcs_types.jl index b3382863e..926daaeb2 100644 --- a/src/mcs/mcs_types.jl +++ b/src/mcs/mcs_types.jl @@ -103,13 +103,13 @@ struct TransformSpec end end -struct TransformSpec_ExternalParams +struct TransformSpec_ModelParams paramnames::Vector{Symbol} op::Symbol rvname::Symbol dims::Vector{Any} - function TransformSpec_ExternalParams(paramnames::Vector{Symbol}, op::Symbol, rvname::Symbol, dims::Vector{T}=[]) where T + 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 @@ -183,7 +183,7 @@ mutable struct SimulationInstance{T} models::Vector{M} where M <: AbstractModel results::Vector{Dict{Tuple, DataFrame}} payload::Any - translist_externalparams::Vector{TransformSpec_ExternalParams} + translist_modelparams::Vector{TransformSpec_ModelParams} function SimulationInstance{T}(sim_def::SimulationDef{T}) where T <: AbstractSimulationData self = new() @@ -194,11 +194,11 @@ mutable struct SimulationInstance{T} 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 external parameter + # 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_externalparams = Vector{TransformSpec_ExternalParams}(undef, 0) + 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) diff --git a/src/mcs/montecarlo.jl b/src/mcs/montecarlo.jl index beb9d9c9c..fb237586c 100644 --- a/src/mcs/montecarlo.jl +++ b/src/mcs/montecarlo.jl @@ -33,7 +33,7 @@ end function Base.show(io::IO, sim_inst::SimulationInstance{T}) where T <: AbstractSimulationData println("SimulationInstance{$T}") - print_nonempty("translist for external_params", sim_inst.translist_externalparams) + print_nonempty("translist for model params", sim_inst.translist_modelparams) Base.show(io, sim_inst.sim_def) @@ -233,7 +233,7 @@ function _copy_sim_params(sim_inst::SimulationInstance{T}) where T <: AbstractSi for (i, m) in enumerate(flat_model_list) md = modelinstance_def(m) - param_vec[i] = Dict{Symbol, ModelParameter}(trans.paramnames[i] => copy(external_param(md, trans.paramnames[i])) for trans in sim_inst.translist_externalparams) + 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,7 +249,7 @@ function _restore_sim_params!(sim_inst::SimulationInstance{T}, for (i, m) in enumerate(flat_model_list) params = param_vec[i] md = m.mi.md - for trans in sim_inst.translist_externalparams + for trans in sim_inst.translist_modelparams name = trans.paramnames[i] param = params[name] _restore_param!(param, name, md, i, trans) @@ -259,18 +259,18 @@ function _restore_sim_params!(sim_inst::SimulationInstance{T}, return nothing end -function _restore_param!(param::ScalarModelParameter{T}, name::Symbol, md::ModelDef, i::Int, trans::TransformSpec_ExternalParams) 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, i::Int, trans::TransformSpec_ExternalParams) where T - md_param = external_param(md, name) +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, i::Int, trans::TransformSpec_ExternalParams) 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) @@ -285,7 +285,7 @@ function _param_indices(param::ArrayModelParameter{T}, md::ModelDef, i::Int, tra if num_pdims != num_dims pname = trans.paramnames[i] - error("Dimension mismatch: external parameter :$pname has $num_pdims dimensions ($pdims); Sim has $num_dims") + error("Dimension mismatch: model parameter :$pname has $num_pdims dimensions ($pdims); Sim has $num_dims") end indices = Vector() @@ -299,7 +299,7 @@ function _param_indices(param::ArrayModelParameter{T}, md::ModelDef, i::Int, tra return indices end -function _perturb_param!(param::ScalarModelParameter{T}, md::ModelDef, i::Int, trans::TransformSpec_ExternalParams, 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 == :(=) @@ -316,7 +316,7 @@ 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, i::Int, - trans::TransformSpec_ExternalParams, rvalue::Array{<: Number, N}) where {T, N} + trans::TransformSpec_ModelParams, rvalue::Array{<: Number, N}) where {T, N} op = trans.op pvalue = value(param) @@ -336,7 +336,7 @@ end # rvalue is a Number so we might need to deal with broadcasting function _perturb_param!(param::ArrayModelParameter{T}, md::ModelDef, i::Int, - trans::TransformSpec_ExternalParams, rvalue::Number) where {T, N} + trans::TransformSpec_ModelParams, rvalue::Number) where {T, N} op = trans.op pvalue = value(param) indices = _param_indices(param, md, i, trans) @@ -392,8 +392,8 @@ function _perturb_params!(sim_inst::SimulationInstance{T}, trialnum::Int) where # 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_externalparams - param = external_param(m.mi.md, trans.paramnames[i]) + 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 @@ -509,7 +509,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_externalparams!(sim_inst) # should this use m.md or m.mi.md (after building below)? + 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") @@ -691,24 +691,24 @@ 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_externalparams!(sim_inst::SimulationInstance{T}) + set_translist_modelparams!(sim_inst::SimulationInstance{T}) Create the transform spec list for the simulation instance, finding the matching -external parameter names for each transform spec parameter for each model. +model parameter names for each transform spec parameter for each model. """ -function set_translist_externalparams!(sim_inst::SimulationInstance{T}) where T <: AbstractSimulationData +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_externalparams = Vector{TransformSpec_ExternalParams}(undef, length(sim_inst.sim_def.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 external parameters - external_parameters_vec = Vector{Symbol}(undef, length(flat_model_list)) + # 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 @@ -718,7 +718,7 @@ function set_translist_externalparams!(sim_inst::SimulationInstance{T}) where T # 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]).") - external_parameters_vec[model_idx] = get_model_param_name(m.md, compname, trans.paramname) + 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 @@ -732,7 +732,7 @@ function set_translist_externalparams!(sim_inst::SimulationInstance{T}) where T # found the shared parameter if has_parameter(m.md, paramname) - external_parameters_vec[model_idx] = paramname + model_parameters_vec[model_idx] = paramname # didn't find the shared parameter, will try to resolve else @@ -753,14 +753,14 @@ function set_translist_externalparams!(sim_inst::SimulationInstance{T}) where T 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 external parameter name $unshared_paramname. Will use this external parameter, but in the future we suggest you $suggestion_string") - external_parameters_vec[model_idx] = unshared_paramname + @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_ExternalParams(external_parameters_vec, trans.op, trans.rvname, trans.dims) - sim_inst.translist_externalparams[trans_idx] = new_trans + new_trans = TransformSpec_ModelParams(model_parameters_vec, trans.op, trans.rvname, trans.dims) + sim_inst.translist_modelparams[trans_idx] = new_trans end end diff --git a/test/mcs/test_defmcs.jl b/test/mcs/test_defmcs.jl index 0796546f1..a33ca40ca 100644 --- a/test/mcs/test_defmcs.jl +++ b/test/mcs/test_defmcs.jl @@ -75,7 +75,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) @@ -292,7 +292,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) diff --git a/test/mcs/test_defmcs_delta.jl b/test/mcs/test_defmcs_delta.jl index 3872a93d3..44d047192 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) diff --git a/test/mcs/test_defmcs_sobol.jl b/test/mcs/test_defmcs_sobol.jl index 4cb4a039b..bde75cf78 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) diff --git a/test/mcs/test_reshaping.jl b/test/mcs/test_reshaping.jl index 5b1371fd4..28e8f5976 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) diff --git a/test/mcs/test_translist.jl b/test/mcs/test_translist.jl index d02bc2ebc..56b5977d2 100644 --- a/test/mcs/test_translist.jl +++ b/test/mcs/test_translist.jl @@ -18,7 +18,7 @@ end end ## -## Tests for set_translist_externalparams +## Tests for set_translist_modelparams ## sd = @defsim begin @@ -101,7 +101,7 @@ add_comp!(m2, test2) run(sd, [m1, m2], 100) ## -## Tests for set_translist_externalparams with a default (not shared) +## Tests for set_translist_modelparams with a default (not shared) ## sd = @defsim begin diff --git a/test/runtests.jl b/test/runtests.jl index 411ef3071..7a0b09dfe 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -53,8 +53,8 @@ Electron.prep_test_env() @info("test_replace_comp.jl") @time include("test_replace_comp.jl") - # @info("test_tools.jl") - # @time include("test_tools.jl") + @info("test_tools.jl") + @time include("test_tools.jl") @info("test_parameter_labels.jl") @time include("test_parameter_labels.jl") diff --git a/test/test_composite_parameters.jl b/test/test_composite_parameters.jl index 48d0c2d05..73d038c90 100644 --- a/test/test_composite_parameters.jl +++ b/test/test_composite_parameters.jl @@ -3,7 +3,7 @@ module TestCompositeParameters using Mimi using Test -import Mimi: external_params +import Mimi: model_params @defcomp A begin p1 = Parameter(unit = "\$", default=3) @@ -128,12 +128,12 @@ m2 = get_model() 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.external_param(m2.md, :p1).value == 2 -@test Mimi.external_param(m2.md, :p1).is_shared +@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.md, :B, :p1) -@test Mimi.external_param(m2.md, sym).value == 3 -@test !(Mimi.external_param(m2.md, sym).is_shared) +@test Mimi.model_param(m2.md, sym).value == 3 +@test !(Mimi.model_param(m2.md, sym).is_shared) # test defaults m3 = get_model() @@ -147,9 +147,9 @@ 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.external_param(m3.md, :B_p1).value == 2 -@test Mimi.external_param(m3.md, :B_p1).is_shared -@test issubset(Set([:p1, :B_p1]), Set(keys(m3.md.external_params))) +@test Mimi.model_param(m3.md, :B_p1).value == 2 +@test Mimi.model_param(m3.md, :B_p1).is_shared +@test issubset(Set([:p1, :B_p1]), Set(keys(m3.md.model_params))) #------------------------------------------------------------------------------ # Unit tests on default behavior @@ -171,10 +171,10 @@ end m = Model() set_dimension!(m, :time, 2000:2005) add_comp!(m, top); -@test length(external_params(m)) == 1 -ext_param_name = Mimi.get_model_param_name(m.md, :top, :superp1) -@test Mimi.is_nothing_param(external_params(m)[ext_param_name]) -@test !external_params(m)[ext_param_name].is_shared +@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) @@ -185,10 +185,10 @@ end m = Model() set_dimension!(m, :time, 2000:2005) add_comp!(m, top); -@test length(external_params(m)) == 1 -ext_param_name = Mimi.get_model_param_name(m.md, :top, :superp1) -@test external_params(m)[ext_param_name].value == 8.0 -@test !external_params(m)[ext_param_name].is_shared +@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 @@ -207,10 +207,10 @@ end m = Model() set_dimension!(m, :time, 2000:2005) add_comp!(m, top); -@test length(external_params(m)) == 1 -ext_param_name = Mimi.get_model_param_name(m.md, :top, :superp1) -@test external_params(m)[ext_param_name].value == 2 -@test !external_params(m)[ext_param_name].is_shared +@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 @@ -228,13 +228,13 @@ end m = Model() set_dimension!(m, :time, 2000:2005) add_comp!(m, top); -@test length(external_params(m)) == 2 -ext_param_name = Mimi.get_model_param_name(m.md, :top, :p1) -@test external_params(m)[ext_param_name].value == 2 -@test !external_params(m)[ext_param_name].is_shared -ext_param_name = Mimi.get_model_param_name(m.md, :top, :p2) -@test external_params(m)[ext_param_name].value == 3 -@test !external_params(m)[ext_param_name].is_shared +@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 diff --git a/test/test_defaults.jl b/test/test_defaults.jl index 3e910d698..b2d024896 100644 --- a/test/test_defaults.jl +++ b/test/test_defaults.jl @@ -3,7 +3,7 @@ module TestDefaults using Mimi using Test -import Mimi: external_params +import Mimi: model_params @defcomp A begin p1 = Parameter(default = 1) @@ -16,22 +16,22 @@ add_comp!(m, A) set_param!(m, :p2, 2) # So far only :p2 is in the model definition's dictionary -@test :p2 in keys(external_params(m)) -@test length(external_params(m)) == 2 +@test :p2 in keys(model_params(m)) +@test length(model_params(m)) == 2 run(m) # :p1's value is it's default @test m[:A, :p1] == 1 -# This errors because p1 isn't in the model definition's external params +# This errors because p1 isn't in the model definition's model params @test_throws ErrorException update_param!(m, :p1, 10) # Need to use set_param! instead set_param!(m, :p1, 10) # Now there is a :p1 in the model definition's dictionary -@test :p1 in keys(external_params(m)) +@test :p1 in keys(model_params(m)) run(m) @test m[:A, :p1] == 10 diff --git a/test/test_delete.jl b/test/test_delete.jl index 3428c5ecd..00bc311b0 100644 --- a/test/test_delete.jl +++ b/test/test_delete.jl @@ -26,21 +26,21 @@ 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 +@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 -@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 diff --git a/test/test_dimensions.jl b/test/test_dimensions.jl index c50b908fe..1fbc00f50 100644 --- a/test/test_dimensions.jl +++ b/test/test_dimensions.jl @@ -5,7 +5,7 @@ using Test import Mimi: compdef, AbstractDimension, RangeDimension, Dimension, key_type, first_period, last_period, - ComponentReference, ComponentPath, ComponentDef, time_labels, external_params + ComponentReference, ComponentPath, ComponentDef, time_labels, model_params ## ## Constants @@ -161,7 +161,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 = external_params(m)[: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 +170,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 = external_params(m)[: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_sim.jl b/test/test_explorer_sim.jl index b4400b8d2..10673d956 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) diff --git a/test/test_getdataframe.jl b/test/test_getdataframe.jl index 812b42a4c..5a71e467f 100644 --- a/test/test_getdataframe.jl +++ b/test/test_getdataframe.jl @@ -43,7 +43,7 @@ set_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 +@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 set_param!(model1, :testcomp2, :par2, years) # Test running before model built diff --git a/test/test_main.jl b/test/test_main.jl index ef79ef62d..222a57fc5 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 @@ -35,11 +35,11 @@ set_param!(x1, :foo1, :par1, 5.0) @test length(dimension(x1.md, :index1)) == 3 -par1 = external_param(x1, :par1) +par1 = model_param(x1, :par1) @test par1.value == 5.0 update_param!(x1, :par1, 6.0) -par1 = external_param(x1, :par1) +par1 = model_param(x1, :par1) @test par1.value == 6.0 set_param!(x1, :foo1, :par2, [true true false; true false false; true true true]) diff --git a/test/test_main_variabletimestep.jl b/test/test_main_variabletimestep.jl index a389611f5..1020d9169 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 @@ -35,11 +35,11 @@ set_param!(x1, :foo1, :par1, 5.0) @test length(dimension(x1.md, :index1)) == 3 -par1 = external_param(x1, :par1) +par1 = model_param(x1, :par1) @test par1.value == 5.0 update_param!(x1, :par1, 6.0) -par1 = external_param(x1, :par1) +par1 = model_param(x1, :par1) @test par1.value == 6.0 set_param!(x1, :foo1, :par2, [true true false; true false false; true true true]) diff --git a/test/test_parametertypes.jl b/test/test_parametertypes.jl index f1ab320da..a6d599dd0 100644 --- a/test/test_parametertypes.jl +++ b/test/test_parametertypes.jl @@ -4,7 +4,7 @@ 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 @@ -93,7 +93,7 @@ set_param!(m, :MyComp, :f, reshape(1:16, 4, 4)) set_param!(m, :MyComp, :j, [1,2,3]) Mimi.build!(m) -extpars = external_params(m.mi.md) +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) @@ -127,7 +127,7 @@ h_sym = Mimi.get_model_param_name(m.mi.md, :MyComp, :h) 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 +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].value == 0.5 # The original dictionary still has the old value @test new_extpars[:d].value == 5. # The new dictionary has the updated value @test_throws ErrorException update_param!(m, :e, 5) # wrong type; should be array @@ -176,7 +176,7 @@ set_dimension!(m, :time, 1999:2001) # 2000 1 # 2001 2 last first, last -x = external_param(m.md, :x) +x = model_param(m.md, :x) @test ismissing(x.values.data[1]) @test x.values.data[2:3] == [1.0, 2.0] @test _get_param_times(x) == 1999:2001 @@ -188,7 +188,7 @@ update_param!(m, :x, [2, 3, 4]) # change x to match # 2000 3 # 2001 4 last first, last -x = external_param(m.md, :x) +x = model_param(m.md, :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) @@ -223,7 +223,7 @@ set_dimension!(m, :time, [2000, 2005, 2020, 2100]) # 2020 3 last # 2100 missing last -x = external_param(m.md, :x) +x = model_param(m.md, :x) @test ismissing(x.values.data[4]) @test x.values.data[1:3] == [1.0, 2.0, 3.0] @@ -234,7 +234,7 @@ update_param!(m, :x, [2, 3, 4, 5]) # change x to match # 2020 4 last # 2100 5 last -x = external_param(m.md, :x) +x = model_param(m.md, :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) @@ -263,7 +263,7 @@ set_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) +x = model_param(m.md, :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) @@ -293,7 +293,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.md, :x) @test x.values isa Mimi.TimestepArray{Mimi.FixedTimestep{1999, 1, 2003}, Union{Missing, Float64}, 1, 1} @test x.values.data == [2., 3., 4., 5., 6.] @@ -334,19 +334,19 @@ set_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.] +@test model_param(m.md, :y).values == [10., 15.] update_param!(m, :z, 1) -@test external_param(m.md, :z).value == 1 +@test model_param(m.md, :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 -@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.md, :x).values isa Mimi.TimestepArray{Mimi.FixedTimestep{1999,1, 2001},Union{Missing,Float64},1} +@test model_param(m.md, :x).values.data == [3.,4.,5.] +@test model_param(m.md, :y).values == [10.,20.] +@test model_param(m.md, :z).value == 0 #------------------------------------------------------------------------------ # Test the three different set_param! methods for a Symbol type parameter @@ -406,6 +406,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 !(:p1 in keys(external_params(m))) # 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 41a9a3d64..1d0dc7035 100644 --- a/test/test_references.jl +++ b/test/test_references.jl @@ -3,7 +3,7 @@ module TestReferences using Test using Mimi -import Mimi: external_params +import Mimi: model_params @defcomp A begin p1 = Parameter() @@ -25,12 +25,12 @@ refB = add_comp!(m, B) refA[:p1] = 3 # creates a parameter specific to this component, with name "foo_p1" @test Mimi.get_model_param_name(m.md, :foo, :p1) == :foo_p1 -@test :foo_p1 in keys(external_params(m)) +@test :foo_p1 in keys(model_params(m)) @test Mimi.UnnamedReference(:B, :p1) in Mimi.nothing_params(m.md) refB[:p1] = 5 @test Mimi.get_model_param_name(m.md, :B, :p1) == :B_p1 -@test :B_p1 in keys(external_params(m)) +@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 709a177c7..21dfa668d 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]) @@ -93,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 +set_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( @@ -108,25 +108,25 @@ 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)) == 1 # The external parameter connection was removed, so just :z is there -@test length(external_params(m)) == 2 # The external parameter still exists for both :x and :z +@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 +set_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 +set_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 @@ -177,7 +177,7 @@ set_dimension!(m, :time, 2000:2005) add_comp!(m, X) # Original component X set_param!(m, :X, :x, zeros(6)) replace!(m, :X => X_repl_extraparams) # Replace X with X_repl_extraparams -@test length(external_params(m)) == 3 # should have two new parameters in the external parameters list +@test length(model_params(m)) == 3 # should have two new parameters in the model parameters list set_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 diff --git a/test/test_show.jl b/test/test_show.jl index 30d4fb075..d68df1ca4 100644 --- a/test/test_show.jl +++ b/test/test_show.jl @@ -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 From 788dd9ae584dd75fafc793651e1afd800ad1e5d1 Mon Sep 17 00:00:00 2001 From: lrennels Date: Fri, 28 May 2021 01:06:28 -0700 Subject: [PATCH 25/47] Work on deprecations --- contrib/test_all_models.jl | 10 ++- src/core/connections.jl | 73 ++++++++-------------- src/core/model.jl | 105 ++++++++++++++++--------------- src/core/time.jl | 48 ++++++++------- src/core/time_arrays.jl | 123 +++++++++++++++++++------------------ src/core/types/defs.jl | 24 +++++++- src/core/types/model.jl | 9 ++- src/core/types/params.jl | 12 ++++ 8 files changed, 217 insertions(+), 187 deletions(-) diff --git a/contrib/test_all_models.jl b/contrib/test_all_models.jl index e4116e8f4..752143293 100644 --- a/contrib/test_all_models.jl +++ b/contrib/test_all_models.jl @@ -11,8 +11,8 @@ # packages_to_test = [ - "MimiDICE2010" => ("https://github.com/anthofflab/MimiDICE2010.jl", "master"), # fails because need to use new DataFrames syngax - "MimiDICE2013" => ("https://github.com/anthofflab/MimiDICE2013.jl", "master"), # fails because need to use new DataFrames syngax + "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/AlexandrePavlov/MimiDICE2016R2.jl", "master"), # doesn't pass in repo, just look for new failures "MimiRICE2010" => ("https://github.com/anthofflab/MimiRICE2010.jl", "master"), @@ -23,9 +23,13 @@ packages_to_test = [ "MimiFAIR" => ("https://github.com/anthofflab/MimiFAIR.jl", "master"), "MimiMAGICC" => ("https://github.com/anthofflab/MimiMAGICC.jl", "master"), "MimiHector" => ("https://github.com/anthofflab/MimiHector.jl", "master"), - "MimiIWG" => ("https://github.com/rffscghg/MimiIWG.jl", "mcs") # note here we use the mcs branch, we can only testing this one if we have the Mimi registry in our current environment ] +# 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 +# ] + using Pkg mktempdir() do folder_name diff --git a/src/core/connections.jl b/src/core/connections.jl index 9c476b600..d96c11623 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -112,7 +112,7 @@ function connect_param!(obj::AbstractCompositeComponentDef, comp_def::AbstractCo comp_path = @or(comp_def.comp_path, ComponentPath(obj.comp_path, comp_def.name)) conn = ExternalParameterConnection(comp_path, param_name, model_param_name) - add_model_param_conn!(obj, conn) + add_external_param_conn!(obj, conn) return nothing end @@ -465,7 +465,7 @@ function get_model_param_name(obj::Model, comp_name::Symbol, param_name::Symbol; get_model_param_name(obj.md, comp_name, param_name; missing_ok = missing_ok) end -function add_model_param_conn!(obj::ModelDef, conn::ExternalParameterConnection) +function add_external_param_conn!(obj::ModelDef, conn::ExternalParameterConnection) push!(obj.external_param_conns, conn) dirty!(obj) end @@ -483,11 +483,7 @@ function add_model_param!(md::ModelDef, name::Symbol, value::ModelParameter) dirty!(md) return value end -# deprecated version of above -function set_external_param!(obj::ModelDef, name::Symbol, value::ModelParameter) - @warn "`set_external_param! is deprecated and will be removed in the future, please use `add_model_param` with the same arguments." - add_model_param!(obj, name, value) -end + """ add_model_param!(md::ModelDef, name::Symbol, value::Number; @@ -504,13 +500,6 @@ function add_model_param!(md::ModelDef, name::Symbol, value::Number; is_shared::Bool = false) add_model_scalar_param!(md, name, value, is_shared = is_shared) end -# deprecated version of above -function set_external_param!(obj::ModelDef, name::Symbol, value::Number; - param_dims::Union{Nothing,Array{Symbol}} = nothing, - is_shared::Bool = false) - @warn "`set_external_param! is deprecated and will be removed in the future, please use `add_model_param` with the same arguments." - add_model_param!(obj, name, value; param_dims = param_dims, is_shared = is_shared) -end """ add_model_param!(md::ModelDef, name::Symbol, value::Number; @@ -538,14 +527,6 @@ function add_model_param!(md::ModelDef, name::Symbol, add_model_array_param!(md, name, values, param_dims, is_shared = is_shared) end -# deprecated version of above -function set_external_param!(obj::ModelDef, name::Symbol, - value::Union{AbstractArray, AbstractRange, Tuple}; - param_dims::Union{Nothing,Array{Symbol}} = nothing, - is_shared::Bool = false) - @warn "`set_external_param! is deprecated and will be removed in the future, please use `add_model_param` with the same arguments." - add_model_param!(obj, name, value; param_dims = param_dims, is_shared = is_shared) -end """ add_model_array_param!(md::ModelDef, @@ -562,13 +543,6 @@ function add_model_array_param!(md::ModelDef, param = ArrayModelParameter(value, [:time], is_shared) # must be :time add_model_param!(md, name, param) end -# deprecated version of above -function set_external_array_param!(obj::ModelDef, - name::Symbol, value::TimestepVector, - dims; is_shared::Bool = false) - @warn "`set_external_array_param! is deprecated and will be removed in the future, please use `add_model_array_param` with the same arguments." - add_model_array_param!(obj, name, value, dims; is_shared = is_shared) -end """ add_model_array_param!(md::ModelDef, @@ -585,13 +559,6 @@ function add_model_array_param!(md::ModelDef, param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims, is_shared) add_model_param!(md, name, param) end -# deprecated version of above -function set_external_array_param!(obj::ModelDef, - name::Symbol, value::TimestepArray, dims; - is_shared::Bool = false) - @warn "`set_external_array_param! is deprecated and will be removed in the future, please use `add_model_array_param` with the same arguments." - add_model_array_param(obj, name, value, dims; is_shared = is_shared) -end """ add_model_array_param!(md::ModelDef, @@ -608,13 +575,6 @@ function add_model_array_param!(md::ModelDef, param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims, is_shared) add_model_param!(md, name, param) end -# deprecated version of above -function set_external_array_param!(obj::ModelDef, - name::Symbol, value::AbstractArray, dims; - is_shared::Bool = false) - @warn "`set_external_array_param! is deprecated and will be removed in the future, please use `add_model_array_param` with the same arguments." - add_model_array_param(obj, name, value, dims; is_shared = is_shared) -end """ add_model_scalar_param!(md::ModelDef, name::Symbol, value::Any; is_shared::Bool = false) @@ -625,11 +585,6 @@ function add_model_scalar_param!(md::ModelDef, name::Symbol, value::Any; is_shar param = ScalarModelParameter(value, is_shared) add_model_param!(md, name, param) end -# deprecated version of above -function set_external_scalar_param!(obj::ModelDef, name::Symbol, value::Any; is_shared::Bool = false) - @warn "`set_external_scalar_param! is deprecated and will be removed in the future, please use `add_model_scalar_param` with the same arguments." - add_model_scalar_param(obj, name, value; is_shared = is_shared) -end """ update_param!(obj::AbstractCompositeComponentDef, name::Symbol, value; update_timesteps = nothing) @@ -829,7 +784,7 @@ function add_connector_comps!(obj::AbstractCompositeComponentDef) conn.ignoreunits)) # add a connection between ConnectorComp and the external backup data - add_model_param_conn!(obj, ExternalParameterConnection(conn_path, :input2, conn.backup)) + add_external_param_conn!(obj, ExternalParameterConnection(conn_path, :input2, conn.backup)) # set the first and last parameters for WITHIN the component which # decide when backup is used and when connection is used @@ -1046,3 +1001,23 @@ function create_model_param(md::ModelDef, param_def::AbstractParameterDef, value 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/model.jl b/src/core/model.jl index d4ecedd81..9bada72cb 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -89,13 +89,6 @@ 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 - -@delegate set_external_param!(m::Model, name::Symbol, - value::Union{Number, AbstractArray, AbstractRange, Tuple}; - param_dims::Union{Nothing,Array{Symbol}} = nothing) => md - @delegate add_model_param!(m::Model, name::Symbol, value::ModelParameter) => md @delegate add_model_param!(m::Model, name::Symbol, @@ -182,46 +175,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, @@ -408,7 +361,6 @@ Add a one or two dimensional (optionally, time-indexed) array parameter `name` with value `value` to the model `m`. """ @delegate add_model_array_param!(m::Model, name::Symbol, value::Union{AbstractArray, TimestepArray}, dims) => md -@delegate set_external_array_param!(m::Model, name::Symbol, value::Union{AbstractArray, TimestepArray}, dims) => md """ add_model_scalar_param!(m::Model, name::Symbol, value::Any) @@ -416,7 +368,6 @@ with value `value` to the model `m`. Add a scalar type parameter `name` with value `value` to the model `m`. """ @delegate add_model_scalar_param!(m::Model, name::Symbol, value::Any) => md -@delegate set_external_scalar_param!(m::Model, name::Symbol, value::Any) => md """ delete!(m::Model, component::Symbol; deep::Bool=false) @@ -487,3 +438,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/time.jl b/src/core/time.jl index 2704d13d1..bcbf2fb6f 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) @@ -240,4 +220,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/defs.jl b/src/core/types/defs.jl index 7cb7ce1a5..1c3e08bba 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} @@ -252,3 +249,24 @@ end end var_name(comp_ref::VariableReference) = getfield(comp_ref, :var_name) + +## +## DEPRECATIONS - Should move from warning --> error --> removal +## + +# -- throw errors -- + +# -- throw warnings -- + +function Base.getproperty(md::ModelDef, field::Symbol) + if field == :external_params + @warn "ModelDef's `external_params` field is renamed to `model_params`, please change code accordingly." + field = :model_params + end + return getfield(etc, field) +end + +@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 05f3ed5ee..1a9018678 100644 --- a/src/core/types/params.jl +++ b/src/core/types/params.jl @@ -94,3 +94,15 @@ 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(etc::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(etc, field) +end From 4bfd2479427933a7327a71756748838062cbc56a Mon Sep 17 00:00:00 2001 From: lrennels Date: Fri, 28 May 2021 08:55:18 -0700 Subject: [PATCH 26/47] Fix bug --- src/core/defs.jl | 1 - src/core/types/defs.jl | 2 +- src/core/types/params.jl | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/core/defs.jl b/src/core/defs.jl index 7694a83c7..0c193f9c1 100644 --- a/src/core/defs.jl +++ b/src/core/defs.jl @@ -1076,6 +1076,5 @@ function _replace!(obj::AbstractCompositeComponentDef, ref = add_comp!(obj, comp_id, comp_name; before=before, after=after) - # return ref end diff --git a/src/core/types/defs.jl b/src/core/types/defs.jl index 1c3e08bba..ceea822cf 100644 --- a/src/core/types/defs.jl +++ b/src/core/types/defs.jl @@ -263,7 +263,7 @@ function Base.getproperty(md::ModelDef, field::Symbol) @warn "ModelDef's `external_params` field is renamed to `model_params`, please change code accordingly." field = :model_params end - return getfield(etc, field) + return getfield(md, field) end @deprecate external_params(md::ModelDef) model_params(md) diff --git a/src/core/types/params.jl b/src/core/types/params.jl index 1a9018678..95ac13d0e 100644 --- a/src/core/types/params.jl +++ b/src/core/types/params.jl @@ -99,10 +99,10 @@ Base.nameof(obj::ExternalParameterConnection) = obj.param_name ## DEPRECATIONS - Should move from warning --> error --> removal ## -function Base.getproperty(etc::ExternalParameterConnection, field::Symbol) +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(etc, field) + return getfield(epc, field) end From 68dfa189a5b2b8348f4d0cb1668429122caaed67 Mon Sep 17 00:00:00 2001 From: lrennels Date: Fri, 28 May 2021 12:08:33 -0700 Subject: [PATCH 27/47] Add docstrings and rearrange code for clarity --- src/components/adder.jl | 1 - src/core/build.jl | 94 ++++++++++++++++++++-- src/core/connections.jl | 60 +++++++++++++- src/core/defcomp.jl | 2 +- src/core/defs.jl | 168 ++++++++++++++++++++++++++++++++++------ src/core/dimensions.jl | 35 ++++++++- src/core/instances.jl | 10 +++ src/core/model.jl | 91 ++++++++++++++++++++-- src/core/time.jl | 57 +++++++++++++- src/core/types/core.jl | 9 +++ src/explorer/explore.jl | 5 ++ src/explorer/results.jl | 2 - src/mcs/defmcs.jl | 15 ++++ src/mcs/montecarlo.jl | 37 +++++++-- 14 files changed, 532 insertions(+), 54 deletions(-) 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 ebe9bcbfb..6b3246b58 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) @@ -207,7 +237,16 @@ function _get_leaf_level_epcs(md::ModelDef, epc::ExternalParameterConnection) 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)" @@ -251,6 +290,12 @@ function _collect_params(md::ModelDef, var_dict::Dict{ComponentPath, Any}) 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}, @@ -318,6 +382,11 @@ function _get_parameters(comp_def::AbstractCompositeComponentDef) return parameters end +""" + _build(md::ModelDef) + +Build ModelDef `md` (lowest build function called by `build(md::ModelDef)`) and return the ModelInstance.. +""" function _build(md::ModelDef) # @info "_build(md)" @@ -347,6 +416,11 @@ 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) @@ -354,6 +428,11 @@ function build(m::Model) 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 @@ -384,6 +463,11 @@ function Base.run(mm::MarginalModel; ntimesteps::Int=typemax(Int)) 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 d96c11623..1c768e4c7 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -49,6 +49,15 @@ end # Default string, string unit check function verify_units(unit1::AbstractString, unit2::AbstractString) = (unit1 == unit2) +""" + _check_labels(obj::AbstractCompositeComponentDef, + comp_def::AbstractComponentDef, param_name::Symbol, + mod_param::ArrayModelParameter) + +Check that the labels of the ArrayModelParameter `mod_param` match the labels +of the model parameter `param_name` in component `comp_def` of object `obj`, +including datatype and dimensions. +""" function _check_labels(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, param_name::Symbol, mod_param::ArrayModelParameter) param_def = parameter(comp_def, param_name) @@ -100,6 +109,13 @@ function connect_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, connect_param!(obj, comp_def, param_name, model_param_name, check_labels=check_labels) end +""" + connect_param!(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, + param_name::Symbol, model_param_name::Symbol; check_labels::Bool=true) + +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_labels::Bool=true) mod_param = model_param(obj, model_param_name) @@ -232,6 +248,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, @@ -368,9 +401,9 @@ function unconnected_params(obj::AbstractCompositeComponentDef) end """ - set_leftover_params!(m::Model, parameters::Dict) + set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) -Set all of the parameters in model `m` that don't have a value and are not connected +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 strings that match the names of unset parameters in the model, and all resulting new model parameters will be shared parameters. @@ -644,8 +677,12 @@ function update_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, valu end -function _update_param!(obj::AbstractCompositeComponentDef, - name::Symbol, value) +""" + _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 not found in composite's model parameters.") @@ -660,6 +697,11 @@ function _update_param!(obj::AbstractCompositeComponentDef, 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 @@ -672,6 +714,11 @@ 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 @@ -735,6 +782,11 @@ function update_params!(obj::AbstractCompositeComponentDef, parameters::Dict; up 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 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/defs.jl b/src/core/defs.jl index 0c193f9c1..3cef32d5a 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,6 +73,10 @@ 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) @@ -120,13 +140,10 @@ function delete_param!(md::ModelDef, model_param_name::Symbol) 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) @@ -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,16 +485,51 @@ 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 +""" + 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, model_param_name, value, dims=dims) end +""" + 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) has_parameter(comp_def, param_name) || error("Cannot find parameter :$param_name in component $(pathof(comp_def))") @@ -441,11 +538,11 @@ function set_param!(md::ModelDef, comp_def::AbstractComponentDef, param_name::Sy error("Cannot set parameter :$model_param_name, the model already has a parameter with this name.", " IF you wish to change the name of unshared parameter :$param_name connected to component :$(nameof(compdef))", - " use `update_param!(m, comp_name, param_name, value)", + " 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 ", - "`set_param!(m, comp_name, param_name, unique_param_name, value)`.") + "`create_shared_param` paired with `connect_param!`.") end set_param!(md, param_name, value, dims = dims, comps = [comp_def], model_param_name = model_param_name) @@ -454,8 +551,10 @@ 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 @@ -528,6 +627,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) @@ -541,8 +642,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) """ @@ -552,8 +663,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) @@ -619,12 +734,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) 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..c5607d638 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) """ diff --git a/src/core/model.jl b/src/core/model.jl index 9bada72cb..dbe0fb9c7 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 @@ -36,8 +58,10 @@ is_built(mm::MarginalModel) = (is_built(mm.base) && is_built(mm.modified)) @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 @@ -48,11 +72,10 @@ 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, model_param_name::Symbol) @@ -89,15 +112,58 @@ 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 +""" + add_model_param!(m::Model, name::Symbol, value::ModelParameter) + +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... +""" + 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 that match the names of unset parameters in the model, +and all resulting new model parameters will be shared parameters. +""" function set_leftover_params!(m::Model, parameters::Dict{T, Any}) where T set_leftover_params!(m.md, parameters) end @@ -216,7 +282,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 """ @@ -235,6 +305,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) """ diff --git a/src/core/time.jl b/src/core/time.jl index bcbf2fb6f..4493c00a6 100644 --- a/src/core/time.jl +++ b/src/core/time.jl @@ -48,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.") @@ -63,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.") @@ -70,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.") @@ -155,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) @@ -168,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] @@ -181,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 @@ -192,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 @@ -212,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[] 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/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 c7677070b..58b713bf6 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,6 +64,11 @@ function _make_dims(args) return dims end +""" + defsim(expr) + +Define a Mimi `SimulationDef` with the expressions in `expr`. +""" macro defsim(expr) let # to make vars local to each macro invocation local _rvs = [] diff --git a/src/mcs/montecarlo.jl b/src/mcs/montecarlo.jl index fb237586c..ed2c28ecb 100644 --- a/src/mcs/montecarlo.jl +++ b/src/mcs/montecarlo.jl @@ -49,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 @@ -99,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 @@ -132,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") @@ -145,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) From d7718f1f8f4f8214f9d6761a2334cede7a807dad Mon Sep 17 00:00:00 2001 From: lrennels Date: Fri, 28 May 2021 15:38:44 -0700 Subject: [PATCH 28/47] Edit docs; start testing new features --- docs/src/ref/ref_API.md | 3 +- docs/src/tutorials/tutorial_2.md | 2 - docs/src/tutorials/tutorial_3.md | 32 ++++-- docs/src/tutorials/tutorial_4.md | 2 - docs/src/tutorials/tutorial_5.md | 2 - examples/01-onecomponent.jl | 2 +- examples/compositecomp-model.jl | 11 +- .../01-one-region-model/one-region-model.jl | 21 ++-- .../multi-region-model.jl | 19 ++-- src/Mimi.jl | 1 + src/core/connections.jl | 106 +++++++++++++----- src/core/model.jl | 24 ++-- test/mcs/test-model-2/main.jl | 10 -- test/mcs/test-model-2/multi-region-model.jl | 19 ++-- test/mcs/test-model/main.jl | 10 -- test/mcs/test-model/test-model.jl | 20 ++-- test/mcs/test_defmcs.jl | 26 ++--- test/mcs/test_defmcs_delta.jl | 8 +- test/mcs/test_defmcs_modifications.jl | 10 +- test/mcs/test_defmcs_sobol.jl | 8 +- test/mcs/test_reshaping.jl | 10 +- test/runtests.jl | 2 +- test/test_explorer_sim.jl | 8 +- 23 files changed, 203 insertions(+), 153 deletions(-) delete mode 100644 test/mcs/test-model-2/main.jl delete mode 100644 test/mcs/test-model/main.jl diff --git a/docs/src/ref/ref_API.md b/docs/src/ref/ref_API.md index cda519728..980f90245 100644 --- a/docs/src/ref/ref_API.md +++ b/docs/src/ref/ref_API.md @@ -4,7 +4,8 @@ @defcomp MarginalModel Model -add_comp! +add_comp! +add_shared_param! connect_param! create_marginal_model delete_param! diff --git a/docs/src/tutorials/tutorial_2.md b/docs/src/tutorials/tutorial_2.md index 4ec107405..30e12e86e 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, diff --git a/docs/src/tutorials/tutorial_3.md b/docs/src/tutorials/tutorial_3.md index bde32e152..5ae39c79e 100644 --- a/docs/src/tutorials/tutorial_3.md +++ b/docs/src/tutorials/tutorial_3.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. - ## 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. @@ -22,15 +20,21 @@ Possible modifications range in complexity, from simply altering parameter value 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. -When the original model calls [`set_param!`](@ref), Mimi creates an shared model 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 shared model parameter. +The parameters in the original model receive their values either from exogenously set model parameters through external parameter connections, 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 model parameter. If the model parameter is shared, obtain the shared model parameter name (often this will be the same as the parameter name by default) and use the following to update it: +```julia +update_param!(mymodel, :model_parameter_name, newvalues) +``` +If the model parameter is not shared, and thus the value can only be connected to one component/parameter pair, 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 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. +The functions [`disconnect_param!`](@ref) and [`connect_param!`](@ref) can be used to 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. ## Parametric Modifications: DICE Example @@ -76,7 +80,7 @@ In the case that you wish to alter an exogenous parameter, you may use the [`upd 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 with the same name that is connected to 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 +89,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 @@ -96,7 +100,7 @@ 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 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!` 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 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: +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 +111,23 @@ 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) +... +``` + 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..d2ea18157 100644 --- a/docs/src/tutorials/tutorial_4.md +++ b/docs/src/tutorials/tutorial_4.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. - ## 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. diff --git a/docs/src/tutorials/tutorial_5.md b/docs/src/tutorials/tutorial_5.md index ed5dead7e..3e1c8e6b8 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 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..4d8479769 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, diff --git a/src/core/connections.jl b/src/core/connections.jl index 1c768e4c7..bb50e8b46 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -653,28 +653,29 @@ Update the `value` of the unshared model parameter in Model Def `md` connected t """ function update_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, value) - # first check if we need to create an unshared model parameter, which may happen - # in the case of a previously unshared parameter being connected internally model_param_name = get_model_param_name(md, comp_name, param_name; missing_ok = true) - # create an unshared parameter - 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) - add_model_param!(md, model_param_name, param) - name = get_model_param_name - - # make sure the model parameter is unshared - elseif model_param(md, name).is_shared - error("Parameter $param_name is a shared model parameter, to safely update", - "please call `update_param!(m, param_name, value)` to explicitly update", - "a shared parameter that may be connected to several components") - end - - # update the parameter - update_param!(md, model_param_name, value) + # check if we need a new parameter, maybe because it was previously a nothing + # parameter that got disconnected + if isnothing(model_param_name) + + model_param_name = gensym() + add_model_param!(md, model_param_name, value; is_shared = false) + connect_param!(md, comp_name, param_name, model_param_name) + dirty!(md) + # update existing parameter + else + # make sure the model parameter is unshared + if model_param(md, model_param_name).is_shared + error("Parameter $param_name is a shared model parameter, to safely update", + "please call `update_param!(m, param_name, value)` to explicitly update", + "a shared parameter that may be connected to several components") + end + + # update the parameter + _update_param!(md, model_param_name, value) + end end """ @@ -688,12 +689,16 @@ function _update_param!(obj::AbstractCompositeComponentDef, name::Symbol, value) error("Cannot update parameter; $name not found in composite's model parameters.") end - if param isa ScalarModelParameter - _update_scalar_param!(param, name, value) + # handle nothing params + if is_nothing_param(param) + _update_nothing_param!(obj, name, value) else - _update_array_param!(obj, name, value) + if param isa ScalarModelParameter + _update_scalar_param!(param, name, value) + else + _update_array_param!(obj, name, value) + end end - dirty!(obj) end @@ -766,12 +771,39 @@ 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.namespace[param_name] + + # create the unshared model parameter + param = Mimi.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 + if param isa ArrayModelParameter && !isnothing(value) + _check_labels(obj, comp_def, param_name, param) + end + + # 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 model parameter by name k to value v. Each key k must be a symbol or convert to a -symbol matching the name of an model parameter that already exists in the -component definition. +is called to update the model parameter 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 component definition. """ 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 @@ -782,6 +814,22 @@ function update_params!(obj::AbstractCompositeComponentDef, parameters::Dict; up nothing end +""" + update_params!(obj::AbstractCompositeComponentDef, parameters::Dict{Tuple, Any}) + +For each (k, v) in the provided `parameters` dictionary, `update_param!` +is called to update the model parameter by name k to value v. Each key k must be a +Tuple matching the name of a component in `obj` and the name of an parameter in +that component. +""" +function update_params!(obj::AbstractCompositeComponentDef, parameters::Dict{Tuple, Any}) + parameters = Dict(get_model_param_name(obj, first(k), last(k)) => v for (k, v) in parameters) + for (param_name, value) in parameters + _update_param!(obj, param_name, value) + end + nothing +end + """ add_connector_comps!(obj::AbstractCompositeComponentDef) @@ -955,14 +1003,14 @@ function _get_param_times(param::ArrayModelParameter{TimestepArray{VariableTimes end """ - add_shared_parameter(md::ModelDef, name::Symbol, value::Any; + add_shared_param!(md::ModelDef, name::Symbol, value::Any; param_dims::Union{Nothing,Array{Symbol}} = nothing) User-facing API function to add a shared parameter to Model Def `md` with name `name` and value `value`, and optional dimensions `param_dims`. The `is_shared` attribute of the added Model Parameter will be `true`. """ -function add_shared_parameter(md::ModelDef, name::Symbol, value::Any; +function add_shared_param!(md::ModelDef, name::Symbol, value::Any; param_dims::Union{Nothing,Array{Symbol}} = nothing) has_parameter(md, name) && error("Cannot set parameter :$name, the model already has a shared parameter with this name.") diff --git a/src/core/model.jl b/src/core/model.jl index dbe0fb9c7..2e95b4ef4 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -188,14 +188,24 @@ to component `comp_name`'s parameter `param_name`. """ update_params!(m::Model, parameters::Dict{T, Any}; update_timesteps = nothing) where T -For each (k, v) in the provided `parameters` dictionary, `update_param!`` -is called to update the model parameter by name k to value v. Each key k -must be a symbol or convert to a symbol matching the name of an model 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 each (k, v) in the provided `parameters` dictionary, `update_param!` +is called to update the model parameter 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 component definition. """ @delegate update_params!(m::Model, parameters::Dict; update_timesteps = nothing) => md +""" + update_params!(m::Model, parameters::Dict{Tuple, Any}) + +For each (k, v) in the provided `parameters` dictionary, `update_param!` +is called to update the model parameter by name k to value v. Each key k must be a +Tuple matching the name of a component in `obj` and the name of an parameter in +that component. +""" +@delegate update_params!(m::Model, parameters::Dict{Tuple, Any}) => md + + """ add_comp!( m::Model, comp_id::ComponentId, comp_name::Symbol=comp_id.comp_name; @@ -419,14 +429,14 @@ variables(m::Model, comp_name::Symbol) = variables(compdef(m, comp_name)) @delegate variable_names(m::Model, comp_name::Symbol) => md """ - add_shared_parameter(m::Model, name::Symbol, value::Any; + add_shared_param!(m::Model, name::Symbol, value::Any; param_dims::Union{Nothing,Array{Symbol}} = nothing) User-facing API function to add a shared parameter to Model `m`'s ModelDef` with name `name` and value `value`, and optional dimensions `param_dims`. The `is_shared` attribute of the added Model Parameter will be `true`. """ -@delegate add_shared_parameter(m::Model, name::Symbol, value::Any; +@delegate add_shared_param!(m::Model, name::Symbol, value::Any; param_dims::Union{Nothing,Array{Symbol}} = nothing) => md """ 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 a33ca40ca..c35df33ad 100644 --- a/test/mcs/test_defmcs.jl +++ b/test/mcs/test_defmcs.jl @@ -83,11 +83,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)] @@ -300,11 +300,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)] @@ -328,13 +328,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 44d047192..faf19e701 100644 --- a/test/mcs/test_defmcs_delta.jl +++ b/test/mcs/test_defmcs_delta.jl @@ -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 61114d3cb..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)] @@ -70,7 +70,7 @@ run(sd, m, N; trials_output_filename = joinpath(output_dir, "trialdata.csv"), re rvs = get_simdef_rvnames(sd, :share) delete_RV!(sd, rvs[1]) add_RV!(sd, :new_RV, Uniform(0.2, 0.8)) -add_transform!(sd, :share, :(=), :new_RV) +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) diff --git a/test/mcs/test_defmcs_sobol.jl b/test/mcs/test_defmcs_sobol.jl index bde75cf78..e2d220682 100644 --- a/test/mcs/test_defmcs_sobol.jl +++ b/test/mcs/test_defmcs_sobol.jl @@ -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_reshaping.jl b/test/mcs/test_reshaping.jl index 28e8f5976..73ae6bf18 100644 --- a/test/mcs/test_reshaping.jl +++ b/test/mcs/test_reshaping.jl @@ -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/runtests.jl b/test/runtests.jl index 7a0b09dfe..cf3c8caa5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -113,7 +113,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_explorer_sim.jl b/test/test_explorer_sim.jl index 10673d956..7187b942d 100644 --- a/test/test_explorer_sim.jl +++ b/test/test_explorer_sim.jl @@ -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)] From 6aabb185788d8c77ef266a7515c9fc3a24a9d216 Mon Sep 17 00:00:00 2001 From: lrennels Date: Fri, 28 May 2021 17:23:43 -0700 Subject: [PATCH 29/47] Transition tests from set param to update param --- src/core/connections.jl | 17 ++++++++++++-- src/core/model.jl | 1 + test/mcs/test_translist.jl | 4 +++- test/test_adder.jl | 8 +++---- test/test_components.jl | 8 +++---- test/test_connectorcomp.jl | 12 +++++----- test/test_delete.jl | 15 +++++++++--- test/test_explorer_model.jl | 32 +++++++++++++------------- test/test_getdataframe.jl | 12 +++++----- test/test_getindex.jl | 2 +- test/test_getindex_variabletimestep.jl | 2 +- test/test_main_variabletimestep.jl | 13 ++++++----- test/test_model_structure.jl | 4 ++-- test/test_mult_getdataframe.jl | 16 ++++++------- test/test_multiplier.jl | 8 +++---- test/test_replace_comp.jl | 14 +++++------ test/test_show.jl | 2 +- test/test_timesteparrays.jl | 2 +- test/test_timesteps.jl | 8 +++---- test/test_variables_model_instance.jl | 2 +- 20 files changed, 104 insertions(+), 78 deletions(-) diff --git a/src/core/connections.jl b/src/core/connections.jl index bb50e8b46..5e13710cd 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -466,6 +466,19 @@ function model_param(obj::ModelDef, name::Symbol; missing_ok=false) error("$name not found in model parameter list") end +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) @@ -1013,9 +1026,9 @@ attribute of the added Model Parameter will be `true`. function add_shared_param!(md::ModelDef, name::Symbol, value::Any; param_dims::Union{Nothing,Array{Symbol}} = nothing) - has_parameter(md, name) && error("Cannot set parameter :$name, the model already has a shared parameter with this name.") + # check to make sure the parameter doesn't already exist + has_parameter(md, name) && error("Cannot add parameter :$name, the model already has a shared parameter with this name.") - # check to make sure the parameter doesn't already exist add_model_param!(md, name, value; param_dims = param_dims, is_shared = true) end diff --git a/src/core/model.jl b/src/core/model.jl index 2e95b4ef4..8816191c7 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -49,6 +49,7 @@ is_built(mm::MarginalModel) = (is_built(mm.base) && is_built(mm.modified)) @delegate external_param_conns(m::Model) => 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 diff --git a/test/mcs/test_translist.jl b/test/mcs/test_translist.jl index 56b5977d2..d98d999af 100644 --- a/test/mcs/test_translist.jl +++ b/test/mcs/test_translist.jl @@ -86,7 +86,9 @@ m1 = Model() set_dimension!(m1, :time, 2000:10:2050) add_comp!(m1, test1) add_comp!(m1, test2) -set_param!(m1, :p, 5) +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 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_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_delete.jl b/test/test_delete.jl index 00bc311b0..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 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_getdataframe.jl b/test/test_getdataframe.jl index 5a71e467f..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_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 -set_param!(model1, :testcomp2, :par2, years) +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_variabletimestep.jl b/test/test_main_variabletimestep.jl index 1020d9169..fe93a713a 100644 --- a/test/test_main_variabletimestep.jl +++ b/test/test_main_variabletimestep.jl @@ -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 = model_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 = model_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_model_structure.jl b/test/test_model_structure.jl index cb7199299..67136c1a4 100644 --- a/test/test_model_structure.jl +++ b/test/test_model_structure.jl @@ -166,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_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_replace_comp.jl b/test/test_replace_comp.jl index 21dfa668d..764629a1a 100644 --- a/test/test_replace_comp.jl +++ b/test/test_replace_comp.jl @@ -58,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) @@ -98,7 +98,7 @@ first = compdef(m, :first) m = Model() set_dimension!(m, :time, 2000:2005) add_comp!(m, X) -set_param!(m, :X, :x, zeros(6)) # Set model 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( @@ -116,7 +116,7 @@ set_param!(m, :X, :x, zeros(6)) # Set model parameter for :x m = Model() set_dimension!(m, :time, 2000:2005) add_comp!(m, X) -set_param!(m, :X, :x, zeros(6)) # Set model parameter for :x +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 @@ -125,7 +125,7 @@ set_param!(m, :X, :x, zeros(6)) # Set model parameter fo m = Model() set_dimension!(m, :time, 2000:2005) add_comp!(m, X) -set_param!(m, :X, :x, zeros(6)) # Set model parameter for :x +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 @@ -165,7 +165,7 @@ 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 @@ -175,10 +175,10 @@ run(m) 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)) 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 -set_param!(m, :X, :b, 8.0) # need to set b since it doesn't have a default, a will have a default +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 diff --git a/test/test_show.jl b/test/test_show.jl index d68df1ca4..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 diff --git a/test/test_timesteparrays.jl b/test/test_timesteparrays.jl index 2f1672f07..1c27de44f 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) 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]) From 6ab2c7001e55e3900d08f8015286af6a8aa77a54 Mon Sep 17 00:00:00 2001 From: lrennels Date: Fri, 28 May 2021 21:46:42 -0700 Subject: [PATCH 30/47] FInish updating testing to remove set_param; fix various bugs --- src/core/connections.jl | 112 +++++++++++++++++++++++++----- src/core/defs.jl | 2 +- src/core/model.jl | 30 ++++---- test/mcs/test_defmcs.jl | 19 +++-- test/test_composite.jl | 17 +++-- test/test_composite_parameters.jl | 80 ++++++++++++++++++--- test/test_composite_simple.jl | 16 +++-- test/test_defaults.jl | 30 +++++--- test/test_dimensions.jl | 12 ++-- test/test_firstlast.jl | 76 ++++++++++---------- test/test_main.jl | 50 +++++++++---- test/test_marginal_models.jl | 7 +- test/test_parameter_labels.jl | 51 +++++++++----- test/test_parametertypes.jl | 24 +++---- test/test_timesteparrays.jl | 4 +- 15 files changed, 373 insertions(+), 157 deletions(-) diff --git a/src/core/connections.jl b/src/core/connections.jl index 5e13710cd..c322d9816 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -97,33 +97,60 @@ end """ connect_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, param_name::Symbol, model_param_name::Symbol; - check_labels::Bool=true) + check_labels::Bool=true, ignoreunits::Bool=false)) Connect a parameter `param_name` in the component `comp_name` of composite `obj` to the model parameter `model_param_name`. """ function connect_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, param_name::Symbol, model_param_name::Symbol; - check_labels::Bool=true) + check_labels::Bool=true, ignoreunits::Bool = false) comp_def = compdef(obj, comp_name) - connect_param!(obj, comp_def, param_name, model_param_name, check_labels=check_labels) + connect_param!(obj, comp_def, param_name, model_param_name, check_labels=check_labels, ignoreunits = ignoreunits) end """ connect_param!(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, - param_name::Symbol, model_param_name::Symbol; check_labels::Bool=true) + param_name::Symbol, model_param_name::Symbol; check_labels::Bool=true, + ignoreunits::Bool = false) 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_labels::Bool=true) + param_name::Symbol, model_param_name::Symbol; check_labels::Bool=true, + ignoreunits::Bool = false) mod_param = model_param(obj, model_param_name) if mod_param isa ArrayModelParameter && check_labels _check_labels(obj, comp_def, param_name, mod_param) end + if is_shared(mod_param) && !ignoreunits + + param_units = parameter_unit(comp_def, param_name) + units_match = true + errorstring = string("Units of $(nameof(compdef)):$param_name ($param_units) do not match ", + "the following other parameters connected to the same shared ", + "model parameter $model_param_name. 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: ") + + for conn in filter(i -> i.model_param_name == model_param_name, external_param_conns(obj)) + conn_comp_def = compdef(obj, conn.comp_path) + conn_comp_name = nameof(conn_comp_def) + conn_param_name = conn.param_name + conn_units = parameter_unit(conn_comp_def, conn_param_name) + + if ! verify_units(param_units, conn_units) + units_match = false + errorstring = string(errorstring, "[$conn_comp_name:$conn_param_name with units $conn_units] ") + end + end + + units_match || error(errorstring) + end + disconnect_param!(obj, comp_def, param_name) # calls dirty!() comp_path = @or(comp_def.comp_path, ComponentPath(obj.comp_path, comp_def.name)) @@ -563,7 +590,7 @@ function add_model_param!(md::ModelDef, name::Symbol, is_shared::Bool = false) ti = get_time_index_position(param_dims) - if ti != nothing + if !isnothing(ti) value = convert(Array{number_type(md)}, value) num_dims = length(param_dims) values = get_timestep_array(md, eltype(value), num_dims, ti, value) @@ -597,12 +624,13 @@ end Add a multi-dimensional time-indexed array parameter `name` with value `value` to the Model Def `md`. The `is_shared` attribute of the ArrayModelParameter -will default to false. In this case `dims` must be `[:time]`. +will default to false. In this case `dims` must contain `[:time]`. """ function add_model_array_param!(md::ModelDef, name::Symbol, value::TimestepArray, dims; is_shared::Bool = false) - param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims, is_shared) + !(: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 @@ -658,6 +686,20 @@ function update_param!(mi::ModelInstance, name::Symbol, value) return nothing end +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 + 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) @@ -681,7 +723,7 @@ function update_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, valu else # make sure the model parameter is unshared if model_param(md, model_param_name).is_shared - error("Parameter $param_name is a shared model parameter, to safely update", + error("Parameter $param_name is a shared model parameter, to safely update ", "please call `update_param!(m, param_name, value)` to explicitly update", "a shared parameter that may be connected to several components") end @@ -1016,20 +1058,56 @@ function _get_param_times(param::ArrayModelParameter{TimestepArray{VariableTimes end """ - add_shared_param!(md::ModelDef, name::Symbol, value::Any; - param_dims::Union{Nothing,Array{Symbol}} = nothing) +function 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 optional dimensions `param_dims`. The `is_shared` -attribute of the added Model Parameter will be `true`. +`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. """ -function add_shared_param!(md::ModelDef, name::Symbol, value::Any; - param_dims::Union{Nothing,Array{Symbol}} = nothing) +function add_shared_param!(md::ModelDef, name::Symbol, value::Any; dims::Array{Symbol}=Symbol[]) - # check to make sure the parameter doesn't already exist + # check to make sure the parameter doesn't already exist has_parameter(md, name) && error("Cannot add parameter :$name, the model already has a shared parameter with this name.") - add_model_param!(md, name, value; param_dims = param_dims, is_shared = true) + # check dimensions + if value isa NamedArray + dims = dimnames(value) + end + + if ndims(value) != length(dims) + error("Please provide $(ndims(value)) dimension names for value, $(length(dims))", + " were given but value is $value. This is done with the `dims` keyword argument ", + " ie. : `add_shared_param!(md, name, value; dims = [:time])") + end + + # create a parameter def + param_def = ParameterDef(name, nothing, md.number_type, dims, "", "", nothing) + + # create shared model parameter + param = create_model_param(md, param_def, value; is_shared = true) + + # check dimensions + model_dims = dim_names(md) + 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 """ diff --git a/src/core/defs.jl b/src/core/defs.jl index 3cef32d5a..befd6e2d1 100644 --- a/src/core/defs.jl +++ b/src/core/defs.jl @@ -619,7 +619,7 @@ function set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing, ignor for comp in comps # Set check_labels = 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_labels = false) + connect_param!(md, comp, param_name, model_param_name, check_labels = false, ignoreunits = ignoreunits) end nothing end diff --git a/src/core/model.jl b/src/core/model.jl index 8816191c7..e13ebf376 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -78,13 +78,16 @@ data for the second timestep and beyond. 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, model_param_name::Symbol) + connect_param!(m::Model, comp_name::Symbol, param_name::Symbol, model_param_name::Symbol; + check_labels::Bool=true, ignoreunits::Bool=false)) -Bind the parameter `param_name` in the component `comp_name` of model `m` to the model parameter -`model_param_name` already present in the model's list of model 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, model_param_name::Symbol) => md +@delegate connect_param!(m::Model, comp_name::Symbol, param_name::Symbol, model_param_name::Symbol; + check_labels::Bool=true, ignoreunits::Bool = false) => md """ connect_param!(m::Model, dst::Pair{Symbol, Symbol}, src::Pair{Symbol, Symbol}, backup::Array; ignoreunits::Bool=false) @@ -430,16 +433,19 @@ variables(m::Model, comp_name::Symbol) = variables(compdef(m, comp_name)) @delegate variable_names(m::Model, comp_name::Symbol) => md """ - add_shared_param!(m::Model, name::Symbol, value::Any; - param_dims::Union{Nothing,Array{Symbol}} = nothing) +function add_shared_param!(m::Model, name::Symbol, value::Any; dims::Array{Symbol}=Symbol[]) + +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`. -User-facing API function to add a shared parameter to Model `m`'s ModelDef` with name -`name` and value `value`, and optional dimensions `param_dims`. 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. """ -@delegate add_shared_param!(m::Model, name::Symbol, value::Any; - param_dims::Union{Nothing,Array{Symbol}} = nothing) => md - +@delegate add_shared_param!(m::Model, name::Symbol, value::Any; dims::Array{Symbol}=Symbol[]) => md + """ add_model_array_param!(m::Model, name::Symbol, value::Union{AbstractArray, TimestepArray}, dims) diff --git a/test/mcs/test_defmcs.jl b/test/mcs/test_defmcs.jl index c35df33ad..6678e5cce 100644 --- a/test/mcs/test_defmcs.jl +++ b/test/mcs/test_defmcs.jl @@ -59,10 +59,21 @@ m = Model() set_dimension!(m, :time, 2015:5:2110) set_dimension!(m, :regions, [:Region1, :Region2, :Region3]) add_comp!(m, test) -set_param!(m, :p_shared1, -5) -set_param!(m, :p_shared2, -collect(1:20)) -set_param!(m, :p_shared3, -fill(10,20,3)) -set_param!(m, :p_shared4, -collect(1:3)) + +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 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 73d038c90..8a4dfa9b2 100644 --- a/test/test_composite_parameters.jl +++ b/test/test_composite_parameters.jl @@ -128,12 +128,13 @@ m2 = get_model() 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.md, :p1).value == 2 -@test Mimi.model_param(m2.md, :p1).is_shared +@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.md, :B, :p1) -@test Mimi.model_param(m2.md, sym).value == 3 -@test !(Mimi.model_param(m2.md, sym).is_shared) +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() @@ -147,8 +148,65 @@ 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.md, :B_p1).value == 2 -@test Mimi.model_param(m3.md, :B_p1).is_shared +@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("Units of compdef:p1 (thous \$) do not match the following", 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 or set:", 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))) #------------------------------------------------------------------------------ @@ -240,7 +298,11 @@ model_param_name = Mimi.get_model_param_name(m.md, :top, :p2) # 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_defaults.jl b/test/test_defaults.jl index b2d024896..d610aae0f 100644 --- a/test/test_defaults.jl +++ b/test/test_defaults.jl @@ -13,7 +13,9 @@ end m = Model() set_dimension!(m, :time, 1:10) add_comp!(m, A) -set_param!(m, :p2, 2) + +add_shared_param!(m, :p2, 2) +connect_param!(m, :A, :p2, :p2) # So far only :p2 is in the model definition's dictionary @test :p2 in keys(model_params(m)) @@ -24,18 +26,28 @@ run(m) # :p1's value is it's default @test m[:A, :p1] == 1 -# This errors because p1 isn't in the model definition's model params -@test_throws ErrorException update_param!(m, :p1, 10) +# 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))) -# Need to use set_param! instead -set_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) -# Now there is a :p1 in the model definition's dictionary -@test :p1 in keys(model_params(m)) +# Now there is a :model p1 in the model definition's dictionary but not :pq +@test !(:p1 in keys(model_params(m))) +@test :model_p1 in keys(model_params(m)) run(m) -@test m[:A, :p1] == 10 -update_param!(m, :p1, 11) # Now we can use update_param! +@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) +@test_throws ErrorException update_param!(m, :A, :p1, 11) +update_param!(m, :model_p1, 11) end \ No newline at end of file diff --git a/test/test_dimensions.jl b/test/test_dimensions.jl index 1fbc00f50..553b0915a 100644 --- a/test/test_dimensions.jl +++ b/test/test_dimensions.jl @@ -12,8 +12,8 @@ import Mimi: ## 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) 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_main.jl b/test/test_main.jl index 222a57fc5..d17c1145b 100644 --- a/test/test_main.jl +++ b/test/test_main.jl @@ -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 = model_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 = model_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_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_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 a6d599dd0..1e4c8635b 100644 --- a/test/test_parametertypes.jl +++ b/test/test_parametertypes.jl @@ -86,11 +86,11 @@ 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]) +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) @@ -158,7 +158,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 @@ -210,7 +210,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 @@ -258,7 +258,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]) set_dimension!(m, :time, [2000, 2005, 2020, 2100]) @@ -278,7 +278,7 @@ 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 @@ -328,9 +328,9 @@ 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]) diff --git a/test/test_timesteparrays.jl b/test/test_timesteparrays.jl index 1c27de44f..d3162a759 100644 --- a/test/test_timesteparrays.jl +++ b/test/test_timesteparrays.jl @@ -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) From 78fd9bc1d3122d53cef3013f4c1990149c8d8a5f Mon Sep 17 00:00:00 2001 From: lrennels Date: Sat, 29 May 2021 01:49:43 -0700 Subject: [PATCH 31/47] Fix type problems, work on tests --- src/core/connections.jl | 87 ++++++++++++++++---------- src/core/model.jl | 21 +++---- src/mcs/defmcs.jl | 1 + test/mcs/test_defmcs.jl | 12 ++-- test/mcs/test_empirical.jl | 17 ------ test/test_parametertypes.jl | 119 ++++++++++++++++++++++++------------ wip/create_composite.jl | 10 +-- 7 files changed, 155 insertions(+), 112 deletions(-) diff --git a/src/core/connections.jl b/src/core/connections.jl index c322d9816..36f04a301 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -95,6 +95,30 @@ function _check_labels(obj::AbstractCompositeComponentDef, end end +""" + _check_labels(obj::AbstractCompositeComponentDef, + comp_def::AbstractComponentDef, param_name::Symbol, + mod_param::ScalarModelParameter) + +Check that the labels of the ScalarModelParameter `mod_param` match the labels +of the model parameter `param_name` in component `comp_def` of object `obj`, +including datatype. +""" +function _check_labels(obj::AbstractCompositeComponentDef, + comp_def::AbstractComponentDef, param_name::Symbol, + mod_param::ScalarModelParameter) + + # check datatype conversion + parameter_datatype = parameter(comp_def, param_name).datatype + parameter_datatype = Union{Nothing, Missing, (parameter_datatype == Number ? number_type(obj) : parameter_datatype)} + + try convert(parameter_datatype, mod_param.value) catch; + error("Cannot connect $(nameof(compdef)):$param_name, with datatype + $parameter_datatype, to shared model parameter $model_param_name + with datatype $(typeof(mod_param.value)) because of type incompatibilty.") + end +end + """ connect_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, param_name::Symbol, model_param_name::Symbol; check_labels::Bool=true, ignoreunits::Bool=false)) @@ -120,9 +144,11 @@ the model parameter `model_param_name`. function connect_param!(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, param_name::Symbol, model_param_name::Symbol; check_labels::Bool=true, ignoreunits::Bool = false) + mod_param = model_param(obj, model_param_name) - if mod_param isa ArrayModelParameter && check_labels + # check the labels + if check_labels _check_labels(obj, comp_def, param_name, mod_param) end @@ -852,35 +878,29 @@ function _update_nothing_param!(obj::AbstractCompositeComponentDef, name::Symbol # 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) + 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 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 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 component definition. """ 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) - end - nothing -end - -""" - update_params!(obj::AbstractCompositeComponentDef, parameters::Dict{Tuple, Any}) - -For each (k, v) in the provided `parameters` dictionary, `update_param!` -is called to update the model parameter by name k to value v. Each key k must be a -Tuple matching the name of a component in `obj` and the name of an parameter in -that component. -""" -function update_params!(obj::AbstractCompositeComponentDef, parameters::Dict{Tuple, Any}) - parameters = Dict(get_model_param_name(obj, first(k), last(k)) => v for (k, v) in parameters) - for (param_name, value) in parameters - _update_param!(obj, param_name, value) + + 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 = Symbol(k) + end + _update_param!(obj, model_param_name, v) end nothing end @@ -1058,7 +1078,7 @@ function _get_param_times(param::ArrayModelParameter{TimestepArray{VariableTimes end """ -function add_shared_param!(md::ModelDef, name::Symbol, value::Any; dims::Array{Symbol}=Symbol[]) + 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 @@ -1074,21 +1094,26 @@ function add_shared_param!(md::ModelDef, name::Symbol, value::Any; dims::Array{S # check to make sure the parameter doesn't already exist has_parameter(md, name) && error("Cannot add parameter :$name, the model already has a shared parameter with this name.") - # check dimensions + # make sure all parameter dims are in model and have the appropriate number of elements if value isa NamedArray dims = dimnames(value) end - if ndims(value) != length(dims) + 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 value is $value. This is done with the `dims` keyword argument ", " ie. : `add_shared_param!(md, name, value; dims = [:time])") end - # create a parameter def - param_def = ParameterDef(name, nothing, md.number_type, dims, "", "", nothing) - - # create shared model parameter + # create shared model parameter with a ParameterDef, which takes advantage of + # the checks and parameterization etc. in `check_model_param` + data_type = value isa AbstractArray ? eltype(value) : typeof(value) + data_type = data_type <: Number ? Number : data_type # raise any Number type to Number to avoid small errors + param_def = ParameterDef(name, nothing, data_type, dims, "", "", nothing) param = create_model_param(md, param_def, value; is_shared = true) # check dimensions diff --git a/src/core/model.jl b/src/core/model.jl index e13ebf376..d876dc491 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -190,25 +190,18 @@ 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{T, Any}; update_timesteps = nothing) where T + 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 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 component definition. -""" -@delegate update_params!(m::Model, parameters::Dict; update_timesteps = nothing) => md +is called to update the model parameter identified by k to value v. -""" - update_params!(m::Model, parameters::Dict{Tuple, Any}) +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 model parameter by name k to value v. 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 component definition. """ -@delegate update_params!(m::Model, parameters::Dict{Tuple, Any}) => md - +@delegate update_params!(m::Model, parameters::Dict; update_timesteps = nothing) => md """ add_comp!( diff --git a/src/mcs/defmcs.jl b/src/mcs/defmcs.jl index 58b713bf6..c28ccbbac 100644 --- a/src/mcs/defmcs.jl +++ b/src/mcs/defmcs.jl @@ -334,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/test/mcs/test_defmcs.jl b/test/mcs/test_defmcs.jl index 6678e5cce..0eadea5a7 100644 --- a/test/mcs/test_defmcs.jl +++ b/test/mcs/test_defmcs.jl @@ -60,18 +60,18 @@ 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) +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]) +@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]) +@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]) +add_shared_param!(m, :p_shared4, collect(1:3), dims = [:regions]) connect_param!(m, :test, :p_shared4, :p_shared4) run(sd_toy, m, 10) diff --git a/test/mcs/test_empirical.jl b/test/mcs/test_empirical.jl index 59ba6b425..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") diff --git a/test/test_parametertypes.jl b/test/test_parametertypes.jl index 1e4c8635b..7ec7737c9 100644 --- a/test/test_parametertypes.jl +++ b/test/test_parametertypes.jl @@ -97,6 +97,10 @@ 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) @@ -104,18 +108,18 @@ h_sym = Mimi.get_model_param_name(m.mi.md, :MyComp, :h) @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], 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 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].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[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 @@ -124,21 +128,21 @@ h_sym = Mimi.get_model_param_name(m.mi.md, :MyComp, :h) @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 +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].value == 0.5 # The original dictionary still has the old value -@test new_extpars[:d].value == 5. # The new dictionary has the updated value +@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) == length(new_extpars) == 9 # we replaced the unshared default for :a with a shared for :a -@test typeof(new_extpars[:a].values) == TimestepMatrix{FixedTimestep{2000, 1, 2100}, arrtype, 1, Array{arrtype, 2}} +@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} #------------------------------------------------------------------------------ @@ -166,9 +170,9 @@ update_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 @@ -176,19 +180,19 @@ set_dimension!(m, :time, 1999:2001) # 2000 1 # 2001 2 last first, last -x = model_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 = model_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) @@ -223,18 +227,18 @@ set_dimension!(m, :time, [2000, 2005, 2020, 2100]) # 2020 3 last # 2100 missing last -x = model_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 = model_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) @@ -262,8 +266,8 @@ 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 = model_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) @@ -285,7 +289,7 @@ update_param!(m, :MyComp2, :x, [1, 2, 3]) # 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 @@ -293,7 +297,7 @@ update_param!(m, :x, [2, 3, 4, 5, 6]) # 2002 5 last # 2003 6 last -x = model_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.] @@ -333,20 +337,20 @@ 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 model_param(m.md, :y).values == [10., 15.] -update_param!(m, :z, 1) -@test model_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 model_param(m.md, :x).values isa Mimi.TimestepArray{Mimi.FixedTimestep{1999,1, 2001},Union{Missing,Float64},1} -@test model_param(m.md, :x).values.data == [3.,4.,5.] -@test model_param(m.md, :y).values == [10.,20.] -@test model_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 @@ -387,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 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 From 6b50a795a26497542d8cc00e5718e062fe2419a8 Mon Sep 17 00:00:00 2001 From: lrennels Date: Sat, 29 May 2021 02:30:02 -0700 Subject: [PATCH 32/47] Fix typing --- src/core/connections.jl | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/core/connections.jl b/src/core/connections.jl index 36f04a301..4874aca94 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -110,12 +110,27 @@ function _check_labels(obj::AbstractCompositeComponentDef, # check datatype conversion parameter_datatype = parameter(comp_def, param_name).datatype + model_parameter_dataype = typeof(mod_param.value) + + # if the parameter_datatype is Number (the default), we just need our value + # to be a subtype of Number + if parameter_datatype == Number && !(model_parameter_dataype <: Number) + error("Cannot connect $(nameof(compdef)):$param_name, with datatype $parameter_datatype, ", + "to shared model parameter $(nameof(model_param)) with datatype $model_parameter_dataype ", + "because of $model_parameter_dataype is not a subtype of $parameter_datatype.") + + # if the parameter_datatype is not Number, it was specified exactly and needs + # an error + elseif parameter_datatype !== Number && parameter_datatype!== (typeof(mod_param.value)) + error("Cannot connect $(nameof(compdef)):$param_name, with datatype $parameter_datatype, " , + "to shared model parameter $(nameof(model_param)) with datatype $model_parameter_dataype because ", + "the two should match.") + end + parameter_datatype = Union{Nothing, Missing, (parameter_datatype == Number ? number_type(obj) : parameter_datatype)} try convert(parameter_datatype, mod_param.value) catch; - error("Cannot connect $(nameof(compdef)):$param_name, with datatype - $parameter_datatype, to shared model parameter $model_param_name - with datatype $(typeof(mod_param.value)) because of type incompatibilty.") + end end From 3ebf7bdcb7dbeea3ed1756e768145f864ff97ff8 Mon Sep 17 00:00:00 2001 From: lrennels Date: Sat, 29 May 2021 15:05:29 -0700 Subject: [PATCH 33/47] Shift to use of create_model_param over add_model_param --- src/core/connections.jl | 62 ++++++++++++++----------------- src/core/defs.jl | 67 +++++++++++++++++++--------------- test/mcs/test_marginalmodel.jl | 2 +- test/mcs/test_translist.jl | 2 +- test/runtests.jl | 4 +- test/test_defaults.jl | 4 +- 6 files changed, 72 insertions(+), 69 deletions(-) diff --git a/src/core/connections.jl b/src/core/connections.jl index 4874aca94..e4cef7fec 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -126,12 +126,6 @@ function _check_labels(obj::AbstractCompositeComponentDef, "to shared model parameter $(nameof(model_param)) with datatype $model_parameter_dataype because ", "the two should match.") end - - parameter_datatype = Union{Nothing, Missing, (parameter_datatype == Number ? number_type(obj) : parameter_datatype)} - - try convert(parameter_datatype, mod_param.value) catch; - - end end """ @@ -163,7 +157,7 @@ function connect_param!(obj::AbstractCompositeComponentDef, comp_def::AbstractCo mod_param = model_param(obj, model_param_name) # check the labels - if check_labels + if check_labels && !is_nothing_param(mod_param) _check_labels(obj, comp_def, param_name, mod_param) end @@ -276,6 +270,8 @@ function _connect_param!(obj::AbstractCompositeComponentDef, end + # TODO: potentially unsafe way to add parameter, advise using create_model_param! + # and add_model_param! combo if possible ... but would need a specific ParameterDef add_model_array_param!(obj, dst_par_name, values, dst_dims) backup_param_name = dst_par_name @@ -432,8 +428,8 @@ function nothing_params(obj::AbstractCompositeComponentDef) refs = UnnamedReference[] for conn in obj.external_param_conns - value = model_param(obj, conn.model_param_name) - if is_nothing_param(value) + 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 @@ -484,12 +480,11 @@ function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T param_def = comp_def[param_name] # check whether we need to add the model parameter to the ModelDef - if model_param(md, param_name, missing_ok=true) === nothing + if isnothing(model_param(md, param_name, missing_ok=true)) if haskey(parameters, string(param_name)) value = parameters[string(param_name)] - param_dims = parameter_dimensions(md, comp_name, param_name) - - add_model_param!(md, param_name, value; param_dims = param_dims, is_shared = true) + param = create_model_param(md, param_def, value; is_shared = true) + add_model_param!(md, param_name, param) else error("Cannot set parameter :$param_name, not found in provided dictionary.") end @@ -608,6 +603,10 @@ 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, @@ -755,8 +754,12 @@ function update_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, valu # parameter that got disconnected if isnothing(model_param_name) + param_def = parameter(compdef(md, comp_name), param_name) + param = create_model_param(md, param_def, value; is_shared = false) + model_param_name = gensym() - add_model_param!(md, model_param_name, value; is_shared = false) + add_model_param!(md, model_param_name, param) + connect_param!(md, comp_name, param_name, model_param_name) dirty!(md) @@ -765,8 +768,11 @@ function update_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, valu # make sure the model parameter is unshared if model_param(md, model_param_name).is_shared error("Parameter $param_name is a shared model parameter, to safely update ", - "please call `update_param!(m, param_name, value)` to explicitly update", - "a shared parameter that may be connected to several components") + "please call `update_param!(m, param_name, value)` to explicitly update ", + "a shared parameter that may be connected to several components. If you wish ", + "to disconnect from the shared model parameter and use an unshared ", + "model parameter, first use `disconnect_param!` and then you can use this same ", + "call to `update_param!`.") end # update the parameter @@ -854,6 +860,8 @@ 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) + # TODO: potentially unsafe way to add parameter, advise using create_model_param! + # and add_model_param! combo if possible ... but would need a specific ParameterDef add_model_param!(obj, name, ArrayModelParameter(new_timestep_array, dim_names(param), param.is_shared)) else copyto!(param.values.data, value) @@ -881,11 +889,11 @@ function _update_nothing_param!(obj::AbstractCompositeComponentDef, name::Symbol param_def = comp_def.namespace[param_name] # create the unshared model parameter - param = Mimi.create_model_param(obj, param_def, value) + 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 - if param isa ArrayModelParameter && !isnothing(value) + if !is_nothing_param(param) # shouldn't be a nothing param since we're updating to non-nothing! _check_labels(obj, comp_def, param_name, param) end @@ -1176,6 +1184,7 @@ function create_model_param(md::ModelDef, param_def::AbstractParameterDef, value end # have a value - in the initiliazation of parameters case this is a default + # value set in defcomp else if num_dims > 0 # array parameter case @@ -1203,22 +1212,7 @@ function create_model_param(md::ModelDef, param_def::AbstractParameterDef, value ti = get_time_index_position(param_dims) if ti !== nothing # there is a time dimension T = eltype(value) - - # 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) - last = last_period(md) - - 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 - + values = get_timestep_array(md, T, num_dims, ti, value) else values = value end diff --git a/src/core/defs.jl b/src/core/defs.jl index befd6e2d1..661dd1fc5 100644 --- a/src/core/defs.jl +++ b/src/core/defs.jl @@ -603,10 +603,8 @@ function set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing, ignor # Need to check the dimensions of the parameter data against each component # before adding it to the model's model parameters - if param isa ArrayModelParameter - for comp in comps - _check_labels(md, comp, param_name, param) - end + for comp in comps + _check_labels(md, comp, param_name, param) end # add the shared model parameter to the model def @@ -857,42 +855,53 @@ function _initialize_parameters!(md::ModelDef, comp_def::AbstractComponentDef) param_name = nameof(param_def) comp_name = nameof(comp_def) - # Make some checks to see if the parameter needs to be created, specifically - # tends to be the case if we are using replace! with the default reconnect - # = true. The parameter could be: + # 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)) - + # 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) - - if !connected - 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 - if param isa ArrayModelParameter && !isnothing(value) - _check_labels(md, comp_def, param_name, param) - end - - # add the unshared model parameter to the model def - add_model_param!(md, model_param_name, param) + !connected && _initialize_parameter!(md, comp_def, param_def) - # connect - don't need to check labels since did it above - connect_param!(md, comp_def, param_name, model_param_name; check_labels = false) - end 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 + if !is_nothing_param(param) + _check_labels(md, comp_def, param_name, param) + end + + # add the unshared model parameter to the model def + add_model_param!(md, model_param_name, param) + + # connect - don't need to check labels since did it above + connect_param!(md, comp_def, param_name, model_param_name; check_labels = false) + +end + """ _propagate_first_last!(obj::AbstractComponentDef; first::NothingInt=nothing, last::NothingInt=nothing) 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_translist.jl b/test/mcs/test_translist.jl index d98d999af..1f6b3b0d9 100644 --- a/test/mcs/test_translist.jl +++ b/test/mcs/test_translist.jl @@ -23,7 +23,7 @@ end sd = @defsim begin sampling(LHSData) - p = Normal(0, 1) # should be shared, but was set with default so have to find it + p = Normal(0, 1) end #------------------------------------------------------------------------------ diff --git a/test/runtests.jl b/test/runtests.jl index cf3c8caa5..94ea80f56 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -53,8 +53,8 @@ Electron.prep_test_env() @info("test_replace_comp.jl") @time include("test_replace_comp.jl") - @info("test_tools.jl") - @time include("test_tools.jl") + # @info("test_tools.jl") + # @time include("test_tools.jl") @info("test_parameter_labels.jl") @time include("test_parameter_labels.jl") diff --git a/test/test_defaults.jl b/test/test_defaults.jl index d610aae0f..75b686948 100644 --- a/test/test_defaults.jl +++ b/test/test_defaults.jl @@ -7,14 +7,14 @@ 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) -add_shared_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 From 27aa789f4d4968743344305f6bd7e8ff9ac5741a Mon Sep 17 00:00:00 2001 From: lrennels Date: Sat, 29 May 2021 15:11:35 -0700 Subject: [PATCH 34/47] More cleanup --- src/core/connections.jl | 9 +++++++-- test/runtests.jl | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/core/connections.jl b/src/core/connections.jl index e4cef7fec..5e791f739 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -474,8 +474,10 @@ and all resulting new model parameters will be shared parameters. """ function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T 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] @@ -754,7 +756,9 @@ function update_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, valu # parameter that got disconnected if isnothing(model_param_name) - param_def = parameter(compdef(md, comp_name), 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() @@ -885,8 +889,9 @@ function _update_nothing_param!(obj::AbstractCompositeComponentDef, name::Symbol # 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.namespace[param_name] + param_def = comp_def[param_name] # create the unshared model parameter param = create_model_param(obj, param_def, value) diff --git a/test/runtests.jl b/test/runtests.jl index 94ea80f56..cf3c8caa5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -53,8 +53,8 @@ Electron.prep_test_env() @info("test_replace_comp.jl") @time include("test_replace_comp.jl") - # @info("test_tools.jl") - # @time include("test_tools.jl") + @info("test_tools.jl") + @time include("test_tools.jl") @info("test_parameter_labels.jl") @time include("test_parameter_labels.jl") From 1e5f460cb2e6898b5d0d43c948beb016909eb8b7 Mon Sep 17 00:00:00 2001 From: lrennels Date: Sat, 29 May 2021 16:14:32 -0700 Subject: [PATCH 35/47] Add testing --- src/core/connections.jl | 8 +- test/runtests.jl | 3 + test/test_new_paramAPI.jl | 173 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 test/test_new_paramAPI.jl diff --git a/src/core/connections.jl b/src/core/connections.jl index 5e791f739..fd7f43823 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -124,7 +124,7 @@ function _check_labels(obj::AbstractCompositeComponentDef, elseif parameter_datatype !== Number && parameter_datatype!== (typeof(mod_param.value)) error("Cannot connect $(nameof(compdef)):$param_name, with datatype $parameter_datatype, " , "to shared model parameter $(nameof(model_param)) with datatype $model_parameter_dataype because ", - "the two should match.") + "the two types should match.") end end @@ -769,8 +769,8 @@ function update_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, valu # update existing parameter else - # make sure the model parameter is unshared - if model_param(md, model_param_name).is_shared + # make sure the original model parameter name is not shared + if model_param_name == param_name && model_param(md, model_param_name).is_shared error("Parameter $param_name is a shared model parameter, to safely update ", "please call `update_param!(m, param_name, value)` to explicitly update ", "a shared parameter that may be connected to several components. If you wish ", @@ -1206,7 +1206,7 @@ function create_model_param(md::ModelDef, param_def::AbstractParameterDef, value # check that number of dimensions matches value_dims = length(size(value)) if num_dims != value_dims - error("Mismatched data size for an _initialize_parameters call: dimension :$param_name", + error("Mismatched data size: dimension :$param_name", " in has $num_dims dimensions; indicated value", " has $value_dims dimensions.") end diff --git a/test/runtests.jl b/test/runtests.jl index cf3c8caa5..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") diff --git a/test/test_new_paramAPI.jl b/test/test_new_paramAPI.jl new file mode 100644 index 000000000..d66382daa --- /dev/null +++ b/test/test_new_paramAPI.jl @@ -0,0 +1,173 @@ +## 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 + +# DataType, Shared vs. Unshared +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 + +# we don't have to disconnect first because they have their own names, not :shared_param +update_param!(m, :A, :p2, 1) +update_param!(m, :A, :p3, 2) + + +# Units, Shared vs. Unshared +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 + +# Default +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))) + +# arrays and 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 + +# +# Section 2. set_leftover_params! +# + +# TODO + +# +# 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 From 22da79240ce1cf5fe1941f85a783b056b0811fe4 Mon Sep 17 00:00:00 2001 From: lrennels Date: Sat, 29 May 2021 18:25:59 -0700 Subject: [PATCH 36/47] Clarify behavior of parameter moving from shared to unshared --- src/core/connections.jl | 14 +++++++------- test/test_defaults.jl | 14 +++++++++++--- test/test_new_paramAPI.jl | 10 +++++++--- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/core/connections.jl b/src/core/connections.jl index fd7f43823..5b0cd3bec 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -769,15 +769,15 @@ function update_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, valu # update existing parameter else - # make sure the original model parameter name is not shared - if model_param_name == param_name && model_param(md, model_param_name).is_shared - error("Parameter $param_name is a shared model parameter, to safely update ", - "please call `update_param!(m, param_name, value)` to explicitly update ", - "a shared parameter that may be connected to several components. If you wish ", - "to disconnect from the shared model parameter and use an unshared ", + 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, 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!`.") - end # update the parameter _update_param!(md, model_param_name, value) diff --git a/test/test_defaults.jl b/test/test_defaults.jl index 75b686948..289f823ba 100644 --- a/test/test_defaults.jl +++ b/test/test_defaults.jl @@ -37,7 +37,7 @@ update_param!(m, :A, :p1, 10) add_shared_param!(m, :model_p1, 20) connect_param!(m, :A, :p1, :model_p1) -# Now there is a :model p1 in the model definition's dictionary but not :pq +# 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)) @@ -46,8 +46,16 @@ run(m) # 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) -@test_throws ErrorException update_param!(m, :A, :p1, 11) -update_param!(m, :model_p1, 11) +update_param!(m, :model_p1, 30) +run(m) +@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_new_paramAPI.jl b/test/test_new_paramAPI.jl index d66382daa..c7703fdec 100644 --- a/test/test_new_paramAPI.jl +++ b/test/test_new_paramAPI.jl @@ -55,10 +55,14 @@ 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 -# we don't have to disconnect first because they have their own names, not :shared_param -update_param!(m, :A, :p2, 1) -update_param!(m, :A, :p3, 2) +# 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 # Units, Shared vs. Unshared m = _get_model() From 0997d69a4a86c61b0b0402896da5e921860b6efe Mon Sep 17 00:00:00 2001 From: lrennels Date: Sun, 30 May 2021 17:59:31 -0700 Subject: [PATCH 37/47] Add docs --- README.md | 2 +- docs/make.jl | 9 +- docs/src/howto/howto_4.md | 59 +------ docs/src/howto/howto_5.md | 247 +++++++++++++++++++++++------- docs/src/howto/howto_6.md | 158 ++++++++----------- docs/src/howto/howto_7.md | 254 ++++++++----------------------- docs/src/howto/howto_8.md | 233 ++++++++++++++++++++++++++++ docs/src/howto/howto_main.md | 11 +- docs/src/tutorials/tutorial_3.md | 13 +- docs/src/tutorials/tutorial_4.md | 43 +++--- docs/src/tutorials/tutorial_5.md | 34 +++-- src/core/connections.jl | 4 +- src/core/model.jl | 2 +- 13 files changed, 619 insertions(+), 450 deletions(-) create mode 100644 docs/src/howto/howto_8.md diff --git a/README.md b/README.md index a0a94d8b6..f8340f9c7 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Mimi is a [Julia](http://julialang.org) package that provides a component model 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/docs/make.jl b/docs/make.jl index 02f234593..57f7fa583 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -20,10 +20,11 @@ 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", + "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" ], "Advanced How-to Guides" => Any[ "Advanced How-to Guides Intro" => "howto_advanced/howto_adv_main.md", diff --git a/docs/src/howto/howto_4.md b/docs/src/howto/howto_4.md index 09c30ac1b..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 a model parameter - -When `set_param!` is called, it creates a shared model 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 shared model 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, noting that all resulting model parameters will be shared 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 d3749a51b..cc0bbd460 100644 --- a/docs/src/howto/howto_5.md +++ b/docs/src/howto/howto_5.md @@ -1,69 +1,214 @@ -# 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) +- [`set_leftover_params!`](@ref) + +### 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) ``` -set_dimension!(m, :time, time_keys) +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 connect all parameters to values. We have three cases here, (1) we want to update the value of an unshared parameter from `nothing` to a value or (2) we want to add a shared parameter and connect one or, more commonly, several component parameters to it (3) we want to connect a parameter to another component's variable. ----- -#### For example, one may wish to replace the FUND model's climate module with a different one, such as FAIR: +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. -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. +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. Note the `dims` argument above. When you add a shared model parameter that will be connected to non-scalar parameters like above, you need to specify the dimensions in a similar fashion to what is done in the `@defcomp` call so that appropriate allocation and checks can be made. This is not necessary for parameters without (named) dimensions. Appropriate error messages will instruct you to designate these if you forget to do so, and also recognize if you try to connect a parameter to a shared model parameter that does not have the right data size. As above, checks on data types will also be performed. + +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. +``` +run(m) +``` + +### 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 put togther should outline how to make any changes you need. 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) +``` + +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, [10,11,12,13,14,15]) +``` -We start with FUND +To **connect a parameter to another component's variable**, the below will disconnect the connection to the model parameter and make the internal parameter connection: +```julia +connect_param!(m, :B, :p3, :A, :v1) ``` -using Mimi -using MimiFUND -m = MimiFUND.get_model() +while a 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) ``` -where `MimiFUND.get_model` includes the call `set_dimension!(m, time, 1950:3000)`. ----- -#### Now we need to change the `time` dimension to be 1765 to 2500: +To **move from connection to a shared model parameter 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]) +``` +noting that this last call could also be a [`connect_param!`](@ref) to another parameter or variable etc., it is now free to be reset in any way you want. + +### Other Details -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. +#### Units -- 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. +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 ----- -#### We now go ahead and change the `time` dimension to be 1765 to 2500: +@defcomp B begin + p2 = Parameter(unit = "thousands of \$") + function run_timestep(p, v, d, t) + end +end + +m = Model() +set_dimension!(m, :time, 2000:2005) +add_comp!(m, A) +add_comp!(m, B) ``` -set_dimension!(m, :time, 1765:2500) +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) + +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 $] ``` -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. +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) ``` -add_comp!(m, FAIR_component) # will run from 1765 to 1950 +#### Setting Parameters with a Dictionary with `set_leftover_params!` + +[TODO] + +#### Batch Updating Parameters with `update_params!` + +You can batch update a 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) ``` -**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: +For each (k, v) pair in the provided `parameters` dictionary, `update_param!` 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) +``` + +#### Anonymous Parameter Indices -- 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: +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: -- 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!(m, :ParameterName, values)` function +```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'). + +#### 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 + +[PLACEHOLDER for requested details on Variables] + +## 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`. diff --git a/docs/src/howto/howto_6.md b/docs/src/howto/howto_6.md index 2530ffa63..830134fa6 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 `update_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!(m, :ParameterName, values)` 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_main.md b/docs/src/howto/howto_main.md index 314acd117..e5cf53f18 100644 --- a/docs/src/howto/howto_main.md +++ b/docs/src/howto/howto_main.md @@ -15,13 +15,16 @@ 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) diff --git a/docs/src/tutorials/tutorial_3.md b/docs/src/tutorials/tutorial_3.md index 5ae39c79e..16b4c2c70 100644 --- a/docs/src/tutorials/tutorial_3.md +++ b/docs/src/tutorials/tutorial_3.md @@ -18,9 +18,9 @@ Possible modifications range in complexity, from simply altering parameter value ## 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), [`disconnect_param!`](@ref), [`add_shared_param`](@ref) and [`connect_param!`](@ref)**. For detail on these functions see the How To Guide 5: Parameters and Variables and the API reference guide, Reference Guide: The Mimi API. -The parameters in the original model receive their values either from exogenously set model parameters through external parameter connections, or from another component's variable through an internal parameter connection. +The parameters in the original model receive their values either from exogenously set model parameters (shared or unshared as described in How To Guide 5) through external parameter connections, 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 model parameter. If the model parameter is shared, obtain the shared model parameter name (often this will be the same as the parameter name by default) and use the following to update it: ```julia @@ -34,7 +34,9 @@ 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. -The functions [`disconnect_param!`](@ref) and [`connect_param!`](@ref) can be used to 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. +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. + +**Once again, for specific instructions and details on various cases of updating and changing parameters, and their connections, please view How To Guide 5. We do not repeat all information here for brevity and to avoid duplication.** ## Parametric Modifications: DICE Example @@ -74,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 is a shared model parameter with the same name that is connected to 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) @@ -121,6 +123,7 @@ 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 diff --git a/docs/src/tutorials/tutorial_4.md b/docs/src/tutorials/tutorial_4.md index d2ea18157..312641d2c 100644 --- a/docs/src/tutorials/tutorial_4.md +++ b/docs/src/tutorials/tutorial_4.md @@ -24,7 +24,7 @@ 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 @@ -80,7 +80,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_parameter`](@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, :shared_param_name, value)` followed by `connect_param!(model_name, :component_name, :parameter_name, :shared_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]`. @@ -99,16 +99,16 @@ 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) + # 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) # Note that connect_param! was used here. @@ -122,7 +122,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!` 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`. Now we can run the model and examine the results: @@ -153,7 +153,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. @@ -297,15 +297,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 @@ -346,4 +345,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 3e1c8e6b8..68264e02c 100644 --- a/docs/src/tutorials/tutorial_5.md +++ b/docs/src/tutorials/tutorial_5.md @@ -134,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) - - # set parameters for emissions component - set_param!(m, :emissions, :sigma, sigma) + 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, :emissions, :sigma, sigma) connect_param!(m, :emissions, :YGROSS, :grosseconomy, :YGROSS) return m @@ -177,13 +176,18 @@ There are two ways of assigning random variables to model parameters in the `@de 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 our shared model parameter `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 it's 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. @@ -206,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)] diff --git a/src/core/connections.jl b/src/core/connections.jl index 5b0cd3bec..636b33dc8 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -908,7 +908,7 @@ function _update_nothing_param!(obj::AbstractCompositeComponentDef, name::Symbol end """ - update_params!(m::Model, parameters::Dict; update_timesteps = nothing) + 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 model parameter identified by k to value v. @@ -917,7 +917,7 @@ For updating unshared parameters, each key k must be a Tuple matching the name o 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 component definition. +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 diff --git a/src/core/model.jl b/src/core/model.jl index d876dc491..4ed74e345 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -199,7 +199,7 @@ For updating unshared parameters, each key k must be a Tuple matching the name o 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 component definition. +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 From cdba19d31465bca061088d72b43b7aa16976652e Mon Sep 17 00:00:00 2001 From: lrennels Date: Sun, 30 May 2021 18:04:52 -0700 Subject: [PATCH 38/47] Add TODOs --- docs/make.jl | 3 ++- docs/src/howto/howto_5.md | 2 +- docs/src/howto/howto_9.md | 4 ++++ docs/src/howto/howto_main.md | 3 +++ src/core/connections.jl | 6 ++++-- 5 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 docs/src/howto/howto_9.md diff --git a/docs/make.jl b/docs/make.jl index 57f7fa583..4c4e1a612 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -24,7 +24,8 @@ makedocs( "5 Parameters + Variables" => "howto/howto_5", "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" + "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/howto/howto_5.md b/docs/src/howto/howto_5.md index cc0bbd460..e0247dc81 100644 --- a/docs/src/howto/howto_5.md +++ b/docs/src/howto/howto_5.md @@ -148,7 +148,7 @@ As you see in the error message, if you want to override this error, you can use ```julia connect_param!(m, :B, :p2, :shared_param, ignoreunits=true) ``` -#### Setting Parameters with a Dictionary with `set_leftover_params!` +#### Setting Parameters with a Dictionary with `update_leftover_params!` [TODO] diff --git a/docs/src/howto/howto_9.md b/docs/src/howto/howto_9.md new file mode 100644 index 000000000..e79513c80 --- /dev/null +++ b/docs/src/howto/howto_9.md @@ -0,0 +1,4 @@ +# How-to Guide 9: Port to New Parameter API +## ... phasing out `set_param!` for all `update_param!` + +[TODO] diff --git a/docs/src/howto/howto_main.md b/docs/src/howto/howto_main.md index e5cf53f18..f9b60da34 100644 --- a/docs/src/howto/howto_main.md +++ b/docs/src/howto/howto_main.md @@ -28,3 +28,6 @@ If you find a bug in these guides, or have a clarifying question or suggestion, [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/src/core/connections.jl b/src/core/connections.jl index 636b33dc8..538c3d22e 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -270,7 +270,7 @@ function _connect_param!(obj::AbstractCompositeComponentDef, end - # TODO: potentially unsafe way to add parameter, advise using create_model_param! + # NB: potentially unsafe way to add parameter, advise using create_model_param! # and add_model_param! combo if possible ... but would need a specific ParameterDef add_model_array_param!(obj, dst_par_name, values, dst_dims) backup_param_name = dst_par_name @@ -464,6 +464,8 @@ function unconnected_params(obj::AbstractCompositeComponentDef) return setdiff(subcomp_params(obj), connection_refs(obj)) end +# TODO enhance to work for unshared mode parameters, similar to how update_params! +# works and turn into update_leftover_params! """ set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) @@ -864,7 +866,7 @@ 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) - # TODO: potentially unsafe way to add parameter, advise using create_model_param! + # NB: potentially unsafe way to add parameter, advise using create_model_param! # and add_model_param! combo if possible ... but would need a specific ParameterDef add_model_param!(obj, name, ArrayModelParameter(new_timestep_array, dim_names(param), param.is_shared)) else From 5f6122deb76ff2ddfc7af6f9faf1eef1bac167c8 Mon Sep 17 00:00:00 2001 From: lrennels Date: Mon, 31 May 2021 14:08:39 -0700 Subject: [PATCH 39/47] Work on documentation --- docs/make.jl | 2 +- docs/src/howto/howto_1.md | 10 +- docs/src/howto/howto_2.md | 8 +- docs/src/howto/howto_3.md | 12 +- docs/src/howto/howto_5.md | 12 +- docs/src/howto/howto_9.md | 178 ++++++++++++++++++ .../src/howto_advanced/howto_adv_buildinit.md | 4 +- .../src/howto_advanced/howto_adv_datumrefs.md | 10 +- .../Mimi Meeting_5_26_2021.ipynb | 0 .../Mimi Meetings -5_26_2021.png} | Bin docs/src/ref/ref_composites.md | 2 + docs/src/ref/ref_structures_classes_types.md | 2 + docs/src/ref/ref_structures_definitions.md | 2 +- docs/src/tutorials/tutorial_3.md | 2 +- docs/src/tutorials/tutorial_4.md | 6 +- docs/src/tutorials/tutorial_5.md | 6 +- src/Mimi.jl | 1 + src/core/build.jl | 17 ++ src/core/connections.jl | 79 +++++++- src/core/instances.jl | 6 + src/core/model.jl | 3 +- src/mcs/defmcs.jl | 4 +- src/mcs/delta.jl | 16 +- src/mcs/mcs_types.jl | 2 +- src/mcs/montecarlo.jl | 16 +- src/mcs/sobol.jl | 13 +- test/test_new_paramAPI.jl | 2 +- 27 files changed, 352 insertions(+), 63 deletions(-) rename docs/src/{wip => internals}/Mimi Meeting_5_26_2021.ipynb (100%) rename docs/src/{wip/Mimi Meetings - 20210506.png => internals/Mimi Meetings -5_26_2021.png} (100%) diff --git a/docs/make.jl b/docs/make.jl index 4c4e1a612..b3f305fda 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -21,7 +21,7 @@ makedocs( "2 Explore Results" => "howto/howto_2.md", "3 Monte Carlo + SA" => "howto/howto_3.md", "4 Timesteps" => "howto/howto_4.md", - "5 Parameters + Variables" => "howto/howto_5", + "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", diff --git a/docs/src/howto/howto_1.md b/docs/src/howto/howto_1.md index 4c19c6273..08f1715d9 100644 --- a/docs/src/howto/howto_1.md +++ b/docs/src/howto/howto_1.md @@ -48,7 +48,7 @@ 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 @@ -103,7 +103,7 @@ 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 shared model parameter that will point to all subcomponent parameters with that name. For example, given leaf components `A` and `B`: @@ -161,9 +161,9 @@ 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!` 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) @@ -308,4 +308,4 @@ set_param!(m, :foo4, 20) set_param!(m, :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! +Take a look at what you've created now using [`explore(m)`](@ref), 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 bfebb01f0..269193cdd 100644 --- a/docs/src/howto/howto_3.md +++ b/docs/src/howto/howto_3.md @@ -2,13 +2,13 @@ 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. -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` 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. @@ -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]) @@ -328,14 +328,14 @@ 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) ``` @@ -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) ``` diff --git a/docs/src/howto/howto_5.md b/docs/src/howto/howto_5.md index e0247dc81..ad6bfecf0 100644 --- a/docs/src/howto/howto_5.md +++ b/docs/src/howto/howto_5.md @@ -13,7 +13,7 @@ In the next few subsections we will present the API for setting, connecting, and along with the useful functions for batch setting: - [`update_params!`](@ref) -- [`set_leftover_params!`](@ref) +- `update_leftover_params!` ### Creating a Model @@ -65,13 +65,13 @@ 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. -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. +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. Note the `dims` argument above. When you add a shared model parameter that will be connected to non-scalar parameters like above, you need to specify the dimensions in a similar fashion to what is done in the `@defcomp` call so that appropriate allocation and checks can be made. This is not necessary for parameters without (named) dimensions. Appropriate error messages will instruct you to designate these if you forget to do so, and also recognize if you try to connect a parameter to a shared model parameter that does not have the right data size. As above, checks on data types will also be performed. +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. Note the `dims` argument above. When you add a shared model parameter that will be connected to non-scalar parameters like above, you need to specify the dimensions in a similar fashion to what is done in the [`@defcomp`](@ref) call so that appropriate allocation and checks can be made. This is not necessary for parameters without (named) dimensions. Appropriate error messages will instruct you to designate these if you forget to do so, and also recognize if you try to connect a parameter to a shared model parameter that does not have the right data size. As above, checks on data types will also be performed. In the third case we want to connect `B`'s `p5` to `A`'s `v1`, and we can do so with: ```julia @@ -92,7 +92,7 @@ To **update a parameter connected to an unshared model parameter**, use the same update_param!(m, :A, :p1, 5) ``` -To **update parameters connected to a shared model parameter**, use [`update_param`](@ref) with different arguments, specifying the shared model parameter name: +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, [10,11,12,13,14,15]) ``` @@ -158,13 +158,11 @@ You can batch update a set of parameters using a `Dict` and the function [`updat ```julia update_params!(m::Model, parameters::Dict) ``` -For each (k, v) pair in the provided `parameters` dictionary, `update_param!` 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 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) diff --git a/docs/src/howto/howto_9.md b/docs/src/howto/howto_9.md index e79513c80..4238e0ccc 100644 --- a/docs/src/howto/howto_9.md +++ b/docs/src/howto/howto_9.md @@ -1,4 +1,182 @@ # How-to Guide 9: Port to New Parameter API ## ... phasing out `set_param!` for all `update_param!` +In the most recent feature release, Mimi has moved towards a new 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!`, to the new API, which phases out `set_param!`. 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. + +## Summary of the New API (See How-to Guide 5 for Details) + +Here we briefly summarize the new, encouraged parameter API. We encourage users to follow-up by reading How-to Guide 5's "Parameters" section for a detailed description of this new API, since the below is a summary for brevity and to avoid duplication. Below a short section will also note a related change to the `@defsim` 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!` + +### Monte Carlo Simulations + +We have introduced new syntax to the monte carlo simulation definition macro `@defsim` to handle both shared and unshared parameters. This is presented below: +Previously, one would always assign a random variable to a model parameter with syntax like: +```julia +myparameter = Normal(0,1) +# or +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 +```julia +mycomponent.myparameter = Normal(0,1) +# or +rv(myrv) = Normal(0,1) +mycomponent.myparameter = myrv +``` + +## Porting to the new API + +On a high level, calls to `set_param!` 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` 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 will also need to make related updates to `@defsim` monte carlo simulation definitions. + +** This section is not exhaustive, especially since `set_param!` 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* + +An old API call to `set_param!` is equivalent to a combination of calls to `add_shared_param!` and `connect_param!`. For example, +```julia +set_param!(m, comp_name, param_name, model_param_name, value) +``` +is equivalent to +```julia +add_shared_param!(m, shared_param_name, value) +connect_param!(m, comp_name, param_name, shared_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` +``` + +An old API call to `update_param!` has the same function as previously: +```julia +update_param!(m, shared_param_name, value) +``` +will update a shared model parameter with name `shared_param_name` to `value`, thus updating all component/parameter pairs externally connected to this shared model parameter, while our new call that previously was not in the API +```julia +update_param!(m, comp_name, param_name, value) +``` +will update the unshared model parameter externally connected to `comp_name`'s `param_name` to `value`. + +*The User Change* + +Taking a look at your code, if you see a call to `set_param!`, 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!` 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!` to `update_param!` with the same arguments: +```julia +update_param!(m, comp_name, param_name, value) +``` +Recall that now you do not have a model parameter accessible using just `param_name`, your unshared model parameter has an under-the-hood unique name to prevent collisions, and you will only be able to access it with a combination of `comp_name` and `param_name`. Updating this parameter in the future can thus use 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!` 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!` 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 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, shared_param_name, value) +connect_param!(m, comp_name_1, param_name_1, shared_param_name) +connect_param!(m, comp_name_2, param_name_2, shared_param_name) +``` +Now you have a shared model parameter accessible with `shared_param_name` and updating this parameter in the future can thus use the three argument `update_param!` syntax: +```julia +update_param!(m, shared_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_param!` 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) +``` + +*The User Change* + +Current calls to `update_params!` 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!` + [TODO] + +### 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! + +Thus, 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_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/wip/Mimi Meeting_5_26_2021.ipynb b/docs/src/internals/Mimi Meeting_5_26_2021.ipynb similarity index 100% rename from docs/src/wip/Mimi Meeting_5_26_2021.ipynb rename to docs/src/internals/Mimi Meeting_5_26_2021.ipynb diff --git a/docs/src/wip/Mimi Meetings - 20210506.png b/docs/src/internals/Mimi Meetings -5_26_2021.png similarity index 100% rename from docs/src/wip/Mimi Meetings - 20210506.png rename to docs/src/internals/Mimi Meetings -5_26_2021.png diff --git a/docs/src/ref/ref_composites.md b/docs/src/ref/ref_composites.md index 5da2bbf14..06f04653b 100644 --- a/docs/src/ref/ref_composites.md +++ b/docs/src/ref/ref_composites.md @@ -7,3 +7,5 @@ To the degree possible, composite components are designed to operate the same as 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. 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_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 b84efbf91..9abe1de6d 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` and `@defcomposite` macros, and the public API (`add_comp!`, `update_param!`, `connect_param!`, 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. diff --git a/docs/src/tutorials/tutorial_3.md b/docs/src/tutorials/tutorial_3.md index 16b4c2c70..263a78648 100644 --- a/docs/src/tutorials/tutorial_3.md +++ b/docs/src/tutorials/tutorial_3.md @@ -18,7 +18,7 @@ Possible modifications range in complexity, from simply altering parameter value ## 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), [`add_shared_param`](@ref) and [`connect_param!`](@ref)**. For detail on these functions see the How To Guide 5: Parameters and Variables and 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 and the API reference guide, Reference Guide: The Mimi API. The parameters in the original model receive their values either from exogenously set model parameters (shared or unshared as described in How To Guide 5) through external parameter connections, or from another component's variable through an internal parameter connection. diff --git a/docs/src/tutorials/tutorial_4.md b/docs/src/tutorials/tutorial_4.md index 312641d2c..1fe0a52f5 100644 --- a/docs/src/tutorials/tutorial_4.md +++ b/docs/src/tutorials/tutorial_4.md @@ -15,7 +15,7 @@ Working through the following tutorial will require: 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. @@ -80,7 +80,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, [`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_parameter`](@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, :shared_param_name, value)` followed by `connect_param!(model_name, :component_name, :parameter_name, :shared_param_name)` twice, once for each component. +* 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, :shared_param_name, value)` followed by `connect_param!(model_name, :component_name, :parameter_name, :shared_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]`. @@ -122,7 +122,7 @@ construct_model (generic function with 1 method) ``` -Note that as an alternative to using many of the `update_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: diff --git a/docs/src/tutorials/tutorial_5.md b/docs/src/tutorials/tutorial_5.md index 68264e02c..803c02a26 100644 --- a/docs/src/tutorials/tutorial_5.md +++ b/docs/src/tutorials/tutorial_5.md @@ -255,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: @@ -269,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) @@ -381,7 +381,7 @@ A small set of unexported functions are available to modify an existing `Simulat * `add_save!` * `get_simdef_rvnames` * `set_payload!` -* `payload` +* `payload`] #### Full list of keyword options for running a simulation diff --git a/src/Mimi.jl b/src/Mimi.jl index 4d8479769..2aab7ec92 100644 --- a/src/Mimi.jl +++ b/src/Mimi.jl @@ -48,6 +48,7 @@ export TimestepValue, update_param!, update_params!, + update_leftover_params!, # variables, variable_dimensions, variable_names diff --git a/src/core/build.jl b/src/core/build.jl index 6b3246b58..29bdb9f5b 100644 --- a/src/core/build.jl +++ b/src/core/build.jl @@ -358,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) @@ -371,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 @@ -458,6 +470,11 @@ 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) diff --git a/src/core/connections.jl b/src/core/connections.jl index 538c3d22e..af841ef37 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -369,7 +369,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 @@ -464,17 +464,15 @@ function unconnected_params(obj::AbstractCompositeComponentDef) return setdiff(subcomp_params(obj), connection_refs(obj)) end -# TODO enhance to work for unshared mode parameters, similar to how update_params! -# works and turn into update_leftover_params! """ - set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) + update_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T -Set all of the parameters in `ModelDef` `md` 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, and all resulting new model parameters will be shared parameters. """ -function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T +function update_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T for param_ref in nothing_params(md) param_name = param_ref.datum_name @@ -498,15 +496,41 @@ function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T nothing end -# Find internal param conns to a given destination component +""" + set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) + +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 strings that match the names of unset parameters in the model, +and all resulting new model parameters will be shared parameters. +""" +function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T + @warn "The function `set_leftover_params! has been deprecated, please use `update_leftover_params!` with the same arguments." + update_leftover_params(!md, parameters) +end +""" + internal_param_conns(obj::AbstractCompositeComponentDef, dst_comp_path::ComponentPath) + +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) @@ -516,23 +540,45 @@ 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 +""" + 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") + 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) @@ -716,6 +762,13 @@ 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.model_params[name] @@ -730,6 +783,14 @@ function update_param!(mi::ModelInstance, name::Symbol, value) return nothing 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)] diff --git a/src/core/instances.jl b/src/core/instances.jl index c5607d638..e54ba0e05 100644 --- a/src/core/instances.jl +++ b/src/core/instances.jl @@ -329,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 4ed74e345..9378e889a 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -504,7 +504,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. """ diff --git a/src/mcs/defmcs.jl b/src/mcs/defmcs.jl index c28ccbbac..4c6622b41 100644 --- a/src/mcs/defmcs.jl +++ b/src/mcs/defmcs.jl @@ -65,11 +65,11 @@ function _make_dims(args) end """ - defsim(expr) + defsim(expr::Expr) Define a Mimi `SimulationDef` with the expressions in `expr`. """ -macro defsim(expr) +macro defsim(expr::Expr) let # to make vars local to each macro invocation local _rvs = [] local _transforms = [] 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 926daaeb2..e0963a197 100644 --- a/src/mcs/mcs_types.jl +++ b/src/mcs/mcs_types.jl @@ -218,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 ed2c28ecb..857aa026e 100644 --- a/src/mcs/montecarlo.jl +++ b/src/mcs/montecarlo.jl @@ -455,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, @@ -466,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. @@ -698,8 +698,8 @@ function _get_flat_model_list_names(sim_inst::SimulationInstance{T}) where T <: 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`. """ @@ -708,8 +708,8 @@ function set_models!(sim_inst::SimulationInstance{T}, models::Vector{M}) where { _reset_results!(sim_inst) # sets results vector to same length end -""" - 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 Simulation held by `sim_inst`. """ 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/test_new_paramAPI.jl b/test/test_new_paramAPI.jl index c7703fdec..1d94ce2c9 100644 --- a/test/test_new_paramAPI.jl +++ b/test/test_new_paramAPI.jl @@ -99,7 +99,7 @@ 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 # -# Section 2. set_leftover_params! +# Section 2. update_leftover_params! # # TODO From 801ad26de74482eea5897809ba5c57466c2116da Mon Sep 17 00:00:00 2001 From: lrennels Date: Mon, 31 May 2021 14:51:52 -0700 Subject: [PATCH 40/47] Fix docstrings --- docs/src/howto/howto_1.md | 6 ++-- docs/src/howto/howto_3.md | 16 +++++------ docs/src/howto/howto_5.md | 6 ++-- docs/src/howto/howto_9.md | 32 +++++++++++----------- docs/src/ref/ref_API.md | 26 ++++++++++++------ docs/src/ref/ref_composites.md | 2 +- docs/src/ref/ref_structures_definitions.md | 12 ++++---- docs/src/tutorials/tutorial_3.md | 2 +- docs/src/tutorials/tutorial_5.md | 8 +++--- src/Mimi.jl | 1 + src/core/connections.jl | 4 +-- src/core/defcomposite.jl | 2 +- src/core/model.jl | 12 ++++++++ 13 files changed, 75 insertions(+), 54 deletions(-) diff --git a/docs/src/howto/howto_1.md b/docs/src/howto/howto_1.md index 08f1715d9..69f9cbacc 100644 --- a/docs/src/howto/howto_1.md +++ b/docs/src/howto/howto_1.md @@ -54,7 +54,7 @@ By default, all parameters and variables defined in the [`@defcomp`](@ref) will 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. @@ -161,7 +161,7 @@ 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`](@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 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!`](@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. @@ -308,4 +308,4 @@ set_param!(m, :foo4, 20) set_param!(m, :par_1_1, collect(1:length(2005:2020))) run(m) ``` -Take a look at what you've created now using [`explore(m)`](@ref), a peek into what you can learn in How To Guide 2! +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_3.md b/docs/src/howto/howto_3.md index 269193cdd..da8a74571 100644 --- a/docs/src/howto/howto_3.md +++ b/docs/src/howto/howto_3.md @@ -6,15 +6,15 @@ Mimi includes a host of routines which support running Monte Carlo simulations a 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 within that`, generates all trial data with `generate_trials!`, and has several with optional parameters and optional callback functions to customize simulation behavior. +2. The `run` 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 +1. The [`@defsim`](@ref) macro 2. The `run` function 3. The `analyze` function 4. Plotting and the Explorer UI @@ -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`: @@ -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 @@ -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)) @@ -106,7 +106,7 @@ The macro next defines how to apply the values generated by each RV to model par - `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 @@ -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`) diff --git a/docs/src/howto/howto_5.md b/docs/src/howto/howto_5.md index ad6bfecf0..c7570a97a 100644 --- a/docs/src/howto/howto_5.md +++ b/docs/src/howto/howto_5.md @@ -13,7 +13,7 @@ In the next few subsections we will present the API for setting, connecting, and along with the useful functions for batch setting: - [`update_params!`](@ref) -- `update_leftover_params!` +- [`update_leftover_params!`](@ref) ### Creating a Model @@ -192,7 +192,7 @@ When a user sets a parameter, Mimi checks that the size and dimensions match wha ## Variables -[PLACEHOLDER for requested details on Variables] +[TODO] ## DataType specification of Parameters and Variables @@ -200,7 +200,7 @@ By default, the Parameters and Variables defined by a user will be allocated sto ``` 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: +But you can also specify individual Parameters or Variables to have different data types with the following syntax in a [`@defcomp`](@ref) macro: ``` @defcomp example begin p1 = Parameter{Bool}() # ScalarModelParameter that is a Bool diff --git a/docs/src/howto/howto_9.md b/docs/src/howto/howto_9.md index 4238e0ccc..9ddbe548b 100644 --- a/docs/src/howto/howto_9.md +++ b/docs/src/howto/howto_9.md @@ -3,11 +3,11 @@ In the most recent feature release, Mimi has moved towards a new 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!`, to the new API, which phases out `set_param!`. 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 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. ## Summary of the New API (See How-to Guide 5 for Details) -Here we briefly summarize the new, encouraged parameter API. We encourage users to follow-up by reading How-to Guide 5's "Parameters" section for a detailed description of this new API, since the below is a summary for brevity and to avoid duplication. Below a short section will also note a related change to the `@defsim` Monte Carlo Simulation macro. +Here we briefly summarize the new, encouraged parameter API. We encourage users to follow-up by reading How-to Guide 5's "Parameters" section for a detailed description of this new API, since the below is a summary for brevity and to avoid duplication. Below a short section will also note a related change to the [`@defsim`](@ref) Monte Carlo Simulation macro. ### Parameters @@ -22,11 +22,11 @@ In the next few subsections we will present the API for setting, connecting, and along with the useful functions for batch setting: - [`update_params!`](@ref) -- `update_leftover_params!` +- [`update_leftover_params!`]@ref) ### Monte Carlo Simulations -We have introduced new syntax to the monte carlo simulation definition macro `@defsim` to handle both shared and unshared parameters. This is presented below: +We have introduced new syntax to the monte carlo simulation definition macro [`@defsim`](@ref) to handle both shared and unshared parameters. This is presented below: Previously, one would always assign a random variable to a model parameter with syntax like: ```julia myparameter = Normal(0,1) @@ -44,17 +44,17 @@ mycomponent.myparameter = myrv ## Porting to the new API -On a high level, calls to `set_param!` 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` and then never reset, these will automatically be **unshared** model parameters. +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 will also need to make related updates to `@defsim` monte carlo simulation definitions. +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 will also need to make related updates to [`@defsim`](@ref) Monte Carlo simulation definitions. -** This section is not exhaustive, especially since `set_param!` 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!** +** 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* -An old API call to `set_param!` is equivalent to a combination of calls to `add_shared_param!` and `connect_param!`. For example, +An old API call to [`set_param!`](@ref) is equivalent to a 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) ``` @@ -73,7 +73,7 @@ add_shared_param!(m, model_param_name, value) # shared parameter gets the same n connect_param!(m, comp_name, param_name, param_name) # once per component with a parameter named `param_name` ``` -An old API call to `update_param!` has the same function as previously: +An old API call to [`update_param!`](@ref) has the same function as previously: ```julia update_param!(m, shared_param_name, value) ``` @@ -85,11 +85,11 @@ will update the unshared model parameter externally connected to `comp_name`'s ` *The User Change* -Taking a look at your code, if you see a call to `set_param!`, 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!` with four arguments: +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!` to `update_param!` with the same arguments: +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) ``` @@ -98,7 +98,7 @@ Recall that now you do not have a model parameter accessible using just `param_n 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!` like: +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) ``` @@ -108,13 +108,13 @@ 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!` 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 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: +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 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, shared_param_name, value) connect_param!(m, comp_name_1, param_name_1, shared_param_name) connect_param!(m, comp_name_2, param_name_2, shared_param_name) ``` -Now you have a shared model parameter accessible with `shared_param_name` and updating this parameter in the future can thus use the three argument `update_param!` syntax: +Now you have a shared model parameter accessible with `shared_param_name` and updating this parameter in the future can thus use the three argument [`update_param!`](@ref) syntax: ```julia update_param!(m, shared_param_name, new_value) ``` @@ -129,7 +129,7 @@ The signature for this function is: ```julia update_params!(m::Model, parameters::Dict) ``` -For each (k, v) pair in the provided `parameters` dictionary, `update_param!` 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 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 `shared_param` connected to several component parameters, and two unshared model parameters `p1` and `p2` in a component `A`: ```julia @@ -146,7 +146,7 @@ update_params!(m, dict) *The User Change* -Current calls to `update_params!` 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. +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!` diff --git a/docs/src/ref/ref_API.md b/docs/src/ref/ref_API.md index 980f90245..c8c565e61 100644 --- a/docs/src/ref/ref_API.md +++ b/docs/src/ref/ref_API.md @@ -2,15 +2,17 @@ ```@docs @defcomp +@defsim +@defcomposite MarginalModel Model -add_comp! -add_shared_param! +add_comp! +add_shared_param! connect_param! create_marginal_model delete_param! dim_count -dim_keys +dim_key dim_key_dict disconnect_param! explore @@ -21,17 +23,23 @@ get_var_value hasvalue is_first is_last +is_time +is_timestep modeldef -parameter_names +name parameter_dimensions +parameter_names replace! -set_dimension! -set_leftover_params! -set_param! +replace_comp! +set_dimension! +set_leftover_params! +set_param! TimestepIndex TimestepValue -variable_dimensions -variable_names update_param! update_params! +update_leftover_params! +# variables +variable_dimensions +variable_names ``` diff --git a/docs/src/ref/ref_composites.md b/docs/src/ref/ref_composites.md index 06f04653b..19268c34c 100644 --- a/docs/src/ref/ref_composites.md +++ b/docs/src/ref/ref_composites.md @@ -4,7 +4,7 @@ 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. diff --git a/docs/src/ref/ref_structures_definitions.md b/docs/src/ref/ref_structures_definitions.md index 9abe1de6d..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!`, `update_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 @@ -100,9 +100,9 @@ storage allocated for the variable. `ExternalParameterConnection` Values that are exogenous to the model are defined in model 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!()`. +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. diff --git a/docs/src/tutorials/tutorial_3.md b/docs/src/tutorials/tutorial_3.md index 263a78648..ebc43cde7 100644 --- a/docs/src/tutorials/tutorial_3.md +++ b/docs/src/tutorials/tutorial_3.md @@ -100,7 +100,7 @@ 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 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!` 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. 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: diff --git a/docs/src/tutorials/tutorial_5.md b/docs/src/tutorials/tutorial_5.md index 803c02a26..b28469753 100644 --- a/docs/src/tutorials/tutorial_5.md +++ b/docs/src/tutorials/tutorial_5.md @@ -169,9 +169,9 @@ 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 @@ -190,7 +190,7 @@ Note here that if we have a shared model parameter we can assign based on it's n **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 @@ -381,7 +381,7 @@ A small set of unexported functions are available to modify an existing `Simulat * `add_save!` * `get_simdef_rvnames` * `set_payload!` -* `payload`] +* `payload` #### Full list of keyword options for running a simulation diff --git a/src/Mimi.jl b/src/Mimi.jl index 2aab7ec92..31de7b397 100644 --- a/src/Mimi.jl +++ b/src/Mimi.jl @@ -40,6 +40,7 @@ export # parameters, parameter_dimensions, parameter_names, + replace!, replace_comp!, set_dimension!, set_leftover_params!, diff --git a/src/core/connections.jl b/src/core/connections.jl index af841ef37..b342e0cea 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -497,7 +497,7 @@ function update_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T end """ - set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) + set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T 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 @@ -506,7 +506,7 @@ and all resulting new model parameters will be shared parameters. """ function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T @warn "The function `set_leftover_params! has been deprecated, please use `update_leftover_params!` with the same arguments." - update_leftover_params(!md, parameters) + update_leftover_params(md, parameters) end """ internal_param_conns(obj::AbstractCompositeComponentDef, dst_comp_path::ComponentPath) diff --git a/src/core/defcomposite.jl b/src/core/defcomposite.jl index 8ae3672ee..49ccee3ff 100644 --- a/src/core/defcomposite.jl +++ b/src/core/defcomposite.jl @@ -125,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/model.jl b/src/core/model.jl index 9378e889a..c9086c702 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -172,6 +172,18 @@ function set_leftover_params!(m::Model, parameters::Dict{T, Any}) where T set_leftover_params!(m.md, parameters) end +""" + update_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 that match the names of unset parameters in the model, +and all resulting new model parameters will be shared parameters. +""" +function update_leftover_params!(m::Model, parameters::Dict{T, Any}) where T + update_leftover_params!(m.md, parameters) +end + """ update_param!(m::Model, name::Symbol, value; update_timesteps = nothing) From 4e6419a1b582e83336ca2108e3ee82d5111e48b0 Mon Sep 17 00:00:00 2001 From: lrennels Date: Mon, 31 May 2021 15:06:46 -0700 Subject: [PATCH 41/47] Fix bug --- src/core/connections.jl | 2 +- test/runtests.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/connections.jl b/src/core/connections.jl index b342e0cea..95b1a64aa 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -506,7 +506,7 @@ and all resulting new model parameters will be shared parameters. """ function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T @warn "The function `set_leftover_params! has been deprecated, please use `update_leftover_params!` with the same arguments." - update_leftover_params(md, parameters) + update_leftover_params!(md, parameters) end """ internal_param_conns(obj::AbstractCompositeComponentDef, dst_comp_path::ComponentPath) diff --git a/test/runtests.jl b/test/runtests.jl index ab34a2f01..3dfc41117 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -53,8 +53,8 @@ Electron.prep_test_env() @info("test_replace_comp.jl") @time include("test_replace_comp.jl") - @info("test_tools.jl") - @time include("test_tools.jl") + # @info("test_tools.jl") + # @time include("test_tools.jl") @info("test_parameter_labels.jl") @time include("test_parameter_labels.jl") From 20a0af0ab4dd4eb781f528b6de20600afd6cd89f Mon Sep 17 00:00:00 2001 From: lrennels Date: Mon, 31 May 2021 20:30:12 -0700 Subject: [PATCH 42/47] Fix tests and an error message --- src/core/connections.jl | 2 +- test/runtests.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/connections.jl b/src/core/connections.jl index 95b1a64aa..5defbf464 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -165,7 +165,7 @@ function connect_param!(obj::AbstractCompositeComponentDef, comp_def::AbstractCo param_units = parameter_unit(comp_def, param_name) units_match = true - errorstring = string("Units of $(nameof(compdef)):$param_name ($param_units) do not match ", + errorstring = string("Units of $(nameof(comp_def)):$param_name ($param_units) do not match ", "the following other parameters connected to the same shared ", "model parameter $model_param_name. To override this error and connect anyways, ", "set the `ignoreunits` flag to true: `connect_param!(m, comp_def, param_name, ", diff --git a/test/runtests.jl b/test/runtests.jl index 3dfc41117..ab34a2f01 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -53,8 +53,8 @@ Electron.prep_test_env() @info("test_replace_comp.jl") @time include("test_replace_comp.jl") - # @info("test_tools.jl") - # @time include("test_tools.jl") + @info("test_tools.jl") + @time include("test_tools.jl") @info("test_parameter_labels.jl") @time include("test_parameter_labels.jl") From 6e6638dcab215bbd5f07fde01ca8d5812b69f664 Mon Sep 17 00:00:00 2001 From: lrennels Date: Mon, 31 May 2021 22:10:25 -0700 Subject: [PATCH 43/47] Fix dattype check --- contrib/test_all_models.jl | 34 +++++++++++++++---------------- src/core/connections.jl | 33 ++++++++++++------------------ test/runtests.jl | 2 +- test/test_composite_parameters.jl | 2 +- 4 files changed, 32 insertions(+), 39 deletions(-) diff --git a/contrib/test_all_models.jl b/contrib/test_all_models.jl index 752143293..276a0e7b7 100644 --- a/contrib/test_all_models.jl +++ b/contrib/test_all_models.jl @@ -10,26 +10,26 @@ # julia --color=yes test_all_models.jl # -packages_to_test = [ - "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/AlexandrePavlov/MimiDICE2016R2.jl", "master"), # doesn't pass in repo, just look for new failures - "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"), -] - -# 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 +# "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/AlexandrePavlov/MimiDICE2016R2.jl", "master"), # doesn't pass in repo, just look for new failures +# "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"), # ] +# 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 +] + using Pkg mktempdir() do folder_name diff --git a/src/core/connections.jl b/src/core/connections.jl index 5defbf464..52de05ce1 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -65,7 +65,7 @@ function _check_labels(obj::AbstractCompositeComponentDef, 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: $(comp_def.comp_id) Parameter: $param_name ($t1) to Model Parameter ($t2).") end comp_dims = dim_names(param_def) @@ -74,7 +74,7 @@ function _check_labels(obj::AbstractCompositeComponentDef, if ! isempty(param_dims) && size(param_dims) != size(comp_dims) d1 = size(comp_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: $(comp_def.comp_id) Parameter: $param_name ($d1) to Model Parameter ($d2).") end # Don't check sizes for ConnectorComps since they won't match. @@ -89,7 +89,7 @@ function _check_labels(obj::AbstractCompositeComponentDef, 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; model parameter :$param_name has $param_length elements.") + error("Mismatched data size for a parameter connection: dimension :$dim in $(comp_def.comp_id)'s parameter $param_name has $comp_length elements; model parameter has $param_length elements.") end end end @@ -108,24 +108,14 @@ function _check_labels(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, param_name::Symbol, mod_param::ScalarModelParameter) - # check datatype conversion - parameter_datatype = parameter(comp_def, param_name).datatype - model_parameter_dataype = typeof(mod_param.value) + param_def = parameter(comp_def, param_name) + t1 = typeof(mod_param.value) + t2 = param_def.datatype - # if the parameter_datatype is Number (the default), we just need our value - # to be a subtype of Number - if parameter_datatype == Number && !(model_parameter_dataype <: Number) - error("Cannot connect $(nameof(compdef)):$param_name, with datatype $parameter_datatype, ", - "to shared model parameter $(nameof(model_param)) with datatype $model_parameter_dataype ", - "because of $model_parameter_dataype is not a subtype of $parameter_datatype.") - - # if the parameter_datatype is not Number, it was specified exactly and needs - # an error - elseif parameter_datatype !== Number && parameter_datatype!== (typeof(mod_param.value)) - error("Cannot connect $(nameof(compdef)):$param_name, with datatype $parameter_datatype, " , - "to shared model parameter $(nameof(model_param)) with datatype $model_parameter_dataype because ", - "the two types should match.") + if !(t1 <: Union{Missing, Nothing, t2}) + error("Mismatched datatype of parameter connection: Component: $(comp_def.comp_id) Parameter: $param_name ($t1) to Model Parameter with type ($t2).") end + end """ @@ -503,9 +493,12 @@ Set all of the parameters in `ModelDef` `md` that don't have a value and are not 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, and all resulting new model parameters will be shared parameters. + +This function has been deprecated for use of `use_leftover_params!` with the same +arguments. """ function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T - @warn "The function `set_leftover_params! has been deprecated, please use `update_leftover_params!` with the same arguments." + # @warn "The function `set_leftover_params! has been deprecated, please use `update_leftover_params!` with the same arguments." update_leftover_params!(md, parameters) end """ diff --git a/test/runtests.jl b/test/runtests.jl index ab34a2f01..c444489be 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -54,7 +54,7 @@ Electron.prep_test_env() @time include("test_replace_comp.jl") @info("test_tools.jl") - @time include("test_tools.jl") + # @time include("test_tools.jl") @info("test_parameter_labels.jl") @time include("test_parameter_labels.jl") diff --git a/test/test_composite_parameters.jl b/test/test_composite_parameters.jl index 8a4dfa9b2..72ba9e785 100644 --- a/test/test_composite_parameters.jl +++ b/test/test_composite_parameters.jl @@ -168,7 +168,7 @@ 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("Units of compdef:p1 (thous \$) do not match the following", sprint(showerror, err9)) +@test occursin("Units of B:p1 (thous \$) do not match the following", sprint(showerror, err9)) # use ignoreunits flag connect_param!(m1, :B, :p1, :p1, ignoreunits=true) From c417e7c4fe7fdaed83dd2788ea586e4c7d8472cc Mon Sep 17 00:00:00 2001 From: lrennels Date: Tue, 1 Jun 2021 11:09:59 -0700 Subject: [PATCH 44/47] Add update_leftover_params --- docs/src/howto/howto_5.md | 13 +++- docs/src/howto/howto_9.md | 11 +++- docs/src/ref/ref_API.md | 5 +- src/core/connections.jl | 66 ++++++++++++-------- src/core/model.jl | 30 ++++----- test/runtests.jl | 2 +- test/test_new_paramAPI.jl | 126 +++++++++++++++++++++++++++++++++++++- 7 files changed, 206 insertions(+), 47 deletions(-) diff --git a/docs/src/howto/howto_5.md b/docs/src/howto/howto_5.md index c7570a97a..af6c3f309 100644 --- a/docs/src/howto/howto_5.md +++ b/docs/src/howto/howto_5.md @@ -148,9 +148,18 @@ As you see in the error message, if you want to override this error, you can use ```julia connect_param!(m, :B, :p2, :shared_param, ignoreunits=true) ``` -#### Setting Parameters with a Dictionary with `update_leftover_params!` +#### Batch Update Unset Parameters with a Dictionary with `update_leftover_params!` -[TODO] +It may be helpful to use a dictionary to batch update all parameters in a model `m` that have not been explicitly updated, and thus still hold the a unusable value sentinal `nothing` from intialization. In some cases, users may create this dictionary from exogenous `csv` files for ease of use. The [`update_leftover_params!`](@ref) updates the values of the sentinal `nothing` model parameters by searching for its `(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) +``` +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 Updating Parameters with `update_params!` diff --git a/docs/src/howto/howto_9.md b/docs/src/howto/howto_9.md index 9ddbe548b..b11b9d548 100644 --- a/docs/src/howto/howto_9.md +++ b/docs/src/howto/howto_9.md @@ -150,7 +150,16 @@ Current calls to [`update_params!`](@ref) will still work as long as the keys ar ### `update_leftover_params!` -[TODO] +*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` diff --git a/docs/src/ref/ref_API.md b/docs/src/ref/ref_API.md index c8c565e61..d8376698b 100644 --- a/docs/src/ref/ref_API.md +++ b/docs/src/ref/ref_API.md @@ -12,7 +12,7 @@ connect_param! create_marginal_model delete_param! dim_count -dim_key +dim_keys dim_key_dict disconnect_param! explore @@ -26,7 +26,7 @@ is_last is_time is_timestep modeldef -name +nameof parameter_dimensions parameter_names replace! @@ -39,7 +39,6 @@ TimestepValue update_param! update_params! update_leftover_params! -# variables variable_dimensions variable_names ``` diff --git a/src/core/connections.jl b/src/core/connections.jl index 52de05ce1..803365993 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -455,14 +455,48 @@ function unconnected_params(obj::AbstractCompositeComponentDef) end """ - update_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T + update_leftover_params!(md::ModelDef, parameters::Dict) 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, -and all resulting new model parameters will be shared parameters. +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 update_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T +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 @@ -473,8 +507,8 @@ function update_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T # 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, string(param_name)) - value = parameters[string(param_name)] + 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 @@ -485,22 +519,6 @@ function update_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T end nothing end - -""" - set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T - -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 strings that match the names of unset parameters in the model, -and all resulting new model parameters will be shared parameters. - -This function has been deprecated for use of `use_leftover_params!` with the same -arguments. -""" -function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T - # @warn "The function `set_leftover_params! has been deprecated, please use `update_leftover_params!` with the same arguments." - update_leftover_params!(md, parameters) -end """ internal_param_conns(obj::AbstractCompositeComponentDef, dst_comp_path::ComponentPath) @@ -977,12 +995,12 @@ 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 (k, v) in parameters if k isa Tuple model_param_name = get_model_param_name(obj, first(k), last(k)) else - model_param_name = Symbol(k) + model_param_name = k end _update_param!(obj, model_param_name, v) end diff --git a/src/core/model.jl b/src/core/model.jl index c9086c702..5d10b4273 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -158,31 +158,33 @@ 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... """ 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 +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 that match the names of unset parameters in the model, -and all resulting new model parameters will be shared parameters. +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. """ -function set_leftover_params!(m::Model, parameters::Dict{T, Any}) where T - set_leftover_params!(m.md, parameters) -end +@delegate set_leftover_params!(m::Model, parameters) => md """ update_leftover_params!(m::Model, 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 `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 that match the names of unset parameters in the model, -and all resulting new model parameters will be shared parameters. +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 update_leftover_params!(m::Model, parameters::Dict{T, Any}) where T - update_leftover_params!(m.md, parameters) -end +@delegate update_leftover_params!(m::Model, parameters) => md + """ update_param!(m::Model, name::Symbol, value; update_timesteps = nothing) diff --git a/test/runtests.jl b/test/runtests.jl index c444489be..ab34a2f01 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -54,7 +54,7 @@ Electron.prep_test_env() @time include("test_replace_comp.jl") @info("test_tools.jl") - # @time include("test_tools.jl") + @time include("test_tools.jl") @info("test_parameter_labels.jl") @time include("test_parameter_labels.jl") diff --git a/test/test_new_paramAPI.jl b/test/test_new_paramAPI.jl index 1d94ce2c9..2842b721a 100644 --- a/test/test_new_paramAPI.jl +++ b/test/test_new_paramAPI.jl @@ -99,10 +99,132 @@ 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 # -# Section 2. update_leftover_params! +# Section 2. update_leftover_params! and set_leftover_params! # -# TODO +@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! From 9a39b915abd2b9829983a4bd81fe9354ce03f870 Mon Sep 17 00:00:00 2001 From: lrennels Date: Tue, 1 Jun 2021 15:05:50 -0700 Subject: [PATCH 45/47] Edit documentation --- README.md | 2 + docs/src/explanations/exp_pkgs.md | 2 + docs/src/howto/howto_1.md | 28 +++++++----- docs/src/howto/howto_3.md | 26 +++++------ docs/src/howto/howto_5.md | 52 +++++++++++++--------- docs/src/howto/howto_6.md | 2 +- docs/src/howto/howto_9.md | 71 ++++++++++++++++--------------- docs/src/ref/ref_API.md | 1 + docs/src/tutorials/tutorial_1.md | 2 +- docs/src/tutorials/tutorial_2.md | 2 +- docs/src/tutorials/tutorial_3.md | 20 +++++---- docs/src/tutorials/tutorial_4.md | 30 +++++++------ docs/src/tutorials/tutorial_5.md | 6 +-- src/core/connections.jl | 2 +- src/core/model.jl | 2 +- 15 files changed, 139 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index f8340f9c7..74fbaf441 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ 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_7/ for a run-down of how you should update your own code.** 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 69f9cbacc..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 @@ -169,13 +169,21 @@ The [`add_comp!`](@ref) function has two more optional keyword arguments, `first 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_3.md b/docs/src/howto/howto_3.md index da8a74571..ecf7acda8 100644 --- a/docs/src/howto/howto_3.md +++ b/docs/src/howto/howto_3.md @@ -8,14 +8,14 @@ Running Monte Carlo simulations, and proximal sensitivity analysis, in Mimi can 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 within that, 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`](@ref) macro -2. The `run` function +2. The [`run`](@ref) function 3. The `analyze` function 4. Plotting and the Explorer UI 5. Other Useful Functions @@ -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) @@ -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 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. +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]])` @@ -339,7 +339,7 @@ There are several optional keyword arguments for the [`explore`](@ref) method, a ```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) diff --git a/docs/src/howto/howto_5.md b/docs/src/howto/howto_5.md index af6c3f309..22d0c983b 100644 --- a/docs/src/howto/howto_5.md +++ b/docs/src/howto/howto_5.md @@ -15,7 +15,7 @@ along with the useful functions for batch setting: - [`update_params!`](@ref) - [`update_leftover_params!`](@ref) -### Creating a Model +### Parameters when Creating a Model Take the example case of a user starting out building a two-component toy model. ```julia @@ -51,29 +51,32 @@ After the calls to [`add_comp!`](@ref), all four parameters are connected to a r 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: +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 connect all parameters to values. We have three cases here, (1) we want to update the value of an unshared parameter from `nothing` to a value or (2) we want to add a shared parameter and connect one or, more commonly, several component parameters to it (3) we want to connect a parameter to another component's variable. +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. -In the first case, we simply call [`update_param!`](@ref) ie. +**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. -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. +**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. Note the `dims` argument above. When you add a shared model parameter that will be connected to non-scalar parameters like above, you need to specify the dimensions in a similar fashion to what is done in the [`@defcomp`](@ref) call so that appropriate allocation and checks can be made. This is not necessary for parameters without (named) dimensions. Appropriate error messages will instruct you to designate these if you forget to do so, and also recognize if you try to connect a parameter to a shared model parameter that does not have the right data size. As above, checks on data types will also be performed. +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. -In the third case we want to connect `B`'s `p5` to `A`'s `v1`, and we can do so with: +Also note the `dims` argument above. When you add a shared model parameter that will be connected to non-scalar parameters like above, you need to specify the dimensions in a similar fashion to what is done in the [`@defcomp`](@ref) block so that appropriate allocation and checks can be made. This is not necessary for parameters without (named) dimensions. Appropriate error messages will instruct you to designate these if you forget to do so, and also recognize if you try to connect a parameter to a shared model parameter that does not have the right data size. As above, checks on data types will also be performed. + +**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) ``` @@ -83,35 +86,36 @@ Now all your parameters are properly connected and you may run your model. run(m) ``` -### Modifying a Model +### 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 put togther should outline how to make any changes you need. 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. +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. 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, [10,11,12,13,14,15]) +update_param!(m, :shared_param, 5) ``` -To **connect a parameter to another component's variable**, the below will disconnect the connection to the model parameter and make the internal parameter connection: +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) ``` -while a 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`: +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) ``` -To **move from connection to a shared model parameter to an unshared model parameter** use [`disconnect_param!`](@ref) followed by [`update_param!`](@ref) : +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]) ``` -noting that this last call could also be a [`connect_param!`](@ref) to another parameter or variable etc., it is now free to be reset in any way you want. +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 @@ -142,15 +146,21 @@ add_shared_param!(m, :shared_param, 100) connect_param!(m, :A, :p1, :shared_param) # no error here connect_param!(m, :B, :p2, :shared_param) -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 $] +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 Unset Parameters with a Dictionary with `update_leftover_params!` +#### Batch Update all Unset Parameters with a Dictionary + +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. -It may be helpful to use a dictionary to batch update all parameters in a model `m` that have not been explicitly updated, and thus still hold the a unusable value sentinal `nothing` from intialization. In some cases, users may create this dictionary from exogenous `csv` files for ease of use. The [`update_leftover_params!`](@ref) updates the values of the sentinal `nothing` model parameters by searching for its `(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 +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) ``` @@ -161,9 +171,9 @@ 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 Updating Parameters with `update_params!` +#### Batch Update Specified Parameters with a Dictionary -You can batch update a 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: +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) ``` @@ -206,11 +216,11 @@ When a user sets a parameter, Mimi checks that the size and dimensions match wha ## 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 diff --git a/docs/src/howto/howto_6.md b/docs/src/howto/howto_6.md index 830134fa6..d4f525753 100644 --- a/docs/src/howto/howto_6.md +++ b/docs/src/howto/howto_6.md @@ -66,4 +66,4 @@ add_comp!(m, FAIR_component) # will run from 1765 to 1950 #### 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!(m, :ParameterName, values)` function +- 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_9.md b/docs/src/howto/howto_9.md index b11b9d548..341345249 100644 --- a/docs/src/howto/howto_9.md +++ b/docs/src/howto/howto_9.md @@ -1,13 +1,13 @@ # How-to Guide 9: Port to New Parameter API -## ... phasing out `set_param!` for all `update_param!` +### ... phasing out `set_param!` for all `update_param!` -In the most recent feature release, Mimi has moved towards a new 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. +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. -## Summary of the New API (See How-to Guide 5 for Details) +## The New API -Here we briefly summarize the new, encouraged parameter API. We encourage users to follow-up by reading How-to Guide 5's "Parameters" section for a detailed description of this new API, since the below is a summary for brevity and to avoid duplication. Below a short section will also note a related change to the [`@defsim`](@ref) Monte Carlo Simulation macro. +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 @@ -22,46 +22,47 @@ In the next few subsections we will present the API for setting, connecting, and along with the useful functions for batch setting: - [`update_params!`](@ref) -- [`update_leftover_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. This is presented below: +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 -myparameter = Normal(0,1) -# or 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 -mycomponent.myparameter = Normal(0,1) -# or rv(myrv) = Normal(0,1) mycomponent.myparameter = myrv +# or the shortcut: +mycomponent.myparameter = Normal(0,1) ``` -## Porting to the new API +## 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 will also need to make related updates to [`@defsim`](@ref) Monte Carlo simulation definitions. +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!** +**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` +### `set_param!` and `update_param!` *The Mimi Change* -An old API call to [`set_param!`](@ref) is equivalent to a combination of calls to [`add_shared_param!`](@ref) and [`connect_param!`](@ref). For example, +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, shared_param_name, value) -connect_param!(m, comp_name, param_name, shared_param_name) +add_shared_param!(m, model_param_name, value) +connect_param!(m, comp_name, param_name, model_param_name) ``` and similarly a call to ```julia @@ -73,19 +74,19 @@ add_shared_param!(m, model_param_name, value) # shared parameter gets the same n connect_param!(m, comp_name, param_name, param_name) # once per component with a parameter named `param_name` ``` -An old API call to [`update_param!`](@ref) has the same function as previously: +A call to [`update_param!`](@ref) retains the same functionality, such that ```julia -update_param!(m, shared_param_name, value) +update_param!(m, model_param_name, value) ``` -will update a shared model parameter with name `shared_param_name` to `value`, thus updating all component/parameter pairs externally connected to this shared model parameter, while our new call that previously was not in the API +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) ``` -will update the unshared model parameter externally connected to `comp_name`'s `param_name` to `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. *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: +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) ``` @@ -93,7 +94,7 @@ and the desired behavior is that this component/parameter pair be connected to a ```julia update_param!(m, comp_name, param_name, value) ``` -Recall that now you do not have a model parameter accessible using just `param_name`, your unshared model parameter has an under-the-hood unique name to prevent collisions, and you will only be able to access it with a combination of `comp_name` and `param_name`. Updating this parameter in the future can thus use the same syntax: +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) ``` @@ -108,15 +109,17 @@ 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 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: +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, shared_param_name, value) -connect_param!(m, comp_name_1, param_name_1, shared_param_name) -connect_param!(m, comp_name_2, param_name_2, shared_param_name) +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 `shared_param_name` and updating this parameter in the future can thus use the three argument [`update_param!`](@ref) syntax: +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, shared_param_name, new_value) +update_param!(m, model_param_name, new_value) ``` ### `update_params!` @@ -129,18 +132,18 @@ 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 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 `shared_param` connected to several component parameters, and two unshared model parameters `p1` and `p2` in a component `A`: +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(:shared_param => 1) +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(:shared_param => 1, (:A, :p5) => 2, (:A, :p6) => 3) +dict = Dict(:model_param_name => 1, (:A, :p5) => 2, (:A, :p6) => 3) update_params!(m, dict) ``` @@ -188,4 +191,4 @@ mycomponent.myparameter = myrv 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! -Thus, 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. +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/ref/ref_API.md b/docs/src/ref/ref_API.md index d8376698b..ebbc3fb78 100644 --- a/docs/src/ref/ref_API.md +++ b/docs/src/ref/ref_API.md @@ -31,6 +31,7 @@ parameter_dimensions parameter_names replace! replace_comp! +run set_dimension! set_leftover_params! set_param! 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 30e12e86e..6c4c608e9 100644 --- a/docs/src/tutorials/tutorial_2.md +++ b/docs/src/tutorials/tutorial_2.md @@ -36,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 ebc43cde7..5acf63e64 100644 --- a/docs/src/tutorials/tutorial_3.md +++ b/docs/src/tutorials/tutorial_3.md @@ -12,31 +12,31 @@ Working through the following tutorial will require: ## 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), [`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 and 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). -The parameters in the original model receive their values either from exogenously set model parameters (shared or unshared as described in How To Guide 5) through external parameter connections, or from another component's variable through an internal parameter connection. +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 model parameter. If the model parameter is shared, obtain the shared model parameter name (often this will be the same as the parameter name by default) and use the following to update it: +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 not shared, and thus the value can only be connected to one component/parameter pair, use the following to update it: +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, :comp_name, :param_name 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. -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. 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. -**Once again, for specific instructions and details on various cases of updating and changing parameters, and their connections, please view How To Guide 5. We do not repeat all information here for brevity and to avoid duplication.** +**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 @@ -102,6 +102,8 @@ 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 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. +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 diff --git a/docs/src/tutorials/tutorial_4.md b/docs/src/tutorials/tutorial_4.md index 1fe0a52f5..d86daa89b 100644 --- a/docs/src/tutorials/tutorial_4.md +++ b/docs/src/tutorials/tutorial_4.md @@ -31,18 +31,19 @@ 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 @@ -61,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 @@ -80,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, [`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, :shared_param_name, value)` followed by `connect_param!(model_name, :component_name, :parameter_name, :shared_param_name)` twice, once for each component. +* 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]`. @@ -107,10 +108,11 @@ function construct_model() update_param!(m, :grosseconomy, :k0, 130.) update_param!(m, :grosseconomy, :share, 0.3) - # Update and connect parameters for the emissions component + # 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 diff --git a/docs/src/tutorials/tutorial_5.md b/docs/src/tutorials/tutorial_5.md index b28469753..27cf40f56 100644 --- a/docs/src/tutorials/tutorial_5.md +++ b/docs/src/tutorials/tutorial_5.md @@ -186,7 +186,7 @@ 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 it's 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. +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 @@ -237,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 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. +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 diff --git a/src/core/connections.jl b/src/core/connections.jl index 803365993..f3a827621 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -846,7 +846,7 @@ function update_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, valu 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, param_name, value)` ", + "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 ", diff --git a/src/core/model.jl b/src/core/model.jl index 5d10b4273..cdd83c51e 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -440,7 +440,7 @@ variables(m::Model, comp_name::Symbol) = variables(compdef(m, comp_name)) @delegate variable_names(m::Model, comp_name::Symbol) => md """ -function add_shared_param!(m::Model, name::Symbol, value::Any; dims::Array{Symbol}=Symbol[]) + add_shared_param!(m::Model, name::Symbol, value::Any; dims::Array{Symbol}=Symbol[]) 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 From 4bfe8b7f632630457eeea4c23dc4e04ca0b50558 Mon Sep 17 00:00:00 2001 From: lrennels Date: Wed, 2 Jun 2021 16:36:41 -0700 Subject: [PATCH 46/47] Add testing, typing clarifcication, and general cleanup --- contrib/test_all_models.jl | 65 +++++-- docs/src/howto/howto_5.md | 7 +- src/Mimi.jl | 1 - src/core/build.jl | 2 +- src/core/connections.jl | 311 +++++++++++++++++++----------- src/core/defs.jl | 27 +-- src/core/model.jl | 11 +- test/test_composite_parameters.jl | 6 +- test/test_new_paramAPI.jl | 56 +++++- 9 files changed, 332 insertions(+), 154 deletions(-) diff --git a/contrib/test_all_models.jl b/contrib/test_all_models.jl index 276a0e7b7..62cd687a6 100644 --- a/contrib/test_all_models.jl +++ b/contrib/test_all_models.jl @@ -10,24 +10,19 @@ # julia --color=yes test_all_models.jl # -# packages_to_test = [ -# "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/AlexandrePavlov/MimiDICE2016R2.jl", "master"), # doesn't pass in repo, just look for new failures -# "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"), -# ] - -# 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 + "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"), ] using Pkg @@ -63,3 +58,39 @@ mktempdir() do folder_name 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_that_errored = [] + 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 + + println() + println() + println() + + println("The following packages errored:") + for p in pkg_that_errored + println(p) + end + +end \ No newline at end of file diff --git a/docs/src/howto/howto_5.md b/docs/src/howto/howto_5.md index 22d0c983b..59a97ba06 100644 --- a/docs/src/howto/howto_5.md +++ b/docs/src/howto/howto_5.md @@ -74,7 +74,9 @@ 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. -Also note the `dims` argument above. When you add a shared model parameter that will be connected to non-scalar parameters like above, you need to specify the dimensions in a similar fashion to what is done in the [`@defcomp`](@ref) block so that appropriate allocation and checks can be made. This is not necessary for parameters without (named) dimensions. Appropriate error messages will instruct you to designate these if you forget to do so, and also recognize if you try to connect a parameter to a shared model parameter that does not have the right data size. As above, checks on data types will also be performed. +Note that there are two optional keyword arguments for `add_shared_param!`: +- **dims::Vector{Symbol}**: When you add a shared model parameter that will be connected to non-scalar parameters like above, you need to specify the dimensions in a similar fashion to what is done in the [`@defcomp`](@ref) block so that appropriate allocation and checks can be made. This is not necessary for parameters without (named) dimensions. Appropriate error messages will instruct you to designate these if you forget to do so, and also recognize if you try to connect a parameter to a shared model parameter that does not have the right data size. As above, checks on data types will also be performed. +- **data_type::DataType**: In some more advanced cases, such as when you specifify a `DataType` for a specific Parameter, you may need to specify the `DataType` of your `value` (the element `DataType` if it is an array or value `DataType` if it is Scalar) to pass later checks when connecting parameter with constrained `DataType` specifications. **Case 3.:** In the third case we want to connect `B`'s `p5` to `A`'s `v1`, and we can do so with: ```julia @@ -229,3 +231,6 @@ But you can also specify individual Parameters or Variables to have different da 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 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/src/Mimi.jl b/src/Mimi.jl index 31de7b397..2aab7ec92 100644 --- a/src/Mimi.jl +++ b/src/Mimi.jl @@ -40,7 +40,6 @@ export # parameters, parameter_dimensions, parameter_names, - replace!, replace_comp!, set_dimension!, set_leftover_params!, diff --git a/src/core/build.jl b/src/core/build.jl index 29bdb9f5b..18465bbc0 100644 --- a/src/core/build.jl +++ b/src/core/build.jl @@ -409,7 +409,7 @@ function _build(md::ModelDef) 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 or set:\n $params") + error("Cannot build model; the following parameters still have values of `nothing` and need to be updated:\n $params") end vdict = _instantiate_vars(md) diff --git a/src/core/connections.jl b/src/core/connections.jl index f3a827621..2fa48f117 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -50,31 +50,37 @@ end verify_units(unit1::AbstractString, unit2::AbstractString) = (unit1 == unit2) """ - _check_labels(obj::AbstractCompositeComponentDef, + _check_attributes(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, param_name::Symbol, mod_param::ArrayModelParameter) -Check that the labels of the ArrayModelParameter `mod_param` match the labels +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_labels(obj::AbstractCompositeComponentDef, +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(mod_param.values) t2 = eltype(param_def.datatype) if !(t1 <: Union{Missing, t2}) - error("Mismatched datatype of parameter connection: Component: $(comp_def.comp_id) Parameter: $param_name ($t1) to Model Parameter ($t2).") + error("Mismatched datatype of parameter connection: Component: $(nameof(comp_def)) ", + "Parameter: $param_name ($t2) to Model Parameter ($t1). 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(mod_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) Parameter: $param_name ($d1) to Model Parameter ($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. @@ -84,96 +90,102 @@ 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(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)'s parameter $param_name has $comp_length elements; model parameter 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 """ - _check_labels(obj::AbstractCompositeComponentDef, + _check_attributes(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, param_name::Symbol, mod_param::ScalarModelParameter) -Check that the labels of the ScalarModelParameter `mod_param` match the labels +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_labels(obj::AbstractCompositeComponentDef, +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, Nothing, t2}) - error("Mismatched datatype of parameter connection: Component: $(comp_def.comp_id) Parameter: $param_name ($t1) to Model Parameter with type ($t2).") + 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_labels::Bool=true, ignoreunits::Bool=false)) + check_attributes::Bool=true, ignoreunits::Bool=false)) Connect a parameter `param_name` in the component `comp_name` of composite `obj` to the model parameter `model_param_name`. """ function connect_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, param_name::Symbol, model_param_name::Symbol; - check_labels::Bool=true, ignoreunits::Bool = false) + check_attributes::Bool=true, ignoreunits::Bool = false) comp_def = compdef(obj, comp_name) - connect_param!(obj, comp_def, param_name, model_param_name, check_labels=check_labels, ignoreunits = ignoreunits) + connect_param!(obj, comp_def, param_name, model_param_name, check_attributes=check_attributes, ignoreunits = ignoreunits) end """ connect_param!(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, - param_name::Symbol, model_param_name::Symbol; check_labels::Bool=true, + param_name::Symbol, model_param_name::Symbol; check_attributes::Bool=true, ignoreunits::Bool = false) 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_labels::Bool=true, + param_name::Symbol, model_param_name::Symbol; check_attributes::Bool=true, ignoreunits::Bool = false) mod_param = model_param(obj, model_param_name) - # check the labels - if check_labels && !is_nothing_param(mod_param) - _check_labels(obj, comp_def, param_name, mod_param) - end + # check the attributes between the shared model parameter and the component parameter + check_attributes && _check_attributes(obj, comp_def, param_name, mod_param) - if is_shared(mod_param) && !ignoreunits - - param_units = parameter_unit(comp_def, param_name) - units_match = true - errorstring = string("Units of $(nameof(comp_def)):$param_name ($param_units) do not match ", - "the following other parameters connected to the same shared ", - "model parameter $model_param_name. 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: ") - - for conn in filter(i -> i.model_param_name == model_param_name, external_param_conns(obj)) - conn_comp_def = compdef(obj, conn.comp_path) - conn_comp_name = nameof(conn_comp_def) - conn_param_name = conn.param_name - conn_units = parameter_unit(conn_comp_def, conn_param_name) + # 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 + fields = ignoreunits ? (:dim_names, :datatype) : (:dim_names, :datatype, :unit) + collisions = _find_collisions(fields, Vector(pairs)) - if ! verify_units(param_units, conn_units) - units_match = false - errorstring = string(errorstring, "[$conn_comp_name:$conn_param_name with units $conn_units] ") + 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 - - units_match || error(errorstring) end disconnect_param!(obj, comp_def, param_name) # calls dirty!() @@ -260,8 +272,8 @@ function _connect_param!(obj::AbstractCompositeComponentDef, end - # NB: potentially unsafe way to add parameter, advise using create_model_param! - # and add_model_param! combo if possible ... but would need a specific ParameterDef + # NB: potentially unsafe way to add parameter/might be duplicating work so + # advise shifting to create_model_param! add_model_array_param!(obj, dst_par_name, values, dst_dims) backup_param_name = dst_par_name @@ -512,7 +524,7 @@ function set_leftover_params!(md::ModelDef, parameters::Dict) where T param = create_model_param(md, param_def, value; is_shared = true) add_model_param!(md, param_name, param) else - error("Cannot set parameter :$param_name, not found in provided dictionary.") + error("Cannot set shared model parameter :$param_name, not found in provided dictionary.") end end connect_param!(md, comp_name, param_name, param_name) @@ -667,7 +679,7 @@ 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? +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, @@ -866,7 +878,7 @@ 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 not found in composite's model parameters.") + error("Cannot update parameter $name; $name not found in composite's model parameters.") end # handle nothing params @@ -938,8 +950,9 @@ 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) - # NB: potentially unsafe way to add parameter, advise using create_model_param! - # and add_model_param! combo if possible ... but would need a specific ParameterDef + # NB: potentially unsafe way to add parameter/might be duplicating work so + # advise shifting to create_model_param! ... but here we are updating + # a ParameterDef instead of creating one add_model_param!(obj, name, ArrayModelParameter(new_timestep_array, dim_names(param), param.is_shared)) else copyto!(param.values.data, value) @@ -972,9 +985,7 @@ function _update_nothing_param!(obj::AbstractCompositeComponentDef, name::Symbol # Need to check the dimensions of the parameter data against component # before adding it to the model's parameter list - if !is_nothing_param(param) # shouldn't be a nothing param since we're updating to non-nothing! - _check_labels(obj, comp_def, param_name, param) - end + _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 @@ -1188,40 +1199,75 @@ an empty vector. The `is_shared` attribute of the added Model Parameter will be 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. +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[]) +function add_shared_param!(md::ModelDef, name::Symbol, value::Any; dims::Array{Symbol}=Symbol[], data_type::DataType=Nothing) - # check to make sure the parameter doesn't already exist + # 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.") - # make sure all parameter dims are in model and have the appropriate number of elements - if value isa NamedArray + # 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 value is $value. This is done with the `dims` keyword argument ", + " 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 - # create shared model parameter with a ParameterDef, which takes advantage of - # the checks and parameterization etc. in `check_model_param` - data_type = value isa AbstractArray ? eltype(value) : typeof(value) - data_type = data_type <: Number ? Number : data_type # raise any Number type to Number to avoid small errors + # get the data type to use to create ParameterDef + value_data_type = value isa AbstractArray ? eltype(value) : typeof(value) + + if data_type == Nothing # if a data_type is not provided get it from `value` + data_type = value_data_type + + # if it is not a DataType, try manually converting first ... + !(data_type isa DataType) && try convert(DataType, data_type) catch; end + # if it is still not a DataType, try converting it to the model's datatype + !(data_type isa DataType) && try convert(Number, value) catch ; end + data_type = eltype(value) + # if it still isn't a datatype, then just go with Any + if !(data_type isa DataType) + data_type = Any + end + # raise to Number to lower the constraints + if data_type <: Number + data_type = Number + end + + else # otherwise check it against value to be sure and convert + if value_data_type != data_type + try value = convert.(data_type, value) + catch + if value isa AbstractArray + error("Mismatched datatypes: elements of provided `value` have DataType $value_data_type and cannot be converted to provided `data_type` argument is $data_type.") + else + error("Mismatched datatypes: provided `value` has DataType $value_data_type and cannot be converted to provided `data_type` argument is $data_type.") + end + end + end + end + + # create the ParameterDef param_def = ParameterDef(name, nothing, data_type, dims, "", "", nothing) + + # create the model parameter param = create_model_param(md, param_def, value; is_shared = true) - - # check dimensions - model_dims = dim_names(md) - param_dims = dim_names(param_def) + # 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] @@ -1246,63 +1292,104 @@ is_shared defaults to false, and thus an unshared parameter would be created, wh 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 = param_def.dim_names - num_dims = length(param_dims) + 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) - if num_dims > 0 - param = ArrayModelParameter(value, param_dims, is_shared) - else - param = ScalarModelParameter(value, is_shared) - end - + 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 - if num_dims > 0 # array parameter case - - # check dimensions - if value isa NamedArray - dims = dimnames(value) - dims !== nothing && check_parameter_dimensions(md, value, dims, param_name) - end + + # 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) + # 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 + # 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) + param = ArrayModelParameter(values, param_dims, is_shared) + end + return param +end - else # scalar parameter case - value = convert(dtype, value) - param = ScalarModelParameter(value, is_shared) - 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 diff --git a/src/core/defs.jl b/src/core/defs.jl index 661dd1fc5..d18c72f0d 100644 --- a/src/core/defs.jl +++ b/src/core/defs.jl @@ -86,7 +86,7 @@ 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) @@ -531,18 +531,21 @@ of the dimension names of the provided data, and will be used to check that they model's index labels. """ function set_param!(md::ModelDef, comp_def::AbstractComponentDef, param_name::Symbol, model_param_name::Symbol, value; dims=nothing) - has_parameter(comp_def, param_name) || + + # 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, model_param_name) + # 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 name of unshared parameter :$param_name connected to component :$(nameof(compdef))", + " 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 ", - "`create_shared_param` paired with `connect_param!`.") + "`add_shared_param` paired with `connect_param!`.") end set_param!(md, param_name, value, dims = dims, comps = [comp_def], model_param_name = model_param_name) @@ -604,7 +607,7 @@ function set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing, ignor # 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_labels(md, comp, param_name, param) + _check_attributes(md, comp, param_name, param) end # add the shared model parameter to the model def @@ -615,9 +618,9 @@ function set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing, ignor # connect for comp in comps - # Set check_labels = false because we already checked above + # 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_labels = false, ignoreunits = ignoreunits) + connect_param!(md, comp, param_name, model_param_name, check_attributes = false, ignoreunits = ignoreunits) end nothing end @@ -890,15 +893,13 @@ function _initialize_parameter!(md::ModelDef, comp_def::AbstractComponentDef, pa # Need to check the dimensions of the parameter data against component # before adding it to the model's parameter list - if !is_nothing_param(param) - _check_labels(md, comp_def, param_name, param) - end + _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 labels since did it above - connect_param!(md, comp_def, param_name, model_param_name; check_labels = false) + # 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 diff --git a/src/core/model.jl b/src/core/model.jl index cdd83c51e..22610fe8b 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -81,13 +81,13 @@ data for the second timestep and beyond. """ connect_param!(m::Model, comp_name::Symbol, param_name::Symbol, model_param_name::Symbol; - check_labels::Bool=true, ignoreunits::Bool=false)) + check_attributes::Bool=true, ignoreunits::Bool=false)) 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, model_param_name::Symbol; - check_labels::Bool=true, ignoreunits::Bool = false) => md + 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) @@ -440,7 +440,7 @@ variables(m::Model, comp_name::Symbol) = variables(compdef(m, comp_name)) @delegate variable_names(m::Model, comp_name::Symbol) => md """ - add_shared_param!(m::Model, name::Symbol, value::Any; dims::Array{Symbol}=Symbol[]) + 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 @@ -449,9 +449,10 @@ an empty vector. The `is_shared` attribute of the added Model Parameter will be 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. +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[]) => md +@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) diff --git a/test/test_composite_parameters.jl b/test/test_composite_parameters.jl index 72ba9e785..64498b98e 100644 --- a/test/test_composite_parameters.jl +++ b/test/test_composite_parameters.jl @@ -121,7 +121,7 @@ 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 still have values of nothing and need to be updated or 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() @@ -168,13 +168,13 @@ 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("Units of B:p1 (thous \$) do not match the following", sprint(showerror, err9)) +@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 or set:", sprint(showerror, err10)) +@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() diff --git a/test/test_new_paramAPI.jl b/test/test_new_paramAPI.jl index 2842b721a..a2d2ed56a 100644 --- a/test/test_new_paramAPI.jl +++ b/test/test_new_paramAPI.jl @@ -28,7 +28,7 @@ function _get_model() return m end -# DataType, Shared vs. Unshared +# General m = _get_model() @test_throws MethodError update_param!(m, :A, :p1, 3) # can't convert @@ -98,6 +98,60 @@ 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 +# DataTypes +@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 + +# no data_type argument in add_shared_param! +m = _get_model() # number_type(m) == Float64 +add_shared_param!(m, :myparam, 5) +@test model_param(m, :myparam) isa Mimi.ScalarModelParameter{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 specified 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)) # pA1 should have a specified parameter type of Symbol and !(Float64 <: Symbol) +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)) +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} + +connect_param!(m, :B, :pB2, :myparam) + +exp = :(connect_param!(m, :A, :pA2, :myparam)) # can't connect this one it conflicts with :pB2 +myerr4 = try eval(exp) catch err err end +@test occursin("Cannot connect A:pA2 to shared model parameter myparam, it has conflicting values for the datatype of other parameters connected to this shared model parameter", sprint(showerror, myerr4)) + # # Section 2. update_leftover_params! and set_leftover_params! # From 5881f4dfe8851da9d8fb6c048f6b9a7645067f51 Mon Sep 17 00:00:00 2001 From: lrennels Date: Wed, 9 Jun 2021 01:57:09 -0600 Subject: [PATCH 47/47] Update datatype conversion rules for add_shared_param --- contrib/test_all_models.jl | 29 ++++---- docs/src/howto/howto_5.md | 10 ++- docs/src/howto/howto_9.md | 8 +++ src/core/connections.jl | 143 ++++++++++++++++++++++++++----------- src/core/defs.jl | 2 +- src/core/types/defs.jl | 8 --- test/test_new_paramAPI.jl | 59 +++++++-------- 7 files changed, 163 insertions(+), 96 deletions(-) diff --git a/contrib/test_all_models.jl b/contrib/test_all_models.jl index 62cd687a6..306755b08 100644 --- a/contrib/test_all_models.jl +++ b/contrib/test_all_models.jl @@ -10,6 +10,10 @@ # 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", "df"), # note here we use the df branch "MimiDICE2013" => ("https://github.com/anthofflab/MimiDICE2013.jl", "df"), # note here we use the df branch @@ -25,10 +29,8 @@ packages_to_test = [ "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__, ".."))) @@ -55,17 +57,15 @@ 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_that_errored = [] + Pkg.activate(folder_name) Pkg.develop(PackageSpec(path=joinpath(@__DIR__, ".."))) @@ -83,14 +83,13 @@ mktempdir() do folder_name push!(pkg_that_errored, pkg) end end +end - println() - println() - println() +println() +println() +println() - println("The following packages errored:") - for p in pkg_that_errored - println(p) - end - -end \ No newline at end of file +println("The following packages errored:") +for p in pkg_that_errored + println(p) +end diff --git a/docs/src/howto/howto_5.md b/docs/src/howto/howto_5.md index 59a97ba06..3f01b4b3b 100644 --- a/docs/src/howto/howto_5.md +++ b/docs/src/howto/howto_5.md @@ -74,9 +74,13 @@ 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. -Note that there are two optional keyword arguments for `add_shared_param!`: -- **dims::Vector{Symbol}**: When you add a shared model parameter that will be connected to non-scalar parameters like above, you need to specify the dimensions in a similar fashion to what is done in the [`@defcomp`](@ref) block so that appropriate allocation and checks can be made. This is not necessary for parameters without (named) dimensions. Appropriate error messages will instruct you to designate these if you forget to do so, and also recognize if you try to connect a parameter to a shared model parameter that does not have the right data size. As above, checks on data types will also be performed. -- **data_type::DataType**: In some more advanced cases, such as when you specifify a `DataType` for a specific Parameter, you may need to specify the `DataType` of your `value` (the element `DataType` if it is an array or value `DataType` if it is Scalar) to pass later checks when connecting parameter with constrained `DataType` specifications. +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 diff --git a/docs/src/howto/howto_9.md b/docs/src/howto/howto_9.md index 341345249..0d59d2bb8 100644 --- a/docs/src/howto/howto_9.md +++ b/docs/src/howto/howto_9.md @@ -84,6 +84,14 @@ 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: diff --git a/src/core/connections.jl b/src/core/connections.jl index 2fa48f117..aaf51618f 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -69,9 +69,10 @@ function _check_attributes(obj::AbstractCompositeComponentDef, t2 = eltype(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 ($t1). If you are using ", - "`add_shared_param!` try using the `data_type` keyword argument to specifiy ", - "data_type = $(eltype(param_def.datatype))") + "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 param_dims = dim_names(param_def) @@ -114,7 +115,7 @@ function _check_attributes(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, param_name::Symbol, mod_param::ScalarModelParameter) - is_nothing_param(mod_param) && return + is_nothing_param(mod_param) && return param_def = parameter(comp_def, param_name) t1 = typeof(mod_param.value) @@ -168,7 +169,12 @@ function connect_param!(obj::AbstractCompositeComponentDef, comp_def::AbstractCo push!(pairs, comp_def => param_name) # which fields to check for collisions in subcomponents - fields = ignoreunits ? (:dim_names, :datatype) : (:dim_names, :datatype, :unit) + # 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) @@ -273,7 +279,7 @@ function _connect_param!(obj::AbstractCompositeComponentDef, end # NB: potentially unsafe way to add parameter/might be duplicating work so - # advise shifting to create_model_param! + # 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 @@ -951,8 +957,8 @@ function _update_array_param!(obj::AbstractCompositeComponentDef, name, value) ti = get_time_index_position(param) new_timestep_array = get_timestep_array(obj, T, N, ti, value) # NB: potentially unsafe way to add parameter/might be duplicating work so - # advise shifting to create_model_param! ... but here we are updating - # a ParameterDef instead of creating one + # 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) @@ -1227,40 +1233,17 @@ function add_shared_param!(md::ModelDef, name::Symbol, value::Any; dims::Array{S " ie. : `add_shared_param!(md, name, value; dims = [:time])") end - # get the data type to use to create ParameterDef - value_data_type = value isa AbstractArray ? eltype(value) : typeof(value) - - if data_type == Nothing # if a data_type is not provided get it from `value` - data_type = value_data_type - - # if it is not a DataType, try manually converting first ... - !(data_type isa DataType) && try convert(DataType, data_type) catch; end - # if it is still not a DataType, try converting it to the model's datatype - !(data_type isa DataType) && try convert(Number, value) catch ; end - data_type = eltype(value) - # if it still isn't a datatype, then just go with Any - if !(data_type isa DataType) - data_type = Any - end - # raise to Number to lower the constraints - if data_type <: Number - data_type = Number - end - - else # otherwise check it against value to be sure and convert - if value_data_type != data_type - try value = convert.(data_type, value) - catch - if value isa AbstractArray - error("Mismatched datatypes: elements of provided `value` have DataType $value_data_type and cannot be converted to provided `data_type` argument is $data_type.") - else - error("Mismatched datatypes: provided `value` has DataType $value_data_type and cannot be converted to provided `data_type` argument is $data_type.") - end - end - end - 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 @@ -1283,6 +1266,86 @@ function add_shared_param!(md::ModelDef, name::Symbol, value::Any; dims::Array{S 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) diff --git a/src/core/defs.jl b/src/core/defs.jl index d18c72f0d..69c28fcd8 100644 --- a/src/core/defs.jl +++ b/src/core/defs.jl @@ -574,7 +574,7 @@ function set_param!(md::ModelDef, param_name::Symbol, value; dims=nothing, ignor # 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 diff --git a/src/core/types/defs.jl b/src/core/types/defs.jl index ceea822cf..d1eaedf61 100644 --- a/src/core/types/defs.jl +++ b/src/core/types/defs.jl @@ -258,14 +258,6 @@ var_name(comp_ref::VariableReference) = getfield(comp_ref, :var_name) # -- throw warnings -- -function Base.getproperty(md::ModelDef, field::Symbol) - if field == :external_params - @warn "ModelDef's `external_params` field is renamed to `model_params`, please change code accordingly." - field = :model_params - end - return getfield(md, field) -end - @deprecate external_params(md::ModelDef) model_params(md) # Deprecate old definition in favor of standard name diff --git a/test/test_new_paramAPI.jl b/test/test_new_paramAPI.jl index a2d2ed56a..02a86b465 100644 --- a/test/test_new_paramAPI.jl +++ b/test/test_new_paramAPI.jl @@ -28,7 +28,7 @@ function _get_model() return m end -# General +# General Functionality m = _get_model() @test_throws MethodError update_param!(m, :A, :p1, 3) # can't convert @@ -64,20 +64,7 @@ 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 -# Units, Shared vs. Unshared -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 - -# Default +# Defaults m = _get_model() @test model_param(m, :A, :p2).value == 2 @@ -85,7 +72,7 @@ m = _get_model() update_param!(m, :A, :p2, 100) @test !(is_shared(model_param(m, :A, :p2))) -# arrays and dimensions +# Dimensions m = _get_model() @test_throws ErrorException add_shared_param!(m, :x, [1:10]) # need dimensions to be specified @@ -98,7 +85,23 @@ 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 -# DataTypes +# 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 @@ -121,36 +124,34 @@ function _get_model() return m end -# no data_type argument in add_shared_param! -m = _get_model() # number_type(m) == Float64 +# typical behavior +m = _get_model() add_shared_param!(m, :myparam, 5) -@test model_param(m, :myparam) isa Mimi.ScalarModelParameter{Float64} +@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 specified parameter type of Number by default and Float64 <: Number +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)) # pA1 should have a specified parameter type of Symbol and !(Float64 <: Symbol) +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)) +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} -connect_param!(m, :B, :pB2, :myparam) +add_shared_param!(m, :myparam, 5; data_type = Int64) +@test model_param(m, :myparam) isa Mimi.ScalarModelParameter{Int64} # 5 is convertible to Int64 -exp = :(connect_param!(m, :A, :pA2, :myparam)) # can't connect this one it conflicts with :pB2 -myerr4 = try eval(exp) catch err err end -@test occursin("Cannot connect A:pA2 to shared model parameter myparam, it has conflicting values for the datatype of other parameters connected to this shared model parameter", sprint(showerror, myerr4)) +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!