diff --git a/.gitignore b/.gitignore index 3b3035c9f..f7c09a04b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ docs/site/ .DS_Store benchmark/tune.json docs/Manifest.toml +types-old.jl .vscode diff --git a/Project.toml b/Project.toml index 8d1d453f0..55d367192 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ version = "0.9.5-DEV" [deps] CSVFiles = "5d742f6a-9f54-50ce-8119-2520741973ca" +Classes = "1a9c1350-211b-5766-99cd-4544d885a0d1" Compose = "a81c6b42-2e10-5240-aca2-a61377ecd94b" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" diff --git a/docs/src/figs/Mimi-model-schematic-v3.png b/docs/src/figs/Mimi-model-schematic-v3.png new file mode 100644 index 000000000..77d79e1d0 Binary files /dev/null and b/docs/src/figs/Mimi-model-schematic-v3.png differ diff --git a/docs/src/internals/composite_components.md b/docs/src/internals/composite_components.md new file mode 100644 index 000000000..c1f6dc07a --- /dev/null +++ b/docs/src/internals/composite_components.md @@ -0,0 +1,82 @@ +# Composite Components + +## Goals + +In Mimi v0.4, we have two levels of model elements, (i) `Model` and (ii) `Component`. (For now, we ignore the distinction between model / component "definition" and "instance" types.) The primary goal of the `MetaComponent` construct is to extend this structure recursively to _N_ levels, by allowing a `MetaComponent` to contain other `MetaComponent`s as well as `LeafComponent`s, which cannot contain other components. + +## Major elements +This suggests three types of elements: + +1. `LeafComponent(Def|Instance)` -- equivalent to the Mimi v0.4 `Component(Def|Instance)` concept. + +1. `MetaComponent(Def|Instance)` -- presents the same API as a `LeafComponent(Def|Instance)`, but the variables and parameters it exposes are the aggregated sets of variables and parameters exposed by its components, each of which can be a `MetaComponent` or `LeafComponent`. A `MetaComponentInstance` creates no new storage for variables and parameters; it references the storage in its internal components. By default, the `run_timestep` method of a `MetaComponentInstance` simply calls the `run_timestep` method of each of its internal components in dependency order. + +1. `Model` -- Contains a top-level `MetaComponentInstance` that holds all the actual user-defined components, which are instances of `MetaComponentInstance` or `LeafComponentInstance`. The API for `Model` delegates some calls to its top-level `MetaComponentInstance` while providing additional functionality including running a Monte Carlo simulation. + +## Implementation Notes + +### Model + +* A `Model` will be defined using the `@defmodel` macro. + +* As with the currently defined (but not exported) `@defmodel`, component ordering will be determined automatically based on defined connections, with loops avoided by referencing timestep `[t-1]`. This simplifies the API for `addcomponent!`. + +* We will add support for two optional functions defined inside `@defmodel`: + * `before(m::Model)`, called before the model runs its first timestep + * `after(m:Model)`, called after the model runs its final timestep. + +* A `Model` will be implemented as a wrapper around a single top-level `MetaComponent` that handles the ordering and iteration over sub-components. (In an OOP language, `Model` would subclass `MetaComponent`, but in Julia, we use composition.) + +![MetaComponent Schematic](../figs/Mimi-model-schematic-v3.png) + + +### MetaComponent + +* Defined using `@defcomp` as with `LeafComponent`. It's "meta" nature is defined by including a new term: + + `subcomps = [sc1, sc2, sc3, ...]`, where the referenced sub-components (`sc1`, etc.) refer to previously defined `ComponentId`s. + +* A `MetaComponent`'s `run_timestep` function is optional. The default function simply calls `run_timestep(subcomps::Vector)` to iterate over sub-components and calls `run_timestep` on each. If a `MetaComponent` defines its own `run_timestep` function, it should either call `run_timestep` on the vector of sub-components or perform a variant of this function itself. + +* The `@defcomp` macro allows definition of an optional `init` method. To this, we will add support for an `after` method as in `@defmodel`. We will allow `before` as an alias for `init` (perhaps with a deprecation) for consistency with `@defmodel`. + +## Other Notes + +* Currently, `run()` calls `_run_components(mi, clock, firsts, lasts, comp_clocks)` with simple vectors of firsts, lasts, and comp_clocks. To handle this with the recursive component structure: + + * Aggregate from the bottom up building `_firsts` and `_lasts` in each `MetaComponentInstance` holding the values for its sub-components. + + * Also store the `MetaComponentInstance`'s own summary `first` and `last` which are just `min(firsts)` and `max(lasts)`, respectively. + +* Currently, the `run()` function creates a vector of `Clock` instances, corresponding to each model component. I see two options here: + + 1. Extend the current approach to have each `MetaComponentInstance` hold a vector of `Clock` instances for its sub-components. + + 2. Store a `Clock` instance with each `MetaComponentInstance` or `LeafComponentInstance` and provide a recursive method to reset all clocks. + + +### Other stuff + +* This might be is a good time to reconsider the implementation of external parameters. The main question is about naming these and whether they need to be globally unique or merely unique within a (meta) component. + +* Unit conversion components should be simple "multiplier" components that are bound with specific conversion factors, conceptually like a "closure" on a component. + +* An "identity" component takes an input (external input, bound constant) and allows multiple components to access it. One issue is how to handle the type of argument. Could function wrappers be useful here? + * Identity is simply a unit conversion of 1. + +* If > 1 component exports parameters of the same name, it's an error. At least one comp must rename. + +* David suggested making composite comps immutable, generating a new one each time a change is made. (Why not just have the CompositeComponentInstance be immutable?) + +## Notes from 9/21/18 Meeting + +``` +@defcomp foo begin + Component(bar; export=[var_1, var_2, param_1]) + Component(Mimi.adder, comp_1; # rename locally as :comp_1 + bind=[par_3 => 5, # set a parameter to a fixed value + par_4 => bar.var_1]) # connect a parameter to a variable + + +end +``` \ No newline at end of file diff --git a/examples/tutorial/02-two-region-model/two-region-model.jl b/examples/tutorial/02-two-region-model/two-region-model.jl index 2864678c4..addc268f7 100644 --- a/examples/tutorial/02-two-region-model/two-region-model.jl +++ b/examples/tutorial/02-two-region-model/two-region-model.jl @@ -12,8 +12,9 @@ function construct_MyModel() m = Model() - set_dimension!(m, :time, collect(2015:5:2110)) - set_dimension!(m, :regions, [:Region1, :Region2, :Region3]) # Note that the regions of your model must be specified here + set_dimension!(m, :time, 2015:5:2110) + # Note that the regions of your model must be specified here + set_dimension!(m, :regions, [:Region1, :Region2, :Region3]) add_comp!(m, grosseconomy) add_comp!(m, emissions) @@ -21,7 +22,7 @@ function construct_MyModel() 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, :depk, depk) set_param!(m, :grosseconomy, :k0, k0) set_param!(m, :grosseconomy, :share, 0.3) diff --git a/src/Mimi.jl b/src/Mimi.jl index b47ca1836..6384de72b 100644 --- a/src/Mimi.jl +++ b/src/Mimi.jl @@ -1,5 +1,6 @@ module Mimi +using Classes using DataFrames using DataStructures using Distributions @@ -11,9 +12,10 @@ using StringBuilders export @defcomp, @defsim, + @defcomposite, MarginalModel, Model, - add_comp!, + add_comp!, components, connect_param!, create_marginal_model, @@ -48,47 +50,37 @@ export variable_dimensions, variable_names -include("core/types.jl") - -# After loading types, the rest can just be alphabetical +include("core/delegate.jl") +include("core/types/includes.jl") +# +# After loading types and delegation macro, the rest can be loaded in any order. +# include("core/build.jl") include("core/connections.jl") include("core/defs.jl") include("core/defcomp.jl") +include("core/defmodel.jl") +include("core/defcomposite.jl") include("core/dimensions.jl") include("core/instances.jl") include("core/references.jl") include("core/time.jl") +include("core/time_arrays.jl") include("core/model.jl") +include("core/order.jl") +include("core/paths.jl") +include("core/show.jl") + include("mcs/mcs.jl") # need mcs types for explorer include("explorer/explore.jl") -include("utils/graph.jl") -include("utils/plotting.jl") include("utils/getdataframe.jl") +include("utils/graph.jl") include("utils/lint_helper.jl") include("utils/misc.jl") +include("utils/plotting.jl") -""" - load_comps(dirname::String="./components") - -Call include() on all the files in the indicated directory `dirname`. -This avoids having modelers create a long list of include() -statements. Just put all the components in a directory. -""" -function load_comps(dirname::String="./components") - files = readdir(dirname) - for file in files - if endswith(file, ".jl") - pathname = joinpath(dirname, file) - include(pathname) - end - end -end - -# Components are defined here to allow pre-compilation to work -function __init__() - compdir = joinpath(@__DIR__, "components") - load_comps(compdir) -end +# Load built-in components +include("components/adder.jl") +include("components/connector.jl") end # module diff --git a/src/components/adder.jl b/src/components/adder.jl index 0a9bc6298..2fd191989 100644 --- a/src/components/adder.jl +++ b/src/components/adder.jl @@ -1,7 +1,5 @@ using Mimi -# When evaluated in the __init__() function, the surrounding module -# is Main rather than Mimi. @defcomp adder begin add = Parameter(index=[time]) input = Parameter(index=[time]) diff --git a/src/core/_preliminary.jl b/src/core/_preliminary.jl new file mode 100644 index 000000000..7590fd9de --- /dev/null +++ b/src/core/_preliminary.jl @@ -0,0 +1,42 @@ +# Preliminary file to think through David's suggestion of splitting defs between "registered" +# readonly "templates" and user-constructed models so it's clear which functions operate on +# templates vs defs within a model. + +@class ComponentDef <: NamedObj begin + comp_id::Union{Nothing, ComponentId} + variables::OrderedDict{Symbol, VariableDef} + parameters::OrderedDict{Symbol, ParameterDef} + dim_names::Set{Symbol} +end + +@class CompositeComponentDef <: ComponentDef begin + comps_dict::OrderedDict{Symbol, AbstractComponentDef} + exports::ExportsDict + internal_param_conns::Vector{InternalParameterConnection} + external_params::Dict{Symbol, ModelParameter} +end + +# Define these for building out a ModelDef, which reference the +# central definitions using the classes above. + +@class mutable ModelComponentDef <: NamedObj begin + comp_id::ComponentId # references registered component def + comp_path::Union{Nothing, ComponentPath} + dim_dict::OrderedDict{Symbol, Union{Nothing, Dimension}} + first::Union{Nothing, Int} + last::Union{Nothing, Int} + is_uniform::Bool +end + +@class mutable ModelCompositeComponentDef <: ModelComponentDef begin + comps_dict::OrderedDict{Symbol, AbstractModelComponentDef} + bindings::Vector{Binding} + exports::ExportsDict + external_param_conns::Vector{ExternalParameterConnection} + external_params::Dict{Symbol, ModelParameter} + + # Names of external params that the ConnectorComps will use as their :input2 parameters. + backups::Vector{Symbol} + + sorted_comps::Union{Nothing, Vector{Symbol}} +end diff --git a/src/core/build.jl b/src/core/build.jl index d5e364314..92f4cb011 100644 --- a/src/core/build.jl +++ b/src/core/build.jl @@ -1,9 +1,9 @@ connector_comp_name(i::Int) = Symbol("ConnectorComp$i") # Return the datatype to use for instance variables/parameters -function _instance_datatype(md::ModelDef, def::DatumDef) +function _instance_datatype(md::ModelDef, def::AbstractDatumDef) dtype = def.datatype == Number ? number_type(md) : def.datatype - dims = dimensions(def) + dims = dim_names(def) num_dims = dim_count(def) ti = get_time_index_position(def) @@ -11,12 +11,13 @@ function _instance_datatype(md::ModelDef, def::DatumDef) if num_dims == 0 T = ScalarModelParameter{dtype} - elseif ti == nothing # there's no time dimension + elseif ti === nothing # there's no time dimension T = Array{dtype, num_dims} - - else + + else if isuniform(md) first, stepsize = first_and_step(md) + first === nothing && @warn "_instance_datatype: first === nothing" T = TimestepArray{FixedTimestep{first, stepsize}, Union{dtype, Missing}, num_dims, ti} else times = time_labels(md) @@ -29,27 +30,27 @@ function _instance_datatype(md::ModelDef, def::DatumDef) end # Create the Ref or Array that will hold the value(s) for a Parameter or Variable -function _instantiate_datum(md::ModelDef, def::DatumDef) +function _instantiate_datum(md::ModelDef, def::AbstractDatumDef) dtype = _instance_datatype(md, def) - dims = dimensions(def) + dims = dim_names(def) num_dims = length(dims) - + # Scalar datum if num_dims == 0 value = dtype(0) - + # Array datum, with :time dimension - elseif dims[1] == :time + elseif dims[1] == :time if num_dims == 1 value = dtype(dim_count(md, :time)) - else + else counts = dim_counts(md, Vector{Symbol}(dims)) value = dtype <: AbstractArray ? dtype(undef, counts...) : dtype(counts...) end # Array datum, without :time dimension - else + else # TBD: Handle unnamed indices properly counts = dim_counts(md, Vector{Symbol}(dims)) value = dtype <: AbstractArray ? dtype(undef, counts...) : dtype(counts...) @@ -61,124 +62,196 @@ end """ _instantiate_component_vars(md::ModelDef, comp_def::ComponentDef) -Instantiate a component `comp_def` in the model `md` and its variables (but not its parameters). -Return the resulting ComponentInstance. +Instantiate a component `comp_def` in the model `md` and its variables (but not its +parameters). Return the resulting ComponentInstanceVariables. """ function _instantiate_component_vars(md::ModelDef, comp_def::ComponentDef) - comp_name = name(comp_def) - var_defs = variables(comp_def) + var_defs = variables(comp_def) - names = ([name(vdef) for vdef in var_defs]...,) - types = Tuple{[_instance_datatype(md, vdef) for vdef in var_defs]...} - values = [_instantiate_datum(md, def) for def in var_defs] + names = Symbol[nameof(def) for def in var_defs] + values = Any[_instantiate_datum(md, def) for def in var_defs] + types = DataType[_instance_datatype(md, def) for def in var_defs] + paths = repeat(Any[comp_def.comp_path], length(names)) - return ComponentInstanceVariables(names, types, values) + return ComponentInstanceVariables(names, types, values, paths) end -# Save a reference to the model's dimension dictionary to make it -# available in calls to run_timestep. -function save_dim_dict_reference(mi::ModelInstance) - dim_dict = dim_value_dict(mi.md) +# Create ComponentInstanceVariables for a composite component from the list of exported vars +function _combine_exported_vars(comp_def::AbstractCompositeComponentDef, var_dict::Dict{ComponentPath, Any}) + names = Symbol[] + values = Any[] - for ci in values(mi.components) - ci.dim_dict = dim_dict - end + types = DataType[typeof(val) for val in values] + paths = repeat(Any[comp_def.comp_path], length(names)) + ci_vars = ComponentInstanceVariables(names, types, values, paths) + # @info "ci_vars: $ci_vars"] + return ci_vars +end - return nothing +function _combine_exported_pars(comp_def::AbstractCompositeComponentDef, par_dict::Dict{Tuple{ComponentPath, Symbol}, Any}) + names = Symbol[] + values = Any[] + paths = repeat(Any[comp_def.comp_path], length(names)) + types = DataType[typeof(val) for val in values] + return ComponentInstanceParameters(names, types, values, paths) end -function build(m::Model) - # Reference a copy in the ModelInstance to avoid changes underfoot - m.mi = build(copy(m.md)) - return nothing +function _instantiate_vars(comp_def::ComponentDef, md::ModelDef, var_dict::Dict{ComponentPath, Any}) + var_dict[comp_def.comp_path] = _instantiate_component_vars(md, comp_def) +end + +# Creates the top-level vars for the model +function _instantiate_vars(md::ModelDef, var_dict::Dict{ComponentPath, Any}) + _instantiate_vars(md, md, var_dict) end -function build(md::ModelDef) - add_connector_comps(md) - - # check if all parameters are set - not_set = unconnected_params(md) - if ! isempty(not_set) - params = join(not_set, " ") - msg = "Cannot build model; the following parameters are not set: $params" - error(msg) +# Recursively instantiate all variables and store refs in the given dict. +function _instantiate_vars(comp_def::AbstractCompositeComponentDef, md::ModelDef, var_dict::Dict{ComponentPath, Any}) + comp_path = comp_def.comp_path + # @info "_instantiate_vars composite $comp_path" + + for cd in compdefs(comp_def) + _instantiate_vars(cd, md, var_dict) end + var_dict[comp_path] = _combine_exported_vars(comp_def, var_dict) +end - var_dict = Dict{Symbol, Any}() # collect all var defs and - par_dict = Dict{Symbol, Dict{Symbol, Any}}() # store par values as we go +# Do nothing if called on a leaf component +_collect_params(comp_def::ComponentDef, var_dict, par_dict) = nothing - comp_defs = compdefs(md) - for comp_def in comp_defs - comp_name = name(comp_def) - var_dict[comp_name] = _instantiate_component_vars(md, comp_def) - par_dict[comp_name] = Dict() # param value keyed by param name +# Recursively collect all parameters with connections to allocated storage for variables +function _collect_params(comp_def::AbstractCompositeComponentDef, + var_dict::Dict{ComponentPath, Any}, + par_dict::Dict{Tuple{ComponentPath, Symbol}, Any}) + # depth-first search of composites + for cd in compdefs(comp_def) + _collect_params(cd, var_dict, par_dict) end - # Iterate over connections to create parameters, referencing storage in vars - for ipc in internal_param_conns(md) - comp_name = ipc.src_comp_name + # @info "Collecting params for $(comp_def.comp_id)" - vars = var_dict[comp_name] - var_value_obj = get_property_obj(vars, ipc.src_var_name) - - par_values = par_dict[ipc.dst_comp_name] - par_values[ipc.dst_par_name] = var_value_obj + # Iterate over connections to create parameters, referencing storage in vars + for ipc in internal_param_conns(comp_def) + src_vars = var_dict[ipc.src_comp_path] + var_value_obj = get_property_obj(src_vars, ipc.src_var_name) + par_dict[(ipc.dst_comp_path, ipc.dst_par_name)] = var_value_obj + # @info "internal conn: $(ipc.src_comp_path):$(ipc.src_var_name) => $(ipc.dst_comp_path):$(ipc.dst_par_name)" end - - for ext in external_param_conns(md) - comp_name = ext.comp_name - param = external_param(md, ext.external_param) - par_values = par_dict[comp_name] - par_values[ext.param_name] = param isa ScalarModelParameter ? param : value(param) + + for ext in external_param_conns(comp_def) + param = external_param(comp_def, ext.external_param) + par_dict[(ext.comp_path, ext.param_name)] = (param isa ScalarModelParameter ? param : value(param)) + # @info "external conn: $(ext.comp_name).$(ext.param_name) => $(param)" end # Make the external parameter connections for the hidden ConnectorComps. # Connect each :input2 to its associated backup value. - for (i, backup) in enumerate(md.backups) - comp_name = connector_comp_name(i) - param = external_param(md, backup) + for (i, backup) in enumerate(comp_def.backups) + conn_comp = compdef(comp_def, connector_comp_name(i)) + conn_path = conn_comp.comp_path - par_values = par_dict[comp_name] - par_values[:input2] = param isa ScalarModelParameter ? param : value(param) + param = external_param(comp_def, backup) + par_dict[(conn_path, :input2)] = (param isa ScalarModelParameter ? param : value(param)) end +end - mi = ModelInstance(md) +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 + names = parameter_names(comp_def) + vals = Any[par_dict[(comp_path, name)] for name in names] + types = DataType[typeof(val) for val in vals] + paths = repeat([comp_def.comp_path], length(names)) - # instantiate parameters - for comp_def in comp_defs - comp_name = name(comp_def) + return ComponentInstanceParameters(names, types, vals, paths) +end + +function _instantiate_params(comp_def::AbstractCompositeComponentDef, par_dict::Dict{Tuple{ComponentPath, Symbol}, Any}) + _combine_exported_pars(comp_def, par_dict) +end - vars = var_dict[comp_name] - - par_values = par_dict[comp_name] - pnames = Tuple(parameter_names(comp_def)) - pvals = [par_values[pname] for pname in pnames] - ptypes = Tuple{map(typeof, pvals)...} - pars = ComponentInstanceParameters(pnames, ptypes, pvals) +# Return a built leaf or composite LeafComponentInstance +function _build(comp_def::ComponentDef, + var_dict::Dict{ComponentPath, Any}, + par_dict::Dict{Tuple{ComponentPath, Symbol}, Any}, + time_bounds::Tuple{Int, Int}) + # @info "_build leaf $(comp_def.comp_id)" + # @info " var_dict $(var_dict)" + # @info " par_dict $(par_dict)" - first = first_period(md, comp_def) - last = last_period(md, comp_def) + pars = _instantiate_params(comp_def, par_dict) + vars = var_dict[comp_def.comp_path] - ci = ComponentInstance{typeof(vars), typeof(pars)}(comp_def, vars, pars, first, last, comp_name) - add_comp!(mi, ci) + return LeafComponentInstance(comp_def, vars, pars, time_bounds) +end + +function _build(comp_def::AbstractCompositeComponentDef, + var_dict::Dict{ComponentPath, Any}, + par_dict::Dict{Tuple{ComponentPath, Symbol}, Any}, + time_bounds::Tuple{Int, Int}) + # @info "_build composite $(comp_def.comp_id)" + # @info " var_dict $(var_dict)" + # @info " par_dict $(par_dict)" + + comps = [_build(cd, var_dict, par_dict, time_bounds) for cd in compdefs(comp_def)] + return CompositeComponentInstance(comps, comp_def, time_bounds) +end + +function _build(md::ModelDef) + # @info "_build(md)" + add_connector_comps!(md) + + # check if all parameters are set + not_set = unconnected_params(md) + + if ! isempty(not_set) + params = join(not_set, "\n ") + error("Cannot build model; the following parameters are not set:\n $params") end - save_dim_dict_reference(mi) + var_dict = Dict{ComponentPath, Any}() # collect all var defs and + par_dict = Dict{Tuple{ComponentPath, Symbol}, Any}() # store par values as we go + + _instantiate_vars(md, var_dict) + _collect_params(md, var_dict, par_dict) + + # @info "var_dict: $var_dict" + # @info "par_dict: $par_dict" + + t = dimension(md, :time) + time_bounds = (firstindex(t), lastindex(t)) + + propagate_time!(md, t) + + ci = _build(md, var_dict, par_dict, time_bounds) + mi = ModelInstance(ci, md) return mi end +function build(m::Model) + # fix paths and propagate imports + fix_comp_paths!(m.md) + import_params!(m.md) + + # Reference a copy in the ModelInstance to avoid changes underfoot + md = deepcopy(m.md) + m.mi = _build(md) + m.md.dirty = false + return nothing +end + """ create_marginal_model(base::Model, delta::Float64=1.0) -Create a `MarginalModel` where `base` is the baseline model and `delta` is the +Create a `MarginalModel` where `base` is the baseline model and `delta` is the difference used to create the `marginal` model. Return the resulting `MarginaModel` which shares the internal `ModelDef` between the `base` and `marginal`. """ function create_marginal_model(base::Model, delta::Float64=1.0) # Make sure the base has a ModelInstance before we copy since this # copies the ModelDef to avoid being affected by later changes. - if base.mi === nothing + if ! is_built(base) build(base) end diff --git a/src/core/connections.jl b/src/core/connections.jl index 33c952dea..f7ba08fa1 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -2,34 +2,66 @@ using LightGraphs using MetaGraphs """ - disconnect_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol) + disconnect_param!(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, param_name::Symbol) Remove any parameter connections for a given parameter `param_name` in a given component -`comp_name` of model `md`. +`comp_def` which must be a direct subcomponent of composite `obj`. """ -function disconnect_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol) - # println("disconnect_param!($comp_name, $param_name)") - filter!(x -> !(x.dst_comp_name == comp_name && x.dst_par_name == param_name), internal_param_conns(md)) - filter!(x -> !(x.comp_name == comp_name && x.param_name == param_name), external_param_conns(md)) +function disconnect_param!(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, param_name::Symbol) + # If the path isn't set yet, we look for a comp in the eventual location + path = @or(comp_def.comp_path, ComponentPath(obj, comp_def.name)) + + # @info "disconnect_param!($(obj.comp_path), $path, :$param_name)" + + if is_descendant(obj, comp_def) === nothing + error("Cannot disconnect a component ($path) that is not within the given composite ($(obj.comp_path))") + end + + filter!(x -> !(x.dst_comp_path == path && x.dst_par_name == param_name), obj.internal_param_conns) + filter!(x -> !(x.comp_path == path && x.param_name == param_name), obj.external_param_conns) + dirty!(obj) end -# Default string, string unit check function -function verify_units(one::AbstractString, two::AbstractString) - # True if and only if they match - return one == two +""" + disconnect_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, param_name::Symbol) + +Remove any parameter connections for a given parameter `param_name` in a given component +`comp_def` which must be a direct subcomponent of composite `obj`. +""" +function disconnect_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, param_name::Symbol) + comp = compdef(obj, comp_name) + comp === nothing && error("Did not find $comp_name in composite $(printable(obj.comp_path))") + disconnect_param!(obj, comp, param_name) end -function _check_labels(md::ModelDef, comp_def::ComponentDef, param_name::Symbol, ext_param::ArrayModelParameter) +""" + disconnect_param!(obj::AbstractCompositeComponentDef, comp_path::ComponentPath, param_name::Symbol) + +Remove any parameter connections for a given parameter `param_name` in the component identified by +`comp_path` which must be under the composite `obj`. +""" +function disconnect_param!(obj::AbstractCompositeComponentDef, comp_path::ComponentPath, param_name::Symbol) + if (comp_def = find_comp(obj, comp_path)) === nothing + return + end + disconnect_param!(obj, comp_def, param_name) +end + +# Default string, string unit check function +verify_units(unit1::AbstractString, unit2::AbstractString) = (unit1 == unit2) + +function _check_labels(obj::AbstractCompositeComponentDef, + comp_def::ComponentDef, param_name::Symbol, ext_param::ArrayModelParameter) param_def = parameter(comp_def, param_name) t1 = eltype(ext_param.values) - t2 = eltype(datatype(param_def)) - if !(t1 <: t2) + 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 = dimensions(param_def) - param_dims = dimensions(ext_param) + comp_dims = dim_names(param_def) + param_dims = dim_names(ext_param) if ! isempty(param_dims) && size(param_dims) != size(comp_dims) d1 = size(comp_dims) @@ -38,22 +70,22 @@ function _check_labels(md::ModelDef, comp_def::ComponentDef, param_name::Symbol, end # Don't check sizes for ConnectorComps since they won't match. - if name(comp_def) in (:ConnectorCompVector, :ConnectorCompMatrix) + if nameof(comp_def) in (:ConnectorCompVector, :ConnectorCompMatrix) return nothing end - # index_values = indexvalues(md) + # index_values = indexvalues(obj) for (i, dim) in enumerate(comp_dims) if isa(dim, Symbol) param_length = size(ext_param.values)[i] if dim == :time - t = dimensions(md)[:time] - first = first_period(md, comp_def) - last = last_period(md, comp_def) + t = dimension(obj, :time) + first = find_first_period(comp_def) + last = find_last_period(comp_def) comp_length = t[last] - t[first] + 1 else - comp_length = dim_count(md, dim) + comp_length = dim_count(obj, dim) end 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.") @@ -63,71 +95,81 @@ function _check_labels(md::ModelDef, comp_def::ComponentDef, param_name::Symbol, end """ - connect_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, ext_param_name::Symbol) + connect_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, param_name::Symbol, ext_param_name::Symbol; + check_labels::Bool=true) -Connect a parameter `param_name` in the component `comp_name` of model `md` to +Connect a parameter `param_name` in the component `comp_name` of composite `obj` to the external parameter `ext_param_name`. """ -function connect_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, ext_param_name::Symbol) - comp_def = compdef(md, comp_name) - ext_param = external_param(md, ext_param_name) +function connect_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, param_name::Symbol, ext_param_name::Symbol; + check_labels::Bool=true) + comp_def = compdef(obj, comp_name) + ext_param = external_param(obj, ext_param_name) - if isa(ext_param, ArrayModelParameter) - _check_labels(md, comp_def, param_name, ext_param) + if ext_param isa ArrayModelParameter && check_labels + _check_labels(obj, comp_def, param_name, ext_param) end - disconnect_param!(md, comp_name, param_name) + disconnect_param!(obj, comp_def, param_name) # calls dirty!() - conn = ExternalParameterConnection(comp_name, param_name, ext_param_name) - add_external_param_conn(md, conn) + 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) return nothing end """ - connect_param!(md::ModelDef, dst_comp_name::Symbol, dst_par_name::Symbol, - src_comp_name::Symbol, src_var_name::Symbol backup::Union{Nothing, Array}=nothing; + connect_param!(obj::AbstractCompositeComponentDef, + dst_comp_path::ComponentPath, dst_par_name::Symbol, + src_comp_path::ComponentPath, src_var_name::Symbol, + backup::Union{Nothing, Array}=nothing; ignoreunits::Bool=false, offset::Int=0) -Bind the parameter `dst_par_name` of one component `dst_comp_name` of model `md` -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 `offset` argument indicates the offset between the destination -and the source ie. the value would be `1` if the destination component parameter -should only be calculated for the second timestep and beyond. +Bind the parameter `dst_par_name` of one component `dst_comp_path` of composite `obj` to a +variable `src_var_name` in another component `src_comp_path` 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 `offset` argument indicates the offset between +the destination and the source ie. the value would be `1` if the destination component +parameter should only be calculated for the second timestep and beyond. """ -function connect_param!(md::ModelDef, - dst_comp_name::Symbol, dst_par_name::Symbol, - src_comp_name::Symbol, src_var_name::Symbol, - backup::Union{Nothing, Array}=nothing; ignoreunits::Bool=false, offset::Int=0) +function connect_param!(obj::AbstractCompositeComponentDef, + dst_comp_path::ComponentPath, dst_par_name::Symbol, + src_comp_path::ComponentPath, src_var_name::Symbol, + backup::Union{Nothing, Array}=nothing; + ignoreunits::Bool=false, offset::Int=0) # remove any existing connections for this dst parameter - disconnect_param!(md, dst_comp_name, dst_par_name) + disconnect_param!(obj, dst_comp_path, dst_par_name) # calls dirty!() - dst_comp_def = compdef(md, dst_comp_name) - src_comp_def = compdef(md, src_comp_name) + dst_comp_def = compdef(obj, dst_comp_path) + src_comp_def = compdef(obj, src_comp_path) + + # @info "dst_comp_def: $dst_comp_def" + # @info "src_comp_def: $src_comp_def" if backup !== nothing # If value is a NamedArray, we can check if the labels match if isa(backup, NamedArray) dims = dimnames(backup) - check_parameter_dimensions(md, backup, dims, dst_par_name) + check_parameter_dimensions(obj, backup, dims, dst_par_name) else dims = nothing end # Check that the backup data is the right size - if size(backup) != datum_size(md, dst_comp_def, dst_par_name) - error("Cannot connect parameter; the provided backup data is the wrong size. Expected size $(datum_size(md, dst_comp_def, dst_par_name)) but got $(size(backup)).") + if size(backup) != datum_size(obj, dst_comp_def, dst_par_name) + error("Cannot connect parameter; the provided backup data is the wrong size. Expected size $(datum_size(obj, dst_comp_def, dst_par_name)) but got $(size(backup)).") end # some other check for second dimension?? dst_param = parameter(dst_comp_def, dst_par_name) - dst_dims = dimensions(dst_param) + dst_dims = dim_names(dst_param) + + backup = convert(Array{Union{Missing, number_type(obj)}}, backup) # converts number type and, if it's a NamedArray, it's converted to Array + first = first_period(obj, dst_comp_def) - backup = convert(Array{Union{Missing, number_type(md)}}, backup) # converts number type and, if it's a NamedArray, it's converted to Array - first = first_period(md, dst_comp_def) - T = eltype(backup) + T = eltype(backup) dim_count = length(dst_dims) @@ -136,12 +178,13 @@ function connect_param!(md::ModelDef, else ti = get_time_index_position(dst_param) - if isuniform(md) + if isuniform(obj) # use the first from the comp_def not the ModelDef - _, stepsize = first_and_step(md) - values = TimestepArray{FixedTimestep{first, stepsize}, T, dim_count, ti}(backup) + stepsize = step_size(obj) + last = last_period(obj, dst_comp_def) + values = TimestepArray{FixedTimestep{first, stepsize, last}, T, dim_count, ti}(backup) else - times = time_labels(md) + times = time_labels(obj) # use the first from the comp_def first_index = findfirst(isequal(first), times) values = TimestepArray{VariableTimestep{(times[first_index:end]...,)}, T, dim_count, ti}(backup) @@ -149,78 +192,150 @@ function connect_param!(md::ModelDef, end - set_external_array_param!(md, dst_par_name, values, dst_dims) + set_external_array_param!(obj, dst_par_name, values, dst_dims) backup_param_name = dst_par_name else # If backup not provided, make sure the source component covers the span of the destination component - src_first, src_last = first_period(md, src_comp_def), last_period(md, src_comp_def) - dst_first, dst_last = first_period(md, dst_comp_def), last_period(md, dst_comp_def) - if dst_first < src_first || dst_last > src_last - error("Cannot connect parameter; $src_comp_name only runs from $src_first to $src_last, whereas $dst_comp_name runs from $dst_first to $dst_last. Backup data must be provided for missing years. Try calling: - `connect_param!(m, comp_name, par_name, comp_name, var_name, backup_data)`") + src_first, src_last = first_and_last(src_comp_def) + dst_first, dst_last = first_and_last(dst_comp_def) + if (dst_first !== nothing && src_first !== nothing && dst_first < src_first) || + (dst_last !== nothing && src_last !== nothing && dst_last > src_last) + src_first = printable(src_first) + src_last = printable(src_last) + dst_first = printable(dst_first) + dst_last = printable(dst_last) + error("""Cannot connect parameter: $src_comp_path runs only from $src_first to $src_last, +whereas $dst_comp_path runs from $dst_first to $dst_last. Backup data must be provided for missing years. +Try calling: + `connect_param!(m, comp_name, par_name, comp_name, var_name, backup_data)`""") end backup_param_name = nothing end # Check the units, if provided - if ! ignoreunits && ! verify_units(variable_unit(md, src_comp_name, src_var_name), - parameter_unit(md, dst_comp_name, dst_par_name)) - error("Units of $src_comp_name.$src_var_name do not match $dst_comp_name.$dst_par_name.") + if ! ignoreunits && ! verify_units(variable_unit(src_comp_def, src_var_name), + parameter_unit(dst_comp_def, dst_par_name)) + error("Units of $src_comp_path:$src_var_name do not match $dst_comp_path:$dst_par_name.") end - # println("connect($src_comp_name.$src_var_name => $dst_comp_name.$dst_par_name)") - conn = InternalParameterConnection(src_comp_name, src_var_name, dst_comp_name, dst_par_name, ignoreunits, backup_param_name, offset=offset) - add_internal_param_conn(md, conn) + conn = InternalParameterConnection(src_comp_path, src_var_name, dst_comp_path, dst_par_name, ignoreunits, backup_param_name, offset=offset) + add_internal_param_conn!(obj, conn) return nothing end +function 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, offset::Int=0) + connect_param!(obj, ComponentPath(obj, dst_comp_name), dst_par_name, + ComponentPath(obj, src_comp_name), src_var_name, + backup; ignoreunits=ignoreunits, offset=offset) +end + """ - connect_param!(md::ModelDef, dst::Pair{Symbol, Symbol}, src::Pair{Symbol, Symbol}, - backup::Union{Nothing, Array}=nothing; ignoreunits::Bool=false, offset::Int=0) + connect_param!(obj::AbstractCompositeComponentDef, + dst::Pair{Symbol, Symbol}, src::Pair{Symbol, Symbol}, + backup::Union{Nothing, Array}=nothing; + ignoreunits::Bool=false, offset::Int=0) -Bind the parameter `dst[2]` of one component `dst[1]` of model `md` -to a variable `src[2]` in another component `src[1]` of the same model +Bind the parameter `dst[2]` of one component `dst[1]` of composite `obj` +to a variable `src[2]` in another component `src[1]` of the same composite using `backup` to provide default values and the `ignoreunits` flag to indicate the need to check match units between the two. The `offset` argument indicates the offset between the destination and the source ie. the value would be `1` if the destination component parameter should only be calculated for the second timestep and beyond. """ -function connect_param!(md::ModelDef, dst::Pair{Symbol, Symbol}, src::Pair{Symbol, Symbol}, - backup::Union{Nothing, Array}=nothing; ignoreunits::Bool=false, offset::Int=0) - connect_param!(md, dst[1], dst[2], src[1], src[2], backup; ignoreunits=ignoreunits, offset=offset) +function connect_param!(obj::AbstractCompositeComponentDef, + dst::Pair{Symbol, Symbol}, src::Pair{Symbol, Symbol}, + backup::Union{Nothing, Array}=nothing; ignoreunits::Bool=false, offset::Int=0) + connect_param!(obj, dst[1], dst[2], src[1], src[2], backup; ignoreunits=ignoreunits, offset=offset) end """ - connected_params(md::ModelDef, comp_name::Symbol) + split_datum_path(obj::AbstractCompositeComponentDef, s::AbstractString) -Return list of parameters that have been set for component `comp_name` in model `md`. +Split a string of the form "/path/to/component:datum_name" into the component path, +`ComponentPath(:path, :to, :component)` and name `:datum_name`. """ -function connected_params(md::ModelDef, comp_name::Symbol) - ext_set_params = map(x->x.param_name, external_param_conns(md, comp_name)) - int_set_params = map(x->x.dst_par_name, internal_param_conns(md, comp_name)) +function split_datum_path(obj::AbstractCompositeComponentDef, s::AbstractString) + elts = split(s, ":") + length(elts) != 2 && error("Can't split datum path '$s' into ComponentPath and datum name") + return (ComponentPath(obj, elts[1]), Symbol(elts[2])) +end + +""" +Connect a parameter and variable using string notation "/path/to/component:datum_name" where +the potion before the ":" is the string representation of a component path from `obj` and the +portion after is the name of the src or dst datum. +""" +function connect_param!(obj::AbstractCompositeComponentDef, dst::AbstractString, src::AbstractString, + backup::Union{Nothing, Array}=nothing; ignoreunits::Bool=false, offset::Int=0) + dst_path, dst_name = split_datum_path(obj, dst) + src_path, src_name = split_datum_path(obj, src) + + connect_param!(obj, dst_path, dst_name, src_path, src_name, + backup; ignoreunits=ignoreunits, offset=offset) +end + + +const ParamVector = Vector{ParamPath} + +_collect_connected_params(obj::ComponentDef, connected) = nothing + +function _collect_connected_params(obj::AbstractCompositeComponentDef, connected::ParamVector) + for comp_def in compdefs(obj) + _collect_connected_params(comp_def, connected) + end + + ext_set_params = map(x->(x.comp_path, x.param_name), external_param_conns(obj)) + int_set_params = map(x->(x.dst_comp_path, x.dst_par_name), internal_param_conns(obj)) + + append!(connected, union(ext_set_params, int_set_params)) +end + +""" + connected_params(md::ModelDef) + +Recursively search the component tree to find connected parameters in leaf components. +Return a vector of tuples of the form `(path::ComponentPath, param_name::Symbol)`. +""" +function connected_params(md::ModelDef) + connected = ParamVector() + _collect_connected_params(md, connected) + return connected +end + +""" +Depth-first search for unconnected parameters, which are appended to `unconnected`. Parameter +connections are made to the "original" component, not to a composite that exports the parameter. +Thus, only the leaf (non-composite) variant of this method actually collects unconnected params. +""" +function _collect_unconnected_params(obj::ComponentDef, connected::ParamVector, unconnected::ParamVector) + params = [(obj.comp_path, x) for x in parameter_names(obj)] + diffs = setdiff(params, connected) + append!(unconnected, diffs) +end - return union(ext_set_params, int_set_params) +function _collect_unconnected_params(obj::AbstractCompositeComponentDef, connected::ParamVector, unconnected::ParamVector) + for comp_def in compdefs(obj) + _collect_unconnected_params(comp_def, connected, unconnected) + end end """ unconnected_params(md::ModelDef) -Return a list of tuples (componentname, parametername) of parameters -that have not been connected to a value in the model `md`. +Return a list of tuples (comp_path, param_name) of parameters +that have not been connected to a value anywhere in `md`. """ function unconnected_params(md::ModelDef) - unconnected = Vector{Tuple{Symbol,Symbol}}() - - for comp_def in compdefs(md) - comp_name = name(comp_def) - params = parameter_names(comp_def) - connected = connected_params(md, comp_name) - append!(unconnected, map(x->(comp_name, x), setdiff(params, connected))) - end + unconnected = ParamVector() + connected = connected_params(md) + _collect_unconnected_params(md, connected, unconnected) return unconnected end @@ -232,13 +347,13 @@ 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 - parameters = Dict(k => v for (k, v) in parameters) - leftovers = unconnected_params(md) - external_params = md.external_params + for (comp_path, param_name) in unconnected_params(md) + comp_def = compdef(md, comp_path) + comp_name = nameof(comp_def) - for (comp_name, param_name) in leftovers + # @info "set_leftover_params: comp_name=$comp_name, param=$param_name" # check whether we need to set the external parameter - if ! haskey(md.external_params, param_name) + if external_param(md, param_name, missing_ok=true) === nothing value = parameters[string(param_name)] param_dims = parameter_dimensions(md, comp_name, param_name) @@ -250,144 +365,156 @@ function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T nothing end -internal_param_conns(md::ModelDef) = md.internal_param_conns +# Find internal param conns to a given destination component +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 -external_param_conns(md::ModelDef) = md.external_param_conns +function internal_param_conns(obj::AbstractCompositeComponentDef, comp_name::Symbol) + return internal_param_conns(obj, ComponentPath(obj.comp_path, comp_name)) +end -# Find internal param conns to a given destination component -function internal_param_conns(md::ModelDef, dst_comp_name::Symbol) - return filter(x->x.dst_comp_name == dst_comp_name, internal_param_conns(md)) +function add_internal_param_conn!(obj::AbstractCompositeComponentDef, conn::InternalParameterConnection) + push!(obj.internal_param_conns, conn) + dirty!(obj) end # Find external param conns for a given comp -function external_param_conns(md::ModelDef, comp_name::Symbol) - return filter(x -> x.comp_name == comp_name, external_param_conns(md)) +function external_param_conns(obj::AbstractCompositeComponentDef, comp_path::ComponentPath) + return filter(x -> x.comp_path == comp_path, external_param_conns(obj)) end -function external_param(md::ModelDef, name::Symbol) - try - return md.external_params[name] - catch err - if err isa KeyError - error("$name not found in external parameter list") - else - rethrow(err) - end - end +function external_param_conns(obj::AbstractCompositeComponentDef, comp_name::Symbol) + return external_param_conns(obj, ComponentPath(obj.comp_path, comp_name)) end -function add_internal_param_conn(md::ModelDef, conn::InternalParameterConnection) - push!(md.internal_param_conns, conn) +function external_param(obj::AbstractCompositeComponentDef, name::Symbol; missing_ok=false) + haskey(obj.external_params, name) && return obj.external_params[name] + + missing_ok && return nothing + + error("$name not found in external parameter list") end -function add_external_param_conn(md::ModelDef, conn::ExternalParameterConnection) - push!(md.external_param_conns, conn) +function add_external_param_conn!(obj::AbstractCompositeComponentDef, conn::ExternalParameterConnection) + push!(obj.external_param_conns, conn) + dirty!(obj) end -function set_external_param!(md::ModelDef, name::Symbol, value::ModelParameter) - md.external_params[name] = value +function set_external_param!(obj::AbstractCompositeComponentDef, 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) end -function set_external_param!(md::ModelDef, name::Symbol, value::Number; param_dims::Union{Nothing,Array{Symbol}} = nothing) - set_external_scalar_param!(md, name, value) +function set_external_param!(obj::AbstractCompositeComponentDef, name::Symbol, value::Number; + param_dims::Union{Nothing,Array{Symbol}} = nothing) + set_external_scalar_param!(obj, name, value) end -function set_external_param!(md::ModelDef, name::Symbol, value::Union{AbstractArray, AbstractRange, Tuple}; +function set_external_param!(obj::AbstractCompositeComponentDef, name::Symbol, + value::Union{AbstractArray, AbstractRange, Tuple}; param_dims::Union{Nothing,Array{Symbol}} = nothing) ti = get_time_index_position(param_dims) if ti != nothing - value = convert(Array{md.number_type}, value) + value = convert(Array{number_type(obj)}, value) num_dims = length(param_dims) - values = get_timestep_array(md, eltype(value), num_dims, ti, value) + values = get_timestep_array(obj, eltype(value), num_dims, ti, value) else values = value end - set_external_array_param!(md, name, values, param_dims) + set_external_array_param!(obj, name, values, param_dims) end """ - set_external_array_param!(md::ModelDef, name::Symbol, value::TimestepVector, dims) + set_external_array_param!(obj::AbstractCompositeComponentDef, + name::Symbol, value::TimestepVector, dims) Add a one dimensional time-indexed array parameter indicated by `name` and -`value` to the model `md`. In this case `dims` must be `[:time]`. +`value` to the composite `obj`. In this case `dims` must be `[:time]`. """ -function set_external_array_param!(md::ModelDef, name::Symbol, value::TimestepVector, dims) - # println("set_external_array_param!: dims=$dims, setting dims to [:time]") +function set_external_array_param!(obj::AbstractCompositeComponentDef, + name::Symbol, value::TimestepVector, dims) param = ArrayModelParameter(value, [:time]) # must be :time - set_external_param!(md, name, param) + set_external_param!(obj, name, param) end """ - set_external_array_param!(md::ModelDef, name::Symbol, value::TimestepMatrix, dims) + set_external_array_param!(obj::AbstractCompositeComponentDef, + name::Symbol, value::TimestepMatrix, dims) Add a multi-dimensional time-indexed array parameter `name` with value -`value` to the model `md`. In this case `dims` must be `[:time]`. +`value` to the composite `obj`. In this case `dims` must be `[:time]`. """ -function set_external_array_param!(md::ModelDef, name::Symbol, value::TimestepArray, dims) +function set_external_array_param!(obj::AbstractCompositeComponentDef, + name::Symbol, value::TimestepArray, dims) param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims) - set_external_param!(md, name, param) + set_external_param!(obj, name, param) end """ - set_external_array_param!(m::Model, name::Symbol, value::AbstractArray, dims) + set_external_array_param!(obj::AbstractCompositeComponentDef, + name::Symbol, value::AbstractArray, dims) -Add an array type parameter `name` with value `value` and `dims` dimensions to the model 'm'. +Add an array type parameter `name` with value `value` and `dims` dimensions to the composite `obj`. """ -function set_external_array_param!(md::ModelDef, name::Symbol, value::AbstractArray, dims) - numtype = md.number_type - - if !(typeof(value) <: Array{numtype}) - numtype = number_type(md) +function set_external_array_param!(obj::AbstractCompositeComponentDef, + name::Symbol, value::AbstractArray, dims) + numtype = Union{Missing, number_type(obj)} + + if !(typeof(value) <: Array{numtype} || (value isa AbstractArray && eltype(value) <: numtype)) # Need to force a conversion (simple convert may alias in v0.6) value = Array{numtype}(value) end param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims) - set_external_param!(md, name, param) + set_external_param!(obj, name, param) end """ - set_external_scalar_param!(md::ModelDef, name::Symbol, value::Any) + set_external_scalar_param!(obj::AbstractCompositeComponentDef, name::Symbol, value::Any) -Add a scalar type parameter `name` with the value `value` to the model `md`. +Add a scalar type parameter `name` with the value `value` to the composite `obj`. """ -function set_external_scalar_param!(md::ModelDef, name::Symbol, value::Any) +function set_external_scalar_param!(obj::AbstractCompositeComponentDef, name::Symbol, value::Any) p = ScalarModelParameter(value) - set_external_param!(md, name, p) + set_external_param!(obj, name, p) end """ - update_param!(md::ModelDef, name::Symbol, value; update_timesteps = false) + update_param!(obj::AbstractCompositeComponentDef, name::Symbol, value; update_timesteps = false) -Update the `value` of an external model parameter in ModelDef `md`, referenced +Update the `value` of an external model parameter in composite `obj`, referenced by `name`. Optional boolean argument `update_timesteps` with default value `false` indicates whether to update the time keys associated with the parameter values to match the model's time index. """ -function update_param!(md::ModelDef, name::Symbol, value; update_timesteps = false) - _update_param!(md::ModelDef, name::Symbol, value, update_timesteps; raise_error = true) +function update_param!(obj::AbstractCompositeComponentDef, name::Symbol, value; update_timesteps = false) + _update_param!(obj::AbstractCompositeComponentDef, name, value, update_timesteps; raise_error = true) end -function _update_param!(md::ModelDef, name::Symbol, value, update_timesteps; raise_error = true) - ext_params = md.external_params - if ! haskey(ext_params, name) - error("Cannot update parameter; $name not found in model's external parameters.") +function _update_param!(obj::AbstractCompositeComponentDef, + name::Symbol, value, update_timesteps; raise_error = true) + param = external_param(obj, name, missing_ok=true) + if param === nothing + error("Cannot update parameter; $name not found in composite's external parameters.") end - param = ext_params[name] - if param isa ScalarModelParameter if update_timesteps && raise_error error("Cannot update timesteps; parameter $name is a scalar parameter.") end - _update_scalar_param!(param, value) + _update_scalar_param!(param, name, value) else - _update_array_param!(md, name, value, update_timesteps, raise_error) + _update_array_param!(obj, name, value, update_timesteps, raise_error) end + dirty!(obj) end -function _update_scalar_param!(param::ScalarModelParameter, value) +function _update_scalar_param!(param::ScalarModelParameter, name, value) if ! (value isa typeof(param.value)) try value = convert(typeof(param.value), value) @@ -399,13 +526,14 @@ function _update_scalar_param!(param::ScalarModelParameter, value) nothing end -function _update_array_param!(md::ModelDef, name, value, update_timesteps, raise_error) +function _update_array_param!(obj::AbstractCompositeComponentDef, name, value, update_timesteps, raise_error) # Get original parameter - param = md.external_params[name] + param = external_param(obj, name) # Check type of provided parameter if !(typeof(value) <: AbstractArray) error("Cannot update array parameter $name with a value of type $(typeof(value)).") + elseif !(eltype(value) <: eltype(param.values)) try value = convert(Array{eltype(param.values)}, value) @@ -416,7 +544,7 @@ function _update_array_param!(md::ModelDef, name, value, update_timesteps, raise # Check size of provided parameter if update_timesteps && param.values isa TimestepArray - expected_size = ([length(dim_keys(md, d)) for d in param.dimensions]...,) + expected_size = ([length(dim_keys(obj, d)) for d in dim_names(param)]...,) else expected_size = size(param.values) end @@ -429,8 +557,9 @@ function _update_array_param!(md::ModelDef, name, value, update_timesteps, raise T = eltype(value) N = length(size(value)) ti = get_time_index_position(param) - new_timestep_array = get_timestep_array(md, T, N, ti, value) - md.external_params[name] = ArrayModelParameter(new_timestep_array, param.dimensions) + new_timestep_array = get_timestep_array(obj, T, N, ti, value) + set_external_param!(obj, name, ArrayModelParameter(new_timestep_array, dim_names(param))) + elseif raise_error error("Cannot update timesteps; parameter $name is not a TimestepArray.") else @@ -443,43 +572,45 @@ function _update_array_param!(md::ModelDef, name, value, update_timesteps, raise param.values = value end end + dirty!(obj) nothing end """ - update_params!(md::ModelDef, parameters::Dict{T, Any}; update_timesteps = false) where T + update_params!(obj::AbstractCompositeComponentDef, parameters::Dict{T, Any}; + update_timesteps = false) 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, with optional Boolean argument update_timesteps. 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. +component definition. """ -function update_params!(md::ModelDef, parameters::Dict; update_timesteps = false) +function update_params!(obj::AbstractCompositeComponentDef, parameters::Dict; update_timesteps = false) parameters = Dict(Symbol(k) => v for (k, v) in parameters) for (param_name, value) in parameters - _update_param!(md, param_name, value, update_timesteps; raise_error = false) + _update_param!(obj, param_name, value, update_timesteps; raise_error = false) end nothing end +function add_connector_comps!(obj::AbstractCompositeComponentDef) + conns = internal_param_conns(obj) -function add_connector_comps(md::ModelDef) - conns = md.internal_param_conns # we modify this, so we don't use functional API - - for comp_def in compdefs(md) - comp_name = name(comp_def) + for comp_def in compdefs(obj) + comp_name = nameof(comp_def) + comp_path = comp_def.comp_path # first need to see if we need to add any connector components for this component - internal_conns = filter(x -> x.dst_comp_name == comp_name, conns) + internal_conns = filter(x -> x.dst_comp_path == comp_path, conns) need_conn_comps = filter(x -> x.backup !== nothing, internal_conns) - # println("Need connectors comps: $need_conn_comps") + # isempty(need_conn_comps) || @info "Need connectors comps: $need_conn_comps" for (i, conn) in enumerate(need_conn_comps) - push!(md.backups, conn.backup) + add_backup!(obj, conn.backup) - num_dims = length(size(external_param(md, conn.backup).values)) + num_dims = length(size(external_param(obj, conn.backup).values)) if ! (num_dims in (1, 2)) error("Connector components for parameters with > 2 dimensions are not implemented.") @@ -487,100 +618,33 @@ function add_connector_comps(md::ModelDef) # Fetch the definition of the appropriate connector commponent conn_comp_def = (num_dims == 1 ? Mimi.ConnectorCompVector : Mimi.ConnectorCompMatrix) - conn_comp_name = connector_comp_name(i) + conn_comp_name = connector_comp_name(i) # generate a new name # Add the connector component before the user-defined component that required it - # println("add_connector_comps: add_comp!(md, $(conn_comp_def.comp_id), $conn_comp_name, before=$comp_name)") - add_comp!(md, conn_comp_def, conn_comp_name, before=comp_name) + conn_comp = add_comp!(obj, conn_comp_def, conn_comp_name, before=comp_name) + conn_path = conn_comp.comp_path # add a connection between src_component and the ConnectorComp - push!(conns, InternalParameterConnection(conn.src_comp_name, conn.src_var_name, - conn_comp_name, :input1, - conn.ignoreunits)) + add_internal_param_conn!(obj, InternalParameterConnection(conn.src_comp_path, conn.src_var_name, + conn_path, :input1, + conn.ignoreunits)) # add a connection between ConnectorComp and dst_component - push!(conns, InternalParameterConnection(conn_comp_name, :output, - conn.dst_comp_name, conn.dst_par_name, - conn.ignoreunits)) + add_internal_param_conn!(obj, InternalParameterConnection(conn_path, :output, + conn.dst_comp_path, conn.dst_par_name, + conn.ignoreunits)) # add a connection between ConnectorComp and the external backup data - push!(md.external_param_conns, ExternalParameterConnection(conn_comp_name, :input2, conn.backup)) - - src_comp_def = compdef(md, conn.src_comp_name) - set_param!(md, conn_comp_name, :first, first_period(md, src_comp_def)) - set_param!(md, conn_comp_name, :last, last_period(md, src_comp_def)) + add_external_param_conn!(obj, ExternalParameterConnection(conn_path, :input2, conn.backup)) + src_comp_def = compdef(obj, conn.src_comp_path) + set_param!(obj, conn_comp_name, :first, first_period(obj, src_comp_def)) + set_param!(obj, conn_comp_name, :last, last_period(obj, src_comp_def)) end end # Save the sorted component order for processing - # md.sorted_comps = _topological_sort(md) + # obj.sorted_comps = _topological_sort(obj) return nothing end - - -# -# Support for automatic ordering of components -# - -""" - dependencies(md::ModelDef, comp_name::Symbol) - -Return the set of component names that `comp_name` in `md` depends one, i.e., -sources for which `comp_name` is the destination of an internal connection. -""" -function dependencies(md::ModelDef, comp_name::Symbol) - conns = internal_param_conns(md) - # For the purposes of the DAG, we don't treat dependencies on [t-1] as an ordering constraint - deps = Set(c.src_comp_name for c in conns if (c.dst_comp_name == comp_name && c.offset == 0)) - return deps -end - -""" - comp_graph(md::ModelDef) - -Return a MetaGraph containing a directed (LightGraph) graph of the components of -ModelDef `md`. Each vertex has a :name property with its component name. -""" -function comp_graph(md::ModelDef) - comp_names = collect(compkeys(md)) - graph = MetaDiGraph() - - for comp_name in comp_names - add_vertex!(graph, :name, comp_name) - end - - set_indexing_prop!(graph, :name) - - for comp_name in comp_names - for dep_name in dependencies(md, comp_name) - src = graph[dep_name, :name] - dst = graph[comp_name, :name] - add_edge!(graph, src, dst) - end - end - - #TODO: for now we can allow cycles since we aren't using the offset - # if is_cyclic(graph) - # error("Component graph contains a cycle") - # end - - return graph -end - -""" - _topological_sort(md::ModelDef) - -Build a directed acyclic graph referencing the positions of the components in -the OrderedDict of model `md`, tracing dependencies to create the DAG. -Perform a topological sort on the graph for the given model and return a vector -of component names in the order that will ensure dependencies are processed -prior to dependent components. -""" -function _topological_sort(md::ModelDef) - graph = comp_graph(md) - ordered = topological_sort_by_dfs(graph) - names = map(i -> graph[i, :name], ordered) - return names -end diff --git a/src/core/defcomp.jl b/src/core/defcomp.jl index 0642ab1a4..f78ae0da5 100644 --- a/src/core/defcomp.jl +++ b/src/core/defcomp.jl @@ -3,20 +3,13 @@ # using MacroTools -global defcomp_verbosity = false - -function set_defcomp_verbosity(value::Bool) - global defcomp_verbosity = value - nothing -end - -# Store a list of built-in components so we can suppress messages about creating them -# TBD: and (later) suppress their return in the list of components at the user level. +# Store a list of built-in components so we can suppress messages about creating them. +# TBD: suppress returning these in the list of components at the user level. const global built_in_comps = (:adder, :ConnectorCompVector, :ConnectorCompMatrix) is_builtin(comp_name) = comp_name in built_in_comps -function _generate_run_func(comp_name, args, body) +function _generate_run_func(comp_name, module_name, args, body) if length(args) != 4 error("Can't generate run_timestep; requires 4 arguments but got $args") end @@ -25,13 +18,13 @@ function _generate_run_func(comp_name, args, body) # Generate unique function name for each component so we can store a function pointer. # (Methods requiring dispatch cannot be invoked directly. Could use FunctionWrapper here...) - func_name = Symbol("run_timestep_$comp_name") + func_name = Symbol("run_timestep_$(module_name)_$(comp_name)") # Needs "global" so function is defined outside the "let" statement func = :( global function $(func_name)($(p)::Mimi.ComponentInstanceParameters, $(v)::Mimi.ComponentInstanceVariables, - $(d)::Mimi.DimDict, + $(d)::Mimi.DimValueDict, $(t)::T) where {T <: Mimi.AbstractTimestep} $(body...) return nothing @@ -40,7 +33,7 @@ function _generate_run_func(comp_name, args, body) return func end -function _generate_init_func(comp_name, args, body) +function _generate_init_func(comp_name, module_name, args, body) if length(args) != 3 error("Can't generate init function; requires 3 arguments but got $args") @@ -49,12 +42,12 @@ function _generate_init_func(comp_name, args, body) # add types to the parameters (p, v, d) = args - func_name = Symbol("init_$comp_name") + func_name = Symbol("init_$(module_name)_$(comp_name)") func = :( global function $(func_name)($(p)::Mimi.ComponentInstanceParameters, $(v)::Mimi.ComponentInstanceVariables, - $(d)::Mimi.DimDict) + $(d)::Mimi.DimValueDict) $(body...) return nothing end @@ -75,9 +68,33 @@ function _check_for_known_element(name) end end +# Add a variable to a ComponentDef. CompositeComponents have no vars of their own, +# only references to vars in components contained within. +function add_variable(comp_def::ComponentDef, name, datatype, dimensions, description, unit) + v = VariableDef(name, comp_def.comp_path, datatype, dimensions, description, unit) + comp_def[name] = v # adds to namespace and checks for duplicate + return v +end + +# Add a variable to a ComponentDef referenced by ComponentId +function add_variable(comp_id::ComponentId, name, datatype, dimensions, description, unit) + add_variable(compdef(comp_id), name, datatype, dimensions, description, unit) +end + +function add_parameter(comp_def::ComponentDef, name, datatype, dimensions, description, unit, default) + p = ParameterDef(name, comp_def.comp_path, datatype, dimensions, description, unit, default) + comp_def[name] = p # adds to namespace and checks for duplicate + dirty!(comp_def) + return p +end + +function add_parameter(comp_id::ComponentId, name, datatype, dimensions, description, unit, default) + add_parameter(compdef(comp_id), name, datatype, dimensions, description, unit, default) +end + # Generates an expression to construct a Variable or Parameter function _generate_var_or_param(elt_type, name, datatype, dimensions, dflt, desc, unit) - func_name = elt_type == :Parameter ? :addparameter : :addvariable + func_name = elt_type == :Parameter ? :add_parameter : :add_variable args = [datatype, dimensions, desc, unit] if elt_type == :Parameter push!(args, dflt) @@ -131,16 +148,16 @@ macro defcomp(comp_name, ex) @capture(ex, elements__) @debug "Component $comp_name" - # Allow explicit definition of module to define component in + # TBD: Allow explicit definition of module to define component in if @capture(comp_name, module_name_.cmpname_) # e.g., Mimi.adder comp_name = cmpname end # We'll return a block of expressions that will define the component. - # @__MODULE__ is evaluated when the expanded macro is interpreted + # N.B. @__MODULE__ is evaluated when the expanded macro is interpreted. result = :( - let current_module = @__MODULE__, - comp_id = Mimi.ComponentId(current_module, $(QuoteNode(comp_name))), + let calling_module = @__MODULE__, + comp_id = Mimi.ComponentId(calling_module, $(QuoteNode(comp_name))), comp = Mimi.ComponentDef(comp_id) global $comp_name = comp @@ -165,11 +182,11 @@ macro defcomp(comp_name, ex) if @capture(elt, function fname_(args__) body__ end) if fname == :run_timestep body = elt.args[2].args # replace captured body with this, which includes line numbers - expr = _generate_run_func(comp_name, args, body) + expr = _generate_run_func(comp_name, nameof(__module__), args, body) elseif fname == :init body = elt.args[2].args # as above - expr = _generate_init_func(comp_name, args, body) + expr = _generate_init_func(comp_name, nameof(__module__), args, body) else error("@defcomp can contain only these functions: init(p, v, d) and run_timestep(p, v, d, t)") end @@ -249,7 +266,7 @@ macro defcomp(comp_name, ex) end end - vartype = (vartype === nothing ? Number : Base.eval(Main, vartype)) + vartype = (vartype === nothing ? Number : Main.eval(vartype)) addexpr(_generate_var_or_param(elt_type, name, vartype, dimensions, dflt, desc, unit)) else @@ -260,74 +277,3 @@ macro defcomp(comp_name, ex) addexpr(:(nothing)) # reduces noise return esc(result) end - -""" - defmodel(model_name::Symbol, ex::Expr) - -Define a Mimi model. The following types of expressions are supported: - -1. `component(name)` # add comp to model -2. `dst_component.name = ex::Expr` # provide a value for a parameter -3. `src_component.name => dst_component.name` # connect a variable to a parameter -4. `index[name] = iterable-of-values` # define values for an index -""" -macro defmodel(model_name, ex) - @capture(ex, elements__) - - # @__MODULE__ is evaluated in calling module when macro is interpreted - result = :( - let calling_module = @__MODULE__, comp_mod_name = nothing - global $model_name = Model() - end - ) - - # helper function used in loop below - function addexpr(expr) - let_block = result.args[end].args - push!(let_block, expr) - end - - for elt in elements - offset = 0 - - if @capture(elt, component(comp_mod_name_name_.comp_name_) | component(comp_name_) | - component(comp_mod_name_.comp_name_, alias_) | component(comp_name_, alias_)) - - # set local copy of comp_mod_name to the stated or default component module - expr = (comp_mod_name === nothing ? :(comp_mod_name = calling_module) : :(comp_mod_name = comp_mod_name)) - addexpr(expr) - - name = (alias === nothing ? comp_name : alias) - expr = :(add_comp!($model_name, Mimi.ComponentId(comp_mod_name, $(QuoteNode(comp_name))), $(QuoteNode(name)))) - - # TBD: extend comp.var syntax to allow module name, e.g., FUND.economy.ygross - elseif (@capture(elt, src_comp_.src_name_[arg_] => dst_comp_.dst_name_) || - @capture(elt, src_comp_.src_name_ => dst_comp_.dst_name_)) - if (arg !== nothing && (! @capture(arg, t - offset_) || offset <= 0)) - error("Subscripted connection source must have subscript [t - x] where x is an integer > 0") - end - - expr = :(Mimi.connect_param!($model_name, - $(QuoteNode(dst_comp)), $(QuoteNode(dst_name)), - $(QuoteNode(src_comp)), $(QuoteNode(src_name)), - offset=$offset)) - - elseif @capture(elt, index[idx_name_] = rhs_) - expr = :(Mimi.set_dimension!($model_name, $(QuoteNode(idx_name)), $rhs)) - - elseif @capture(elt, comp_name_.param_name_ = rhs_) - expr = :(Mimi.set_param!($model_name, $(QuoteNode(comp_name)), $(QuoteNode(param_name)), $rhs)) - - else - # Pass through anything else to allow the user to define intermediate vars, etc. - println("Passing through: $elt") - expr = elt - end - - addexpr(expr) - end - - # addexpr(:($model_name)) # return this or nothing? - addexpr(:(nothing)) - return esc(result) -end diff --git a/src/core/defcomposite.jl b/src/core/defcomposite.jl new file mode 100644 index 000000000..05bff420d --- /dev/null +++ b/src/core/defcomposite.jl @@ -0,0 +1,241 @@ +using MacroTools + + # 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] +_arg_slurp(arg_tup) = arg_tup[3] +_arg_default(arg_tup) = arg_tup[4] + +const NumericArray = Array{T, N} where {T <: Number, N} + +function _collect_bindings(exprs) + bindings = [] + # @info "_collect_bindings: $exprs" + + for expr in exprs + if @capture(expr, name_ => val_) && name isa Symbol && + (val isa Symbol || val isa Number || val.head in (:vcat, :hcat, :vect)) + push!(bindings, name => val) + else + error("Elements of bindings list must Pair{Symbol, Symbol} or Pair{Symbol, Number or Array of Number} got $expr") + end + end + + # @info "returning $bindings" + return bindings +end + +function _subcomp(calling_module, args, kwargs) + # splitarg produces a tuple for each arg of the form (arg_name, arg_type, slurp, default) + arg_tups = map(splitarg, args) + + if kwargs === nothing + # If a ";" was not used to separate kwargs, move any kwargs from args. + kwarg_tups = filter(tup -> _arg_default(tup) !== nothing, arg_tups) + arg_tups = filter(tup -> _arg_default(tup) === nothing, arg_tups) + else + kwarg_tups = map(splitarg, kwargs) + end + + if 1 > length(arg_tups) > 2 + @error "component() must have one or two non-keyword values" + end + + arg1 = _arg_name(arg_tups[1]) + alias = length(arg_tups) == 2 ? _arg_name(args_tups[2]) : nothing + + cmodule = nothing + if ! (@capture(arg1, cmodule_.cname_) || @capture(arg1, cname_Symbol)) + error("Component name must be a Module.name expression or a symbol, got $arg1") + end + + valid_kws = (:bindings,) # valid keyword args to the component() psuedo-function + kw = Dict([key => [] for key in valid_kws]) + + for (arg_name, arg_type, slurp, default) in kwarg_tups + if arg_name in valid_kws + if default isa Expr && hasmethod(Base.iterate, (typeof(default.args),)) + append!(kw[arg_name], default.args) + else + @error "Value of $arg_name argument must be iterable" + end + else + @error "Unknown keyword $arg_name; valid keywords are $valid_kws" + end + end + + bindings = _collect_bindings(kw[:bindings]) + module_obj = (cmodule === nothing ? calling_module : getfield(calling_module, cmodule)) + return SubComponent(module_obj, cname, alias, bindings) +end + +# Convert an expr like `a.b.c.d` to `[:a, :b, :c, :d]` +function parse_dotted_symbols(expr) + global Args = expr + syms = Symbol[] + + ex = expr + while @capture(ex, left_.right_) && right isa Symbol + push!(syms, right) + ex = left + end + + if ex isa Symbol + push!(syms, ex) + else + # @warn "Expected Symbol or Symbol.Symbol..., got $expr" + return nothing + end + + syms = reverse(syms) + var_or_par = pop!(syms) + return ComponentPath(syms), var_or_par +end + +""" + defcomposite(cc_name::Symbol, ex::Expr) + +Define a Mimi CompositeComponent `cc_name` with the expressions in `ex`. Expressions +are all variations on `component(...)`, which adds a component to the composite. The +calling signature for `component()` processed herein is: + + component(comp_name, local_name; + bindings=[list Pair{Symbol, Symbol or Number or Array of Numbers}]) + +Bindings are expressed as a vector of `Pair` objects, where the first element of the +pair is the name (again, without the `:` prefix) representing a parameter in the component +being added, and the second element is either a numeric constant, a matrix of the +appropriate shape, or the name of a variable in another component. The variable name +is expressed as the component id (which may be prefixed by a module, e.g., `Mimi.adder`) +followed by a `.` and the variable name in that component. So the form is either +`modname.compname.varname` or `compname.varname`, which must be known in the current module. + +Unlike leaf components, composite components do not have user-defined `init` or `run_timestep` +functions; these are defined internally to iterate over constituent components and call the +associated method on each. +""" +macro defcomposite(cc_name, ex) + # @info "defining composite $cc_name in module $(fullname(__module__))" + + @capture(ex, elements__) + comps = SubComponent[] + imports = [] + conns = [] + + calling_module = __module__ + # @info "defcomposite calling module: $calling_module" + + for elt in elements + # @info "parsing $elt"; dump(elt) + + if @capture(elt, (component(args__; kwargs__) | component(args__))) + push!(comps, _subcomp(calling_module, args, kwargs)) + + # distinguish imports, e.g., :(EXP_VAR = CHILD_COMP1.COMP2.VAR3), + # from connections, e.g., :(COMP1.PAR2 = COMP2.COMP5.VAR2) + + # elseif elt.head == :tuple && length(elt.args) > 0 && @capture(elt.args[1], left_ = right_) && left isa Symbol + # # Aliasing a local name to several parameters at once is possible using an expr like + # # :(EXP_PAR1 = CHILD_COMP1.PAR2, CHILD_COMP2.PAR2, CHILD_COMP3.PAR5, CHILD_COMP3.PAR6) + # # Note that this parses as a tuple expression with first element being `EXP_PAR1 = CHILD_COMP1`. + # # Here we parse everything on the right side, at once using broadcasting and add the initial + # # component (immediately after "=") to the list, and then store a Vector of param refs. + # args = [right, elt.args[2:end]...] + # vars_pars = parse_dotted_symbols.(args) + # @info "import as $left = $vars_pars" + # push!(imports, (left, vars_pars)) + + elseif @capture(elt, left_ = right_) + + if left isa Symbol # simple import case + # Save a singletons as a 1-element Vector for consistency with multiple linked params + var_par = right.head == :tuple ? parse_dotted_symbols.(right.args) : [parse_dotted_symbols(right)] + push!(imports, (left, var_par)) + # @info "import as $left = $var_par" + + # note that `comp_Symbol.name_Symbol` failed; bug in MacroTools? + elseif @capture(left, comp_.name_) # simple connection case + dst = parse_dotted_symbols(left) + dst === nothing && error("Expected dot-delimited sequence of symbols, got $left") + + src = parse_dotted_symbols(right) + src === nothing && error("Expected dot-delimited sequence of symbols, got $right") + + push!(conns, (dst, src)) + # @info "connection: $dst = $src" + + else + error("Unrecognized expression on left hand side of '=' in @defcomposite: $elt") + end + else + error("Unrecognized element in @defcomposite: $elt") + end + end + + # TBD: use fullname(__module__) to get "path" to module, as tuple of Symbols, e.g., (:Main, :ABC, :DEF) + # TBD: use Base.moduleroot(__module__) to get the first in that sequence, if needed + # TBD: parentmodule(m) gets the enclosing module (but for root modules returns self) + # TBD: might need to replace the single symbol used for module name in ComponentId with Module path. + + # @info "imports: $imports" + # @info " $(length(imports)) elements" + # global IMP = imports + + result = :( + let conns = $conns, + imports = $imports, + + cc_id = Mimi.ComponentId($calling_module, $(QuoteNode(cc_name))) + + global $cc_name = Mimi.CompositeComponentDef(cc_id, $(QuoteNode(cc_name)), $comps, $__module__) + + # @info "Defining composite $cc_id" + + function _store_in_ns(refs, local_name) + isempty(refs) && return + + if length(refs) == 1 + $cc_name[local_name] = refs[1] + else + # We will eventually allow linking parameters, but not variables. For now, neither. + error("Variables and parameters may only be aliased individually: $refs") + end + end + + # This is more complicated than needed for now since we're leaving in place some of + # the structure to accommodate linking multiple parameters to a single imported name. + # We're postponing this feature to accelerate merging the component branch and will + # return to this later. + for (local_name, item) in imports + var_refs = [] + par_refs = [] + + for (src_path, src_name) in item + dr = Mimi.DatumReference(src_name, $cc_name, src_path) + if Mimi.is_parameter(dr) + push!(par_refs, Mimi.ParameterDefReference(dr)) + else + push!(var_refs, Mimi.VariableDefReference(dr)) + end + end + + _store_in_ns(var_refs, local_name) + _store_in_ns(par_refs, local_name) + end + + # Mimi.import_params!($cc_name) + + for ((dst_path, dst_name), (src_path, src_name)) in conns + # @info "connect_param!($(nameof($cc_name)), $dst_path, :$dst_name, $src_path, :$src_name)" + Mimi.connect_param!($cc_name, dst_path, dst_name, src_path, src_name) + end + + $cc_name + end + ) + + # @info "defcomposite:\n$result" + return esc(result) +end + +nothing diff --git a/src/core/defmodel.jl b/src/core/defmodel.jl new file mode 100644 index 000000000..6ab2d9865 --- /dev/null +++ b/src/core/defmodel.jl @@ -0,0 +1,83 @@ +# +# @defmodel and supporting functions +# +using MacroTools + +""" + defmodel(model_name::Symbol, ex::Expr) + +Define a Mimi model. The following types of expressions are supported: + +1. `component(name)` # add comp to model +2. `dst_component.name = ex::Expr` # provide a value for a parameter +3. `src_component.name => dst_component.name` # connect a variable to a parameter +4. `index[name] = iterable-of-values` # define values for an index +""" +macro defmodel(model_name, ex) + @capture(ex, elements__) + + # @__MODULE__ is evaluated in calling module when macro is interpreted + result = :( + let calling_module = @__MODULE__, comp_mod_name = nothing, comp_mod_obj = nothing + global $model_name = Model() + end + ) + + # helper function used in loop below + function addexpr(expr) + let_block = result.args[end].args + push!(let_block, expr) + end + + for elt in elements + offset = 0 + + if @capture(elt, component(comp_mod_name_.comp_name_) | component(comp_name_) | + component(comp_mod_name_.comp_name_, alias_) | component(comp_name_, alias_)) + + # set local copy of comp_mod_name to the stated or default component module + expr = (comp_mod_name === nothing ? :(comp_mod_obj = calling_module) # nameof(calling_module)) + # TBD: This may still not be right: + : :(comp_mod_obj = getfield(calling_module, $(QuoteNode(comp_mod_name))))) + addexpr(expr) + + name = (alias === nothing ? comp_name : alias) + expr = :(add_comp!($model_name, Mimi.ComponentId(comp_mod_obj, $(QuoteNode(comp_name))), $(QuoteNode(name)))) + + + # TBD: extend comp.var syntax to allow module name, e.g., FUND.economy.ygross + elseif (@capture(elt, src_comp_.src_name_[arg_] => dst_comp_.dst_name_) || + @capture(elt, src_comp_.src_name_ => dst_comp_.dst_name_)) + if (arg !== nothing && (! @capture(arg, t - offset_) || offset <= 0)) + error("Subscripted connection source must have subscript [t - x] where x is an integer > 0") + end + + expr = :(Mimi.connect_param!($model_name, + $(QuoteNode(dst_comp)), $(QuoteNode(dst_name)), + $(QuoteNode(src_comp)), $(QuoteNode(src_name)), + offset=$offset)) + + elseif @capture(elt, index[idx_name_] = rhs_) + expr = :(Mimi.set_dimension!($model_name, $(QuoteNode(idx_name)), $rhs)) + + # elseif @capture(elt, comp_name_.param_name_ = rhs_) + # expr = :(Mimi.set_param!($model_name, $(QuoteNode(comp_name)), $(QuoteNode(param_name)), $rhs)) + + elseif @capture(elt, lhs_ = rhs_) && @capture(lhs, comp_.name_) + (path, param_name) = parse_dotted_symbols(lhs) + expr = :(Mimi.set_param!($model_name, $path, $(QuoteNode(param_name)), $rhs)) + # @info "expr: $expr" + + else + # Pass through anything else to allow the user to define intermediate vars, etc. + @info "Passing through: $elt" + expr = elt + end + + addexpr(expr) + end + + # addexpr(:($model_name)) # return this or nothing? + addexpr(:(nothing)) + return esc(result) +end diff --git a/src/core/defs.jl b/src/core/defs.jl index 22597d26d..2803c0140 100644 --- a/src/core/defs.jl +++ b/src/core/defs.jl @@ -1,125 +1,249 @@ +Base.length(obj::AbstractComponentDef) = 0 # no sub-components +Base.length(obj::AbstractCompositeComponentDef) = length(components(obj)) function compdef(comp_id::ComponentId) - comp_module = comp_id.module_name - comp_def = getfield(comp_module, comp_id.comp_name) - return comp_def -end + # @info "compdef: mod=$(comp_id.module_obj) name=$(comp_id.comp_name)" + return getfield(comp_id.module_obj, comp_id.comp_name) +end + +# Deprecated +# function find_module(path::NTuple{N, Symbol} where N) +# m = Main +# for name in path +# try +# m = getfield(m, name) +# catch +# error("Module name $name was not found in module $m") +# end +# end +# return m +# end -compdefs(md::ModelDef) = values(md.comp_defs) +# Deprecated +# function compdef(comp_id::ComponentId; module_obj::Union{Nothing, Module}=nothing) +# if module_obj === nothing +# name = comp_id.module_name +# path = @or(comp_id.module_path, (:Main, comp_id.module_name)) +# module_obj = find_module(path) +# end -compkeys(md::ModelDef) = keys(md.comp_defs) +# return getfield(module_obj, comp_id.comp_name) +# end -hascomp(md::ModelDef, comp_name::Symbol) = haskey(md.comp_defs, comp_name) +compdef(cr::ComponentReference) = find_comp(cr) -compdef(md::ModelDef, comp_name::Symbol) = md.comp_defs[comp_name] +compdef(dr::AbstractDatumReference) = find_comp(dr.root, dr.comp_path) -function reset_compdefs(reload_builtins=true) - if reload_builtins - compdir = joinpath(@__DIR__, "..", "components") - load_comps(compdir) - end -end +compdef(obj::AbstractCompositeComponentDef, path::ComponentPath) = find_comp(obj, path) -first_period(comp_def::ComponentDef) = comp_def.first -first_period(md::ModelDef, comp_def::ComponentDef) = first_period(comp_def) === nothing ? time_labels(md)[1] : first_period(comp_def) +compdef(obj::AbstractCompositeComponentDef, comp_name::Symbol) = components(obj)[comp_name] -last_period(comp_def::ComponentDef) = comp_def.last -last_period(md::ModelDef, comp_def::ComponentDef) = last_period(comp_def) === nothing ? time_labels(md)[end] : last_period(comp_def) +has_comp(obj::AbstractCompositeComponentDef, comp_name::Symbol) = haskey(components(obj), comp_name) +compdefs(obj::AbstractCompositeComponentDef) = values(components(obj)) +compkeys(obj::AbstractCompositeComponentDef) = keys(components(obj)) -# Return the module object for the component was defined in -compmodule(comp_id::ComponentId) = comp_id.module_name -compname(comp_id::ComponentId) = comp_id.comp_name +# Allows method to be called harmlessly on leaf component defs, which simplifies recursive funcs. +compdefs(c::ComponentDef) = [] -compmodule(comp_def::ComponentDef) = compmodule(comp_def.comp_id) -compname(comp_def::ComponentDef) = compname(comp_def.comp_id) +compmodule(comp_id::ComponentId) = comp_id.module_obj +compname(comp_id::ComponentId) = comp_id.comp_name +compmodule(obj::AbstractComponentDef) = compmodule(obj.comp_id) +compname(obj::AbstractComponentDef) = compname(obj.comp_id) -function Base.show(io::IO, comp_id::ComponentId) - print(io, "") -end +compnames() = map(compname, compdefs()) """ - name(def::NamedDef) = def.name + is_detached(obj::AbstractComponentDef) -Return the name of `def`. Possible `NamedDef`s include `DatumDef`, and `ComponentDef`. +Return true if `obj` is not a ModelDef and it has no parent. """ -name(def::NamedDef) = def.name # old definition; should deprecate this... -Base.nameof(def::NamedDef) = def.name # 'nameof' is the more julian name +is_detached(obj::AbstractComponentDef) = (obj.parent === nothing) +is_detached(obj::ModelDef) = false # by definition + +dirty(md::ModelDef) = md.dirty + +function dirty!(obj::AbstractComponentDef) + root = get_root(obj) + if root === nothing + return + end + + if root isa ModelDef + dirty!(root) + end +end + +dirty!(md::ModelDef) = (md.dirty = true) + +compname(dr::AbstractDatumReference) = dr.comp_path.names[end] + +is_variable(dr::AbstractDatumReference) = has_variable(find_comp(dr), nameof(dr)) +is_parameter(dr::AbstractDatumReference) = has_parameter(find_comp(dr), nameof(dr)) number_type(md::ModelDef) = md.number_type -numcomponents(md::ModelDef) = length(md.comp_defs) +function number_type(obj::AbstractCompositeComponentDef) + root = get_root(obj) + # TBD: hack alert. Need to allow number_type to be specified + # for composites that are not yet connected to a ModelDef? + return root isa ModelDef ? root.number_type : Float64 +end + +first_period(root::AbstractCompositeComponentDef, comp::AbstractComponentDef) = @or(first_period(comp), first_period(root)) +last_period(root::AbstractCompositeComponentDef, comp::AbstractComponentDef) = @or(last_period(comp), last_period(root)) + +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))) """ - delete!(m::ModelDef, component::Symbol + delete!(obj::AbstractCompositeComponentDef, component::Symbol) -Delete a `component` by name from a model definition `m`. +Delete a `component` by name from composite `ccd`. """ -function Base.delete!(md::ModelDef, comp_name::Symbol) - if ! haskey(md.comp_defs, comp_name) - error("Cannot delete '$comp_name' from model; component does not exist.") +function Base.delete!(ccd::AbstractCompositeComponentDef, comp_name::Symbol) + if ! has_comp(ccd, comp_name) + error("Cannot delete '$comp_name': component does not exist.") end - delete!(md.comp_defs, comp_name) + comp_def = compdef(ccd, comp_name) + delete!(ccd.namespace, comp_name) + + # Remove references to the deleted comp + comp_path = comp_def.comp_path - ipc_filter = x -> x.src_comp_name != comp_name && x.dst_comp_name != comp_name - filter!(ipc_filter, md.internal_param_conns) + # TBD: find and delete external_params associated with deleted component? Currently no record of this. - epc_filter = x -> x.comp_name != comp_name - filter!(epc_filter, md.external_param_conns) + ipc_filter = x -> x.src_comp_path != comp_path && x.dst_comp_path != comp_path + filter!(ipc_filter, ccd.internal_param_conns) + + epc_filter = x -> x.comp_path != comp_path + filter!(epc_filter, ccd.external_param_conns) end +@delegate Base.haskey(comp::AbstractComponentDef, key::Symbol) => namespace + +Base.getindex(comp::AbstractComponentDef, key::Symbol) = comp.namespace[key] + # -# Dimensions +# Component namespaces # """ - add_dimension!(comp::ComponentDef, name) + istype(T::DataType) + +Return an anonymous func that can be used to filter a dict by data type of values. +Example: `filter(istype(AbstractComponentDef), obj.namespace)` +""" +istype(T::DataType) = (pair -> pair.second isa T) + +# Namespace filter functions return dicts of values for the given type. +# N.B. only composites hold comps in the namespace. +components(obj::AbstractCompositeComponentDef) = filter(istype(AbstractComponentDef), obj.namespace) + +param_dict(obj::ComponentDef) = filter(istype(ParameterDef), obj.namespace) +param_dict(obj::AbstractCompositeComponentDef) = filter(istype(ParameterDefReference), obj.namespace) + +var_dict(obj::ComponentDef) = filter(istype(VariableDef), obj.namespace) +var_dict(obj::AbstractCompositeComponentDef) = filter(istype(VariableDefReference), obj.namespace) + +""" + parameters(comp_def::AbstractComponentDef) + +Return an iterator of the parameter definitions (or references) for `comp_def`. +""" +parameters(obj::AbstractComponentDef) = values(param_dict(obj)) + -Add a dimension name to a `ComponentDef`, with an optional Dimension definition. -The definition is included only for the case of anonymous dimensions, which are -indicated in `@defcomp` with an Int rather than a symbol name. In this case, the -Int (e.g., 4) is converted to a dimension of the same length, e.g., `Dimension(4)`, -and assigned a new name `Symbol(4)`` """ -function add_dimension!(comp::ComponentDef, name) - # create the Dimension and store it in the ComponentDef until build time - dim = (name isa Int) ? Dimension(name) : nothing - comp.dimensions[Symbol(name)] = dim + variables(comp_def::AbstractComponentDef) + +Return an iterator of the variable definitions (or references) for `comp_def`. +""" +variables(obj::ComponentDef) = values(filter(istype(VariableDef), obj.namespace)) +variables(obj::AbstractCompositeComponentDef) = values(filter(istype(VariableDefReference), obj.namespace)) + +variables(comp_id::ComponentId) = variables(compdef(comp_id)) +parameters(comp_id::ComponentId) = parameters(compdef(comp_id)) + +# Return true if the component namespace has an item `name` that isa `T` +function _ns_has(comp_def::AbstractComponentDef, name::Symbol, T::DataType) + return haskey(comp_def.namespace, name) && comp_def.namespace[name] isa T end -add_dimension!(comp_id::ComponentId, name) = add_dimension!(compdef(comp_id), name) +function _ns_get(obj::AbstractComponentDef, name::Symbol, T::DataType) + haskey(obj.namespace, name) || error("Item :$name was not found in component $(obj.comp_path)") + item = obj[name] + item isa T || error(":$name in component $(obj.comp_path) is a $(typeof(item)); expected type $T") + return item +end -dimensions(comp_def::ComponentDef) = keys(comp_def.dimensions) +function _save_to_namespace(comp::AbstractComponentDef, key::Symbol, value::NamespaceElement) + # Allow replacement of existing values for a key only with items of the same type. + if haskey(comp, key) + elt_type = typeof(comp[key]) + T = typeof(value) + elt_type == T || error("Cannot replace item $key, type $elt_type, with object type $T in component $(comp.comp_path).") + end -dimensions(def::DatumDef) = def.dimensions + comp.namespace[key] = value +end -dimensions(comp_def::ComponentDef, datum_name::Symbol) = dimensions(datumdef(comp_def, datum_name)) +""" + datum_reference(comp::ComponentDef, datum_name::Symbol) -dim_count(def::DatumDef) = length(def.dimensions) +Create a reference to the given datum, which must already exist. +""" +function datum_reference(comp::ComponentDef, datum_name::Symbol) + obj = _ns_get(comp, datum_name, AbstractDatumDef) + path = @or(obj.comp_path, ComponentPath(comp.name)) + ref_type = obj isa ParameterDef ? ParameterDefReference : VariableDefReference + return ref_type(datum_name, get_root(comp), path) +end -datatype(def::DatumDef) = def.datatype +""" + datum_reference(comp::AbstractCompositeComponentDef, datum_name::Symbol) -description(def::DatumDef) = def.description +Create a reference to the given datum, which itself must be a DatumReference. +""" +datum_reference(comp::AbstractCompositeComponentDef, datum_name::Symbol) = _ns_get(comp, datum_name, AbstractDatumReference) -unit(def::DatumDef) = def.unit +function Base.setindex!(comp::AbstractCompositeComponentDef, value::CompositeNamespaceElement, key::Symbol) + _save_to_namespace(comp, key, value) +end -function first_and_step(md::ModelDef) - keys::Vector{Int} = time_labels(md) # labels are the first times of the model runs - return first_and_step(keys) +# Leaf components store ParameterDefReference or VariableDefReference instances in the namespace +function Base.setindex!(comp::ComponentDef, value::LeafNamespaceElement, key::Symbol) + _save_to_namespace(comp, key, value) end -function first_and_step(values::Vector{Int}) - return values[1], (length(values) > 1 ? values[2] - values[1] : 1) +# +# Dimensions +# + +step_size(values::Vector{Int}) = (length(values) > 1 ? values[2] - values[1] : 1) + +# +# TBD: should these be defined as methods of CompositeComponentDef? +# +function step_size(obj::AbstractComponentDef) + keys = time_labels(obj) + return step_size(keys) end -function time_labels(md::ModelDef) - keys::Vector{Int} = dim_keys(md, :time) - return keys +function first_and_step(obj::AbstractComponentDef) + keys = time_labels(obj) + return first_and_step(keys) end +first_and_step(values::Vector{Int}) = (values[1], step_size(values)) + +first_and_last(obj::AbstractComponentDef) = (obj.first, obj.last) + +time_labels(obj::AbstractComponentDef) = dim_keys(obj, :time) + function check_parameter_dimensions(md::ModelDef, value::AbstractArray, dims::Vector, name::Symbol) for dim in dims - if haskey(md, dim) + if has_dim(md, dim) if isa(value, NamedArray) labels = names(value, findnext(isequal(dim), dims, 1)) dim_vals = dim_keys(md, dim) @@ -135,221 +259,230 @@ function check_parameter_dimensions(md::ModelDef, value::AbstractArray, dims::Ve end end -function datum_size(md::ModelDef, comp_def::ComponentDef, datum_name::Symbol) - dims = dimensions(comp_def, datum_name) +# TBD: is this needed for composites? +function datum_size(obj::AbstractCompositeComponentDef, comp_def::ComponentDef, datum_name::Symbol) + dims = dim_names(comp_def, datum_name) if dims[1] == :time - time_length = getspan(md, comp_def)[1] + time_length = getspan(obj, comp_def)[1] rest_dims = filter(x->x!=:time, dims) - datum_size = (time_length, dim_counts(md, rest_dims)...,) + datum_size = (time_length, dim_counts(obj, rest_dims)...,) else - datum_size = (dim_counts(md, dims)...,) + datum_size = (dim_counts(obj, dims)...,) end return datum_size end - -dimensions(md::ModelDef) = md.dimensions -dimensions(md::ModelDef, dims::Vector{Symbol}) = [dimension(md, dim) for dim in dims] -dimension(md::ModelDef, name::Symbol) = md.dimensions[name] - -dim_count_dict(md::ModelDef) = Dict([name => length(value) for (name, value) in dimensions(md)]) -dim_counts(md::ModelDef, dims::Vector{Symbol}) = [length(dim) for dim in dimensions(md, dims)] - """ - dim_count(md::ModelDef, name::Symbol) + _check_run_period(obj::AbstractComponentDef, first, last) -Return the size of index `name` in model definition `md`. +Raise an error if the component has an earlier start than `first` or a later finish than +`last`. Values of `nothing` are not checked. Composites recurse to check sub-components. """ -dim_count(md::ModelDef, name::Symbol) = length(dimension(md, name)) +function _check_run_period(obj::AbstractComponentDef, new_first, new_last) + # @info "_check_run_period($(obj.comp_id), $(printable(new_first)), $(printable(new_last))" + old_first = first_period(obj) + old_last = last_period(obj) -""" - dim_key_dict(md::ModelDef) - -Return a dict of dimension keys for all dimensions in model definition `md`. -""" -dim_key_dict(md::ModelDef) = Dict([name => collect(keys(dim)) for (name, dim) in dimensions(md)]) -""" - dim_keys(md::ModelDef, name::Symbol) + if new_first !== nothing && old_first !== nothing && new_first < old_first + error("Attempted to set first period of $(obj.comp_id) to an earlier period ($new_first) than component indicates ($old_first)") + end -Return keys for dimension `name` in model definition `md`. -""" -dim_keys(md::ModelDef, name::Symbol) = collect(keys(dimension(md, name))) - -dim_values(md::ModelDef, name::Symbol) = collect(values(dimension(md, name))) -dim_value_dict(md::ModelDef) = Dict([name => collect(values(dim)) for (name, dim) in dimensions(md)]) - -Base.haskey(md::ModelDef, name::Symbol) = haskey(md.dimensions, name) - -isuniform(md::ModelDef) = md.is_uniform - - -# Helper function invoked when the user resets the time dimension with set_dimension! -# This function calls set_run_period! on each component definition to reset the first and last values. -function reset_run_periods!(md, first, last) - for comp_def in compdefs(md) - changed = false - first_per = first_period(comp_def) - last_per = last_period(comp_def) - - if first_per !== nothing && first_per < first - @debug "Resetting $(comp_def.name) component's first timestep to $first" - changed = true - else - first = first_per - end - - if last_per !== nothing && last_per > last - @debug "Resetting $(comp_def.name) component's last timestep to $last" - changed = true - else - last = last_per - end + if new_last !== nothing && old_last !== nothing && new_last > old_last + error("Attempted to set last period of $(obj.comp_id) to a later period ($new_last) than component indicates ($old_last)") + end - if changed - set_run_period!(comp_def, first, last) - end + # N.B. compdefs() returns an empty list for leaf ComponentDefs + for subcomp in compdefs(obj) + _check_run_period(subcomp, new_first, new_last) end + nothing end - + """ - set_dimension!(md::ModelDef, name::Symbol, keys::Union{Int, Vector, Tuple, Range}) + _set_run_period!(obj::AbstractComponentDef, first, last) -Set the values of `md` dimension `name` to integers 1 through `count`, if `keys` is -an integer; or to the values in the vector or range if `keys` is either of those types. +Allows user to change the bounds on a AbstractComponentDef's time dimension. +An error is raised if the new time bounds are outside those of any +subcomponent, recursively. """ -set_dimension!(md::ModelDef, name::Symbol, keys::Union{Int, Vector, Tuple, AbstractRange}) = set_dimension!(md, name, Dimension(keys)) +function _set_run_period!(obj::AbstractComponentDef, first, last) + # We've disabled `first` and `last` args to add_comp!, so we don't test bounds + # _check_run_period(obj, first, last) + + first_per = first_period(obj) + last_per = last_period(obj) + changed = false -function set_dimension!(md::ModelDef, name::Symbol, dim::Dimension) - redefined = haskey(md, name) - if redefined - @debug "Redefining dimension :$name" + if first !== nothing + obj.first = first + changed = true end - if name == :time - k = [keys(dim)...] - md.is_uniform = isuniform(k) - if redefined - reset_run_periods!(md, k[1], k[end]) - end + if last !== nothing + obj.last = last + changed = true end - - md.dimensions[name] = dim - return dim + + if changed + dirty!(obj) + end + + nothing end -# helper functions used to determine if the provided time values are +# helper functions used to determine if the provided time values are # a uniform range. -function all_equal(values) - return all(map(val -> val == values[1], values[2:end])) -end - -function isuniform(values) - if length(values) == 0 - return false - else - return all_equal(diff(collect(values))) - end -end +all_equal(values) = all(map(val -> val == values[1], values[2:end])) -#needed when time dimension is defined using a single integer -function isuniform(values::Int) - return true -end +isuniform(values) = (length(values) == 0 ? false : all_equal(diff(collect(values)))) -# function isuniform(values::AbstractRange{Int}) -# return isuniform(collect(values)) -# end +# needed when time dimension is defined using a single integer +isuniform(values::Int) = true # # Parameters # -external_params(md::ModelDef) = md.external_params +# Callable on both ParameterDef and VariableDef +dim_names(obj::AbstractDatumDef) = obj.dim_names + +""" + parameter_names(md::ModelDef, comp_name::Symbol) + +Return a list of all parameter names for a given component `comp_name` in a model def `md`. +""" +parameter_names(md::ModelDef, comp_name::Symbol) = parameter_names(compdef(md, comp_name)) + +parameter_names(comp_def::AbstractComponentDef) = collect(keys(param_dict(comp_def))) + +parameter(obj::ComponentDef, name::Symbol) = _ns_get(obj, name, ParameterDef) + +parameter(obj::AbstractCompositeComponentDef, name::Symbol) = _ns_get(obj, name, ParameterDefReference) + +parameter(obj::AbstractCompositeComponentDef, comp_name::Symbol, param_name::Symbol) = parameter(compdef(obj, comp_name), param_name) + +parameter(dr::ParameterDefReference) = parameter(compdef(dr), nameof(dr)) -function addparameter(comp_def::ComponentDef, name, datatype, dimensions, description, unit, default) - p = DatumDef(name, datatype, dimensions, description, unit, :parameter, default) - comp_def.parameters[name] = p - return p +has_parameter(comp_def::ComponentDef, name::Symbol) = _ns_has(comp_def, name, ParameterDef) + +has_parameter(comp_def::AbstractCompositeComponentDef, name::Symbol) = _ns_has(comp_def, name, ParameterDefReference) + +function parameter_unit(obj::AbstractComponentDef, param_name::Symbol) + param = parameter(obj, param_name) + return unit(param) end -function addparameter(comp_id::ComponentId, name, datatype, dimensions, description, unit, default) - addparameter(compdef(comp_id), name, datatype, dimensions, description, unit, default) +function parameter_dimensions(obj::AbstractComponentDef, param_name::Symbol) + param = parameter(obj, param_name) + return dim_names(param) end -""" - parameters(comp_def::ComponentDef) +function parameter_unit(obj::AbstractComponentDef, comp_name::Symbol, param_name::Symbol) + return parameter_unit(compdef(obj, comp_name), param_name) +end -Return a list of the parameter definitions for `comp_def`. -""" -parameters(comp_def::ComponentDef) = values(comp_def.parameters) +function parameter_dimensions(obj::AbstractComponentDef, comp_name::Symbol, param_name::Symbol) + return parameter_dimensions(compdef(obj, comp_name), param_name) +end """ - parameters(comp_id::ComponentDef) + set_param!(obj::AbstractCompositeComponentDef, comp_path::ComponentPath, + value_dict::Dict{Symbol, Any}, param_names) -Return a list of the parameter definitions for `comp_id`. +Call `set_param!()` for each name in `param_names`, retrieving the corresponding value from +`value_dict[param_name]`. """ -parameters(comp_id::ComponentId) = parameters(compdef(comp_id)) +function set_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, value_dict::Dict{Symbol, Any}, param_names) + for param_name in param_names + set_param!(obj, comp_name, value_dict, param_name) + end +end """ - parameter_names(md::ModelDef, comp_name::Symbol) + set_param!(obj::AbstractCompositeComponentDef, comp_path::ComponentPath, param_name::Symbol, + value_dict::Dict{Symbol, Any}, dims=nothing) -Return a list of all parameter names for a given component `comp_name` in a model def `md`. +Call `set_param!()` with `param_name` and a value dict in which `value_dict[param_name]` references +the value of parameter `param_name`. """ -parameter_names(md::ModelDef, comp_name::Symbol) = parameter_names(compdef(md, comp_name)) +function set_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, value_dict::Dict{Symbol, Any}, + param_name::Symbol, dims=nothing) + value = value_dict[param_name] + set_param!(obj, comp_name, param_name, value, dims) +end -parameter_names(comp_def::ComponentDef) = [name(param) for param in parameters(comp_def)] +function set_param!(obj::AbstractCompositeComponentDef, comp_path::ComponentPath, param_name::Symbol, value, dims=nothing) + # @info "set_param!($(obj.comp_id), $comp_path, $param_name, $value)" + comp = find_comp(obj, comp_path) + @or(comp, error("Component with path $comp_path not found")) + set_param!(comp.parent, nameof(comp), param_name, value, dims) +end -parameter(md::ModelDef, comp_name::Symbol, param_name::Symbol) = parameter(compdef(md, comp_name), param_name) +""" + set_param!(obj::AbstractCompositeComponentDef, path::AbstractString, param_name::Symbol, value, dims=nothing) -function parameter(comp_def::ComponentDef, name::Symbol) - try - return comp_def.parameters[name] - catch - error("Parameter $name was not found in component $(comp_def.name)") - end +Set a parameter for a component with the given relative path (as a string), in which "/x" means the +component with name `:x` beneath the root of the hierarchy in which `obj` is found. If the path does +not begin with "/", it is treated as relative to `obj`. +""" +function set_param!(obj::AbstractCompositeComponentDef, path::AbstractString, param_name::Symbol, value, dims=nothing) + # @info "set_param!($(obj.comp_id), $path, $param_name, $value)" + set_param!(obj, comp_path(obj, path), param_name, value, dims) end -function parameter_unit(md::ModelDef, comp_name::Symbol, param_name::Symbol) - param = parameter(md, comp_name, param_name) - return param.unit +""" + set_param!(obj::AbstractCompositeComponentDef, path::AbstractString, value, dims=nothing) + +Set a parameter using a colon-delimited string to specify the component path (before the ":") +and the param name (after the ":"). +""" +function set_param!(obj::AbstractCompositeComponentDef, path::AbstractString, value, dims=nothing) + comp_path, param_name = split_datum_path(obj, path) + set_param!(obj, comp_path, param_name, value, dims) end """ - parameter_dimensions(md::ModelDef, comp_name::Symbol, param_name::Symbol) + set_param!(obj::AbstractCompositeComponentDef, param_name::Symbol, value, dims=nothing) -Return a vector of the dimensions of `param_name` in component `comp_name` in model `md` +Set the value of a parameter exposed in `obj` by following the ParameterDefReference. This +method cannot be used on composites that are subcomponents of another composite. """ -function parameter_dimensions(md::ModelDef, comp_name::Symbol, param_name::Symbol) - param = parameter(md, comp_name, param_name) - return param.dimensions +function set_param!(obj::AbstractCompositeComponentDef, param_name::Symbol, value, dims=nothing) + if obj.parent !== nothing + error("Parameter setting is supported only for top-level composites. $(obj.comp_path) is a subcomponent.") + end + param_ref = obj[param_name] + set_param!(obj, param_ref.comp_path, param_ref.name, value, dims=dims) end """ - set_param!(m::ModelDef, comp_name::Symbol, name::Symbol, value, dims=nothing) + set_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, name::Symbol, value, dims=nothing) -Set the parameter `name` of a component `comp_name` in a model `m` to a given `value`. The -`value` can by a scalar, an array, or a NamedAray. Optional argument 'dims' is a -list of the dimension names ofthe provided data, and will be used to check that +Set the parameter `name` of a component `comp_name` in a composite `obj` to a given `value`. The +`value` can by a scalar, an array, or a NamedAray. Optional 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) +function set_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, param_name::Symbol, value, dims=nothing) + # @info "set_param!($(obj.comp_id), $comp_name, $param_name, $value)" # perform possible dimension and labels checks - if isa(value, NamedArray) + if value isa NamedArray dims = dimnames(value) end if dims !== nothing - check_parameter_dimensions(md, value, dims, param_name) + check_parameter_dimensions(obj, value, dims, param_name) end - comp_param_dims = parameter_dimensions(md, comp_name, param_name) + comp_def = compdef(obj, comp_name) + comp_param_dims = parameter_dimensions(comp_def, param_name) num_dims = length(comp_param_dims) - comp_def = compdef(md, comp_name) - data_type = datatype(parameter(comp_def, param_name)) - dtype = data_type == Number ? number_type(md) : data_type + param = parameter(comp_def, param_name) + data_type = param.datatype + dtype = Union{Missing, (data_type == Number ? number_type(obj) : data_type)} if length(comp_param_dims) > 0 @@ -357,15 +490,15 @@ function set_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, value, if dtype <: AbstractArray value = convert(dtype, value) else - #check that number of dimensions matches + # 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 $(comp_name) has $num_dims dimensions; indicated value has $value_dims dimensions.") - end + end value = convert(Array{dtype, num_dims}, value) end - ti = get_time_index_position(md, comp_name, param_name) + ti = get_time_index_position(obj, comp_name, param_name) if ti != nothing # there is a time dimension T = eltype(value) @@ -373,88 +506,94 @@ function set_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, value, if num_dims == 0 values = value else - # Want to use the first from the comp_def if it has it, if not use ModelDef - first = first_period(md, comp_def) + # Use the first from the comp_def if it has it, else use the tree root (usu. a ModelDef) + first = first_period(obj, comp_def) + first === nothing && @warn "set_param!: first === nothing" - if isuniform(md) - _, stepsize = first_and_step(md) + if isuniform(obj) + stepsize = step_size(obj) values = TimestepArray{FixedTimestep{first, stepsize}, T, num_dims, ti}(value) else - times = time_labels(md) - #use the first from the comp_def - first_index = findfirst(isequal(first), times) + times = time_labels(obj) + # use the first from the comp_def + first_index = findfirst(isequal(first), times) values = TimestepArray{VariableTimestep{(times[first_index:end]...,)}, T, num_dims, ti}(value) - end + end end else values = value end - set_external_array_param!(md, param_name, values, comp_param_dims) + set_external_array_param!(obj, param_name, values, comp_param_dims) else # scalar parameter case value = convert(dtype, value) - set_external_scalar_param!(md, param_name, value) + set_external_scalar_param!(obj, param_name, value) end - connect_param!(md, comp_name, param_name, param_name) + # connect_param! calls dirty! so we don't have to + # @info "Calling connect_param!($(printable(obj === nothing ? nothing : obj.comp_id)), $comp_name, $param_name)" + connect_param!(obj, comp_name, param_name, param_name) nothing end # # Variables # -variables(comp_def::ComponentDef) = values(comp_def.variables) +variable(obj::ComponentDef, name::Symbol) = _ns_get(obj, name, VariableDef) -variables(comp_id::ComponentId) = variables(compdef(comp_id)) +variable(obj::AbstractCompositeComponentDef, name::Symbol) = _ns_get(obj, name, VariableDefReference) -function variable(comp_def::ComponentDef, var_name::Symbol) - try - return comp_def.variables[var_name] - catch - error("Variable $var_name was not found in component $(comp_def.comp_id)") - end +variable(comp_id::ComponentId, var_name::Symbol) = variable(compdef(comp_id), var_name) + +variable(obj::AbstractCompositeComponentDef, comp_name::Symbol, var_name::Symbol) = variable(compdef(obj, comp_name), var_name) + +function variable(obj::AbstractCompositeComponentDef, comp_path::ComponentPath, var_name::Symbol) + comp_def = find_comp(obj, comp_path) + return variable(comp_def, var_name) end -variable(comp_id::ComponentId, var_name::Symbol) = variable(compdef(comp_id), var_name) +variable(dr::VariableDefReference) = variable(compdef(dr), nameof(dr)) -variable(md::ModelDef, comp_name::Symbol, var_name::Symbol) = variable(compdef(md, comp_name), var_name) + + +has_variable(comp_def::ComponentDef, name::Symbol) = _ns_has(comp_def, name, VariableDef) + +has_variable(comp_def::AbstractCompositeComponentDef, name::Symbol) = _ns_has(comp_def, name, VariableDefReference) """ - variable_names(md::ModelDef, comp_name::Symbol) + variable_names(md::AbstractCompositeComponentDef, comp_name::Symbol) Return a list of all variable names for a given component `comp_name` in a model def `md`. """ -variable_names(md::ModelDef, comp_name::Symbol) = variable_names(compdef(md, comp_name)) +variable_names(obj::AbstractCompositeComponentDef, comp_name::Symbol) = variable_names(compdef(obj, comp_name)) -variable_names(comp_def::ComponentDef) = [name(var) for var in variables(comp_def)] +variable_names(comp_def::AbstractComponentDef) = [nameof(var) for var in variables(comp_def)] -function variable_unit(md::ModelDef, comp_name::Symbol, var_name::Symbol) - var = variable(md, comp_name, var_name) - return var.unit +function variable_unit(obj::AbstractCompositeComponentDef, comp_path::ComponentPath, var_name::Symbol) + var = variable(obj, comp_path, var_name) + return unit(var) end -""" - parameter_dimensions(md::ModelDef, comp_name::Symbol, param_name::Symbol) - -Return a vector of the dimensions of `var_name` in component `comp_name` in model `md` -""" -function variable_dimensions(md::ModelDef, comp_name::Symbol, var_name::Symbol) - var = variable(md, comp_name, var_name) - return var.dimensions +function variable_unit(obj::AbstractComponentDef, name::Symbol) + var = variable(obj, name) + return unit(var) end -# Add a variable to a ComponentDef -function addvariable(comp_def::ComponentDef, name, datatype, dimensions, description, unit) - v = DatumDef(name, datatype, dimensions, description, unit, :variable) - comp_def.variables[name] = v - return v +# Smooth over difference between VariableDef and VariableDefReference +unit(obj::AbstractDatumDef) = obj.unit +unit(obj::VariableDefReference) = variable(obj).unit +unit(obj::ParameterDefReference) = parameter(obj).unit + +function variable_dimensions(obj::AbstractCompositeComponentDef, comp_path::ComponentPath, var_name::Symbol) + var = variable(obj, comp_path, var_name) + return dim_names(var) end -# Add a variable to a ComponentDef referenced by ComponentId -function addvariable(comp_id::ComponentId, name, datatype, dimensions, description, unit) - addvariable(compdef(comp_id), name, datatype, dimensions, description, unit) +function variable_dimensions(obj::AbstractComponentDef, name::Symbol) + var = variable(obj, name) + return dim_names(var) end # @@ -462,225 +601,366 @@ end # # Return the number of timesteps a given component in a model will run for. -function getspan(md::ModelDef, comp_name::Symbol) - comp_def = compdef(md, comp_name) - return getspan(md, comp_def) +function getspan(obj::AbstractComponentDef, comp_name::Symbol) + comp_def = compdef(obj, comp_name) + return getspan(obj, comp_def) end -function getspan(md::ModelDef, comp_def::ComponentDef) - first = first_period(md, comp_def) - last = last_period(md, comp_def) - times = time_labels(md) +function getspan(obj::AbstractCompositeComponentDef, comp_def::ComponentDef) + first = first_period(obj, comp_def) + last = last_period(obj, comp_def) + times = time_labels(obj) first_index = findfirst(isequal(first), times) last_index = findfirst(isequal(last), times) return size(times[first_index:last_index]) end -function set_run_period!(comp_def::ComponentDef, first, last) - comp_def.first = first - comp_def.last = last - return nothing -end - # # Model # -const NothingInt = Union{Nothing, Int} -const NothingSymbol = Union{Nothing, Symbol} -function _add_anonymous_dims!(md::ModelDef, comp_def::ComponentDef) - for (name, dim) in filter(pair -> pair[2] !== nothing, comp_def.dimensions) +function _add_anonymous_dims!(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef) + for (name, dim) in filter(pair -> pair[2] !== nothing, comp_def.dim_dict) # @info "Setting dimension $name to $dim" - set_dimension!(md, name, dim) + set_dimension!(obj, name, dim) end end -""" - add_comp!(md::ModelDef, comp_def::ComponentDef, comp_name::Symbol=comp_def.comp_id.comp_name; - first=nothing, last=nothing, before=nothing, after=nothing) - -Add the component indicated by `comp_def` to the model indcated by `md`. The component is added at the -end of the list unless one of the keywords, `first`, `last`, `before`, `after`. If the `comp_name` -differs from that in the `comp_def`, a copy of `comp_def` is made and assigned the new name. -""" -function add_comp!(md::ModelDef, comp_def::ComponentDef, comp_name::Symbol=comp_def.comp_id.comp_name; - first::NothingInt=nothing, last::NothingInt=nothing, - before::NothingSymbol=nothing, after::NothingSymbol=nothing) - - # check that a time dimension has been set - if !haskey(dimensions(md), :time) - error("Cannot add component to model without first setting time dimension.") - end - - # check that first and last are within the model's time index range - time_index = dim_keys(md, :time) - - if first !== nothing && first < time_index[1] - error("Cannot add component $name with first time before first of model's time index range.") +function _set_comps!(obj::AbstractCompositeComponentDef, comps::OrderedDict{Symbol, AbstractComponentDef}) + for key in keys(components(obj)) + delete!(obj.namespace, key) # delete only from namespace, keeping connections end - if last !== nothing && last > time_index[end] - error("Cannot add component $name with last time after end of model's time index range.") + # add comps to namespace + for (key, value) in comps + obj[key] = value end + + dirty!(obj) +end - if before !== nothing && after !== nothing - error("Cannot specify both 'before' and 'after' parameters") - end +# Save a back-pointer to the container object and set the comp_path +function parent!(child::AbstractComponentDef, parent::AbstractCompositeComponentDef) + child.parent = parent + child.comp_path = ComponentPath(parent, child.name) + nothing +end - # Check if component being added already exists - if hascomp(md, comp_name) - error("Cannot add two components of the same name ($comp_name)") - end +# Recursively ascend the component tree structure to find the root node +get_root(node::AbstractComponentDef) = (node.parent === nothing ? node : get_root(node.parent)) - # Create a shallow copy of the original but with the new name - # TBD: Why do we need to make a copy here? Sort this out. - if compname(comp_def.comp_id) != comp_name - comp_def = copy_comp_def(comp_def, comp_name) - end +const NothingInt = Union{Nothing, Int} +const NothingSymbol = Union{Nothing, Symbol} +const NothingPairList = Union{Nothing, Vector{Pair{Symbol, Symbol}}} - set_run_period!(comp_def, first, last) +function _insert_comp!(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef; + before::NothingSymbol=nothing, after::NothingSymbol=nothing) - _add_anonymous_dims!(md, comp_def) + comp_name = nameof(comp_def) if before === nothing && after === nothing - md.comp_defs[comp_name] = comp_def # just add it to the end + obj[comp_name] = comp_def # add to namespace else - new_comps = OrderedDict{Symbol, ComponentDef}() + new_comps = OrderedDict{Symbol, AbstractComponentDef}() if before !== nothing - if ! hascomp(md, before) + if ! has_comp(obj, before) error("Component to add before ($before) does not exist") end - for i in compkeys(md) - if i == before + for (k, v) in components(obj) + if k == before new_comps[comp_name] = comp_def end - new_comps[i] = md.comp_defs[i] + new_comps[k] = v end else # after !== nothing, since we've handled all other possibilities above - if ! hascomp(md, after) + if ! has_comp(obj, after) error("Component to add before ($before) does not exist") end - for i in compkeys(md) - new_comps[i] = md.comp_defs[i] - if i == after + for (k, v) in components(obj) + new_comps[k] = v + if k == after new_comps[comp_name] = comp_def end end end - md.comp_defs = new_comps - # println("md.comp_defs: $(md.comp_defs)") + _set_comps!(obj, new_comps) end - # Set parameters to any specified defaults - for param in parameters(comp_def) - if param.default !== nothing - set_param!(md, comp_name, name(param), param.default) - end + # @info "parent obj comp_path: $(printable(obj.comp_path))" + # @info "inserted comp's path: $(comp_def.comp_path)" + dirty!(obj) + + nothing +end + +""" +Return True if time Dimension `outer` contains `inner`. +""" +function time_contains(outer::Dimension, inner::Dimension) + outer_idx = keys(outer) + inner_idx = keys(inner) + + return outer_idx[1] <= inner_idx[1] && outer_idx[end] >= inner_idx[end] +end + +function _find_var_par(parent::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, + comp_name::Symbol, datum_name::Symbol) + path = ComponentPath(parent.comp_path, comp_name) + root = get_root(parent) + + root === nothing && error("Component $(parent.comp_id) does not have a root") + + if has_variable(comp_def, datum_name) + return VariableDefReference(datum_name, root, path) end + + if has_parameter(comp_def, datum_name) + return ParameterDefReference(datum_name, root, path) + end + + error("Component $(comp_def.comp_id) does not have a data item named $datum_name") +end + +""" + propagate_time!(obj::AbstractComponentDef, t::Dimension) + +Propagate a time dimension down through the comp def tree. +""" +function propagate_time!(obj::AbstractComponentDef, t::Dimension) + set_dimension!(obj, :time, t) - return nothing + obj.first = firstindex(t) + obj.last = lastindex(t) + + for c in compdefs(obj) # N.B. compdefs returns empty list for leaf nodes + propagate_time!(c, t) + end end """ - add_comp!(md::ModelDef, comp_id::ComponentId; comp_name::Symbol=comp_id.comp_name, - first=nothing, last=nothing, before=nothing, after=nothing) + import_params!(comp::AbstractComponentDef) -Add the component indicated by `comp_id` to the model indicated by `md`. The component is added at the end of -the list unless one of the keywords, `first`, `last`, `before`, `after`. If the `comp_name` -differs from that in the `comp_def`, a copy of `comp_def` is made and assigned the new name. +Recursively (depth-first) import parameters from leaf comps to composites, and from +sub-composites to their parents. N.B. this is done in _build() after calling +fix_comp_paths!(). """ -function add_comp!(md::ModelDef, comp_id::ComponentId, comp_name::Symbol=comp_id.comp_name; - first::NothingInt=nothing, last::NothingInt=nothing, - before::NothingSymbol=nothing, after::NothingSymbol=nothing) +function import_params!(comp::AbstractComponentDef) + # nothing to do if there are no sub-components + length(comp) == 0 && return + + sub_comps = values(components(comp)) + + for sub_comp in sub_comps + import_params!(sub_comp) + end + + # grab any items imported in @defcomposite; create a reverse-lookup map + d = Dict() + for (local_name, ref) in param_dict(comp) + d[(ref.comp_path, ref.name)] = local_name + end + + # import any unreferenced (and usually renamed locally) parameters + for sub_comp in sub_comps + # N.B. param_dict() returns dict of either params (for leafs) or param refs (from composite) + for (local_name, param) in param_dict(sub_comp) + ref = (param isa AbstractDatumReference ? param : datum_reference(sub_comp, nameof(param))) + if ! haskey(d, (ref.comp_path, ref.name)) + comp[local_name] = ref # add the reference to the local namespace + end + end + end +end + +""" + add_comp!(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, + comp_name::Symbol=comp_def.comp_id.comp_name; rename=nothing, + first=nothing, last=nothing, before=nothing, after=nothing) + +Add the component `comp_def` to the composite component indicated by `obj`. The component is +added at the end of the list unless one of the keywords `before` or `after` is specified. +Note that a copy of `comp_id` is made in the composite and assigned the give name. The optional +argument `rename` can be a list of pairs indicating `original_name => imported_name`. + +Note: `first` and `last` keywords are currently disabled. +""" +function add_comp!(obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, + comp_name::Symbol=comp_def.comp_id.comp_name; + first::NothingInt=nothing, last::NothingInt=nothing, + before::NothingSymbol=nothing, after::NothingSymbol=nothing, + rename::NothingPairList=nothing) + + if first !== nothing || last !== nothing + @warn "add_comp!: Keyword arguments 'first' and 'last' are currently disabled." + first = last = nothing + end + + # When adding composites to another composite, we disallow setting first and last periods. + if is_composite(comp_def) && (first !== nothing || last !== nothing) + error("Cannot set first or last period when adding a composite component: $(comp_def.comp_id)") + end + + # Check if component being added already exists + has_comp(obj, comp_name) && error("Cannot add two components of the same name ($comp_name)") + + # check time constraints if the time dimension has been set + if has_dim(obj, :time) + # error("Cannot add component to composite without first setting time dimension.") + + # check that first and last are within the model's time index range + time_index = time_labels(obj) + + if first !== nothing && first < time_index[1] + error("Cannot add component $comp_name with first time before first of model's time index range.") + end + + if last !== nothing && last > time_index[end] + error("Cannot add component $comp_name with last time after end of model's time index range.") + end + + if before !== nothing && after !== nothing + error("Cannot specify both 'before' and 'after' parameters") + end + + propagate_time!(comp_def, dimension(obj, :time)) + end + + # Copy the original so we don't step on other uses of this comp + comp_def = deepcopy(comp_def) + comp_def.name = comp_name + parent!(comp_def, obj) + + _set_run_period!(comp_def, first, last) + _add_anonymous_dims!(obj, comp_def) + _insert_comp!(obj, comp_def, before=before, after=after) + + # Set parameters to any specified defaults, but only for leaf components + if is_leaf(comp_def) + for param in parameters(comp_def) + if param.default !== nothing + x = printable(obj === nothing ? "obj==nothing" : obj.comp_id) + set_param!(obj, comp_name, nameof(param), param.default) + end + end + end + + # Return the comp since it's a copy of what was passed in + return comp_def +end + +""" + add_comp!(obj::CompositeComponentDef, comp_id::ComponentId; comp_name::Symbol=comp_id.comp_name, + first=nothing, last=nothing, before=nothing, after=nothing, rename=nothing) + +Add the component indicated by `comp_id` to the composite component indicated by `obj`. The +component is added at the end of the list unless one of the keywords `before` or `after` is +specified. Note that a copy of `comp_id` is made in the composite and assigned the give name. +The optional argument `rename` can be a list of pairs indicating `original_name => imported_name`. + +Note: `first` and `last` keywords are currently disabled. +""" +function add_comp!(obj::AbstractCompositeComponentDef, comp_id::ComponentId, + comp_name::Symbol=comp_id.comp_name; + first::NothingInt=nothing, last::NothingInt=nothing, + before::NothingSymbol=nothing, after::NothingSymbol=nothing, + rename::NothingPairList=nothing) # println("Adding component $comp_id as :$comp_name") - add_comp!(md, compdef(comp_id), comp_name, first=first, last=last, before=before, after=after) + add_comp!(obj, compdef(comp_id), comp_name, + first=first, last=last, before=before, after=after, rename=rename) end """ - replace_comp!(md::ModelDef, comp_id::ComponentId, comp_name::Symbol=comp_id.comp_name; + replace_comp!(obj::CompositeComponentDef, comp_id::ComponentId, comp_name::Symbol=comp_id.comp_name; first::NothingInt=nothing, last::NothingInt=nothing, before::NothingSymbol=nothing, after::NothingSymbol=nothing, reconnect::Bool=true) -Replace the component with name `comp_name` in model definition `md` with the -component `comp_id` using the same name. The component is added in the same -position as the old component, unless one of the keywords `before` or `after` -is specified. The component is added with the same first and last values, -unless the keywords `first` or `last` are specified. Optional boolean argument -`reconnect` with default value `true` indicates whether the existing parameter -connections should be maintained in the new component. +Replace the component with name `comp_name` in composite component definition `obj` with the +component `comp_id` using the same name. The component is added in the same position as the +old component, unless one of the keywords `before` or `after` is specified. The component is +added with the same first and last values, unless the keywords `first` or `last` are specified. +Optional boolean argument `reconnect` with default value `true` indicates whether the existing +parameter connections should be maintained in the new component. Returns the added comp def. + +Note: `first` and `last` keywords are currently disabled. """ -function replace_comp!(md::ModelDef, comp_id::ComponentId, comp_name::Symbol=comp_id.comp_name; - first::NothingInt=nothing, last::NothingInt=nothing, - before::NothingSymbol=nothing, after::NothingSymbol=nothing, - reconnect::Bool=true) +function replace_comp!(obj::AbstractCompositeComponentDef, comp_id::ComponentId, + comp_name::Symbol=comp_id.comp_name; + first::NothingInt=nothing, last::NothingInt=nothing, + before::NothingSymbol=nothing, after::NothingSymbol=nothing, + reconnect::Bool=true) + + if first !== nothing || last !== nothing + @warn "replace_comp!: Keyword arguments 'first' and 'last' are currently disabled." + first = last = nothing + end - if ! haskey(md.comp_defs, comp_name) + if ! has_comp(obj, comp_name) error("Cannot replace '$comp_name'; component not found in model.") end - # Get original position if new before or after not specified + # Get original position if neither before nor after are specified if before === nothing && after === nothing - comps = collect(keys(md.comp_defs)) + comps = collect(compkeys(obj)) n = length(comps) if n > 1 idx = findfirst(isequal(comp_name), comps) - if idx == n + if idx == n after = comps[idx - 1] else before = comps[idx + 1] end end - end + end # Get original first and last if new run period not specified - old_comp = md.comp_defs[comp_name] + old_comp = compdef(obj, comp_name) first = first === nothing ? old_comp.first : first - last = last === nothing ? old_comp.last : last + last = last === nothing ? old_comp.last : last if reconnect - # Assert that new component definition has same parameters and variables needed for the connections - new_comp = compdef(comp_id) function _compare_datum(dict1, dict2) - set1 = Set([(k, v.datatype, v.dimensions) for (k, v) in dict1]) - set2 = Set([(k, v.datatype, v.dimensions) for (k, v) in dict2]) + set1 = Set([(k, v.datatype, v.dim_names) for (k, v) in dict1]) + set2 = Set([(k, v.datatype, v.dim_names) for (k, v) in dict2]) return set1 >= set2 end # Check incoming parameters - incoming_params = map(ipc -> ipc.dst_par_name, internal_param_conns(md, comp_name)) - old_params = filter(pair -> pair.first in incoming_params, old_comp.parameters) - new_params = new_comp.parameters - if !_compare_datum(new_params, old_params) - error("Cannot replace and reconnect; new component does not contain the same definitions of necessary parameters.") + incoming_params = map(ipc -> ipc.dst_par_name, internal_param_conns(obj, comp_name)) + old_params = filter(pair -> pair.first in incoming_params, param_dict(old_comp)) + new_params = param_dict(new_comp) + if ! _compare_datum(new_params, old_params) + error("Cannot replace and reconnect; new component does not contain the necessary parameters.") end - + # Check outgoing variables - outgoing_vars = map(ipc -> ipc.src_var_name, filter(ipc -> ipc.src_comp_name == comp_name, md.internal_param_conns)) - old_vars = filter(pair -> pair.first in outgoing_vars, old_comp.variables) - new_vars = new_comp.variables + _get_name(obj, name) = nameof(compdef(obj, :first)) + outgoing_vars = map(ipc -> ipc.src_var_name, + filter(ipc -> nameof(compdef(obj, ipc.src_comp_path)) == comp_name, internal_param_conns(obj))) + old_vars = filter(pair -> pair.first in outgoing_vars, var_dict(old_comp)) + new_vars = var_dict(new_comp) if !_compare_datum(new_vars, old_vars) - error("Cannot replace and reconnect; new component does not contain the same definitions of necessary variables.") + error("Cannot replace and reconnect; new component does not contain the necessary variables.") end - + # Check external parameter connections remove = [] - for epc in external_param_conns(md, comp_name) + for epc in external_param_conns(obj, comp_name) param_name = epc.param_name if ! haskey(new_params, param_name) # TODO: is this the behavior we want? don't error in this case? just (warn)? @debug "Removing external parameter connection from component $comp_name; parameter $param_name no longer exists in component." push!(remove, epc) else - old_p = old_comp.parameters[param_name] + old_p = parameter(old_comp, param_name) new_p = new_params[param_name] - if new_p.dimensions != old_p.dimensions + if new_p.dim_names != old_p.dim_names error("Cannot replace and reconnect; parameter $param_name in new component has different dimensions.") end if new_p.datatype != old_p.datatype @@ -688,99 +968,15 @@ function replace_comp!(md::ModelDef, comp_id::ComponentId, comp_name::Symbol=com end end end - filter!(epc -> !(epc in remove), md.external_param_conns) + filter!(epc -> !(epc in remove), external_param_conns(obj)) - # Delete the old component from comp_defs, leaving the existing parameter connections - delete!(md.comp_defs, comp_name) + # Delete the old component from composite's namespace only, leaving parameter connections + delete!(obj.namespace, comp_name) else # Delete the old component and all its internal and external parameter connections - delete!(md, comp_name) + delete!(obj, comp_name) end # Re-add - add_comp!(md, comp_id, comp_name; first=first, last=last, before=before, after=after) - -end - -""" - copy_comp_def(comp_def::ComponentDef, comp_name::Symbol) - -Create a mostly-shallow copy of `comp_def` (named `comp_name`), but make a deep copy of its -ComponentId so we can rename the copy without affecting the original. -""" -function copy_comp_def(comp_def::ComponentDef, comp_name::Symbol) - comp_id = comp_def.comp_id - obj = ComponentDef(comp_id) - - # Use the comp_id as is, since this identifies the run_timestep function, but - # use an alternate name to reference it in the model's component list. - obj.name = comp_name - - obj.variables = comp_def.variables - obj.parameters = comp_def.parameters - obj.dimensions = comp_def.dimensions - obj.first = comp_def.first - obj.last = comp_def.last - - return obj -end - -""" - copy_external_params(md::ModelDef) - -Make copies of ModelParameter subtypes representing external parameters of model `md`. -This is used both in the copy() function below, and in the simulation subsystem -to restore values between trials. - -""" -function copy_external_params(md::ModelDef) - external_params = Dict{Symbol, ModelParameter}(key => copy(obj) for (key, obj) in md.external_params) - return external_params -end - -Base.copy(obj::ScalarModelParameter{T}) where T = ScalarModelParameter{T}(copy(obj.value)) - -Base.copy(obj::ScalarModelParameter{T}) where T <: Union{Symbol, AbstractString} = ScalarModelParameter{T}(obj.value) - -Base.copy(obj::ArrayModelParameter{T}) where T = ArrayModelParameter{T}(copy(obj.values), obj.dimensions) - -function Base.copy(obj::TimestepVector{T_ts, T}) where {T_ts, T} - return TimestepVector{T_ts, T}(copy(obj.data)) -end - -function Base.copy(obj::TimestepMatrix{T_ts, T, ti}) where {T_ts, T, ti} - return TimestepMatrix{T_ts, T, ti}(copy(obj.data)) -end - -function Base.copy(obj::TimestepArray{T_ts, T, N, ti}) where {T_ts, T, N, ti} - return TimestepArray{T_ts, T, N, ti}(copy(obj.data)) -end - -""" - copy(md::ModelDef) - -Create a copy of a ModelDef `md` object that is not entirely shallow, nor completely deep. -The aim is to copy the full structure, reusing references to immutable elements. -""" -function Base.copy(md::ModelDef) - mdcopy = ModelDef(md.number_type) - mdcopy.module_name = md.module_name - - merge!(mdcopy.comp_defs, md.comp_defs) - - mdcopy.dimensions = deepcopy(md.dimensions) - - # These are vectors of immutable structs, so we can (shallow) copy them safely - mdcopy.internal_param_conns = copy(md.internal_param_conns) - mdcopy.external_param_conns = copy(md.external_param_conns) - - # Names of external params that the ConnectorComps will use as their :input2 parameters. - mdcopy.backups = copy(md.backups) - mdcopy.external_params = copy_external_params(md) - - mdcopy.sorted_comps = md.sorted_comps === nothing ? nothing : copy(md.sorted_comps) - - mdcopy.is_uniform = md.is_uniform - - return mdcopy + return add_comp!(obj, comp_id, comp_name; before=before, after=after) # first=first, last=last, end diff --git a/src/core/delegate.jl b/src/core/delegate.jl new file mode 100644 index 000000000..81ee8eaad --- /dev/null +++ b/src/core/delegate.jl @@ -0,0 +1,66 @@ +# +# @delegate macro and support +# +using MacroTools + +function delegated_args(args::Vector) + newargs = [] + for a in args + if a isa Symbol + push!(newargs, a) + + elseif (@capture(a, var_::T_ = val_) || @capture(a, var_ = val_)) + push!(newargs, :($var = $var)) + + elseif @capture(a, var_::T_) + push!(newargs, var) + else + error("Unrecognized argument format: $a") + end + end + return newargs +end + +""" +Macro to define a method that simply delegate to a method with the same signature +but using the specified field name of the original first argument as the first arg +in the delegated call. That is, + + `@delegate compid(ci::CompositeComponentInstance, i::Int, f::Float64) => leaf` + +expands to: + + `compid(ci::CompositeComponentInstance, i::Int, f::Float64) = compid(ci.leaf, i, f)` + +If a second expression is given, it is spliced in, mainly to support the deprecated +decache(m)". We might delete this feature, but why bother? +""" +macro delegate(ex, other=nothing) + result = nothing + other = (other === nothing ? [] : [other]) # make vector so $(other...) disappears if empty + + if (@capture(ex, fname_(varname_::T_, args__; kwargs__) => rhs_) || + @capture(ex, fname_(varname_::T_, args__) => rhs_)) + # @info "args: $args" + new_args = delegated_args(args) + + if kwargs === nothing + kwargs = new_kwargs = [] + else + new_kwargs = delegated_args(kwargs) + end + + result = quote + function $fname($varname::$T, $(args...); $(kwargs...)) + retval = $fname($varname.$rhs, $(new_args...); $(new_kwargs...)) + $(other...) + return retval + end + end + end + + if result === nothing + error("Calls to @delegate must be of the form 'func(obj, args...) => X', where X is a field of obj to delegate to'. Expression was: $ex") + end + return esc(result) +end diff --git a/src/core/dimensions.jl b/src/core/dimensions.jl index 7d80abb18..cab527b6e 100644 --- a/src/core/dimensions.jl +++ b/src/core/dimensions.jl @@ -44,8 +44,9 @@ Base.iterate(dim::RangeDimension, state...) = iterate(dim.range, state...) Base.keys(dim::RangeDimension) = collect(dim.range) Base.values(dim::RangeDimension) = collect(1:length(dim.range)) -# Get last value from OrderedDict of keys -Base.lastindex(dim::AbstractDimension) = dim.dict.keys[length(dim)] +# Get first/last value from OrderedDict of keys +Base.firstindex(dim::AbstractDimension) = dim.dict.keys[1] +Base.lastindex(dim::AbstractDimension) = dim.dict.keys[length(dim)] # # Compute the index of a "key" (e.g., a year) in the range. @@ -58,3 +59,81 @@ end # Support dim[[2010, 2020, 2030]], dim[(:foo, :bar, :baz)], and dim[2010:2050] Base.getindex(dim::RangeDimension, keys::Union{Vector{Int}, Tuple, AbstractRange}) = [get(dim, key, 0) for key in keys] + +# Symbols are added to the dim_dict in @defcomp (with value of nothing), but are set later using set_dimension! +has_dim(obj::AbstractCompositeComponentDef, name::Symbol) = (haskey(obj.dim_dict, name) && obj.dim_dict[name] !== nothing) + +isuniform(obj::AbstractCompositeComponentDef) = obj.is_uniform + +set_uniform!(obj::AbstractCompositeComponentDef, value::Bool) = (obj.is_uniform = value) + +dimension(obj::AbstractCompositeComponentDef, name::Symbol) = obj.dim_dict[name] + +dim_names(obj::AbstractCompositeComponentDef, dims::Vector{Symbol}) = [dimension(obj, dim) for dim in dims] + +dim_count_dict(obj::AbstractCompositeComponentDef) = Dict([name => length(value) for (name, value) in dim_dict(obj)]) + +dim_counts(obj::AbstractCompositeComponentDef, dims::Vector{Symbol}) = [length(dim) for dim in dim_names(obj, dims)] +dim_count(obj::AbstractCompositeComponentDef, name::Symbol) = length(dimension(obj, name)) + +dim_keys(obj::AbstractCompositeComponentDef, name::Symbol) = collect(keys(dimension(obj, name))) +dim_values(obj::AbstractCompositeComponentDef, name::Symbol) = collect(values(dimension(obj, name))) + +""" + set_dimension!(ccd::CompositeComponentDef, name::Symbol, keys::Union{Int, Vector, Tuple, AbstractRange}) + +Set the values of `ccd` dimension `name` to integers 1 through `count`, if `keys` is +an integer; or to the values in the vector or range if `keys` is either of those types. +""" +function set_dimension!(ccd::AbstractCompositeComponentDef, name::Symbol, keys::Union{Int, Vector, Tuple, AbstractRange}) + redefined = has_dim(ccd, name) + # if redefined + # @warn "Redefining dimension :$name" + # end + + dim = Dimension(keys) + + if name == :time + _set_run_period!(ccd, keys[1], keys[end]) + propagate_time!(ccd, dim) + set_uniform!(ccd, isuniform(keys)) + end + + return set_dimension!(ccd, name, dim) +end + +function set_dimension!(obj::AbstractComponentDef, name::Symbol, dim::Dimension) + dirty!(obj) + obj.dim_dict[name] = dim + + if name == :time + for subcomp in compdefs(obj) + set_dimension!(subcomp, :time, dim) + end + end + return dim +end + +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. + dim = (name isa Int) ? Dimension(name) : nothing + comp.dim_dict[Symbol(name)] = dim # TBD: test this +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) + +function dim_names(ccd::AbstractCompositeComponentDef) + dims = OrderedSet{Symbol}() # use a set to eliminate duplicates + for cd in compdefs(ccd) + union!(dims, keys(dim_dict(cd))) # TBD: test this + end + + return collect(dims) +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 192db44dd..7f58f06b2 100644 --- a/src/core/instances.jl +++ b/src/core/instances.jl @@ -9,39 +9,26 @@ Return the `ModelDef` contained by ModelInstance `mi`. """ modeldef(mi::ModelInstance) = mi.md -compinstance(mi::ModelInstance, name::Symbol) = mi.components[name] - -compdef(ci::ComponentInstance) = compdef(ci.comp_id) - -""" - name(ci::ComponentInstance) - -Return the name of the component `ci`. -""" -name(ci::ComponentInstance) = ci.comp_name - -""" - components(mi::ModelInstance) - -Return an iterator on the components in model instance `mi`. -""" -components(mi::ModelInstance) = values(mi.components) +compmodule(obj::AbstractComponentInstance) = compmodule(obj.comp_id) +compname(obj::AbstractComponentInstance) = compname(obj.comp_id) """ - add_comp!(mi::ModelInstance, ci::ComponentInstance) + add_comp!(obj::AbstractCompositeComponentInstance, ci::AbstractComponentInstance) -Add the component `ci` to the `ModelInstance` `mi`'s list of components, and add -the `first` and `last` of `mi` to the ends of the `firsts` and `lasts` lists of -`mi`, respectively. +Add the (leaf or composite) component `ci` to a composite's list of components, and add +the `first` and `last` of `mi` to the ends of the composite's `firsts` and `lasts` lists. """ -function add_comp!(mi::ModelInstance, ci::ComponentInstance) - mi.components[name(ci)] = ci +function add_comp!(obj::AbstractCompositeComponentInstance, ci::AbstractComponentInstance) + obj.comps_dict[nameof(ci)] = ci - push!(mi.firsts, ci.first) - push!(mi.lasts, ci.last) + # push!(obj.firsts, first_period(ci)) # TBD: perhaps this should be set when time is set? + # push!(obj.lasts, last_period(ci)) + nothing end +# # Setting/getting parameter and variable values +# # Get the object stored for the given variable, not the value of the variable. # This is used in the model building process to connect internal parameters. @@ -88,13 +75,15 @@ end end end +comp_paths(obj::AbstractComponentInstanceData) = getfield(obj, :comp_paths) + """ - get_param_value(ci::ComponentInstance, name::Symbol) + get_param_value(ci::AbstractComponentInstance, name::Symbol) -Return the value of parameter `name` in component `ci`. +Return the value of parameter `name` in (leaf or composite) component `ci`. """ -function get_param_value(ci::ComponentInstance, name::Symbol) - try +function get_param_value(ci::AbstractComponentInstance, name::Symbol) + try return getproperty(ci.parameters, name) catch err if isa(err, KeyError) @@ -106,14 +95,15 @@ function get_param_value(ci::ComponentInstance, name::Symbol) end """ - get_var_value(ci::ComponentInstance, name::Symbol) + get_var_value(ci::AbstractComponentInstance, name::Symbol) Return the value of variable `name` in component `ci`. """ -function get_var_value(ci::ComponentInstance, name::Symbol) +function get_var_value(ci::AbstractComponentInstance, name::Symbol) try - # println("Getting $name from $(ci.variables)") - return getproperty(ci.variables, name) + vars = ci.variables + # @info ("Getting $name from $vars") + return getproperty(vars, name) catch err if isa(err, KeyError) error("Component $(ci.comp_id) has no variable named $name") @@ -123,58 +113,77 @@ function get_var_value(ci::ComponentInstance, name::Symbol) end end -set_param_value(ci::ComponentInstance, name::Symbol, value) = setproperty!(ci.parameters, name, value) +set_param_value(ci::AbstractComponentInstance, name::Symbol, value) = setproperty!(ci.parameters, name, value) -set_var_value(ci::ComponentInstance, name::Symbol, value) = setproperty!(ci.variables, name, value) +set_var_value(ci::AbstractComponentInstance, name::Symbol, value) = setproperty!(ci.variables, name, value) -# Allow values to be obtained from either parameter type using one method name. -value(param::ScalarModelParameter) = param.value +""" + variables(obj::AbstractCompositeComponentInstance, comp_name::Symbol) -value(param::ArrayModelParameter) = param.values +Return the `ComponentInstanceVariables` for `comp_name` in CompositeComponentInstance `obj`. +""" +variables(obj::AbstractCompositeComponentInstance, comp_name::Symbol) = variables(compinstance(obj, comp_name)) -dimensions(obj::ArrayModelParameter) = obj.dimensions +variables(obj::AbstractComponentInstance) = obj.variables -dimensions(obj::ScalarModelParameter) = [] + +function variables(m::Model) + if ! is_built(m) + error("Must build model to access variable instances. Use variables(modeldef(m)) to get variable definitions.") + end + return variables(modelinstance(m)) +end """ - variables(mi::ModelInstance, comp_name::Symbol) + parameters(obj::AbstractComponentInstance, comp_name::Symbol) -Return the `ComponentInstanceVariables` for `comp_name` in ModelInstance 'mi'. +Return the `ComponentInstanceParameters` for `comp_name` in CompositeComponentInstance `obj`. """ -variables(mi::ModelInstance, comp_name::Symbol) = variables(compinstance(mi, comp_name)) +parameters(obj::AbstractCompositeComponentInstance, comp_name::Symbol) = parameters(compinstance(obj, comp_name)) -variables(ci::ComponentInstance) = ci.variables +parameters(obj::AbstractComponentInstance) = obj.parameters -""" - parameters(mi::ModelInstance, comp_name::Symbol) +function Base.getindex(mi::ModelInstance, names::NTuple{N, Symbol}) where N + obj = mi -Return the `ComponentInstanceParameters` for `comp_name` in ModelInstance 'mi'. -""" -parameters(mi::ModelInstance, comp_name::Symbol) = parameters(compinstance(mi, comp_name)) + # skip past first element if same as root node + if length(names) > 0 && head(obj.comp_path) == names[1] + names = names[2:end] + end -""" - parameters(ci::ComponentInstance) + for name in names + if has_comp(obj, name) + obj = obj[name] + else + error("Component $(obj.comp_path) does not have sub-component :$name") + end + end + return obj +end -Return an iterable over the parameters in `ci`. -""" -parameters(ci::ComponentInstance) = ci.parameters +Base.getindex(mi::ModelInstance, comp_path::ComponentPath) = getindex(mi, comp_path.names) +Base.getindex(mi::ModelInstance, path_str::AbstractString) = getindex(mi, ComponentPath(mi.md, path_str)) -function Base.getindex(mi::ModelInstance, comp_name::Symbol, datum_name::Symbol) - if !(comp_name in keys(mi.components)) - error("Component :$comp_name does not exist in current model") +function Base.getindex(obj::AbstractCompositeComponentInstance, comp_name::Symbol) + if ! has_comp(obj, comp_name) + error("Component :$comp_name does not exist in the given composite") end - - comp_inst = compinstance(mi, comp_name) - vars = comp_inst.variables - pars = comp_inst.parameters + return compinstance(obj, comp_name) +end + +function _get_datum(ci::AbstractComponentInstance, datum_name::Symbol) + vars = variables(ci) if datum_name in names(vars) which = vars - elseif datum_name in names(pars) - which = pars else - error("$datum_name is not a parameter or a variable in component $comp_name.") + pars = parameters(ci) + if datum_name in names(pars) + which = pars + else + error("$datum_name is not a parameter or a variable in component $(ci.comp_path).") + end end value = getproperty(which, datum_name) @@ -182,45 +191,24 @@ function Base.getindex(mi::ModelInstance, comp_name::Symbol, datum_name::Symbol) return value isa TimestepArray ? value.data : value end -""" - dim_count(mi::ModelInstance, dim_name::Symbol) - -Return the size of index `dim_name` in model instance `mi`. -""" -dim_count(mi::ModelInstance, dim_name::Symbol) = dim_count(mi.md, dim_name) +function Base.getindex(mi::ModelInstance, key::AbstractString, datum::Symbol) + _get_datum(mi[key], datum) +end -""" - dim_key_dict(mi::ModelInstance) +function Base.getindex(obj::AbstractCompositeComponentInstance, comp_name::Symbol, datum::Symbol) + ci = obj[comp_name] + return _get_datum(ci, datum) +end -Return a dict of dimension keys for all dimensions in model instance `mi`. """ -dim_key_dict(mi::ModelInstance) = dim_key_dict(mi.md) + dim_count(mi::ModelInstance, dim_name::Symbol) +Return the size of index `dim_name` in model instance `mi`. """ - dim_keys(mi::ModelInstance, dim_name::Symbol) - -Return keys for dimension `dim_name` in model instance `mi`. -""" -dim_keys(mi::ModelInstance, dim_name::Symbol) = dim_keys(mi.md, dim_name) - -dim_value_dict(mi::ModelInstance) = dim_value_dict(mi.md) - -function make_clock(mi::ModelInstance, ntimesteps, time_keys::Vector{Int}) - last = time_keys[min(length(time_keys), ntimesteps)] - - if isuniform(time_keys) - first, stepsize = first_and_step(time_keys) - return Clock{FixedTimestep}(first, stepsize, last) - - else - last_index = findfirst(isequal(last), time_keys) - times = (time_keys[1:last_index]...,) - return Clock{VariableTimestep}(times) - end -end +@delegate dim_count(mi::ModelInstance, dim_name::Symbol) => md -function reset_variables(ci::ComponentInstance) - # println("reset_variables($(ci.comp_id))") +function reset_variables(ci::AbstractComponentInstance) + # @info "reset_variables($(ci.comp_id))" vars = ci.variables for (name, T) in zip(names(vars), types(vars)) @@ -229,7 +217,7 @@ function reset_variables(ci::ComponentInstance) if (T <: AbstractArray || T <: TimestepArray) && eltype(value) <: AbstractFloat fill!(value, NaN) - elseif T <: AbstractFloat || (T <: ScalarModelParameter && T.parameters[1] <: AbstractFloat) + elseif T <: AbstractFloat || (T <: ScalarModelParameter && T.parameters[1] <: AbstractFloat) setproperty!(vars, name, NaN) elseif (T <: ScalarModelParameter) # integer or bool @@ -238,78 +226,74 @@ function reset_variables(ci::ComponentInstance) end end -function init(mi::ModelInstance) - for ci in components(mi) - init(ci) +function reset_variables(obj::AbstractCompositeComponentInstance) + for ci in components(obj) + reset_variables(ci) end + return nothing end -function init(ci::ComponentInstance) +function init(ci::AbstractComponentInstance, dims::DimValueDict) + # @info "init($(ci.comp_id))" reset_variables(ci) - if ci.init !== nothing - ci.init(ci.parameters, ci.variables, DimDict(ci.dim_dict)) + + if ci.init != nothing + ci.init(parameters(ci), variables(ci), dims) end + return nothing end -function run_timestep(ci::ComponentInstance, clock::Clock) - if ci.run_timestep === nothing - return +function init(obj::AbstractCompositeComponentInstance, dims::DimValueDict) + for ci in components(obj) + init(ci, dims) end + return nothing +end - pars = ci.parameters - vars = ci.variables - dims = ci.dim_dict - t = clock.ts +_runnable(ci::AbstractComponentInstance, clock::Clock) = (ci.first <= gettime(clock) <= ci.last) - ci.run_timestep(pars, vars, DimDict(dims), t) - advance(clock) - nothing +function run_timestep(ci::AbstractComponentInstance, clock::Clock, dims::DimValueDict) + if ci.run_timestep !== nothing && _runnable(ci, clock) + ci.run_timestep(parameters(ci), variables(ci), dims, clock.ts) + end + + return nothing end -function _run_components(mi::ModelInstance, clock::Clock, - firsts::Vector{Int}, lasts::Vector{Int}, comp_clocks::Vector{Clock{T}}) where {T <: AbstractTimestep} - comp_instances = components(mi) - tups = collect(zip(comp_instances, firsts, lasts, comp_clocks)) - - while ! finished(clock) - for (ci, first, last, comp_clock) in tups - if first <= gettime(clock) <= last - run_timestep(ci, comp_clock) - end +function run_timestep(cci::AbstractCompositeComponentInstance, clock::Clock, dims::DimValueDict) + if _runnable(cci, clock) + for ci in components(cci) + run_timestep(ci, clock, dims) end - advance(clock) end - nothing + return nothing end -function Base.run(mi::ModelInstance, ntimesteps::Int=typemax(Int), +function Base.run(mi::ModelInstance, ntimesteps::Int=typemax(Int), dimkeys::Union{Nothing, Dict{Symbol, Vector{T} where T <: DimensionKeyTypes}}=nothing) - if length(mi.components) == 0 + + if (ncomps = length(components(mi))) == 0 error("Cannot run the model: no components have been created.") end - t::Vector{Int} = dimkeys === nothing ? dim_keys(mi.md, :time) : dimkeys[:time] - - firsts = mi.firsts - lasts = mi.lasts + time_keys::Vector{Int} = dimkeys === nothing ? dim_keys(mi.md, :time) : dimkeys[:time] - if isuniform(t) - _, stepsize = first_and_step(t) - comp_clocks = [Clock{FixedTimestep}(first, stepsize, last) for (first, last) in zip(firsts, lasts)] - else - comp_clocks = Array{Clock{VariableTimestep}}(undef, length(firsts)) - for i = 1:length(firsts) - first_index = findfirst(isequal(firsts[i]), t) - last_index = findfirst(isequal(lasts[i]), t) - times = (t[first_index:last_index]...,) - comp_clocks[i] = Clock{VariableTimestep}(times) - end + # truncate time_keys if caller so desires + if ntimesteps < length(time_keys) + time_keys = time_keys[1:ntimesteps] end - clock = make_clock(mi, ntimesteps, t) + # TBD: Pass this, but substitute t from above? + dim_val_dict = DimValueDict(dim_dict(mi.md)) - init(mi) # call module's (or fallback) init function + # recursively initializes all components + init(mi, dim_val_dict) + + clock = Clock(time_keys) + while ! finished(clock) + run_timestep(mi, clock, dim_val_dict) + advance(clock) + end - _run_components(mi, clock, firsts, lasts, comp_clocks) nothing end diff --git a/src/core/model.jl b/src/core/model.jl index 055548f11..c54c032f5 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -4,17 +4,6 @@ # using MacroTools -# Simplify delegation of calls to ::Model to internal ModelInstance or ModelDelegate objects. -macro modelegate(ex) - if @capture(ex, fname_(varname_::Model, args__) => rhs_) - - result = esc(:($fname($varname::Model, $(args...)) = ($varname.$rhs === nothing ? error("This function is not callable on an model that has not been run because it requires a ModelInstance.") : $fname($varname.$rhs, $(args...))))) - #println(result) - return result - end - error("Calls to @modelegate must be of the form 'func(m::Model, args...) => X', where X is either mi or md'. Expression was: $ex") -end - """ modeldef(m) @@ -23,49 +12,56 @@ Return the `ModelDef` contained by Model `m`. modeldef(m::Model) = m.md modelinstance(m::Model) = m.mi +modelinstance_def(m::Model) = modeldef(modelinstance(m)) -@modelegate compinstance(m::Model, name::Symbol) => mi - -@modelegate number_type(m::Model) => md +is_built(m::Model) = !(dirty(m.md) || modelinstance(m) === nothing) -@modelegate external_param_conns(m::Model) => md +is_built(mm::MarginalModel) = (is_built(mm.base) && is_built(mm.marginal)) -@modelegate internal_param_conns(m::Model) => md +@delegate compinstance(m::Model, name::Symbol) => mi +@delegate has_comp(m::Model, name::Symbol) => md -@modelegate external_params(m::Model) => md +@delegate number_type(m::Model) => md -@modelegate external_param(m::Model, name::Symbol) => md +@delegate internal_param_conns(m::Model) => md +@delegate external_param_conns(m::Model) => md -@modelegate connected_params(m::Model, comp_name::Symbol) => md +@delegate external_params(m::Model) => md +@delegate external_param(m::Model, name::Symbol; missing_ok=false) => md -@modelegate unconnected_params(m::Model) => md +@delegate connected_params(m::Model) => md +@delegate unconnected_params(m::Model) => md -@modelegate add_connector_comps(m::Model) => md - -# Forget any previously built model instance (i.e., after changing the model def). -# This should be called by all functions that modify the Model's underlying ModelDef. -function decache(m::Model) - m.mi = nothing -end +@delegate add_connector_comps!(m::Model) => md """ - connect_param!(m::Model, dst_comp_name::Symbol, dst_par_name::Symbol, src_comp_name::Symbol, + connect_param!(m::Model, dst_comp_path::ComponentPath, dst_par_name::Symbol, src_comp_path::ComponentPath, src_var_name::Symbol, backup::Union{Nothing, Array}=nothing; ignoreunits::Bool=false, offset::Int=0) -Bind the parameter `dst_par_name` of one component `dst_comp_name` of model `md` -to a variable `src_var_name` in another component `src_comp_name` of the same model +Bind the parameter `dst_par_name` of one component `dst_comp_path` of model `md` +to a variable `src_var_name` in another component `src_comp_path` 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 `offset` argument indicates the offset -between the destination and the source ie. the value would be `1` if the destination +between the destination and the source ie. the value would be `1` if the destination component parameter should only be calculated for the second timestep and beyond. """ -function 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, offset::Int=0) - connect_param!(m.md, dst_comp_name, dst_par_name, src_comp_name, src_var_name, backup; - ignoreunits=ignoreunits, offset=offset) -end +@delegate connect_param!(m::Model, + dst_comp_path::ComponentPath, dst_par_name::Symbol, + src_comp_path::ComponentPath, src_var_name::Symbol, + backup::Union{Nothing, Array}=nothing; + ignoreunits::Bool=false, offset::Int=0) => 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, offset::Int=0) => md + +@delegate connect_param!(m::Model, comp_name::Symbol, param_name::Symbol, ext_param_name::Symbol) => md + +@delegate connect_param!(m::Model, dst::AbstractString, src::AbstractString, backup::Union{Nothing, Array}=nothing; + ignoreunits::Bool=false, offset::Int=0) => md + """ connect_param!(m::Model, dst::Pair{Symbol, Symbol}, src::Pair{Symbol, Symbol}, backup::Array; ignoreunits::Bool=false) @@ -74,98 +70,84 @@ Bind the parameter `dst[2]` of one component `dst[1]` of model `md` to a variable `src[2]` in another component `src[1]` 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 `offset` argument indicates the offset -between the destination and the source ie. the value would be `1` if the destination +between the destination and the source ie. the value would be `1` if the destination component parameter should only be calculated for the second timestep and beyond. """ -function connect_param!(m::Model, dst::Pair{Symbol, Symbol}, src::Pair{Symbol, Symbol}, - backup::Union{Nothing, Array}=nothing; +function connect_param!(m::Model, dst::Pair{Symbol, Symbol}, src::Pair{Symbol, Symbol}, + backup::Union{Nothing, Array}=nothing; ignoreunits::Bool=false, offset::Int=0) connect_param!(m.md, dst[1], dst[2], src[1], src[2], backup; ignoreunits=ignoreunits, offset=offset) end -""" - disconnect_param!(m::Model, comp_name::Symbol, param_name::Symbol) - -Remove any parameter connections for a given parameter `param_name` in a given component -`comp_name` of model `m`. -""" -function disconnect_param!(m::Model, comp_name::Symbol, param_name::Symbol) - disconnect_param!(m.md, comp_name, param_name) - decache(m) -end +@delegate disconnect_param!(m::Model, comp_path::ComponentPath, param_name::Symbol) => md +@delegate disconnect_param!(m::Model, comp_name::Symbol, param_name::Symbol) => md -function set_external_param!(m::Model, name::Symbol, value::ModelParameter) - set_external_param!(m.md, name, value) - decache(m) -end +@delegate set_external_param!(m::Model, name::Symbol, value::ModelParameter) => md -function set_external_param!(m::Model, name::Symbol, value::Number; param_dims::Union{Nothing,Array{Symbol}} = nothing) - set_external_param!(m.md, name, value; param_dims = param_dims) - decache(m) -end +@delegate set_external_param!(m::Model, name::Symbol, value::Number; + param_dims::Union{Nothing,Array{Symbol}} = nothing) => md -function set_external_param!(m::Model, name::Symbol, value::Union{AbstractArray, AbstractRange, Tuple}; param_dims::Union{Nothing,Array{Symbol}} = nothing) - set_external_param!(m.md, name, value; param_dims = param_dims) -end +@delegate set_external_param!(m::Model, name::Symbol, value::Union{AbstractArray, AbstractRange, Tuple}; + param_dims::Union{Nothing,Array{Symbol}} = nothing) => md -function add_internal_param_conn(m::Model, conn::InternalParameterConnection) - add_internal_param_conn(m.md, conn) - decache(m) -end +@delegate add_internal_param_conn!(m::Model, conn::InternalParameterConnection) => md +# @delegate doesn't handle the 'where T' currently. This is the only instance of it for now... function set_leftover_params!(m::Model, parameters::Dict{T, Any}) where T set_leftover_params!(m.md, parameters) - decache(m) end """ update_param!(m::Model, name::Symbol, value; update_timesteps = false) -Update the `value` of an external model parameter in model `m`, referenced by -`name`. Optional boolean argument `update_timesteps` with default value `false` -indicates whether to update the time keys associated with the parameter values +Update the `value` of an external model parameter in model `m`, referenced by +`name`. Optional boolean argument `update_timesteps` with default value `false` +indicates whether to update the time keys associated with the parameter values to match the model's time index. """ -function update_param!(m::Model, name::Symbol, value; update_timesteps = false) - update_param!(m.md, name, value, update_timesteps = update_timesteps) - decache(m) -end +@delegate update_param!(m::Model, name::Symbol, value; update_timesteps = false) => md """ update_params!(m::Model, parameters::Dict{T, Any}; update_timesteps = false) 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, with optional +For each (k, v) in the provided `parameters` dictionary, `update_param!`` +is called to update the external parameter by name k to value v, with optional Boolean argument update_timesteps. Each key k must be a symbol or convert to a -symbol matching the name of an external parameter that already exists in the +symbol matching the name of an external parameter that already exists in the model definition. """ -function update_params!(m::Model, parameters::Dict; update_timesteps = false) - update_params!(m.md, parameters; update_timesteps = update_timesteps) - decache(m) -end +@delegate update_params!(m::Model, parameters::Dict; update_timesteps = false) => md """ add_comp!(m::Model, comp_id::ComponentId; comp_name::Symbol=comp_id.comp_name; - first=nothing, last=nothing, before=nothing, after=nothing) + first=nothing, last=nothing, before=nothing, after=nothing, rename=nothing) + +Add the component indicated by `comp_id` to the model indicated by `m`. The component is added +at the end of the list unless one of the keywords `before` or `after` is specified. Note +that a copy of `comp_id` is made in the composite and assigned the give name. The optional +argument `rename` can be a list of pairs indicating `original_name => imported_name`. -Add the component indicated by `comp_id` to the model indicated by `m`. The component is added at the end of -the list unless one of the keywords, `first`, `last`, `before`, `after`. If the `comp_name` -differs from that in the `comp_id`, a copy of `comp_id` is made and assigned the new name. +Note: `first` and `last` keywords are currently disabled. """ -function add_comp!(m::Model, comp_id::ComponentId, comp_name::Symbol=comp_id.comp_name; - first=nothing, last=nothing, before=nothing, after=nothing) - add_comp!(m.md, comp_id, comp_name; first=first, last=last, before=before, after=after) - decache(m) - return ComponentReference(m, comp_name) +function add_comp!(m::Model, comp_id::ComponentId, comp_name::Symbol=comp_id.comp_name; kwargs...) + comp_def = add_comp!(m.md, comp_id, comp_name; kwargs...) + return ComponentReference(m.md, comp_name) end -function add_comp!(m::Model, comp_def::ComponentDef, comp_name::Symbol=comp_def.comp_id.comp_name; - first=nothing, last=nothing, before=nothing, after=nothing) - add_comp!(m.md, comp_def, comp_name; first=first, last=last, before=before, after=after) - decache(m) - return ComponentReference(m, comp_name) +""" + add_comp!(m::Model, comp_def::AbstractComponentDef; comp_name::Symbol=comp_id.comp_name; + first=nothing, last=nothing, before=nothing, after=nothing, rename=nothing) + +Add the component `comp_def` to the model indicated by `m`. The component is added at +the end of the list unless one of the keywords, `first`, `last`, `before`, `after`. Note +that a copy of `comp_id` is made in the composite and assigned the give name. The optional +argument `rename` can be a list of pairs indicating `original_name => imported_name`. + +Note: `first` and `last` keywords are currently disabled. +""" +function add_comp!(m::Model, comp_def::AbstractComponentDef, comp_name::Symbol=comp_def.comp_id.comp_name; kwargs...) + return add_comp!(m, comp_def.comp_id, comp_name; kwargs...) end """ @@ -173,50 +155,47 @@ end first::NothingSymbol=nothing, last::NothingSymbol=nothing, before::NothingSymbol=nothing, after::NothingSymbol=nothing, reconnect::Bool=true) - + Replace the component with name `comp_name` in model `m` with the component -`comp_id` using the same name. The component is added in the same position as +`comp_id` using the same name. The component is added in the same position as the old component, unless one of the keywords `before` or `after` is specified. -The component is added with the same first and last values, unless the keywords -`first` or `last` are specified. Optional boolean argument `reconnect` with -default value `true` indicates whether the existing parameter connections -should be maintained in the new component. -""" -function replace_comp!(m::Model, comp_id::ComponentId, comp_name::Symbol=comp_id.comp_name; - first::NothingSymbol=nothing, last::NothingSymbol=nothing, - before::NothingSymbol=nothing, after::NothingSymbol=nothing, - reconnect::Bool=true) - replace_comp!(m.md, comp_id, comp_name; first=first, last=last, before=before, after=after, reconnect=reconnect) - decache(m) - return ComponentReference(m, comp_name) +The component is added with the same first and last values, unless the keywords +`first` or `last` are specified. Optional boolean argument `reconnect` with +default value `true` indicates whether the existing parameter connections +should be maintained in the new component. + +Note: `first` and `last` keywords are currently disabled. +""" +function replace_comp!(m::Model, comp_id::ComponentId, comp_name::Symbol=comp_id.comp_name; kwargs...) + comp_def = replace_comp!(m.md, comp_id, comp_name; kwargs...) + return ComponentReference(m.md, comp_name) end -function replace_comp!(m::Model, comp_def::ComponentDef, comp_name::Symbol=comp_def.comp_id.comp_name; - first::NothingSymbol=nothing, last::NothingSymbol=nothing, - before::NothingSymbol=nothing, after::NothingSymbol=nothing, - reconnect::Bool=true) - replace_comp!(m, comp_def.comp_id, comp_name; first=first, last=last, before=before, after=after, reconnect=reconnect) +function replace_comp!(m::Model, comp_def::ComponentDef, comp_name::Symbol=comp_def.comp_id.comp_name; kwargs...) + return replace_comp!(m, comp_def.comp_id, comp_name; kwargs...) end +@delegate ComponentReference(m::Model, name::Symbol) => md + """ components(m::Model) -Return an iterator on the components in model `m`. +Return an iterator on the components in a model's model instance. """ -@modelegate components(m::Model) => mi +@delegate components(m::Model) => mi -@modelegate compdefs(m::Model) => md +@delegate compdefs(m::Model) => md -@modelegate compdef(m::Model, comp_name::Symbol) => md +@delegate compdef(m::Model, comp_name::Symbol) => md -@modelegate numcomponents(m::Model) => md +@delegate Base.length(m::Model) => md -@modelegate first_and_step(m::Model) => md +@delegate first_and_step(m::Model) => md -@modelegate time_labels(m::Model) => md +@delegate time_labels(m::Model) => md # Return the number of timesteps a given component in a model will run for. -@modelegate getspan(m::Model, comp_name::Symbol) => md +@delegate getspan(m::Model, comp_name::Symbol) => md """ datumdef(comp_def::ComponentDef, item::Symbol) @@ -224,11 +203,11 @@ Return an iterator on the components in model `m`. Return a DatumDef for `item` in the given component `comp_def`. """ function datumdef(comp_def::ComponentDef, item::Symbol) - if haskey(comp_def.variables, item) - return comp_def.variables[item] + if has_variable(comp_def, item) + return variable(comp_def, item) - elseif haskey(comp_def.parameters, item) - return comp_def.parameters[item] + elseif has_parameter(comp_def, item) + return parameter(comp_def, item) else error("Cannot access data item; :$item is not a variable or a parameter in component $(comp_def.comp_id).") end @@ -237,70 +216,71 @@ end datumdef(m::Model, comp_name::Symbol, item::Symbol) = datumdef(compdef(m.md, comp_name), item) """ - dimensions(m::Model, comp_name::Symbol, datum_name::Symbol) + dim_names(m::Model, comp_name::Symbol, datum_name::Symbol) Return the dimension names for the variable or parameter `datum_name` in the given component `comp_name` in model `m`. """ -dimensions(m::Model, comp_name::Symbol, datum_name::Symbol) = dimensions(compdef(m, comp_name), datum_name) -dimensions(mm::MarginalModel, comp_name::Symbol, datum_name::Symbol) = dimensions(compdef(mm.base, comp_name), datum_name) +dim_names(m::Model, comp_name::Symbol, datum_name::Symbol) = dim_names(compdef(m, comp_name), datum_name) +dim_names(mm::MarginalModel, comp_name::Symbol, datum_name::Symbol) = dim_names(mm.base, comp_name, datum_name) -@modelegate dimension(m::Model, dim_name::Symbol) => md +# TBD: Deprecated +dimensions(m::Model, comp_name::Symbol, datum_name::Symbol) = dim_names(m, comp_name, datum_name) +dimensions(mm::MarginalModel, comp_name::Symbol, datum_name::Symbol) = dim_names(mm.base, comp_name, datum_name) + +@delegate dimension(m::Model, dim_name::Symbol) => md # Allow access of the form my_model[:grosseconomy, :tfp] -@modelegate Base.getindex(m::Model, comp_name::Symbol, datum_name::Symbol) => mi +@delegate Base.getindex(m::Model, comp_name::Symbol, datum_name::Symbol) => mi """ dim_count(m::Model, dim_name::Symbol) - + Return the size of index `dim_name` in model `m`. """ -@modelegate dim_count(m::Model, dim_name::Symbol) => md -@modelegate dim_counts(m::Model, dims::Vector{Symbol}) => md -@modelegate dim_count_dict(m::Model) => md +@delegate dim_count(m::Model, dim_name::Symbol) => md +@delegate dim_counts(m::Model, dims::Vector{Symbol}) => md +@delegate dim_count_dict(m::Model) => md """ dim_keys(m::Model, dim_name::Symbol) - + Return keys for dimension `dim-name` in model `m`. """ -@modelegate dim_keys(m::Model, dim_name::Symbol) => md +@delegate dim_keys(m::Model, dim_name::Symbol) => md """ dim_key_dict(m::Model) - + Return a dict of dimension keys for all dimensions in model `m`. """ -@modelegate dim_key_dict(m::Model) => md +@delegate dim_key_dict(m::Model) => md """ dim_values(m::Model, name::Symbol) - + Return values for dimension `name` in Model `m`. """ -@modelegate dim_values(m::Model, name::Symbol) => md +@delegate dim_values(m::Model, name::Symbol) => md """ dim_value_dict(m::Model) - + Return a dictionary of the values of all dimensions in Model `m`. """ -@modelegate dim_value_dict(m::Model) => md +@delegate dim_value_dict(m::Model) => md """ set_dimension!(m::Model, name::Symbol, keys::Union{Vector, Tuple, AbstractRange}) Set the values of `m` dimension `name` to integers 1 through `count`, if `keys`` is an integer; or to the values in the vector or range if `keys`` is either of those types. """ -function set_dimension!(m::Model, name::Symbol, keys::Union{Int, Vector, Tuple, AbstractRange}) - set_dimension!(m.md, name, keys) - decache(m) -end +@delegate set_dimension!(m::Model, name::Symbol, keys::Union{Int, Vector, Tuple, AbstractRange}) => md -@modelegate check_parameter_dimensions(m::Model, value::AbstractArray, dims::Vector, name::Symbol) => md +@delegate check_parameter_dimensions(m::Model, value::AbstractArray, dims::Vector, name::Symbol) => md -@modelegate parameter_names(m::Model, comp_name::Symbol) => md +@delegate parameter_names(m::Model, comp_name::Symbol) => md -@modelegate parameter_dimensions(m::Model, comp_name::Symbol, param_name::Symbol) => md +@delegate parameter_dimensions(m::Model, comp_name::Symbol, param_name::Symbol) => md -@modelegate parameter_unit(m::Model, comp_name::Symbol, param_name::Symbol) => md +@delegate parameter_unit(m::Model, comp_name::Symbol, param_name::Symbol) => md parameter(m::Model, comp_def::ComponentDef, param_name::Symbol) = parameter(comp_def, param_name) @@ -313,130 +293,96 @@ Return a list of the parameter definitions for `comp_name` in model `m`. """ parameters(m::Model, comp_name::Symbol) = parameters(compdef(m, comp_name)) -function variable(m::Model, comp_name::Symbol, var_name::Symbol) - comp_def = compdef(m, comp_name) - return comp_def.variables[var_name] -end +variable(m::Model, comp_name::Symbol, var_name::Symbol) = variable(compdef(m, comp_name), var_name) -function variable_unit(m::Model, comp_name::Symbol, var_name::Symbol) - var = variable(m, comp_name, var_name) - return var.unit -end +@delegate variable_unit(m::Model, comp_path::ComponentPath, var_name::Symbol) => md -function variable_dimensions(m::Model, comp_name::Symbol, var_name::Symbol) - var = variable(m, comp_name, var_name) - return var.dimensions -end +@delegate variable_dimensions(m::Model, comp_path::ComponentPath, var_name::Symbol) => md """ variables(m::Model, comp_name::Symbol) -Return a list of the variable definitions for `comp_name` in model `m`. +Return an iterator on the variable definitions for `comp_name` in model `m`. """ variables(m::Model, comp_name::Symbol) = variables(compdef(m, comp_name)) -@modelegate variable_names(m::Model, comp_name::Symbol) => md +@delegate variable_names(m::Model, comp_name::Symbol) => md """ set_external_array_param!(m::Model, name::Symbol, value::Union{AbstractArray, TimestepArray}, dims) -Add a one or two dimensional (optionally, time-indexed) array parameter `name` +Add a one or two dimensional (optionally, time-indexed) array parameter `name` with value `value` to the model `m`. """ -function set_external_array_param!(m::Model, name::Symbol, value::Union{AbstractArray, TimestepArray}, dims) - set_external_array_param!(m.md, name, value, dims) - decache(m) -end +@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 a scalar type parameter `name` with value `value` to the model `m`. """ -function set_external_scalar_param!(m::Model, name::Symbol, value::Any) - set_external_scalar_param!(m.md, name, value) - decache(m) -end +@delegate set_external_scalar_param!(m::Model, name::Symbol, value::Any) => md """ - delete!(m::ModelDef, component::Symbol + delete!(m::Model, component::Symbol Delete a `component`` by name from a model `m`'s ModelDef, and nullify the ModelInstance. """ -function Base.delete!(m::Model, comp_name::Symbol) - delete!(m.md, comp_name) - decache(m) -end +@delegate Base.delete!(m::Model, comp_name::Symbol) => md """ set_param!(m::Model, comp_name::Symbol, name::Symbol, value, dims=nothing) -Set the parameter of a component `comp_name` in a model `m` to a given `value`. -The `value` can by a scalar, an array, or a NamedAray. Optional argument 'dims' -is a list of the dimension names of the provided data, and will be used to check +Set the parameter of a component `comp_name` in a model `m` to a given `value`. +The `value` can by a scalar, an array, or a NamedAray. Optional 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!(m::Model, comp_name::Symbol, param_name::Symbol, value, dims=nothing) - set_param!(m.md, comp_name, param_name, value, dims) - decache(m) -end +@delegate set_param!(m::Model, comp_name::Symbol, param_name::Symbol, value, dims=nothing) => md + +""" + set_param!(m::Model, path::AbstractString, param_name::Symbol, value, dims=nothing) + +Set a parameter for a component with the given relative path (as a string), in which "/x" means the +component with name `:x` beneath `m.md`. If the path does not begin with "/", it is treated as +relative to `m.md`, which at the top of the hierarchy, produces the same result as starting with "/". +""" +@delegate set_param!(m::Model, path::AbstractString, param_name::Symbol, value, dims=nothing) => md + +""" + set_param!(m::Model, path::AbstractString, value, dims=nothing) + +Similar to above but param_name appears in `path` after a colon delimiter. +""" +@delegate set_param!(m::Model, path::AbstractString, value, dims=nothing) => md + +""" + set_param!(m::Model, param_name::Symbol, value, dims=nothing) + +Set the value of a parameter exposed in the ModelDef (m.md). +""" +@delegate set_param!(m::Model, param_name::Symbol, value, dims=nothing) => md + +@delegate set_param!(m::Model, comp_path::ComponentPath, param_name::Symbol, value, dims=nothing) => md + """ run(m::Model) Run model `m` once. """ -function Base.run(m::Model; ntimesteps::Int=typemax(Int), +function Base.run(m::Model; ntimesteps::Int=typemax(Int), rebuild::Bool=false, dim_keys::Union{Nothing, Dict{Symbol, Vector{T} where T <: DimensionKeyTypes}}=nothing) - if numcomponents(m) == 0 + if length(m) == 0 error("Cannot run a model with no components.") end - if m.mi === nothing + if (rebuild || ! is_built(m)) build(m) end # println("Running model...") - run(m.mi, ntimesteps, dim_keys) + mi = modelinstance(m) + run(mi, ntimesteps, dim_keys) nothing end - -function _show(io::IO, obj::Model, which::Symbol) - println(io, "$(length(obj.md.comp_defs))-component Mimi.Model:") - - md = obj.md - mi = obj.mi - - # println(io, " Module: $(md.module_name)") - - # println(io, " Components:") - for comp in values(md.comp_defs) - println(io, " $(comp.name)::$(comp.comp_id.module_name).$(comp.comp_id.comp_name)") - end - - if which == :full - println(io, " Dimensions:") - for (k, v) in md.dimensions - println(io, " $k => $v") - end - - println(io, " Internal Connections:") - for conn in md.internal_param_conns - println(io, " $(conn)") - end - - println(io, " External Connections:") - for conn in md.external_param_conns - println(io, " $(conn)") - end - - println(io, " Backups: $(md.backups)") - println(io, " Number type: $(md.number_type)") - - println(io, " Built: $(mi !== nothing)") - end -end - -Base.show(io::IO, obj::Model) = _show(io, obj, :short) - -Base.show(io::IO, ::MIME"text/plain", obj::Model) = _show(io, obj, :short) diff --git a/src/core/order.jl b/src/core/order.jl new file mode 100644 index 000000000..caca55533 --- /dev/null +++ b/src/core/order.jl @@ -0,0 +1,64 @@ +# +# Support for automatic ordering of components +# + +""" + dependencies(md::ModelDef, comp_path::ComponentPath) + +Return the set of component names that `comp_path` in `md` depends one, i.e., +sources for which `comp_name` is the destination of an internal connection. +""" +function dependencies(md::ModelDef, comp_path::ComponentPath) + conns = internal_param_conns(md) + # For the purposes of the DAG, we don't treat dependencies on [t-1] as an ordering constraint + deps = Set(c.src_comp_path for c in conns if (c.dst_comp_path == comp_path && c.offset == 0)) + return deps +end + +""" + comp_graph(md::ModelDef) + +Return a MetaGraph containing a directed (LightGraph) graph of the components of +ModelDef `md`. Each vertex has a :name property with its component name. +""" +function comp_graph(md::ModelDef) + comp_paths = [c.comp_path for c in compdefs(md)] + graph = MetaDiGraph() + + for comp_path in comp_paths + add_vertex!(graph, :path, comp_path) + end + + set_indexing_prop!(graph, :path) + + for comp_path in comp_paths + for dep_path in dependencies(md, comp_path) + src = graph[dep_path, :path] + dst = graph[comp_path, :path] + add_edge!(graph, src, dst) + end + end + + #TODO: for now we can allow cycles since we aren't using the offset + # if is_cyclic(graph) + # error("Component graph contains a cycle") + # end + + return graph +end + +""" + _topological_sort(md::ModelDef) + +Build a directed acyclic graph referencing the positions of the components in +the OrderedDict of model `md`, tracing dependencies to create the DAG. +Perform a topological sort on the graph for the given model and return a vector +of component paths in the order that will ensure dependencies are processed +prior to dependent components. +""" +function _topological_sort(md::ModelDef) + graph = comp_graph(md) + ordered = topological_sort_by_dfs(graph) + paths = map(i -> graph[i, :path], ordered) + return paths +end diff --git a/src/core/paths.jl b/src/core/paths.jl new file mode 100644 index 000000000..2bcb36fb1 --- /dev/null +++ b/src/core/paths.jl @@ -0,0 +1,213 @@ +# +# ComponentPath manipulation methods +# + +Base.length(path::ComponentPath) = length(path.names) +Base.isempty(path::ComponentPath) = isempty(path.names) + +head(path::ComponentPath) = (isempty(path) ? Symbol[] : path.names[1]) +tail(path::ComponentPath) = ComponentPath(length(path) < 2 ? Symbol[] : path.names[2:end]) + +# The equivalent of ".." in the file system. +Base.parent(path::ComponentPath) = ComponentPath(path.names[1:end-1]) + +# Return a string like "/##ModelDef#367/top/Comp1" +function Base.string(path::ComponentPath) + s = join(path.names, "/") + return is_abspath(path) ? string("/", s) : s +end + +Base.joinpath(p1::ComponentPath, p2::ComponentPath) = is_abspath(p2) ? p2 : ComponentPath(p1.names..., p2.names...) +Base.joinpath(p1::ComponentPath, other...) = joinpath(joinpath(p1, other[1]), other[2:end]...) + +""" + _fix_comp_path!(child::AbstractComponentDef, parent::AbstractCompositeComponentDef) + +Set the ComponentPath of a child object to extend the path of its composite parent. +For composites, also update the component paths for all internal connections, and +for all DatumReferences in the namespace. For leaf components, also update the +ComponentPath for ParameterDefs and VariableDefs. +""" +function _fix_comp_path!(child::AbstractComponentDef, parent::AbstractCompositeComponentDef) + parent_path = parent.comp_path + child.comp_path = child_path = ComponentPath(parent_path, child.name) + # @info "Setting path of child $(child.name) with parent $parent_path to $child_path" + + # First, fix up child's namespace objs. We later recurse down the hierarchy. + ns = child.namespace + root = get_root(parent) + + for (name, ref) in ns + if ref isa AbstractDatumReference + T = typeof(ref) + ns[name] = new_ref = T(ref.name, root, child_path) + #@info "old ref: $ref, new: $new_ref" + end + end + + # recursively reset all comp_paths to their abspath equivalent + if is_composite(child) + + # Fix internal param conns + conns = child.internal_param_conns + for (i, conn) in enumerate(conns) + src_path = ComponentPath(child_path, conn.src_comp_path) + dst_path = ComponentPath(child_path, conn.dst_comp_path) + + # @info "Resetting IPC src in $child_path from $(conn.src_comp_path) to $src_path" + # @info "Resetting IPC dst in $child_path from $(conn.dst_comp_path) to $dst_path" + + # InternalParameterConnections are immutable, but the vector holding them is not + conns[i] = InternalParameterConnection(src_path, conn.src_var_name, dst_path, conn.dst_par_name, + conn.ignoreunits, conn.backup; offset=conn.offset) + end + + # Fix external param conns + conns = child.external_param_conns + for (i, conn) in enumerate(conns) + path = ComponentPath(parent_path, conn.comp_path) + # @info "Resetting EPC $child_path from $(conn.comp_path) to $path" + + conns[i] = ExternalParameterConnection(path, conn.param_name, conn.external_param) + end + + for cd in compdefs(child) + _fix_comp_path!(cd, child) + end + else + for datum in [variables(child)..., parameters(child)...] + datum.comp_path = child_path + end + end +end + +""" + fix_comp_paths!(md::AbstractModelDef) + +Recursively set the ComponentPaths in a tree below a ModelDef to the absolute path equivalent. +This includes updating the component paths for all internal/external connections, and all +DatumReferences in the namespace. For leaf components, we also update the ComponentPath for +ParameterDefs and VariableDefs. +""" +function fix_comp_paths!(md::AbstractModelDef) + for child in compdefs(md) + _fix_comp_path!(child, md) + end +end + +""" + comp_path(node::AbstractCompositeComponentDef, path::AbstractString) + +Convert a string describing a path from a node to a ComponentPath. The validity +of the path is not checked. If `path` starts with "/", the first element in the +returned component path is set to the root of the hierarchy containing `node`. +""" +function comp_path(node::AbstractCompositeComponentDef, path::AbstractString) + # empty path means just select the node's path + isempty(path) && return node.comp_path + + elts = split(path, "/") + + if elts[1] == "" # path started with "/" + root = get_root(node) + elts[1] = string(nameof(root)) + end + return ComponentPath([Symbol(elt) for elt in elts]) +end + +# For leaf components, we can only "find" the component itself +# when the path is empty. +function find_comp(obj::ComponentDef, path::ComponentPath) + return isempty(path) ? obj : nothing +end + +function find_comp(obj::AbstractComponentDef, name::Symbol) + # N.B. test here since compdef doesn't check existence + return has_comp(obj, name) ? compdef(obj, name) : nothing +end + + +function find_comp(obj::AbstractCompositeComponentDef, path::ComponentPath) + # @info "find_comp($(obj.name), $path)" + # @info "obj.parent = $(printable(obj.parent))" + + if isempty(path) + return obj + end + + # Convert "absolute" path from a root node to relative + if is_abspath(path) + path = rel_path(obj.comp_path, path) + # @info "abspath converted to relpath is $path" + + elseif (child = find_comp(obj, head(path))) !== nothing + # @info "path is unchanged: $path" + + + elseif nameof(obj) == head(path) + # @info "nameof(obj) == head(path); path: $(printable(path))" + path = tail(path) + else + error("Cannot find path $(printable(path)) from component $(printable(obj.comp_id))") + end + + names = path.names + if has_comp(obj, names[1]) + return find_comp(compdef(obj, names[1]), ComponentPath(names[2:end])) + end + + return nothing +end + +function find_comp(obj::AbstractCompositeComponentDef, pathstr::AbstractString) + path = comp_path(obj, pathstr) + find_comp(obj, path) +end + +find_comp(dr::AbstractDatumReference) = find_comp(dr.root, dr.comp_path) + +find_comp(cr::ComponentReference) = find_comp(cr.parent, cr.comp_path) + +""" +Return the relative path of `descendant` if is within the path of composite `ancestor` or +or nothing otherwise. +""" +function rel_path(ancestor_path::ComponentPath, descendant_path::ComponentPath) + a_names = ancestor_path.names + d_names = descendant_path.names + + if ((a_len = length(a_names)) >= (d_len = length(d_names)) || d_names[1:a_len] != a_names) + # @info "rel_path($a_names, $d_names) returning nothing" + return nothing + end + + return ComponentPath(d_names[a_len+1:end]) +end + +rel_path(obj::AbstractComponentDef, descendant_path::ComponentPath) = rel_path(obj.comp_path, descendant_path) + +""" +Return whether component `descendant` is within the composite structure of `ancestor` or +any of its descendants. If the comp_paths check out, the node is located within the +structure to ensure that the component is really where it says it is. (Trust but verify!) +""" +function is_descendant(ancestor::AbstractCompositeComponentDef, descendant::AbstractComponentDef) + a_path = ancestor.comp_path + d_path = descendant.comp_path + if d_path === nothing || (relpath = rel_path(a_path, d_path)) === nothing + return false + end + + # @info "is_descendant calling find_comp($a_path, $relpath)" + return find_comp(ancestor, relpath) +end + +""" + is_abspath(path::ComponentPath) + +Return true if the path starts from a ModelDef, whose name is generated with +gensym("ModelDef") names look like Symbol("##ModelDef#123") +""" +function is_abspath(path::ComponentPath) + return ! isempty(path) && match(r"^##ModelDef#\d+$", string(path.names[1])) !== nothing +end diff --git a/src/core/references.jl b/src/core/references.jl index 3d2156add..12657a488 100644 --- a/src/core/references.jl +++ b/src/core/references.jl @@ -4,16 +4,16 @@ Set a component parameter as `set_param!(reference, name, value)`. """ function set_param!(ref::ComponentReference, name::Symbol, value) - set_param!(ref.model, ref.comp_name, name, value) + set_param!(ref.parent, ref.comp_path, name, value) end """ - set_param!(ref.model, ref.comp_name, name, value) + set_param!(ref.parent, ref.comp_name, name, value) Set a component parameter as `reference[symbol] = value`. """ function Base.setindex!(ref::ComponentReference, value, name::Symbol) - set_param!(ref.model, ref.comp_name, name, value) + set_param!(ref.parent, ref.comp_path, name, value) end """ @@ -22,7 +22,7 @@ end Connect two components as `connect_param!(dst, dst_name, src, src_name)`. """ function connect_param!(dst::ComponentReference, dst_name::Symbol, src::ComponentReference, src_name::Symbol) - connect_param!(dst.model, dst.comp_id, dst_name, src.comp_id, src_name) + connect_param!(dst.parent, dst.comp_path, dst_name, src.comp_path, src_name) end """ @@ -31,7 +31,7 @@ end Connect two components with the same name as `connect_param!(dst, src, name)`. """ function connect_param!(dst::ComponentReference, src::ComponentReference, name::Symbol) - connect_param!(dst.model, dst.comp_id, name, src.comp_id, name) + connect_param!(dst.parent, dst.comp_path, name, src.comp_path, name) end @@ -41,7 +41,12 @@ end Get a variable reference as `comp_ref[var_name]`. """ function Base.getindex(comp_ref::ComponentReference, var_name::Symbol) - VariableReference(comp_ref.model, comp_ref.comp_name, var_name) + VariableReference(comp_ref, var_name) +end + +function _same_composite(ref1::AbstractComponentReference, ref2::AbstractComponentReference) + # @info "same_composite($(ref1.comp_path), $(ref2.comp_path))" + return ref1.comp_path.names[1] == ref2.comp_path.names[1] end """ @@ -50,7 +55,7 @@ end Connect two components as `comp_ref[var_name] = var_ref`. """ function Base.setindex!(comp_ref::ComponentReference, var_ref::VariableReference, var_name::Symbol) - comp_ref.model == var_ref.model || error("Can't connect variables defined in different models") + _same_composite(comp_ref, var_ref)|| error("Can't connect variables defined in different composite trees") - connect_param!(comp_ref.model, comp_ref.comp_name, var_name, var_ref.comp_name, var_ref.var_name) + connect_param!(comp_ref.parent, comp_ref.comp_path, var_name, var_ref.comp_path, var_ref.var_name) end diff --git a/src/core/show.jl b/src/core/show.jl new file mode 100644 index 000000000..1e5eca90e --- /dev/null +++ b/src/core/show.jl @@ -0,0 +1,250 @@ +# +# show() methods to make output of complex structures readable +# + +# printstyled(IOContext(io, :color => true), "string", color=:red) + +import Base: show + +spaces = " " + +function _indent_level!(io::IO, delta::Int) + level = get(io, :indent_level, 0) + return IOContext(io, :indent_level => max(level + delta, 0)) +end + +indent(io::IO) = _indent_level!(io, 1) + +""" + printable(value) + +Return a value that is not nothing. +""" +printable(value) = (value === nothing ? ":nothing:" : value) + +function print_indented(io::IO, args...) + level = get(io, :indent_level, 0) + print(io, repeat(spaces, level), args...) + nothing +end + +function _show_field(io::IO, name::Symbol, value; show_empty=true) + if !show_empty && isempty(value) + return + end + + print(io, "\n") + print_indented(io, name, ": ") + show(io, value) +end + +function _show_field(io::IO, name::Symbol, dict::AbstractDict; show_empty=true) + if !show_empty && isempty(dict) + return + end + + print(io, "\n") + print_indented(io, name, ": ", typeof(dict)) + io = indent(io) + for (k, v) in dict + print(io, "\n") + print_indented(io, k, " => ") + show(io, v) + end + nothing +end + +function _show_field(io::IO, name::Symbol, vec::Vector{T}; show_empty=true) where T + print(io, "\n") + print_indented(io, name, ": ", typeof(vec)) + + count = length(vec) + elide = false + max_shown = 5 + if count > max_shown + last = vec[end] + vec = vec[1:max_shown-1] + elide = true + end + + for (i, value) in enumerate(vec) + print(io, "\n") + print_indented(io, "$i: ", value) + end + + if elide + print(io, "\n") + print_indented(io, "...\n") + print_indented(io, "$count: ") + show(io, last) + end + nothing +end + +function _show_field(io::IO, name::Symbol, vec::Vector{<: AbstractMimiType}; show_empty=true) + if !show_empty && isempty(vec) + return + end + + print(io, "\n") + print_indented(io, name, ": ") + io = indent(io) + for (i, value) in enumerate(vec) + print(io, "\n") + print_indented(io, "$i: ", value) + end +end + +function _show_fields(io::IO, obj, names; show_empty=true) + for name in names + value = getfield(obj, name) + _show_field(io, name, value) + end + nothing +end + +function _show_datum_def(io::IO, obj::AbstractDatumDef) + print(io, typeof(obj), "($(obj.name)::$(obj.datatype))") + io = indent(io) + _show_field(io, :comp_path, obj.comp_path) + _show_field(io, :dim_names, obj.dim_names) + + for field in (:description, :unit) + _show_field(io, field, getfield(obj, field), show_empty=false) + end +end + +function show(io::IO, obj::ComponentId) + print(io, "ComponentId($(obj.module_obj).$(obj.comp_name))") + nothing +end + +function show(io::IO, obj::ComponentPath) + print(io, "ComponentPath$(obj.names)") + nothing +end + + +function show(io::IO, obj::AbstractDimension) + print(io, keys(obj)) + nothing +end + +show(io::IO, obj::VariableDef) = _show_datum_def(io, obj) + +function show(io::IO, obj::ParameterDef) + _show_datum_def(io, obj) + _show_field(indent(io), :default, obj.default) +end + +function show(io::IO, obj::AbstractMimiType) + print(io, typeof(obj)) + + # If a name is present, print it as type(name) + fields = fieldnames(typeof(obj)) + pos = findfirst(x -> x == :name, fields) + + if pos !== nothing + print(io, "($(obj.name))") + fields = deleteat!([fields...], pos) + end + + _show_fields(indent(io), obj, fields) +end + +function show(io::IO, obj::AbstractComponentDef) + print(io, nameof(typeof(obj)), " id:", objectid(obj)) + + fields = fieldnames(typeof(obj)) + + # skip the 'namespace' field since it's redundant + fields = [f for f in fields if f != :namespace] + + # Don't print parent or root since these create circular references + for field in (:parent, :root) + pos = findfirst(x -> x == field, fields) + if pos !== nothing + value = getfield(obj, field) + name = printable(value === nothing ? nothing : nameof(value)) + fields = deleteat!([fields...], pos) + print(io, "\n") + print_indented(indent(io), "$field: $(typeof(value))($name)") + end + end + + io = indent(io) + _show_fields(io, obj, fields) + + if obj isa AbstractComponentDef + # print an abbreviated namespace + print(io, "\n") + print_indented(io, "namespace:") + io = indent(io) + for (name, item) in obj.namespace + print(io, "\n") + print_indented(io, name, ": ") + show(io, typeof(item)) + end + end +end + +function show(io::IO, obj::AbstractDatumReference) + print(io, nameof(typeof(obj)), "(name=:$(obj.name) path=$(obj.comp_path))") +end + +function show(io::IO, obj::ModelInstance) + # Don't print full type signature since it's shown in .variables and .parameters + print(io, "ModelInstance") + + # Don't print the mi's ModelDef since it's redundant + fields = fieldnames(typeof(obj)) + pos = findfirst(x -> x == :md, fields) + fields = deleteat!([fields...], pos) + + io = indent(io) + _show_fields(io, obj, fields) + print(io, "\n") + print_indented(io, "md: (not shown)") +end + +# +# We might want to use a version of this to simplify. Imported forward from Mimi v0.9.1. +# +function _show(io::IO, obj::Model, which::Symbol) + + println(io, "Mimi.Model") + md = obj.md + mi = obj.mi + + println(io, " Module: $(md.comp_id.module_obj)") + + println(io, " Components:") + for comp in values(components(md)) + println(io, " $(comp.comp_id)") + end + + if which == :full + println(io, " Dimensions:") + for (k, v) in md.dim_dict + println(io, " $k => $v") + end + + println(io, " Internal Connections:") + for conn in md.internal_param_conns + println(io, " $(conn)") + end + + println(io, " External Connections:") + for conn in md.external_param_conns + println(io, " $(conn)") + end + + println(io, " Backups: $(md.backups)") + println(io, " Number type: $(md.number_type)") + end + println(io, " Built: $(mi !== nothing)") +end + +Base.show(io::IO, obj::Model) = _show(io, obj, :full) + +Base.show(io::IO, ::MIME"text/plain", obj::Model) = _show(io, obj, :short) diff --git a/src/core/time.jl b/src/core/time.jl index 9a8747d35..23eb70bf9 100644 --- a/src/core/time.jl +++ b/src/core/time.jl @@ -1,5 +1,5 @@ # -# 1. TIMESTEP +# TIMESTEP # """ @@ -76,7 +76,7 @@ end function next_timestep(ts::FixedTimestep{FIRST, STEP, LAST}) where {FIRST, STEP, LAST} if finished(ts) - error("Cannot get next timestep, this is last timestep.") + error("Cannot get next timestep, this is last timestep.") end return FixedTimestep{FIRST, STEP, LAST}(ts.t + 1) end @@ -89,7 +89,7 @@ function next_timestep(ts::VariableTimestep{TIMES}) where {TIMES} end function Base.:-(ts::FixedTimestep{FIRST, STEP, LAST}, val::Int) where {FIRST, STEP, LAST} - if is_first(ts) + if val != 0 && is_first(ts) error("Cannot get previous timestep, this is first timestep.") elseif ts.t - val <= 0 error("Cannot get requested timestep, precedes first timestep.") @@ -98,7 +98,7 @@ function Base.:-(ts::FixedTimestep{FIRST, STEP, LAST}, val::Int) where {FIRST, S end function Base.:-(ts::VariableTimestep{TIMES}, val::Int) where {TIMES} - if is_first(ts) + if val != 0 && is_first(ts) error("Cannot get previous timestep, this is first timestep.") elseif ts.t - val <= 0 error("Cannot get requested timestep, precedes first timestep.") @@ -126,9 +126,22 @@ function Base.:+(ts::VariableTimestep{TIMES}, val::Int) where {TIMES} end # -# 2. CLOCK +# CLOCK # +function Clock(time_keys::Vector{Int}) + last = time_keys[end] + + if isuniform(time_keys) + first, stepsize = first_and_step(time_keys) + return Clock{FixedTimestep}(first, stepsize, last) + else + last_index = findfirst(isequal(last), time_keys) + times = (time_keys[1:last_index]...,) + return Clock{VariableTimestep}(times) + end +end + function timestep(c::Clock) return c.ts end @@ -155,388 +168,7 @@ function finished(c::Clock) return finished(c.ts) end -# -# 3. TimestepVector and TimestepMatrix -# - -# -# 3a. General -# - -# Get a timestep array of type T with N dimensions. Time labels will match those from the time dimension in md -function get_timestep_array(md::ModelDef, T, N, ti, value) - - if isuniform(md) - first, stepsize = first_and_step(md) - return TimestepArray{FixedTimestep{first, stepsize}, T, N, ti}(value) - else - TIMES = (time_labels(md)...,) - return TimestepArray{VariableTimestep{TIMES}, T, N, ti}(value) - end -end - -# Return the index position of the time dimension in the datumdef or parameter. If there is no time dimension, return nothing -get_time_index_position(dims::Union{Nothing, Array{Symbol}}) = findfirst(isequal(:time), dims) -get_time_index_position(datumdef::DatumDef) = get_time_index_position(datumdef.dimensions) -get_time_index_position(param::ArrayModelParameter) = get_time_index_position(param.dimensions) -get_time_index_position(md::ModelDef, comp_name::Symbol, datum_name::Symbol) = get_time_index_position(dimensions(compdef(md, comp_name), datum_name)) - -const AnyIndex = Union{Int, Vector{Int}, Tuple, Colon, OrdinalRange} - -# TBD: can it be reduced to this? -# const AnyIndex = Union{Int, AbstractRange} - -# Helper function for getindex; throws a MissingException if data is missing, otherwise returns data -function _missing_data_check(data) - if data === missing - throw(MissingException("Cannot get index; data is missing. You may have tried to access a value that has not yet been computed.")) - else - return data - end -end - -# Helper macro used by connector -macro allow_missing(expr) - let e = gensym("e") - retexpr = quote - try - $expr - catch $e - if $e isa MissingException - missing - else - rethrow($e) - end - end - end - return esc(retexpr) - end -end - -# -# 3b. TimestepVector -# - -function Base.getindex(v::TimestepVector{FixedTimestep{FIRST, STEP}, T}, ts::FixedTimestep{FIRST, STEP, LAST}) where {T, FIRST, STEP, LAST} - data = v.data[ts.t] - _missing_data_check(data) -end - -function Base.getindex(v::TimestepVector{VariableTimestep{TIMES}, T}, ts::VariableTimestep{TIMES}) where {T, TIMES} - data = v.data[ts.t] - _missing_data_check(data) -end - -function Base.getindex(v::TimestepVector{FixedTimestep{D_FIRST, STEP}, T}, ts::FixedTimestep{T_FIRST, STEP, LAST}) where {T, D_FIRST, T_FIRST, STEP, LAST} - t = Int(ts.t + (T_FIRST - D_FIRST) / STEP) - data = v.data[t] - _missing_data_check(data) -end - -function Base.getindex(v::TimestepVector{VariableTimestep{D_TIMES}, T}, ts::VariableTimestep{T_TIMES}) where {T, D_TIMES, T_TIMES} - t = ts.t + findfirst(isequal(T_TIMES[1]), D_TIMES) - 1 - data = v.data[t] - _missing_data_check(data) -end - -# int indexing version supports old-style components and internal functions, not -# part of the public API - -function Base.getindex(v::TimestepVector{FixedTimestep{FIRST, STEP}, T}, i::AnyIndex) where {T, FIRST, STEP} - return v.data[i] -end - -function Base.getindex(v::TimestepVector{VariableTimestep{TIMES}, T}, i::AnyIndex) where {T, TIMES} - return v.data[i] -end - -function Base.setindex!(v::TimestepVector{FixedTimestep{FIRST, STEP}, T}, val, ts::FixedTimestep{FIRST, STEP, LAST}) where {T, FIRST, STEP, LAST} - setindex!(v.data, val, ts.t) -end - -function Base.setindex!(v::TimestepVector{VariableTimestep{TIMES}, T}, val, ts::VariableTimestep{TIMES}) where {T, TIMES} - setindex!(v.data, val, ts.t) -end - -function Base.setindex!(v::TimestepVector{FixedTimestep{D_FIRST, STEP}, T}, val, ts::FixedTimestep{T_FIRST, STEP, LAST}) where {T, D_FIRST, T_FIRST, STEP, LAST} - t = Int(ts.t + (T_FIRST - D_FIRST) / STEP) - setindex!(v.data, val, t) -end - -function Base.setindex!(v::TimestepVector{VariableTimestep{D_TIMES}, T}, val, ts::VariableTimestep{T_TIMES}) where {T, D_TIMES, T_TIMES} - t = ts.t + findfirst(isequal(T_TIMES[1]), D_TIMES) - 1 - setindex!(v.data, val, t) -end - -# int indexing version supports old-style components and internal functions, not -# part of the public API - -function Base.setindex!(v::TimestepVector{FixedTimestep{Start, STEP}, T}, val, i::AnyIndex) where {T, Start, STEP} - setindex!(v.data, val, i) -end - -function Base.setindex!(v::TimestepVector{VariableTimestep{TIMES}, T}, val, i::AnyIndex) where {T, TIMES} - setindex!(v.data, val, i) -end - -function Base.length(v::TimestepVector) - return length(v.data) -end - -Base.lastindex(v::TimestepVector) = length(v) - -# -# 3c. TimestepMatrix -# - -function Base.getindex(mat::TimestepMatrix{FixedTimestep{FIRST, STEP}, T, 1}, ts::FixedTimestep{FIRST, STEP, LAST}, idx::AnyIndex) where {T, FIRST, STEP, LAST} - data = mat.data[ts.t, idx] - _missing_data_check(data) -end - -function Base.getindex(mat::TimestepMatrix{VariableTimestep{TIMES}, T, 1}, ts::VariableTimestep{TIMES}, idx::AnyIndex) where {T, TIMES} - data = mat.data[ts.t, idx] - _missing_data_check(data) -end - -function Base.getindex(mat::TimestepMatrix{FixedTimestep{D_FIRST, STEP}, T, 1}, ts::FixedTimestep{T_FIRST, STEP, LAST}, idx::AnyIndex) where {T, D_FIRST, T_FIRST, STEP, LAST} - t = Int(ts.t + (T_FIRST - D_FIRST) / STEP) - data = mat.data[t, idx] - _missing_data_check(data) -end - -function Base.getindex(mat::TimestepMatrix{VariableTimestep{D_TIMES}, T, 1}, ts::VariableTimestep{T_TIMES}, idx::AnyIndex) where {T, D_TIMES, T_TIMES} - t = ts.t + findfirst(isequal(T_TIMES[1]), D_TIMES) - 1 - data = mat.data[t, idx] - _missing_data_check(data) -end - -function Base.getindex(mat::TimestepMatrix{FixedTimestep{FIRST, STEP}, T, 2}, idx::AnyIndex, ts::FixedTimestep{FIRST, STEP, LAST}) where {T, FIRST, STEP, LAST} - data = mat.data[idx, ts.t] - _missing_data_check(data) -end - -function Base.getindex(mat::TimestepMatrix{VariableTimestep{TIMES}, T, 2}, idx::AnyIndex, ts::VariableTimestep{TIMES}) where {T, TIMES} - data = mat.data[idx, ts.t] - _missing_data_check(data) -end - -function Base.getindex(mat::TimestepMatrix{FixedTimestep{D_FIRST, STEP}, T, 2}, idx::AnyIndex, ts::FixedTimestep{T_FIRST, STEP, LAST}) where {T, D_FIRST, T_FIRST, STEP, LAST} - t = Int(ts.t + (T_FIRST - D_FIRST) / STEP) - data = mat.data[idx, ts.t] - _missing_data_check(data) -end - -function Base.getindex(mat::TimestepMatrix{VariableTimestep{D_TIMES}, T, 2}, idx::AnyIndex, ts::VariableTimestep{T_TIMES}) where {T, D_TIMES, T_TIMES} - t = ts.t + findfirst(isequal(T_TIMES[1]), D_TIMES) - 1 - data = mat.data[idx, ts.t] - _missing_data_check(data) -end - - -function Base.setindex!(mat::TimestepMatrix{FixedTimestep{FIRST, STEP}, T, 1}, val, ts::FixedTimestep{FIRST, STEP, LAST}, idx::AnyIndex) where {T, FIRST, STEP, LAST} - setindex!(mat.data, val, ts.t, idx) -end - -function Base.setindex!(mat::TimestepMatrix{VariableTimestep{TIMES}, T, 1}, val, ts::VariableTimestep{TIMES}, idx::AnyIndex) where {T, TIMES} - setindex!(mat.data, val, ts.t, idx) -end - -function Base.setindex!(mat::TimestepMatrix{FixedTimestep{D_FIRST, STEP}, T, 1}, val, ts::FixedTimestep{T_FIRST, STEP, LAST}, idx::AnyIndex) where {T, D_FIRST, T_FIRST, STEP, LAST} - t = Int(ts.t + (T_FIRST - D_FIRST) / STEP) - setindex!(mat.data, val, t, idx) -end - -function Base.setindex!(mat::TimestepMatrix{VariableTimestep{D_TIMES}, T, 1}, val, ts::VariableTimestep{T_TIMES}, idx::AnyIndex) where {T, D_TIMES, T_TIMES} - t = ts.t + findfirst(isequal(T_TIMES[1]), D_TIMES) - 1 - setindex!(mat.data, val, t, idx) -end - -function Base.setindex!(mat::TimestepMatrix{FixedTimestep{FIRST, STEP}, T, 2}, val, idx::AnyIndex, ts::FixedTimestep{FIRST, STEP, LAST}) where {T, FIRST, STEP, LAST} - setindex!(mat.data, val, idx, ts.t) -end - -function Base.setindex!(mat::TimestepMatrix{VariableTimestep{TIMES}, T, 2}, val, idx::AnyIndex, ts::VariableTimestep{TIMES}) where {T, TIMES} - setindex!(mat.data, val, idx, ts.t) -end - -function Base.setindex!(mat::TimestepMatrix{FixedTimestep{D_FIRST, STEP}, T, 2}, val, idx::AnyIndex, ts::FixedTimestep{T_FIRST, STEP, LAST}) where {T, D_FIRST, T_FIRST, STEP, LAST} - t = Int(ts.t + (T_FIRST - D_FIRST) / STEP) - setindex!(mat.data, val, idx, t) -end - -function Base.setindex!(mat::TimestepMatrix{VariableTimestep{D_TIMES}, T, 2}, val, idx::AnyIndex, ts::VariableTimestep{T_TIMES}) where {T, D_TIMES, T_TIMES} - t = ts.t + findfirst(isequal(T_TIMES[1]), D_TIMES) - 1 - setindex!(mat.data, val, idx, t) -end - -# int indexing version supports old-style components and internal functions, not -# part of the public API - -function Base.getindex(mat::TimestepMatrix{FixedTimestep{FIRST, STEP}, T, ti}, idx1::AnyIndex, idx2::AnyIndex) where {T, FIRST, STEP, ti} - return mat.data[idx1, idx2] -end - -function Base.getindex(mat::TimestepMatrix{VariableTimestep{TIMES}, T, ti}, idx1::AnyIndex, idx2::AnyIndex) where {T, TIMES, ti} - return mat.data[idx1, idx2] -end - -function Base.setindex!(mat::TimestepMatrix{FixedTimestep{FIRST, STEP}, T, ti}, val, idx1::Int, idx2::Int) where {T, FIRST, STEP, ti} - setindex!(mat.data, val, idx1, idx2) -end - -function Base.setindex!(mat::TimestepMatrix{FixedTimestep{FIRST, STEP}, T, ti}, val, idx1::AnyIndex, idx2::AnyIndex) where {T, FIRST, STEP, ti} - mat.data[idx1, idx2] .= val -end - -function Base.setindex!(mat::TimestepMatrix{VariableTimestep{TIMES}, T, ti}, val, idx1::Int, idx2::Int) where {T, TIMES, ti} - setindex!(mat.data, val, idx1, idx2) -end - -function Base.setindex!(mat::TimestepMatrix{VariableTimestep{TIMES}, T, ti}, val, idx1::AnyIndex, idx2::AnyIndex) where {T, TIMES, ti} - mat.data[idx1, idx2] .= val -end - -# -# 4. TimestepArray methods -# -function Base.dotview(v::Mimi.TimestepArray, args...) - # convert any timesteps to their underlying index - args = map(arg -> (arg isa AbstractTimestep ? arg.t : arg), args) - Base.dotview(v.data, args...) -end - -Base.fill!(obj::TimestepArray, value) = fill!(obj.data, value) - -Base.size(obj::TimestepArray) = size(obj.data) - -Base.size(obj::TimestepArray, i::Int) = size(obj.data, i) - -Base.ndims(obj::TimestepArray{T_ts, T, N, ti}) where {T_ts, T, N, ti} = N - -Base.eltype(obj::TimestepArray{T_ts, T, N, ti}) where {T_ts, T, N, ti} = T - -first_period(obj::TimestepArray{FixedTimestep{FIRST,STEP}, T, N, ti}) where {FIRST, STEP, T, N, ti} = FIRST -first_period(obj::TimestepArray{VariableTimestep{TIMES}, T, N, ti}) where {TIMES, T, N, ti} = TIMES[1] - -last_period(obj::TimestepArray{FixedTimestep{FIRST, STEP}, T, N, ti}) where {FIRST, STEP, T, N, ti} = (FIRST + (size(obj, 1) - 1) * STEP) -last_period(obj::TimestepArray{VariableTimestep{TIMES}, T, N, ti}) where {TIMES, T, N, ti} = TIMES[end] - -time_labels(obj::TimestepArray{FixedTimestep{FIRST, STEP}, T, N, ti}) where {FIRST, STEP, T, N, ti} = collect(FIRST:STEP:(FIRST + (size(obj, 1) - 1) * STEP)) -time_labels(obj::TimestepArray{VariableTimestep{TIMES}, T, N, ti}) where {TIMES, T, N, ti} = collect(TIMES) - -split_indices(idxs, ti) = idxs[1:ti - 1], idxs[ti], idxs[ti + 1:end] - -function Base.getindex(arr::TimestepArray{FixedTimestep{FIRST, STEP}, T, N, ti}, idxs::Union{FixedTimestep{FIRST, STEP, LAST}, AnyIndex}...) where {T, N, ti, FIRST, STEP, LAST} - idxs1, ts, idxs2 = split_indices(idxs, ti) - return arr.data[idxs1..., ts.t, idxs2...] -end - -function Base.getindex(arr::TimestepArray{VariableTimestep{TIMES}, T, N, ti}, idxs::Union{VariableTimestep{TIMES}, AnyIndex}...) where {T, N, ti, TIMES} - idxs1, ts, idxs2 = split_indices(idxs, ti) - return arr.data[idxs1..., ts.t, idxs2...] -end - -function Base.getindex(arr::TimestepArray{FixedTimestep{D_FIRST, STEP}, T, N, ti}, idxs::Union{FixedTimestep{T_FIRST, STEP, LAST}, AnyIndex}...) where {T, N, ti, D_FIRST, T_FIRST, STEP, LAST} - idxs1, ts, idxs2 = split_indices(idxs, ti) - t = Int(ts.t + (FIRST - TIMES[1]) / STEP) - return arr.data[idxs1..., t, idxs2...] -end - -function Base.getindex(arr::TimestepArray{VariableTimestep{D_TIMES}, T, N, ti}, idxs::Union{VariableTimestep{T_TIMES}, AnyIndex}...) where {T, N, ti, D_TIMES, T_TIMES} - idxs1, ts, idxs2 = split_indices(idxs, ti) - t = ts.t + findfirst(isequal(T_TIMES[1]), D_TIMES) - 1 - return arr.data[idxs1..., t, idxs2...] -end - -function Base.setindex!(arr::TimestepArray{FixedTimestep{FIRST, STEP}, T, N, ti}, val, idxs::Union{FixedTimestep{FIRST, STEP, LAST}, AnyIndex}...) where {T, N, ti, FIRST, STEP, LAST} - idxs1, ts, idxs2 = split_indices(idxs, ti) - setindex!(arr.data, val, idxs1..., ts.t, idxs2...) -end - -function Base.setindex!(arr::TimestepArray{VariableTimestep{TIMES}, T, N, ti}, val, idxs::Union{VariableTimestep{TIMES}, AnyIndex}...) where {T, N, ti, TIMES} - idxs1, ts, idxs2 = split_indices(idxs, ti) - setindex!(arr.data, val, idxs1..., ts.t, idxs2...) -end - -function Base.setindex!(arr::TimestepArray{FixedTimestep{D_FIRST, STEP}, T, N, ti}, val, idxs::Union{FixedTimestep{T_FIRST, STEP, LAST}, AnyIndex}...) where {T, N, ti, D_FIRST, T_FIRST, STEP, LAST} - idxs1, ts, idxs2 = split_indices(idxs, ti) - t = ts.t + findfirst(isequal(T_FIRST[1]), D_FIRST) - 1 - setindex!(arr.data, val, idxs1..., t, idxs2...) -end - -function Base.setindex!(arr::TimestepArray{VariableTimestep{D_TIMES}, T, N, ti}, val, idxs::Union{VariableTimestep{T_TIMES}, AnyIndex}...) where {T, N, ti, D_TIMES, T_TIMES} - idxs1, ts, idxs2 = split_indices(idxs, ti) - t = ts.t + findfirst(isequal(T_FIRST[1]), T_TIMES) - 1 - setindex!(arr.data, val, idxs1..., t, idxs2...) -end - -# int indexing version supports old-style components and internal functions, not -# part of the public API; first index is Int or Range, rather than a Timestep - -function Base.getindex(arr::TimestepArray{FixedTimestep{FIRST, STEP}, T, N, ti}, idx1::AnyIndex, idx2::AnyIndex, idxs::AnyIndex...) where {T, N, ti, FIRST, STEP} - return arr.data[idx1, idx2, idxs...] -end - -function Base.getindex(arr::TimestepArray{VariableTimestep{TIMES}, T, N, ti}, idx1::AnyIndex, idx2::AnyIndex, idxs::AnyIndex...) where {T, N, ti, TIMES} - return arr.data[idx1, idx2, idxs...] -end - -function Base.setindex!(arr::TimestepArray{FixedTimestep{FIRST, STEP}, T, N, ti}, val, idx1::AnyIndex, idx2::AnyIndex, idxs::AnyIndex...) where {T, N, ti, FIRST, STEP} - setindex!(arr.data, val, idx1, idx2, idxs...) -end - -function Base.setindex!(arr::TimestepArray{VariableTimestep{TIMES}, T, N, ti}, val, idx1::AnyIndex, idx2::AnyIndex, idxs::AnyIndex...) where {T, N, ti, TIMES} - setindex!(arr.data, val, idx1, idx2, idxs...) -end - -""" - hasvalue(arr::TimestepArray, ts::FixedTimestep) - -Return `true` or `false`, `true` if the TimestepArray `arr` contains the Timestep `ts`. -""" -function hasvalue(arr::TimestepArray{FixedTimestep{FIRST, STEP}, T, N, ti}, ts::FixedTimestep{FIRST, STEP, LAST}) where {T, N, ti, FIRST, STEP, LAST} - return 1 <= ts.t <= size(arr, 1) -end - -""" - hasvalue(arr::TimestepArray, ts::VariableTimestep) - -Return `true` or `false`, `true` if the TimestepArray `arr` contains the Timestep `ts`. -""" -function hasvalue(arr::TimestepArray{VariableTimestep{TIMES}, T, N, ti}, ts::VariableTimestep{TIMES}) where {T, N, ti, TIMES} - return 1 <= ts.t <= size(arr, 1) -end - -function hasvalue(arr::TimestepArray{FixedTimestep{D_FIRST, STEP}, T, N, ti}, ts::FixedTimestep{T_FIRST, STEP, LAST}) where {T, N, ti, D_FIRST, T_FIRST, STEP, LAST} - return D_FIRST <= gettime(ts) <= last_period(arr) -end - -function hasvalue(arr::TimestepArray{VariableTimestep{D_FIRST}, T, N, ti}, ts::VariableTimestep{T_FIRST}) where {T, N, ti, T_FIRST, D_FIRST} - return D_FIRST[1] <= gettime(ts) <= last_period(arr) -end - -""" - hasvalue(arr::TimestepArray, ts::FixedTimestep, idxs::Int...) - -Return `true` or `false`, `true` if the TimestepArray `arr` contains the Timestep `ts` within -indices `idxs`. Used when Array and Timestep have different FIRST, validating all dimensions. -""" -function hasvalue(arr::TimestepArray{FixedTimestep{D_FIRST, STEP}, T, N, ti}, - ts::FixedTimestep{T_FIRST, STEP, LAST}, - idxs::Int...) where {T, N, ti, D_FIRST, T_FIRST, STEP, LAST} - return D_FIRST <= gettime(ts) <= last_period(arr) && all([1 <= idx <= size(arr, i) for (i, idx) in enumerate(idxs)]) -end - -""" - hasvalue(arr::TimestepArray, ts::VariableTimestep, idxs::Int...) - -Return `true` or `false`, `true` if the TimestepArray `arr` contains the Timestep `ts` within -indices `idxs`. Used when Array and Timestep different TIMES, validating all dimensions. -""" -function hasvalue(arr::TimestepArray{VariableTimestep{D_FIRST}, T, N, ti}, - ts::VariableTimestep{T_FIRST}, - idxs::Int...) where {T, N, ti, D_FIRST, T_FIRST} - - return D_FIRST[1] <= gettime(ts) <= last_period(arr) && all([1 <= idx <= size(arr, i) for (i, idx) in enumerate(idxs)]) +function Base.reset(c::Clock) + c.ts = c.ts - (c.ts.t - 1) + nothing end diff --git a/src/core/time_arrays.jl b/src/core/time_arrays.jl new file mode 100644 index 000000000..43a272be9 --- /dev/null +++ b/src/core/time_arrays.jl @@ -0,0 +1,385 @@ +# +# TimestepVector and TimestepMatrix +# + +# +# a. General +# + +# Get a timestep array of type T with N dimensions. Time labels will match those from the time dimension in md +function get_timestep_array(md::ModelDef, T, N, ti, value) + if isuniform(md) + first, stepsize = first_and_step(md) + first === nothing && @warn "get_timestep_array: first === nothing" + return TimestepArray{FixedTimestep{first, stepsize}, T, N, ti}(value) + else + TIMES = (time_labels(md)...,) + return TimestepArray{VariableTimestep{TIMES}, T, N, ti}(value) + end +end + +# Return the index position of the time dimension in the datumdef or parameter. If there is no time dimension, return nothing +get_time_index_position(dims::Union{Nothing, Array{Symbol}}) = findfirst(isequal(:time), dims) +get_time_index_position(obj::Union{AbstractDatumDef, ArrayModelParameter}) = get_time_index_position(dim_names(obj)) + +function get_time_index_position(obj::AbstractCompositeComponentDef, comp_name::Symbol, datum_name::Symbol) + get_time_index_position(dim_names(compdef(obj, comp_name), datum_name)) +end + +const AnyIndex = Union{Int, Vector{Int}, Tuple, Colon, OrdinalRange} + +# Helper function for getindex; throws a MissingException if data is missing, otherwise returns data +function _missing_data_check(data) + if data === missing + throw(MissingException("Cannot get index; data is missing. You may have tried to access a value that has not yet been computed.")) + else + return data + end +end + +# Helper macro used by connector +macro allow_missing(expr) + let e = gensym("e") + retexpr = quote + try + $expr + catch $e + if $e isa MissingException + missing + else + rethrow($e) + end + end + end + return esc(retexpr) + end +end + +# +# b. TimestepVector +# + +function Base.getindex(v::TimestepVector{FixedTimestep{FIRST, STEP}, T}, ts::FixedTimestep{FIRST, STEP, LAST}) where {T, FIRST, STEP, LAST} + data = v.data[ts.t] + _missing_data_check(data) +end + +function Base.getindex(v::TimestepVector{VariableTimestep{TIMES}, T}, ts::VariableTimestep{TIMES}) where {T, TIMES} + data = v.data[ts.t] + _missing_data_check(data) +end + +function Base.getindex(v::TimestepVector{FixedTimestep{D_FIRST, STEP}, T}, ts::FixedTimestep{T_FIRST, STEP, LAST}) where {T, D_FIRST, T_FIRST, STEP, LAST} + t = Int(ts.t + (T_FIRST - D_FIRST) / STEP) + data = v.data[t] + _missing_data_check(data) +end + +function Base.getindex(v::TimestepVector{VariableTimestep{D_TIMES}, T}, ts::VariableTimestep{T_TIMES}) where {T, D_TIMES, T_TIMES} + t = ts.t + findfirst(isequal(T_TIMES[1]), D_TIMES) - 1 + data = v.data[t] + _missing_data_check(data) +end + +# int indexing version supports old-style components and internal functions, not +# part of the public API + +function Base.getindex(v::TimestepVector{FixedTimestep{FIRST, STEP}, T}, i::AnyIndex) where {T, FIRST, STEP} + return v.data[i] +end + +function Base.getindex(v::TimestepVector{VariableTimestep{TIMES}, T}, i::AnyIndex) where {T, TIMES} + return v.data[i] +end + +function Base.setindex!(v::TimestepVector{FixedTimestep{FIRST, STEP}, T}, val, ts::FixedTimestep{FIRST, STEP, LAST}) where {T, FIRST, STEP, LAST} + setindex!(v.data, val, ts.t) +end + +function Base.setindex!(v::TimestepVector{VariableTimestep{TIMES}, T}, val, ts::VariableTimestep{TIMES}) where {T, TIMES} + setindex!(v.data, val, ts.t) +end + +function Base.setindex!(v::TimestepVector{FixedTimestep{D_FIRST, STEP}, T}, val, ts::FixedTimestep{T_FIRST, STEP, LAST}) where {T, D_FIRST, T_FIRST, STEP, LAST} + t = Int(ts.t + (T_FIRST - D_FIRST) / STEP) + setindex!(v.data, val, t) +end + +function Base.setindex!(v::TimestepVector{VariableTimestep{D_TIMES}, T}, val, ts::VariableTimestep{T_TIMES}) where {T, D_TIMES, T_TIMES} + t = ts.t + findfirst(isequal(T_TIMES[1]), D_TIMES) - 1 + setindex!(v.data, val, t) +end + +# int indexing version supports old-style components and internal functions, not +# part of the public API + +function Base.setindex!(v::TimestepVector{FixedTimestep{Start, STEP}, T}, val, i::AnyIndex) where {T, Start, STEP} + setindex!(v.data, val, i) +end + +function Base.setindex!(v::TimestepVector{VariableTimestep{TIMES}, T}, val, i::AnyIndex) where {T, TIMES} + setindex!(v.data, val, i) +end + +function Base.length(v::TimestepVector) + return length(v.data) +end + +Base.lastindex(v::TimestepVector) = length(v) + +# +# c. TimestepMatrix +# + +function Base.getindex(mat::TimestepMatrix{FixedTimestep{FIRST, STEP}, T, 1}, ts::FixedTimestep{FIRST, STEP, LAST}, idx::AnyIndex) where {T, FIRST, STEP, LAST} + data = mat.data[ts.t, idx] + _missing_data_check(data) +end + +function Base.getindex(mat::TimestepMatrix{VariableTimestep{TIMES}, T, 1}, ts::VariableTimestep{TIMES}, idx::AnyIndex) where {T, TIMES} + data = mat.data[ts.t, idx] + _missing_data_check(data) +end + +function Base.getindex(mat::TimestepMatrix{FixedTimestep{D_FIRST, STEP}, T, 1}, ts::FixedTimestep{T_FIRST, STEP, LAST}, idx::AnyIndex) where {T, D_FIRST, T_FIRST, STEP, LAST} + t = Int(ts.t + (T_FIRST - D_FIRST) / STEP) + data = mat.data[t, idx] + _missing_data_check(data) +end + +function Base.getindex(mat::TimestepMatrix{VariableTimestep{D_TIMES}, T, 1}, ts::VariableTimestep{T_TIMES}, idx::AnyIndex) where {T, D_TIMES, T_TIMES} + t = ts.t + findfirst(isequal(T_TIMES[1]), D_TIMES) - 1 + data = mat.data[t, idx] + _missing_data_check(data) +end + +function Base.getindex(mat::TimestepMatrix{FixedTimestep{FIRST, STEP}, T, 2}, idx::AnyIndex, ts::FixedTimestep{FIRST, STEP, LAST}) where {T, FIRST, STEP, LAST} + data = mat.data[idx, ts.t] + _missing_data_check(data) +end + +function Base.getindex(mat::TimestepMatrix{VariableTimestep{TIMES}, T, 2}, idx::AnyIndex, ts::VariableTimestep{TIMES}) where {T, TIMES} + # WAS THIS: data = mat.data[ts.t, idx, ts.t] + data = mat.data[idx, ts.t] + _missing_data_check(data) +end + +function Base.getindex(mat::TimestepMatrix{FixedTimestep{D_FIRST, STEP}, T, 2}, idx::AnyIndex, ts::FixedTimestep{T_FIRST, STEP, LAST}) where {T, D_FIRST, T_FIRST, STEP, LAST} + t = Int(ts.t + (T_FIRST - D_FIRST) / STEP) + data = mat.data[idx, ts.t] + _missing_data_check(data) +end + +function Base.getindex(mat::TimestepMatrix{VariableTimestep{D_TIMES}, T, 2}, idx::AnyIndex, ts::VariableTimestep{T_TIMES}) where {T, D_TIMES, T_TIMES} + t = ts.t + findfirst(isequal(T_TIMES[1]), D_TIMES) - 1 + data = mat.data[idx, ts.t] + _missing_data_check(data) +end + + +function Base.setindex!(mat::TimestepMatrix{FixedTimestep{FIRST, STEP}, T, 1}, val, ts::FixedTimestep{FIRST, STEP, LAST}, idx::AnyIndex) where {T, FIRST, STEP, LAST} + setindex!(mat.data, val, ts.t, idx) +end + +function Base.setindex!(mat::TimestepMatrix{VariableTimestep{TIMES}, T, 1}, val, ts::VariableTimestep{TIMES}, idx::AnyIndex) where {T, TIMES} + setindex!(mat.data, val, ts.t, idx) +end + +function Base.setindex!(mat::TimestepMatrix{FixedTimestep{D_FIRST, STEP}, T, 1}, val, ts::FixedTimestep{T_FIRST, STEP, LAST}, idx::AnyIndex) where {T, D_FIRST, T_FIRST, STEP, LAST} + t = Int(ts.t + (T_FIRST - D_FIRST) / STEP) + setindex!(mat.data, val, t, idx) +end + +function Base.setindex!(mat::TimestepMatrix{VariableTimestep{D_TIMES}, T, 1}, val, ts::VariableTimestep{T_TIMES}, idx::AnyIndex) where {T, D_TIMES, T_TIMES} + t = ts.t + findfirst(isequal(T_TIMES[1]), D_TIMES) - 1 + setindex!(mat.data, val, t, idx) +end + +function Base.setindex!(mat::TimestepMatrix{FixedTimestep{FIRST, STEP}, T, 2}, val, idx::AnyIndex, ts::FixedTimestep{FIRST, STEP, LAST}) where {T, FIRST, STEP, LAST} + setindex!(mat.data, val, idx, ts.t) +end + +function Base.setindex!(mat::TimestepMatrix{VariableTimestep{TIMES}, T, 2}, val, idx::AnyIndex, ts::VariableTimestep{TIMES}) where {T, TIMES} + setindex!(mat.data, val, idx, ts.t) +end + +function Base.setindex!(mat::TimestepMatrix{FixedTimestep{D_FIRST, STEP}, T, 2}, val, idx::AnyIndex, ts::FixedTimestep{T_FIRST, STEP, LAST}) where {T, D_FIRST, T_FIRST, STEP, LAST} + t = Int(ts.t + (T_FIRST - D_FIRST) / STEP) + setindex!(mat.data, val, idx, t) +end + +function Base.setindex!(mat::TimestepMatrix{VariableTimestep{D_TIMES}, T, 2}, val, idx::AnyIndex, ts::VariableTimestep{T_TIMES}) where {T, D_TIMES, T_TIMES} + t = ts.t + findfirst(isequal(T_TIMES[1]), D_TIMES) - 1 + setindex!(mat.data, val, idx, t) +end + +# int indexing version supports old-style components and internal functions, not +# part of the public API + +function Base.getindex(mat::TimestepMatrix{FixedTimestep{FIRST, STEP}, T, ti}, idx1::AnyIndex, idx2::AnyIndex) where {T, FIRST, STEP, ti} + return mat.data[idx1, idx2] +end + +function Base.getindex(mat::TimestepMatrix{VariableTimestep{TIMES}, T, ti}, idx1::AnyIndex, idx2::AnyIndex) where {T, TIMES, ti} + return mat.data[idx1, idx2] +end + +function Base.setindex!(mat::TimestepMatrix{FixedTimestep{FIRST, STEP}, T, ti}, val, idx1::Int, idx2::Int) where {T, FIRST, STEP, ti} + setindex!(mat.data, val, idx1, idx2) +end + +function Base.setindex!(mat::TimestepMatrix{FixedTimestep{FIRST, STEP}, T, ti}, val, idx1::AnyIndex, idx2::AnyIndex) where {T, FIRST, STEP, ti} + mat.data[idx1, idx2] .= val +end + +function Base.setindex!(mat::TimestepMatrix{VariableTimestep{TIMES}, T, ti}, val, idx1::Int, idx2::Int) where {T, TIMES, ti} + setindex!(mat.data, val, idx1, idx2) +end + +function Base.setindex!(mat::TimestepMatrix{VariableTimestep{TIMES}, T, ti}, val, idx1::AnyIndex, idx2::AnyIndex) where {T, TIMES, ti} + mat.data[idx1, idx2] .= val +end + +# +# TimestepArray methods +# +function Base.dotview(v::Mimi.TimestepArray, args...) + # convert any timesteps to their underlying index + args = map(arg -> (arg isa AbstractTimestep ? arg.t : arg), args) + Base.dotview(v.data, args...) +end + +Base.fill!(obj::TimestepArray, value) = fill!(obj.data, value) + +Base.size(obj::TimestepArray) = size(obj.data) + +Base.size(obj::TimestepArray, i::Int) = size(obj.data, i) + +Base.ndims(obj::TimestepArray{T_ts, T, N, ti}) where {T_ts, T, N, ti} = N + +Base.eltype(obj::TimestepArray{T_ts, T, N, ti}) where {T_ts, T, N, ti} = T + +first_period(obj::TimestepArray{FixedTimestep{FIRST,STEP}, T, N, ti}) where {FIRST, STEP, T, N, ti} = FIRST +first_period(obj::TimestepArray{VariableTimestep{TIMES}, T, N, ti}) where {TIMES, T, N, ti} = TIMES[1] + +last_period(obj::TimestepArray{FixedTimestep{FIRST, STEP}, T, N, ti}) where {FIRST, STEP, T, N, ti} = (FIRST + (size(obj, 1) - 1) * STEP) +last_period(obj::TimestepArray{VariableTimestep{TIMES}, T, N, ti}) where {TIMES, T, N, ti} = TIMES[end] + +time_labels(obj::TimestepArray{FixedTimestep{FIRST, STEP}, T, N, ti}) where {FIRST, STEP, T, N, ti} = collect(FIRST:STEP:(FIRST + (size(obj, 1) - 1) * STEP)) +time_labels(obj::TimestepArray{VariableTimestep{TIMES}, T, N, ti}) where {TIMES, T, N, ti} = collect(TIMES) + +split_indices(idxs, ti) = idxs[1:ti - 1], idxs[ti], idxs[ti + 1:end] + +function Base.getindex(arr::TimestepArray{FixedTimestep{FIRST, STEP}, T, N, ti}, idxs::Union{FixedTimestep{FIRST, STEP, LAST}, AnyIndex}...) where {T, N, ti, FIRST, STEP, LAST} + idxs1, ts, idxs2 = split_indices(idxs, ti) + return arr.data[idxs1..., ts.t, idxs2...] +end + +function Base.getindex(arr::TimestepArray{VariableTimestep{TIMES}, T, N, ti}, idxs::Union{VariableTimestep{TIMES}, AnyIndex}...) where {T, N, ti, TIMES} + idxs1, ts, idxs2 = split_indices(idxs, ti) + return arr.data[idxs1..., ts.t, idxs2...] +end + +function Base.getindex(arr::TimestepArray{FixedTimestep{D_FIRST, STEP}, T, N, ti}, idxs::Union{FixedTimestep{T_FIRST, STEP, LAST}, AnyIndex}...) where {T, N, ti, D_FIRST, T_FIRST, STEP, LAST} + idxs1, ts, idxs2 = split_indices(idxs, ti) + t = Int(ts.t + (FIRST - TIMES[1]) / STEP) + return arr.data[idxs1..., t, idxs2...] +end + +function Base.getindex(arr::TimestepArray{VariableTimestep{D_TIMES}, T, N, ti}, idxs::Union{VariableTimestep{T_TIMES}, AnyIndex}...) where {T, N, ti, D_TIMES, T_TIMES} + idxs1, ts, idxs2 = split_indices(idxs, ti) + t = ts.t + findfirst(isequal(T_TIMES[1]), D_TIMES) - 1 + return arr.data[idxs1..., t, idxs2...] +end + +function Base.setindex!(arr::TimestepArray{FixedTimestep{FIRST, STEP}, T, N, ti}, val, idxs::Union{FixedTimestep{FIRST, STEP, LAST}, AnyIndex}...) where {T, N, ti, FIRST, STEP, LAST} + idxs1, ts, idxs2 = split_indices(idxs, ti) + setindex!(arr.data, val, idxs1..., ts.t, idxs2...) +end + +function Base.setindex!(arr::TimestepArray{VariableTimestep{TIMES}, T, N, ti}, val, idxs::Union{VariableTimestep{TIMES}, AnyIndex}...) where {T, N, ti, TIMES} + idxs1, ts, idxs2 = split_indices(idxs, ti) + setindex!(arr.data, val, idxs1..., ts.t, idxs2...) +end + +function Base.setindex!(arr::TimestepArray{FixedTimestep{D_FIRST, STEP}, T, N, ti}, val, idxs::Union{FixedTimestep{T_FIRST, STEP, LAST}, AnyIndex}...) where {T, N, ti, D_FIRST, T_FIRST, STEP, LAST} + idxs1, ts, idxs2 = split_indices(idxs, ti) + t = ts.t + findfirst(isequal(T_FIRST[1]), D_FIRST) - 1 + setindex!(arr.data, val, idxs1..., t, idxs2...) +end + +function Base.setindex!(arr::TimestepArray{VariableTimestep{D_TIMES}, T, N, ti}, val, idxs::Union{VariableTimestep{T_TIMES}, AnyIndex}...) where {T, N, ti, D_TIMES, T_TIMES} + idxs1, ts, idxs2 = split_indices(idxs, ti) + t = ts.t + findfirst(isequal(T_FIRST[1]), T_TIMES) - 1 + setindex!(arr.data, val, idxs1..., t, idxs2...) +end + +# int indexing version supports old-style components and internal functions, not +# part of the public API; first index is Int or Range, rather than a Timestep + +function Base.getindex(arr::TimestepArray{FixedTimestep{FIRST, STEP}, T, N, ti}, idx1::AnyIndex, idx2::AnyIndex, idxs::AnyIndex...) where {T, N, ti, FIRST, STEP} + return arr.data[idx1, idx2, idxs...] +end + +function Base.getindex(arr::TimestepArray{VariableTimestep{TIMES}, T, N, ti}, idx1::AnyIndex, idx2::AnyIndex, idxs::AnyIndex...) where {T, N, ti, TIMES} + return arr.data[idx1, idx2, idxs...] +end + +function Base.setindex!(arr::TimestepArray{FixedTimestep{FIRST, STEP}, T, N, ti}, val, idx1::AnyIndex, idx2::AnyIndex, idxs::AnyIndex...) where {T, N, ti, FIRST, STEP} + setindex!(arr.data, val, idx1, idx2, idxs...) +end + +function Base.setindex!(arr::TimestepArray{VariableTimestep{TIMES}, T, N, ti}, val, idx1::AnyIndex, idx2::AnyIndex, idxs::AnyIndex...) where {T, N, ti, TIMES} + setindex!(arr.data, val, idx1, idx2, idxs...) +end + +""" + hasvalue(arr::TimestepArray, ts::FixedTimestep) + +Return `true` or `false`, `true` if the TimestepArray `arr` contains the Timestep `ts`. +""" +function hasvalue(arr::TimestepArray{FixedTimestep{FIRST, STEP}, T, N, ti}, ts::FixedTimestep{FIRST, STEP, LAST}) where {T, N, ti, FIRST, STEP, LAST} + return 1 <= ts.t <= size(arr, 1) +end + +""" + hasvalue(arr::TimestepArray, ts::VariableTimestep) + +Return `true` or `false`, `true` if the TimestepArray `arr` contains the Timestep `ts`. +""" +function hasvalue(arr::TimestepArray{VariableTimestep{TIMES}, T, N, ti}, ts::VariableTimestep{TIMES}) where {T, N, ti, TIMES} + return 1 <= ts.t <= size(arr, 1) +end + +function hasvalue(arr::TimestepArray{FixedTimestep{D_FIRST, STEP}, T, N, ti}, ts::FixedTimestep{T_FIRST, STEP, LAST}) where {T, N, ti, D_FIRST, T_FIRST, STEP, LAST} + return D_FIRST <= gettime(ts) <= last_period(arr) +end + +function hasvalue(arr::TimestepArray{VariableTimestep{D_FIRST}, T, N, ti}, ts::VariableTimestep{T_FIRST}) where {T, N, ti, T_FIRST, D_FIRST} + return D_FIRST[1] <= gettime(ts) <= last_period(arr) +end + +""" + hasvalue(arr::TimestepArray, ts::FixedTimestep, idxs::Int...) + +Return `true` or `false`, `true` if the TimestepArray `arr` contains the Timestep `ts` within +indices `idxs`. Used when Array and Timestep have different FIRST, validating all dimensions. +""" +function hasvalue(arr::TimestepArray{FixedTimestep{D_FIRST, STEP}, T, N, ti}, + ts::FixedTimestep{T_FIRST, STEP, LAST}, + idxs::Int...) where {T, N, ti, D_FIRST, T_FIRST, STEP, LAST} + return D_FIRST <= gettime(ts) <= last_period(arr) && all([1 <= idx <= size(arr, i) for (i, idx) in enumerate(idxs)]) +end + +""" + hasvalue(arr::TimestepArray, ts::VariableTimestep, idxs::Int...) + +Return `true` or `false`, `true` if the TimestepArray `arr` contains the Timestep `ts` within +indices `idxs`. Used when Array and Timestep different TIMES, validating all dimensions. +""" +function hasvalue(arr::TimestepArray{VariableTimestep{D_FIRST}, T, N, ti}, + ts::VariableTimestep{T_FIRST}, + idxs::Int...) where {T, N, ti, D_FIRST, T_FIRST} + + return D_FIRST[1] <= gettime(ts) <= last_period(arr) && all([1 <= idx <= size(arr, i) for (i, idx) in enumerate(idxs)]) +end diff --git a/src/core/types.jl b/src/core/types.jl deleted file mode 100644 index bdbefc1b5..000000000 --- a/src/core/types.jl +++ /dev/null @@ -1,470 +0,0 @@ -# -# 1. Types supporting parameterized Timestep and Clock objects -# - -abstract type AbstractTimestep end - -struct FixedTimestep{FIRST, STEP, LAST} <: AbstractTimestep - t::Int -end - -struct VariableTimestep{TIMES} <: AbstractTimestep - t::Int - current::Int - - function VariableTimestep{TIMES}(t::Int = 1) where {TIMES} - # The special case below handles when functions like next_step step beyond - # the end of the TIMES array. The assumption is that the length of this - # last timestep, starting at TIMES[end], is 1. - current::Int = t > length(TIMES) ? TIMES[end] + 1 : TIMES[t] - - return new(t, current) - end -end - -mutable struct Clock{T <: AbstractTimestep} - ts::T - - function Clock{T}(FIRST::Int, STEP::Int, LAST::Int) where T - return new(FixedTimestep{FIRST, STEP, LAST}(1)) - end - - function Clock{T}(TIMES::NTuple{N, Int} where N) where T - return new(VariableTimestep{TIMES}()) - end -end - -mutable struct TimestepArray{T_TS <: AbstractTimestep, T, N, ti} - data::Array{T, N} - - function TimestepArray{T_TS, T, N, ti}(d::Array{T, N}) where {T_TS, T, N, ti} - return new(d) - end - - function TimestepArray{T_TS, T, N, ti}(lengths::Int...) where {T_TS, T, N, ti} - return new(Array{T, N}(undef, lengths...)) - end -end - -# Since these are the most common cases, we define methods (in time.jl) -# specific to these type aliases, avoiding some of the inefficiencies -# associated with an arbitrary number of dimensions. -const TimestepMatrix{T_TS, T, ti} = TimestepArray{T_TS, T, 2, ti} -const TimestepVector{T_TS, T} = TimestepArray{T_TS, T, 1, 1} - -# -# 2. Dimensions -# - -abstract type AbstractDimension end - -const DimensionKeyTypes = Union{AbstractString, Symbol, Int, Float64} -const DimensionRangeTypes = Union{UnitRange{Int}, StepRange{Int, Int}} - -struct Dimension{T <: DimensionKeyTypes} <: AbstractDimension - dict::OrderedDict{T, Int} - - function Dimension(keys::Vector{T}) where {T <: DimensionKeyTypes} - dict = OrderedDict(collect(zip(keys, 1:length(keys)))) - return new{T}(dict) - end - - function Dimension(rng::T) where {T <: DimensionRangeTypes} - return Dimension(collect(rng)) - end - - Dimension(i::Int) = Dimension(1:i) - - # Support Dimension(:foo, :bar, :baz) - function Dimension(keys::T...) where {T <: DimensionKeyTypes} - vector = [key for key in keys] - return Dimension(vector) - end -end - -# -# Simple optimization for ranges since indices are computable. -# 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 - -# -# 3. Types supporting Parameters and their connections -# -abstract type ModelParameter end - -# TBD: rename ScalarParameter, ArrayParameter, and AbstractParameter? - -mutable struct ScalarModelParameter{T} <: ModelParameter - value::T - - function ScalarModelParameter{T}(value::T) where T - new(value) - end - - function ScalarModelParameter{T1}(value::T2) where {T1, T2} - try - new(T1(value)) - catch err - error("Failed to convert $value::$T2 to $T1") - end - end -end - -mutable struct ArrayModelParameter{T} <: ModelParameter - values::T - dimensions::Vector{Symbol} # if empty, we don't have the dimensions' name information - - function ArrayModelParameter{T}(values::T, dims::Vector{Symbol}) where T - new(values, dims) - end -end - -ScalarModelParameter(value) = ScalarModelParameter{typeof(value)}(value) - -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) - - -abstract type AbstractConnection end - -struct InternalParameterConnection <: AbstractConnection - src_comp_name::Symbol - src_var_name::Symbol - dst_comp_name::Symbol - dst_par_name::Symbol - ignoreunits::Bool - backup::Union{Symbol, Nothing} # a Symbol identifying the external param providing backup data, or nothing - offset::Int - - function InternalParameterConnection(src_comp::Symbol, src_var::Symbol, dst_comp::Symbol, dst_par::Symbol, - ignoreunits::Bool, backup::Union{Symbol, Nothing}=nothing; offset::Int=0) - self = new(src_comp, src_var, dst_comp, dst_par, ignoreunits, backup, offset) - return self - end -end - -struct ExternalParameterConnection <: AbstractConnection - comp_name::Symbol - param_name::Symbol # name of the parameter in the component - external_param::Symbol # name of the parameter stored in md.external_params -end - -# -# 4. Types supporting structural definition of models and their components -# - -# To identify components, we create a variable with the name of the component -# whose value is an instance of this type, e.g. -# const global adder = ComponentId(module_name, comp_name) -struct ComponentId - module_name::Module - comp_name::Symbol -end - -# Indicates that the object has a `name` attribute -abstract type NamedDef end - -# Supertype for vars and params -# abstract type DatumDef <: NamedDef end - -# The same structure is used for variables and parameters -mutable struct DatumDef <: NamedDef - name::Symbol - datatype::DataType - dimensions::Vector{Symbol} - description::String - unit::String - datum_type::Symbol # :parameter or :variable - default::Any # used only for Parameters - - function DatumDef(name::Symbol, datatype::DataType, dimensions::Vector{Symbol}, - description::String, unit::String, datum_type::Symbol, - default::Any=nothing) - self = new() - self.name = name - self.datatype = datatype - self.dimensions = dimensions - self.description = description - self.unit = unit - self.datum_type = datum_type - self.default = default - return self - end - -end - -mutable struct ComponentDef <: NamedDef - name::Symbol - comp_id::ComponentId - variables::OrderedDict{Symbol, DatumDef} - parameters::OrderedDict{Symbol, DatumDef} - dimensions::OrderedDict{Symbol, Union{Nothing, Dimension}} - first::Union{Nothing, Int} - last::Union{Nothing, Int} - - # ComponentDefs are created "empty"; elements are subsequently added - # to them via addvariable, add_dimension!, etc. - function ComponentDef(comp_id::ComponentId) - self = new() - self.name = comp_id.comp_name - self.comp_id = comp_id - self.variables = OrderedDict{Symbol, DatumDef}() - self.parameters = OrderedDict{Symbol, DatumDef}() - self.dimensions = OrderedDict{Symbol, Union{Nothing, Dimension}}() - self.first = self.last = nothing - return self - end -end - -# Declarative definition of a model, used to create a ModelInstance -mutable struct ModelDef - module_name::Symbol # the module in which this model was defined - - # Components keyed by symbolic name, allowing a given component - # to occur multiple times within a model. - comp_defs::OrderedDict{Symbol, ComponentDef} - - dimensions::Dict{Symbol, Dimension} - - number_type::DataType - - internal_param_conns::Vector{InternalParameterConnection} - external_param_conns::Vector{ExternalParameterConnection} - - # Names of external params that the ConnectorComps will use as their :input2 parameters. - backups::Vector{Symbol} - - external_params::Dict{Symbol, ModelParameter} - - sorted_comps::Union{Nothing, Vector{Symbol}} - - is_uniform::Bool - - function ModelDef(number_type=Float64) - self = new() - self.module_name = nameof(@__MODULE__) # TBD: fix this; should by module model is defined in - self.comp_defs = OrderedDict{Symbol, ComponentDef}() - self.dimensions = Dict{Symbol, Dimension}() - self.number_type = number_type - self.internal_param_conns = Vector{InternalParameterConnection}() - self.external_param_conns = Vector{ExternalParameterConnection}() - self.external_params = Dict{Symbol, ModelParameter}() - self.backups = Vector{Symbol}() - self.sorted_comps = nothing - self.is_uniform = true - return self - end -end - -# -# 5. Types supporting instantiated models and their components -# - -# Supertype for variables and parameters in component instances -abstract type ComponentInstanceData end - -struct ComponentInstanceParameters{NT <: NamedTuple} <: ComponentInstanceData - nt::NT - - function ComponentInstanceParameters{NT}(nt::NT) where {NT <: NamedTuple} - return new{NT}(nt) - end -end - -function ComponentInstanceParameters(names, types, values) - NT = NamedTuple{names, types} - ComponentInstanceParameters{NT}(NT(values)) -end - -function ComponentInstanceParameters{NT}(values::T) where {NT <: NamedTuple, T <: AbstractArray} - ComponentInstanceParameters{NT}(NT(values)) -end - -struct ComponentInstanceVariables{NT <: NamedTuple} <: ComponentInstanceData - nt::NT - - function ComponentInstanceVariables{NT}(nt::NT) where {NT <: NamedTuple} - return new{NT}(nt) - end -end - -function ComponentInstanceVariables{NT}(values::T) where {NT <: NamedTuple, T <: AbstractArray} - ComponentInstanceVariables{NT}(NT(values)) -end - -function ComponentInstanceVariables(names, types, values) - NT = NamedTuple{names, types} - ComponentInstanceVariables{NT}(NT(values)) -end - -# A container class that wraps the dimension dictionary when passed to run_timestep() -# and init(), so we can safely implement Base.getproperty(), allowing `d.regions` etc. -struct DimDict - dict::Dict{Symbol, Vector{Int}} -end - -# Special case support for Dicts so we can use dot notation on dimension. -# The run_timestep() and init() funcs pass a DimDict of dimensions by name -# as the "d" parameter. -@inline function Base.getproperty(dimdict::DimDict, property::Symbol) - return getfield(dimdict, :dict)[property] -end - -# TBD: try with out where clause, i.e., just obj::ComponentInstanceData -nt(obj::T) where {T <: ComponentInstanceData} = getfield(obj, :nt) -Base.names(obj::T) where {T <: ComponentInstanceData} = keys(nt(obj)) -Base.values(obj::T) where {T <: ComponentInstanceData} = values(nt(obj)) -types(obj::T) where {T <: ComponentInstanceData} = typeof(nt(obj)).parameters[2].parameters - -# An instance of this type is passed to the run_timestep function of a -# component, typically as the `p` argument. The main role of this type -# is to provide the convenient `p.nameofparameter` syntax. -mutable struct ComponentInstance{TV <: ComponentInstanceVariables, TP <: ComponentInstanceParameters} - comp_name::Symbol - comp_id::ComponentId - variables::TV - parameters::TP - dim_dict::Dict{Symbol, Vector{Int}} - - first::Int - last::Int - - init::Union{Nothing, Function} - run_timestep::Union{Nothing, Function} - - function ComponentInstance{TV, TP}(comp_def::ComponentDef, - vars::TV, pars::TP, - first::Int, last::Int, - name::Symbol=name(comp_def)) where {TV <: ComponentInstanceVariables, - TP <: ComponentInstanceParameters} - self = new{TV, TP}() - - self.comp_id = comp_id = comp_def.comp_id - self.comp_name = name - self.dim_dict = Dict{Symbol, Vector{Int}}() # set in "build" stage - - self.variables = vars - self.parameters = pars - self.first = first - self.last = last - - comp_name = comp_id.comp_name - module_name = comp_id.module_name - comp_module = module_name - - # TBD: use FunctionWrapper here? - function get_func(name) - func_name = Symbol("$(name)_$(comp_name)") - try - getfield(comp_module, func_name) - catch err - # No need to warn about this... - nothing - end - end - - self.init = get_func("init") - self.run_timestep = get_func("run_timestep") - - return self - end -end - -# This type holds the values of a built model and can actually be run. -mutable struct ModelInstance - md::ModelDef - - # Ordered list of components (including hidden ConnectorComps) - components::OrderedDict{Symbol, ComponentInstance} - - firsts::Vector{Int} # in order corresponding with components - lasts::Vector{Int} - - function ModelInstance(md::ModelDef) - self = new() - self.md = md - self.components = OrderedDict{Symbol, ComponentInstance}() - self.firsts = Vector{Int}() - self.lasts = Vector{Int}() - return self - end -end - -# -# 6. User-facing Model types providing a simplified API to model definitions and instances. -# - -""" - Model - -A user-facing API containing a `ModelInstance` (`mi`) and a `ModelDef` (`md`). -This `Model` can be created with the optional keyword argument `number_type` indicating -the default type of number used for the `ModelDef`. If not specified the `Model` assumes -a `number_type` of `Float64`. -""" -mutable struct Model - md::ModelDef - mi::Union{Nothing, ModelInstance} - - function Model(number_type::DataType=Float64) - return new(ModelDef(number_type), nothing) - end - - # Create a copy of a model, e.g., to create marginal models - function Model(m::Model) - return new(copy(m.md), nothing) - end -end - -""" - MarginalModel - -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`. -""" -struct MarginalModel - base::Model - marginal::Model - delta::Float64 - - function MarginalModel(base::Model, delta::Float64=1.0) - return new(base, Model(base), delta) - end -end - -function Base.getindex(mm::MarginalModel, comp_name::Symbol, name::Symbol) - return (mm.marginal[comp_name, name] .- mm.base[comp_name, name]) ./ mm.delta -end - -# -# 7. Reference types provide more convenient syntax for interrogating Components -# - -""" - ComponentReference - -A container for a component, for interacting with it within a model. -""" -struct ComponentReference - model::Model - comp_name::Symbol -end - -""" - VariableReference - -A container for a variable within a component, to improve connect_param! aesthetics, -by supporting subscripting notation via getindex & setindex . -""" -struct VariableReference - model::Model - comp_name::Symbol - var_name::Symbol -end diff --git a/src/core/types/core.jl b/src/core/types/core.jl new file mode 100644 index 000000000..ba7f1abb9 --- /dev/null +++ b/src/core/types/core.jl @@ -0,0 +1,97 @@ +using Classes +using DataStructures + +""" + @or(args...) + +Return `a` if a !== nothing, else return `b`. Evaluates each expression +at most once. +""" +macro or(a, b) + # @info "or($a, $b)" + tmp = gensym(:tmp) + expr = quote + $tmp = $a + ($tmp === nothing ? $b : $tmp) + end + esc(expr) +end + +# Having all our structs/classes subtype these simplifies "show" methods +abstract type MimiStruct end +@class MimiClass <: Class + +const AbstractMimiType = Union{MimiStruct, AbstractMimiClass} + +# To identify components, @defcomp creates a variable with the name of +# the component whose value is an instance of this type. +struct ComponentId <: MimiStruct + # Deprecated + # module_path::Union{Nothing, NTuple{N, Symbol} where N} + # module_name::Symbol + + module_obj::Union{Nothing, Module} + comp_name::Symbol +end + +# Modules cannot be deepcopied, thus the override +Base.deepcopy_internal(x::ComponentId, dict::IdDict) = ComponentId(x.module_obj, x.comp_name) + +# Deprecated +# ComponentId(module_name::Symbol, comp_name::Symbol) = ComponentId(nothing, module_name, nothing, comp_name) + +# ComponentPath identifies the path through multiple composites to a leaf comp. +struct ComponentPath <: MimiStruct + names::NTuple{N, Symbol} where N +end + +ComponentPath(names::Vector{Symbol}) = ComponentPath(Tuple(names)) +ComponentPath(names::Vararg{Symbol}) = ComponentPath(Tuple(names)) + +ComponentPath(path::ComponentPath, name::Symbol) = ComponentPath(path.names..., name) + +ComponentPath(path1::ComponentPath, path2::ComponentPath) = ComponentPath(path1.names..., path2.names...) + +ComponentPath(::Nothing, name::Symbol) = ComponentPath(name) + +const ParamPath = Tuple{ComponentPath, Symbol} + +# +# Dimensions +# + +abstract type AbstractDimension <: MimiStruct end + +const DimensionKeyTypes = Union{AbstractString, Symbol, Int, Float64} +const DimensionRangeTypes = Union{UnitRange{Int}, StepRange{Int, Int}} + +struct Dimension{T <: DimensionKeyTypes} <: AbstractDimension + dict::OrderedDict{T, Int} + + function Dimension(keys::Vector{T}) where {T <: DimensionKeyTypes} + dict = OrderedDict(collect(zip(keys, 1:length(keys)))) + return new{T}(dict) + end + + function Dimension(rng::T) where {T <: DimensionRangeTypes} + return Dimension(collect(rng)) + end + + Dimension(i::Int) = Dimension(1:i) + + # Support Dimension(:foo, :bar, :baz) + function Dimension(keys::T...) where {T <: DimensionKeyTypes} + vector = [key for key in keys] + return Dimension(vector) + end +end + +# +# Simple optimization for ranges since indices are computable. +# Unclear whether this is really any better than simply using +# a dict for all cases. Might scrap this in the end. +# +mutable struct RangeDimension{T <: DimensionRangeTypes} <: AbstractDimension + range::T + end + diff --git a/src/core/types/defs.jl b/src/core/types/defs.jl new file mode 100644 index 000000000..1c7ecd327 --- /dev/null +++ b/src/core/types/defs.jl @@ -0,0 +1,227 @@ +# +# Types supporting structural definition of models and their components +# + +# Objects with a `name` attribute +@class NamedObj <: MimiClass begin + name::Symbol +end + +""" + nameof(obj::NamedDef) = obj.name + +Return the name of `def`. `NamedDef`s include `DatumDef`, `ComponentDef`, +`CompositeComponentDef`, and `VariableDefReference` and `ParameterDefReference`. +""" +Base.nameof(obj::AbstractNamedObj) = obj.name + +# TBD: old definition; should deprecate this... +name(obj::AbstractNamedObj) = obj.name + +# Similar structure is used for variables and parameters (parameters merely adds `default`) +@class mutable DatumDef <: NamedObj begin + comp_path::Union{Nothing, ComponentPath} + datatype::DataType + dim_names::Vector{Symbol} + description::String + unit::String +end + +@class mutable VariableDef <: DatumDef + +@class mutable ParameterDef <: DatumDef begin + # ParameterDef adds a default value, which can be specified in @defcomp + default::Any +end + +@class mutable ComponentDef <: NamedObj begin + comp_id::Union{Nothing, ComponentId} # allow anonynous top-level (composite) ComponentDefs (must be referenced by a ModelDef) + comp_path::Union{Nothing, ComponentPath} + dim_dict::OrderedDict{Symbol, Union{Nothing, Dimension}} + namespace::OrderedDict{Symbol, Any} + first::Union{Nothing, Int} + last::Union{Nothing, Int} + is_uniform::Bool + + # Store a reference to the AbstractCompositeComponent that contains this comp def. + # That type is defined later, so we declare Any here. Parent is `nothing` for + # detached (i.e., "template") components and is set when added to a composite. + parent::Any + + + function ComponentDef(self::ComponentDef, comp_id::Nothing) + error("Leaf ComponentDef objects must have a valid ComponentId name (not nothing)") + end + + # ComponentDefs are created "empty". Elements are subsequently added. + function ComponentDef(self::AbstractComponentDef, comp_id::Union{Nothing, ComponentId}=nothing; + name::Union{Nothing, Symbol}=nothing) + if comp_id === nothing + # ModelDefs are anonymous, but since they're gensym'd, they can claim the Mimi package + comp_id = ComponentId(Mimi, @or(name, gensym(nameof(typeof(self))))) + end + + name = @or(name, comp_id.comp_name) + NamedObj(self, name) + + self.comp_id = comp_id + self.comp_path = nothing # this is set in add_comp!() and ModelDef() + + self.dim_dict = OrderedDict{Symbol, Union{Nothing, Dimension}}() + self.namespace = OrderedDict{Symbol, Any}() + self.first = self.last = nothing + self.is_uniform = true + self.parent = nothing + return self + end + + function ComponentDef(comp_id::Union{Nothing, ComponentId}; + name::Union{Nothing, Symbol}=nothing) + self = new() + return ComponentDef(self, comp_id; name=name) + end +end + +ns(obj::AbstractComponentDef) = obj.namespace +comp_id(obj::AbstractComponentDef) = obj.comp_id +pathof(obj::AbstractComponentDef) = obj.comp_path +dim_dict(obj::AbstractComponentDef) = obj.dim_dict +first_period(obj::AbstractComponentDef) = obj.first +last_period(obj::AbstractComponentDef) = obj.last +isuniform(obj::AbstractComponentDef) = obj.is_uniform + +Base.parent(obj::AbstractComponentDef) = obj.parent + +# Used by @defcomposite to communicate subcomponent information +struct SubComponent <: MimiStruct + module_obj::Union{Nothing, Module} + comp_name::Symbol + alias::Union{Nothing, Symbol} + bindings::Vector{Pair{Symbol, Any}} +end + +# Stores references to the name of a component variable or parameter +# and the ComponentPath of the component in which it is defined +@class DatumReference <: NamedObj begin + # name::Symbol is inherited from NamedObj + root::AbstractComponentDef + comp_path::ComponentPath +end + +@class ParameterDefReference <: DatumReference + +@class VariableDefReference <: DatumReference + +function _dereference(ref::AbstractDatumReference) + comp = find_comp(ref) + return comp[ref.name] +end + +# Might not be useful +# convert(::Type{VariableDef}, ref::VariableDefReference) = _dereference(ref) +# convert(::Type{ParameterDef}, ref::ParameterDefReference) = _dereference(ref) + + +# Define type aliases to avoid repeating these in several places +global const Binding = Pair{AbstractDatumReference, Union{Int, Float64, AbstractDatumReference}} + +# Define which types can appear in the namespace dict for leaf and composite compdefs +global const LeafNamespaceElement = AbstractDatumDef +global const CompositeNamespaceElement = Union{AbstractComponentDef, AbstractDatumReference} +global const NamespaceElement = Union{LeafNamespaceElement, CompositeNamespaceElement} + +@class mutable CompositeComponentDef <: ComponentDef begin + bindings::Vector{Binding} + + internal_param_conns::Vector{InternalParameterConnection} + external_param_conns::Vector{ExternalParameterConnection} + external_params::Dict{Symbol, ModelParameter} # TBD: make key (ComponentPath, Symbol)? + + # Names of external params that the ConnectorComps will use as their :input2 parameters. + backups::Vector{Symbol} + + sorted_comps::Union{Nothing, Vector{Symbol}} + + function CompositeComponentDef(comp_id::Union{Nothing, ComponentId}=nothing) + self = new() + CompositeComponentDef(self, comp_id) + return self + end + + function CompositeComponentDef(self::AbstractCompositeComponentDef, comp_id::Union{Nothing, ComponentId}=nothing) + ComponentDef(self, comp_id) # call superclass' initializer + + self.comp_path = ComponentPath(self.name) + self.bindings = Vector{Binding}() + self.internal_param_conns = Vector{InternalParameterConnection}() + self.external_param_conns = Vector{ExternalParameterConnection}() + self.external_params = Dict{Symbol, ModelParameter}() + self.backups = Vector{Symbol}() + self.sorted_comps = nothing + end +end + +# Used by @defcomposite +function CompositeComponentDef(comp_id::ComponentId, alias::Symbol, subcomps::Vector{SubComponent}, + calling_module::Module) + # @info "CompositeComponentDef($comp_id, $alias, $subcomps)" + composite = CompositeComponentDef(comp_id) + + for c in subcomps + # @info "subcomp $c: module: $(printable(c.module_obj)), calling module: $(nameof(calling_module))" + comp_module = @or(c.module_obj, calling_module) + subcomp_id = ComponentId(comp_module, c.comp_name) + subcomp = compdef(subcomp_id) + add_comp!(composite, subcomp, @or(c.alias, c.comp_name)) + end + return composite +end + +add_backup!(obj::AbstractCompositeComponentDef, backup) = push!(obj.backups, backup) + +# TBD: Recursively compute the lists on demand? +internal_param_conns(obj::AbstractCompositeComponentDef) = obj.internal_param_conns +external_param_conns(obj::AbstractCompositeComponentDef) = obj.external_param_conns + +external_params(obj::AbstractCompositeComponentDef) = obj.external_params + +is_leaf(c::AbstractComponentDef) = true +is_leaf(c::AbstractCompositeComponentDef) = false +is_composite(c::AbstractComponentDef) = !is_leaf(c) + +ComponentPath(obj::AbstractCompositeComponentDef, name::Symbol) = ComponentPath(obj.comp_path, name) + +ComponentPath(obj::AbstractCompositeComponentDef, path::AbstractString) = comp_path(obj, path) + +ComponentPath(obj::AbstractCompositeComponentDef, names::Symbol...) = ComponentPath(obj.comp_path.names..., names...) + +@class mutable ModelDef <: CompositeComponentDef begin + number_type::DataType + dirty::Bool + + function ModelDef(number_type::DataType=Float64) + self = new() + CompositeComponentDef(self) # call super's initializer + return ModelDef(self, number_type, false) # call @class-generated method + end +end + +# +# Reference types offer a more convenient syntax for interrogating Components. +# + +# A container for a component, for interacting with it within a model. +@class ComponentReference <: MimiClass begin + parent::AbstractComponentDef + comp_path::ComponentPath +end + +function ComponentReference(parent::AbstractComponentDef, name::Symbol) + return ComponentReference(parent, ComponentPath(parent.comp_path, name)) +end + +# A container for a variable within a component, to improve connect_param! aesthetics, +# by supporting subscripting notation via getindex & setindex . +@class VariableReference <: ComponentReference begin + var_name::Symbol +end diff --git a/src/core/types/includes.jl b/src/core/types/includes.jl new file mode 100644 index 000000000..b4a41910c --- /dev/null +++ b/src/core/types/includes.jl @@ -0,0 +1,6 @@ +include("core.jl") +include("time.jl") +include("params.jl") +include("defs.jl") +include("instances.jl") +include("model.jl") diff --git a/src/core/types/instances.jl b/src/core/types/instances.jl new file mode 100644 index 000000000..eba1df320 --- /dev/null +++ b/src/core/types/instances.jl @@ -0,0 +1,260 @@ +# +# Types supporting instantiated models and their components +# + +# Supertype for variables and parameters in component instances +@class ComponentInstanceData{NT <: NamedTuple} <: MimiClass begin + nt::NT + comp_paths::Vector{ComponentPath} # records the origin of each datum +end + +nt(obj::AbstractComponentInstanceData) = getfield(obj, :nt) +types(obj::AbstractComponentInstanceData) = typeof(nt(obj)).parameters[2].parameters +Base.names(obj::AbstractComponentInstanceData) = keys(nt(obj)) +Base.values(obj::AbstractComponentInstanceData) = values(nt(obj)) + +# Centralizes the shared functionality from the two component data subtypes. +function _datum_instance(subtype::Type{<: AbstractComponentInstanceData}, + names, types, values, paths) + # @info "_datum_instance: names=$names, types=$types" + NT = NamedTuple{Tuple(names), Tuple{types...}} + return subtype(NT(values), Vector{ComponentPath}(paths)) +end + +@class ComponentInstanceParameters <: ComponentInstanceData begin + function ComponentInstanceParameters(nt::NT, paths::Vector{ComponentPath}) where {NT <: NamedTuple} + return new{NT}(nt, paths) + end + + function ComponentInstanceParameters(names::Vector{Symbol}, + types::Vector{DataType}, + values::Vector{Any}, + paths) + return _datum_instance(ComponentInstanceParameters, names, types, values, paths) + end +end + +@class ComponentInstanceVariables <: ComponentInstanceData begin + function ComponentInstanceVariables(nt::NT, paths::Vector{ComponentPath}) where {NT <: NamedTuple} + return new{NT}(nt, paths) + end + + function ComponentInstanceVariables(names::Vector{Symbol}, + types::Vector{DataType}, + values::Vector{Any}, + paths) + return _datum_instance(ComponentInstanceVariables, names, types, values, paths) + end +end + +# A container class that wraps the dimension dictionary when passed to run_timestep() +# and init(), so we can safely implement Base.getproperty(), allowing `d.regions` etc. +struct DimValueDict <: MimiStruct + dict::Dict{Symbol, Vector{Int}} + + function DimValueDict(dim_dict::AbstractDict) + d = Dict([name => collect(values(dim)) for (name, dim) in dim_dict]) + new(d) + end +end + +# Special case support for Dicts so we can use dot notation on dimension. +# The run_timestep() and init() funcs pass a DimValueDict of dimensions by name +# as the "d" parameter. +Base.getproperty(obj::DimValueDict, property::Symbol) = getfield(obj, :dict)[property] + +# Superclass for both LeafComponentInstance and CompositeComponentInstance. +# This allows the former to be type-parameterized and the latter to not be. +@class mutable ComponentInstance <: MimiClass begin + comp_name::Symbol + comp_id::ComponentId + comp_path::ComponentPath + first::Union{Nothing, Int} + last::Union{Nothing, Int} + + function ComponentInstance(self::AbstractComponentInstance, + comp_def::AbstractComponentDef, + time_bounds::Tuple{Int,Int}, + name::Symbol=nameof(comp_def)) + self.comp_name = name + self.comp_id = comp_id = comp_def.comp_id + self.comp_path = comp_def.comp_path + + # If first or last is `nothing`, substitute first or last time period + self.first = @or(comp_def.first, time_bounds[1]) + self.last = @or(comp_def.last, time_bounds[2]) + end + + function ComponentInstance(comp_def::AbstractComponentDef, + time_bounds::Tuple{Int,Int}, + name::Symbol=nameof(comp_def)) + self = new() + return ComponentInstance(self, comp_def, time_bounds, name) + end +end + +@class mutable LeafComponentInstance{TV <: ComponentInstanceVariables, + TP <: ComponentInstanceParameters} <: ComponentInstance begin + variables::TV # TBD: write functions to extract these from type instead of storing? + parameters::TP + init::Union{Nothing, Function} + run_timestep::Union{Nothing, Function} + + function LeafComponentInstance(self::AbstractComponentInstance, + comp_def::AbstractComponentDef, + vars::TV, pars::TP, + time_bounds::Tuple{Int,Int}, + name::Symbol=nameof(comp_def)) where + {TV <: ComponentInstanceVariables, + TP <: ComponentInstanceParameters} + + # superclass initializer + ComponentInstance(self, comp_def, time_bounds, name) + + self.variables = vars + self.parameters = pars + + # @info "LeafComponentInstance evaluating $(self.comp_id.module_obj)" + # Deprecated + # module_name = self.comp_id.module_obj + # comp_module = module_name == :Mimi ? Mimi : getfield(Main, module_name) + comp_module = compmodule(self) + + # The try/catch allows components with no run_timestep function (as in some of our test cases) + # CompositeComponentInstances use a standard method that just loops over inner components. + # TBD: use FunctionWrapper here? + function get_func(name) + if is_composite(self) + return nothing + end + + func_name = Symbol("$(name)_$(nameof(comp_module))_$(self.comp_id.comp_name)") + try + getfield(comp_module, func_name) + catch err + # @info "Eval of $func_name in module $comp_module failed" + nothing + end + end + + # `is_composite` indicates a LeafComponentInstance used to store summary + # data for LeafComponentInstance and is not itself runnable. + self.init = get_func("init") + self.run_timestep = get_func("run_timestep") + + return self + end + + # Create an empty instance with the given type parameters + function LeafComponentInstance{TV, TP}() where {TV <: ComponentInstanceVariables, TP <: ComponentInstanceParameters} + return new{TV, TP}() + end +end + +function LeafComponentInstance(comp_def::AbstractComponentDef, vars::TV, pars::TP, + time_bounds::Tuple{Int,Int}, + name::Symbol=nameof(comp_def)) where + {TV <: ComponentInstanceVariables, TP <: ComponentInstanceParameters} + + self = LeafComponentInstance{TV, TP}() + return LeafComponentInstance(self, comp_def, vars, pars, time_bounds, name) +end + +# These can be called on CompositeComponentInstances and ModelInstances +compdef(obj::AbstractComponentInstance) = compdef(comp_id(obj)) +pathof(obj::AbstractComponentInstance) = obj.comp_path +first_period(obj::AbstractComponentInstance) = obj.first +last_period(obj::AbstractComponentInstance) = obj.last + +""" +Return the ComponentInstanceParameters/Variables exported by the given list of +component instances. +""" +function _comp_instance_vars_pars(comp_def::AbstractCompositeComponentDef, + comps::Vector{<: AbstractComponentInstance}) + vdict = Dict([:types => [], :names => [], :values => [], :paths => []]) + pdict = Dict([:types => [], :names => [], :values => [], :paths => []]) + + comps_dict = Dict([comp.comp_name => comp for comp in comps]) + + # for (name, item) in comp_def.namespace + # # Skip component references + # item isa AbstractComponentDef && continue + + # # if ! item isa VariableDefReference + # # item = + + # datum_comp = find_comp(dr) + # datum_name = nameof(dr) + # ci = comps_dict[nameof(datum_comp)] + + # datum = (is_parameter(dr) ? ci.parameters : ci.variables) + # d = (is_parameter(dr) ? pdict : vdict) + + # # Find the position of the desired field in the named tuple + # # so we can extract it's datatype. + # pos = findfirst(isequal(datum_name), names(datum)) + # datatypes = types(datum) + # dtype = datatypes[pos] + # value = getproperty(datum, datum_name) + + # push!(d[:names], export_name) + # push!(d[:types], dtype) + # push!(d[:values], value) + # push!(d[:paths], dr.comp_path) + # end + + vars = ComponentInstanceVariables(Vector{Symbol}(vdict[:names]), Vector{DataType}(vdict[:types]), + Vector{Any}(vdict[:values]), Vector{ComponentPath}(vdict[:paths])) + + pars = ComponentInstanceParameters(Vector{Symbol}(pdict[:names]), Vector{DataType}(pdict[:types]), + Vector{Any}(pdict[:values]), Vector{ComponentPath}(pdict[:paths])) + return vars, pars +end + +@class mutable CompositeComponentInstance <: ComponentInstance begin + comps_dict::OrderedDict{Symbol, AbstractComponentInstance} + var_dict::OrderedDict{Symbol, Any} + par_dict::OrderedDict{Symbol, Any} + + function CompositeComponentInstance(self::AbstractCompositeComponentInstance, + comps::Vector{<: AbstractComponentInstance}, + comp_def::AbstractCompositeComponentDef, + time_bounds::Tuple{Int,Int}, + name::Symbol=nameof(comp_def)) + + comps_dict = OrderedDict{Symbol, AbstractComponentInstance}() + for ci in comps + comps_dict[ci.comp_name] = ci + end + + var_dict = OrderedDict{Symbol, Any}() + par_dict = OrderedDict{Symbol, Any}() + + ComponentInstance(self, comp_def, time_bounds, name) + CompositeComponentInstance(self, comps_dict, var_dict, par_dict) + return self + end + + # TBD: Construct vars and params from sub-components + function CompositeComponentInstance(comps::Vector{<: AbstractComponentInstance}, + comp_def::AbstractCompositeComponentDef, + time_bounds::Tuple{Int,Int}, + name::Symbol=nameof(comp_def)) + CompositeComponentInstance(new(), comps, comp_def, time_bounds, name) + end +end + +# These methods can be called on ModelInstances as well +components(obj::AbstractCompositeComponentInstance) = values(obj.comps_dict) +has_comp(obj::AbstractCompositeComponentInstance, name::Symbol) = haskey(obj.comps_dict, name) +compinstance(obj::AbstractCompositeComponentInstance, name::Symbol) = obj.comps_dict[name] + +is_leaf(ci::LeafComponentInstance) = true +is_leaf(ci::AbstractCompositeComponentInstance) = false +is_composite(ci::AbstractComponentInstance) = !is_leaf(ci) + +# ModelInstance holds the built model that is ready to be run +@class ModelInstance <: CompositeComponentInstance begin + md::ModelDef +end diff --git a/src/core/types/model.jl b/src/core/types/model.jl new file mode 100644 index 000000000..1e501d618 --- /dev/null +++ b/src/core/types/model.jl @@ -0,0 +1,47 @@ +# +# User-facing Model types providing a simplified API to model definitions and instances. +# + +abstract type AbstractModel <: MimiStruct end + +""" + Model + +A user-facing API containing a `ModelInstance` (`mi`) and a `ModelDef` (`md`). +This `Model` can be created with the optional keyword argument `number_type` indicating +the default type of number used for the `ModelDef`. If not specified the `Model` assumes +a `number_type` of `Float64`. +""" +mutable struct Model <: AbstractModel + md::ModelDef + mi::Union{Nothing, ModelInstance} + + function Model(number_type::DataType=Float64) + return new(ModelDef(number_type), nothing) + end + + # Create a copy of a model, e.g., to create marginal models + function Model(m::Model) + return new(deepcopy(m.md), nothing) + end +end + +""" + MarginalModel + +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`. +""" +struct MarginalModel <: AbstractModel + base::Model + marginal::Model + delta::Float64 + + function MarginalModel(base::Model, delta::Float64=1.0) + return new(base, Model(base), delta) + end +end + +function Base.getindex(mm::MarginalModel, comp_name::Symbol, name::Symbol) + return (mm.marginal[comp_name, name] .- mm.base[comp_name, name]) ./ mm.delta +end diff --git a/src/core/types/params.jl b/src/core/types/params.jl new file mode 100644 index 000000000..eef2b8275 --- /dev/null +++ b/src/core/types/params.jl @@ -0,0 +1,80 @@ +# +# Types supporting Parameters and their connections +# + +abstract type ModelParameter <: MimiStruct end + +# TBD: rename as ScalarParameter, ArrayParameter, and AbstractParameter? + +mutable struct ScalarModelParameter{T} <: ModelParameter + value::T + + function ScalarModelParameter{T}(value::T) where T + new(value) + end + + function ScalarModelParameter{T1}(value::T2) where {T1, T2} + try + new(T1(value)) + catch err + error("Failed to convert $value::$T2 to $T1") + end + end +end + +mutable struct ArrayModelParameter{T} <: ModelParameter + values::T + dim_names::Vector{Symbol} # if empty, we don't have the dimensions' name information + + function ArrayModelParameter{T}(values::T, dims::Vector{Symbol}) where T + new(values, dims) + end +end + +ScalarModelParameter(value) = ScalarModelParameter{typeof(value)}(value) + +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) + +# 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) + +dim_names(obj::ArrayModelParameter) = obj.dim_names +dim_names(obj::ScalarModelParameter) = [] + +abstract type AbstractConnection <: MimiStruct end + +struct InternalParameterConnection <: AbstractConnection + src_comp_path::ComponentPath + 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 + offset::Int + + function InternalParameterConnection(src_path::ComponentPath, src_var::Symbol, + dst_path::ComponentPath, dst_par::Symbol, + ignoreunits::Bool, backup::Union{Symbol, Nothing}=nothing; offset::Int=0) + self = new(src_path, src_var, dst_path, dst_par, ignoreunits, backup, offset) + return self + end +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 +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) +end \ No newline at end of file diff --git a/src/core/types/time.jl b/src/core/types/time.jl new file mode 100644 index 000000000..7c1adcdbe --- /dev/null +++ b/src/core/types/time.jl @@ -0,0 +1,53 @@ +# +# Types supporting parameterized Timestep and Clock objects +# + +abstract type AbstractTimestep <: MimiStruct end + +struct FixedTimestep{FIRST, STEP, LAST} <: AbstractTimestep + t::Int +end + +struct VariableTimestep{TIMES} <: AbstractTimestep + t::Int + current::Int + + function VariableTimestep{TIMES}(t::Int = 1) where {TIMES} + # The special case below handles when functions like next_step step beyond + # the end of the TIMES array. The assumption is that the length of this + # last timestep, starting at TIMES[end], is 1. + current::Int = t > length(TIMES) ? TIMES[end] + 1 : TIMES[t] + + return new(t, current) + end +end + +mutable struct Clock{T <: AbstractTimestep} <: MimiStruct + ts::T + + function Clock{T}(FIRST::Int, STEP::Int, LAST::Int) where T + return new(FixedTimestep{FIRST, STEP, LAST}(1)) + end + + function Clock{T}(TIMES::NTuple{N, Int} where N) where T + return new(VariableTimestep{TIMES}()) + end +end + +mutable struct TimestepArray{T_TS <: AbstractTimestep, T, N, ti} <: MimiStruct + data::Array{T, N} + + function TimestepArray{T_TS, T, N, ti}(d::Array{T, N}) where {T_TS, T, N, ti} + return new(d) + end + + function TimestepArray{T_TS, T, N, ti}(lengths::Int...) where {T_TS, T, N, ti} + return new(Array{T, N}(undef, lengths...)) + end +end + +# Since these are the most common cases, we define methods (in time.jl) +# specific to these type aliases, avoiding some of the inefficiencies +# associated with an arbitrary number of dimensions. +const TimestepMatrix{T_TS, T, ti} = TimestepArray{T_TS, T, 2, ti} +const TimestepVector{T_TS, T} = TimestepArray{T_TS, T, 1, 1} diff --git a/src/explorer/buildspecs.jl b/src/explorer/buildspecs.jl index 695388f31..7e9346edf 100644 --- a/src/explorer/buildspecs.jl +++ b/src/explorer/buildspecs.jl @@ -2,13 +2,18 @@ using Dates using CSVFiles +function dataframe_or_scalar(m::Model, comp_name::Symbol, item_name::Symbol) + dims = dim_names(m, comp_name, item_name) + return length(dims) > 0 ? getdataframe(m, comp_name, item_name) : m[comp_name, item_name] +end + ## ## 1. Generate the VegaLite spec for a variable or parameter ## # Get spec function _spec_for_item(m::Model, comp_name::Symbol, item_name::Symbol; interactive::Bool=true) - dims = dimensions(m, comp_name, item_name) + dims = dim_names(m, comp_name, item_name) # Control flow logic selects the correct plot type based on dimensions # and dataframe fields @@ -60,7 +65,7 @@ function _spec_for_sim_item(sim_inst::SimulationInstance, comp_name::Symbol, ite # Control flow logic selects the correct plot type based on dimensions # and dataframe fields m = sim_inst.models[model_index] - dims = dimensions(m, comp_name, item_name) + dims = dim_names(m, comp_name, item_name) dffields = map(string, names(results)) # convert to string once before creating specs name = "$comp_name : $item_name" @@ -97,7 +102,7 @@ end function menu_item_list(model::Model) all_menuitems = [] - for comp_name in map(name, compdefs(model)) + for comp_name in map(nameof, compdefs(model)) items = vcat(variable_names(model, comp_name), parameter_names(model, comp_name)) for item_name in items @@ -127,7 +132,7 @@ function menu_item_list(sim_inst::SimulationInstance) end function _menu_item(m::Model, comp_name::Symbol, item_name::Symbol) - dims = dimensions(m, comp_name, item_name) + dims = dim_names(m, comp_name, item_name) if length(dims) == 0 value = m[comp_name, item_name] @@ -145,7 +150,7 @@ end function _menu_item(sim_inst::SimulationInstance, datum_key::Tuple{Symbol, Symbol}) (comp_name, item_name) = datum_key - dims = dimensions(sim_inst.models[1], comp_name, item_name) + dims = dim_names(sim_inst.models[1], comp_name, item_name) if length(dims) > 2 @warn("$comp_name.$item_name has >2 graphing dims, not yet implemented in explorer") return nothing @@ -1017,12 +1022,6 @@ function getdatapart_2d(cols, dffields, numrows, datasb) return String(datasb) end -# Other helper functions -function dataframe_or_scalar(m::Model, comp_name::Symbol, item_name::Symbol) - dims = dimensions(m, comp_name, item_name) - return length(dims) > 0 ? getdataframe(m, comp_name, item_name) : m[comp_name, item_name] -end - function trumpet_df_reduce(df, plottype::Symbol) if plottype == :trumpet diff --git a/src/mcs/mcs.jl b/src/mcs/mcs.jl index 8aa7bd014..92be7f4de 100644 --- a/src/mcs/mcs.jl +++ b/src/mcs/mcs.jl @@ -15,7 +15,8 @@ include("defmcs.jl") export @defsim, generate_trials!, run, save_trial_inputs, _save_trial_results, set_models!, - EmpiricalDistribution, RandomVariable, TransformSpec, CorrelationSpec, SimulationDef, SimulationInstance, AbstractSimulationData, + EmpiricalDistribution, ReshapedDistribution, RandomVariable, TransformSpec, CorrelationSpec, + SimulationDef, SimulationInstance, AbstractSimulationData, LHSData, LatinHypercubeSimulationDef, MCSData, MonteCarloSimulationDef, SobolData, SobolSimulationDef, INNER, OUTER, sample!, analyze, MonteCarloSimulationInstance, LatinHypercubeSimulationInstance, SobolSimulationInstance diff --git a/src/mcs/mcs_types.jl b/src/mcs/mcs_types.jl index 7a6033b7e..ec655b163 100644 --- a/src/mcs/mcs_types.jl +++ b/src/mcs/mcs_types.jl @@ -159,7 +159,7 @@ mutable struct SimulationInstance{T} current_trial::Int current_data::Any # holds data for current_trial when current_trial > 0 sim_def::SimulationDef{T} where T <: AbstractSimulationData - models::Vector{Union{Model, MarginalModel}} + models::Vector{M} where M <: AbstractModel results::Vector{Dict{Tuple, DataFrame}} payload::Any @@ -172,7 +172,7 @@ mutable struct SimulationInstance{T} self.payload = deepcopy(self.sim_def.payload) # These are parallel arrays; each model has a corresponding results dict - self.models = Vector{Union{Model, MarginalModel}}(undef, 0) + self.models = Vector{AbstractModel}(undef, 0) self.results = [Dict{Tuple, DataFrame}()] return self diff --git a/src/mcs/montecarlo.jl b/src/mcs/montecarlo.jl index 06dc028a7..703cb159f 100644 --- a/src/mcs/montecarlo.jl +++ b/src/mcs/montecarlo.jl @@ -50,11 +50,11 @@ end # Store results for a single parameter and return the dataframe for this particular # trial/scenario -function _store_param_results(m::Union{Model, MarginalModel}, datum_key::Tuple{Symbol, Symbol}, trialnum::Int, scen_name::Union{Nothing, String}, results::Dict{Tuple, DataFrame}) +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 - dims = dimensions(m, comp_name, datum_name) + dims = dim_names(m, comp_name, datum_name) has_scen = ! (scen_name === nothing) if length(dims) == 0 # scalar value @@ -240,7 +240,7 @@ function _copy_sim_params(sim_inst::SimulationInstance{T}) where T <: AbstractSi param_vec = Vector{Dict{Symbol, ModelParameter}}(undef, length(flat_model_list)) for (i, m) in enumerate(flat_model_list) - md = m.mi.md + 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) end @@ -284,7 +284,7 @@ function _restore_param!(param::ArrayModelParameter{T}, name::Symbol, md::ModelD end function _param_indices(param::ArrayModelParameter{T}, md::ModelDef, trans::TransformSpec) where T - pdims = dimensions(param) # returns [] for scalar parameters + pdims = dim_names(param) # returns [] for scalar parameters num_pdims = length(pdims) tdims = trans.dims @@ -395,7 +395,7 @@ end """ run(sim_def::SimulationDef{T}, - models::Union{Vector{Model}, Vector{MarginalModel}, Vector{Union{Model, MarginalModel}}, Model, MarginalModel}, + models::Union{Vector{M <: AbstractModel}, AbstractModel}, samplesize::Int; ntimesteps::Int=typemax(Int), trials_output_filename::Union{Nothing, AbstractString}=nothing, @@ -444,7 +444,7 @@ along with mutated information about trials, in addition to the model list and results information. """ function Base.run(sim_def::SimulationDef{T}, - models::Union{Vector{Model}, Vector{MarginalModel}, Vector{Any}, Model, MarginalModel}, + models::Union{Vector{M}, AbstractModel}, samplesize::Int; ntimesteps::Int=typemax(Int), trials_output_filename::Union{Nothing, AbstractString}=nothing, @@ -454,15 +454,15 @@ function Base.run(sim_def::SimulationDef{T}, scenario_func::Union{Nothing, Function}=nothing, scenario_placement::ScenarioLoopPlacement=OUTER, scenario_args=nothing, - results_in_memory::Bool=true) where T <: AbstractSimulationData + results_in_memory::Bool=true) where {T <: AbstractSimulationData, M <: AbstractModel} # If the provided models list has both a Model and a MarginalModel, it will be a Vector{Any}, and needs to be converted if models isa Vector{Any} - models = convert(Vector{Union{Model, MarginalModel}}, models) + models = convert(Vector{AbstractModel}, models) end # Quick check for results saving - if (!results_in_memory) && (results_output_dir===nothing) + if (!results_in_memory) && (results_output_dir === nothing) error("The results_in_memory keyword arg is set to ($results_in_memory) and results_output_dir keyword arg is set to ($results_output_dir), thus results will not be saved either in memory or in a file.") @@ -479,13 +479,7 @@ function Base.run(sim_def::SimulationDef{T}, end for m in sim_inst.models - if m isa MarginalModel - if m.base.mi === nothing || m.marginal.mi === nothing - build(m) - end - elseif m.mi === nothing - build(m) - end + is_built(m) || build(m) end trials = 1:sim_inst.trials @@ -602,22 +596,22 @@ end # Set models """ - set_models!(sim_inst::SimulationInstance{T}, models::Union{Vector{Model}, Vector{MarginalModel}, Vector{Union{Model, MarginalModel}}}) + set_models!(sim_inst::SimulationInstance{T}, models::Union{Vector{M <: AbstractModel}}) Set the `models` to be used by the SimulationDef held by `sim_inst`. """ -function set_models!(sim_inst::SimulationInstance{T}, models::Union{Vector{Model}, Vector{MarginalModel}, Vector{Union{Model, MarginalModel}}}) where T <: AbstractSimulationData +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::Union{Model, MarginalModel}) +set_models!(sim_inst::SimulationInstance{T}, m::AbstractModel) Set the model `m` to be used by the Simulatoin held by `sim_inst`. """ -set_models!(sim_inst::SimulationInstance{T}, m::Union{Model, MarginalModel}) where T <: AbstractSimulationData = set_models!(sim_inst, [m]) +set_models!(sim_inst::SimulationInstance{T}, m::AbstractModel) where T <: AbstractSimulationData = set_models!(sim_inst, [m]) # diff --git a/src/utils/getdataframe.jl b/src/utils/getdataframe.jl index cf94f9676..fc2e6f743 100644 --- a/src/utils/getdataframe.jl +++ b/src/utils/getdataframe.jl @@ -1,16 +1,15 @@ using DataFrames """ - _load_dataframe(m::Union{Model, MarginalModel}, comp_name::Symbol, item_name::Symbol), df::Union{Nothing,DataFrame}=nothing) + _load_dataframe(m::AbstractModel, comp_name::Symbol, item_name::Symbol), df::Union{Nothing,DataFrame}=nothing) Load a DataFrame from the variable or parameter `item_name` in component `comp_name`. If `df` is nothing, a new DataFrame is allocated. Returns the populated DataFrame. """ -function _load_dataframe(m::Union{Model, MarginalModel}, comp_name::Symbol, item_name::Symbol, df::Union{Nothing,DataFrame}=nothing) - md = m isa MarginalModel ? m.base.md : m.md - mi = m isa MarginalModel ? m.base.mi : m.mi +function _load_dataframe(m::AbstractModel, comp_name::Symbol, item_name::Symbol, df::Union{Nothing,DataFrame}=nothing) + md, mi = m isa MarginalModel ? (m.base.md, m.base.mi) : (m.md, m.mi) - dims = dimensions(m, comp_name, item_name) + dims = dim_names(m, comp_name, item_name) # Create a new df if one was not passed in df = df === nothing ? DataFrame() : df @@ -55,9 +54,8 @@ function _load_dataframe(m::Union{Model, MarginalModel}, comp_name::Symbol, item return df end -function _df_helper(m::Union{Model, MarginalModel}, comp_name::Symbol, item_name::Symbol, dims::Vector{Symbol}, data::AbstractArray) - md = m isa MarginalModel ? m.base.md : m.md - mi = m isa MarginalModel ? m.base.mi : m.mi +function _df_helper(m::AbstractModel, comp_name::Symbol, item_name::Symbol, dims::Vector{Symbol}, data::AbstractArray) + md, mi = m isa MarginalModel ? (m.base.md, m.base.mi) : (m.md, m.mi) num_dims = length(dims) dim1name = dims[1] @@ -76,7 +74,7 @@ function _df_helper(m::Union{Model, MarginalModel}, comp_name::Symbol, item_name df[!, dim1name] = repeat(keys1, inner = [len_dim2]) df[!, dim2name] = repeat(keys2, outer = [len_dim1]) - if dim1name == :time && size(data)[1] != len_dim1 #length(time_labels(md)) + if dim1name == :time && size(data)[1] != len_dim1 ci = compinstance(mi, comp_name) t = dimension(m, :time) first = t[ci.first] @@ -91,7 +89,7 @@ function _df_helper(m::Union{Model, MarginalModel}, comp_name::Symbol, item_name else # shift the data to be padded with missings if this data is shorter than the model - if dim1name == :time && size(data)[1] != len_dim1 #length(time_labels(md)) + if dim1name == :time && size(data)[1] != len_dim1 ci = compinstance(mi, comp_name) t = dimension(m, :time) first = t[ci.first] @@ -130,20 +128,20 @@ end """ - getdataframe(m::Union{Model, MarginalModel}, comp_name::Symbol, pairs::Pair{Symbol, Symbol}...) + getdataframe(m::AbstractModel, comp_name::Symbol, pairs::Pair{Symbol, Symbol}...) Return a DataFrame with values for the given variables or parameters of model `m` indicated by `pairs`, where each pair is of the form `comp_name => item_name`. If more than one pair is provided, all must refer to items with the same dimensions, which are used to join the respective item values. """ -function getdataframe(m::Union{Model, MarginalModel}, pairs::Pair{Symbol, Symbol}...) +function getdataframe(m::AbstractModel, pairs::Pair{Symbol, Symbol}...) (comp_name1, item_name1) = pairs[1] - dims = dimensions(m, comp_name1, item_name1) + dims = dim_names(m, comp_name1, item_name1) df = getdataframe(m, comp_name1, item_name1) for (comp_name, item_name) in pairs[2:end] - next_dims = dimensions(m, comp_name, item_name) + next_dims = dim_names(m, comp_name, item_name) if dims != next_dims error("Can't create DataFrame from items with different dimensions ($comp_name1.$item_name1: $dims vs $comp_name.$item_name: $next_dims)") end @@ -155,27 +153,27 @@ function getdataframe(m::Union{Model, MarginalModel}, pairs::Pair{Symbol, Symbol end """ - getdataframe(m::Union{Model, MarginalModel}, pair::Pair{Symbol, NTuple{N, Symbol}}) + getdataframe(m::AbstractModel, pair::Pair{Symbol, NTuple{N, Symbol}}) Return a DataFrame with values for the given variables or parameters indicated by `pairs`, where each pair is of the form `comp_name => item_name`. If more than one pair is provided, all must refer to items with the same dimensions, which are used to join the respective item values. """ -function getdataframe(m::Union{Model, MarginalModel}, pair::Pair{Symbol, NTuple{N, Symbol}}) where N +function getdataframe(m::AbstractModel, pair::Pair{Symbol, NTuple{N, Symbol}}) where N comp_name = pair.first expanded = [comp_name => param_name for param_name in pair.second] return getdataframe(m, expanded...) end """ - getdataframe(m::Union{Model, MarginalModel}, comp_name::Symbol, item_name::Symbol) + getdataframe(m::AbstractModel, comp_name::Symbol, item_name::Symbol) Return the values for variable or parameter `item_name` in `comp_name` of model `m` as a DataFrame. """ -function getdataframe(m::Union{Model, MarginalModel}, comp_name::Symbol, item_name::Symbol) - if (m isa MarginalModel && (m.base.mi === nothing || m.marginal.mi === nothing)) || (m isa Model && m.mi === nothing) +function getdataframe(m::AbstractModel, comp_name::Symbol, item_name::Symbol) + if ! is_built(m) error("Cannot get DataFrame: model has not been built yet.") end diff --git a/src/utils/graph.jl b/src/utils/graph.jl index d8ec463b0..90b3904d1 100644 --- a/src/utils/graph.jl +++ b/src/utils/graph.jl @@ -2,7 +2,6 @@ # Graph Functionality # - function _show_conns(io, m, comp_name, which::Symbol) datumtype = which == :incoming ? "parameters" : "variables" println(io, " $which $datumtype:") @@ -14,36 +13,34 @@ function _show_conns(io, m, comp_name, which::Symbol) else for conn in conns if which == :incoming - println(io, " - $(conn.src_comp_name).$(conn.dst_par_name)") + println(io, " - $(conn.src_comp_path).$(conn.dst_par_name)") else - println(io, " - $(conn.dst_comp_name).$(conn.src_var_name)") + println(io, " - $(conn.dst_comp_path).$(conn.src_var_name)") end end end end -function show(io::IO, m::Model) +show_conns(m::Model) = show_conns(stdout, m) + +function show_conns(io::IO, m::Model) println(io, "Model component connections:") for (i, comp_name) in enumerate(compkeys(m.md)) comp_def = compdef(m.md, comp_name) - println(io, "$i. $(comp_def.comp_id) as :$(comp_def.name)") + println(io, "$i. $(comp_def.comp_id) as :$(nameof(comp_def))") _show_conns(io, m, comp_name, :incoming) _show_conns(io, m, comp_name, :outgoing) end end -function get_connections(m::Model, ci::ComponentInstance, which::Symbol) - return get_connections(m, name(ci.comp), which) -end - -function _filter_connections(conns::Vector{InternalParameterConnection}, comp_name::Symbol, which::Symbol) +function _filter_connections(conns::Vector{InternalParameterConnection}, comp_path::ComponentPath, which::Symbol) if which == :all - f = obj -> (obj.src_comp_name == comp_name || obj.dst_comp_name == comp_name) + f = obj -> (obj.src_comp_path == comp_path || obj.dst_comp_path == comp_path) elseif which == :incoming - f = obj -> obj.dst_comp_name == comp_name + f = obj -> obj.dst_comp_path == comp_path elseif which == :outgoing - f = obj -> obj.src_comp_name == comp_name + f = obj -> obj.src_comp_path == comp_path else error("Invalid parameter for the 'which' argument; must be 'all' or 'incoming' or 'outgoing'.") end @@ -51,10 +48,27 @@ function _filter_connections(conns::Vector{InternalParameterConnection}, comp_na return collect(Iterators.filter(f, conns)) end +get_connections(m::Model, ci::LeafComponentInstance, which::Symbol) = get_connections(m, pathof(ci), which) + +function get_connections(m::Model, cci::CompositeComponentInstance, which::Symbol) + conns = [] + for ci in components(cci) + append!(conns, get_connections(m, pathof(ci), which)) + end + return conns +end + +function get_connections(m::Model, comp_path::ComponentPath, which::Symbol) + md = modeldef(m) + return _filter_connections(internal_param_conns(md), comp_path, which) +end + function get_connections(m::Model, comp_name::Symbol, which::Symbol) - return _filter_connections(internal_param_conns(m.md), comp_name, which) + comp = compdef(m, comp_name) + get_connections(m, comp.comp_path, which) end -function get_connections(mi::ModelInstance, comp_name::Symbol, which::Symbol) - return _filter_connections(internal_param_conns(mi.md), comp_name, which) +function get_connections(mi::ModelInstance, comp_path::ComponentPath, which::Symbol) + md = modeldef(mi) + return _filter_connections(internal_param_conns(md), comp_path, which) end diff --git a/src/utils/misc.jl b/src/utils/misc.jl index 2c1207693..1be1d2f6e 100644 --- a/src/utils/misc.jl +++ b/src/utils/misc.jl @@ -34,17 +34,13 @@ function interpolate(oldvalues::Vector{T}, ts::Int=10) where T <: Union{Float64, return newvalues end -# MacroTools has a "prettify", so we have to import to "extend" -# even though our function is unrelated. This seems unfortunate. -import MacroTools.prettify - """ - MacroTools.prettify(s::String) + pretty_string(s::String) Accepts a camelcase or snakecase string, and makes it human-readable e.g. camelCase -> Camel Case; snake_case -> Snake Case """ -function MacroTools.prettify(s::String) +function pretty_string(s::String) s = replace(s, r"_" => s" ") s = replace(s, r"([a-z])([A-Z])" => s"\1 \2") s = replace(s, r"([A-Z]+)([A-Z])" => s"\1 \2") # handle case of consecutive caps by splitting last from rest @@ -60,4 +56,21 @@ function MacroTools.prettify(s::String) return join(s_arr, " ") end -prettify(s::Symbol) = prettify(string(s)) +pretty_string(s::Symbol) = pretty_string(string(s)) + +""" + load_comps(dirname::String="./components") + +Call include() on all the files in the indicated directory `dirname`. +This avoids having modelers create a long list of include() +statements. Just put all the components in a directory. +""" +function load_comps(dirname::String="./components") + files = readdir(dirname) + for file in files + if endswith(file, ".jl") + pathname = joinpath(dirname, file) + include(pathname) + end + end +end \ No newline at end of file diff --git a/src/utils/plotting.jl b/src/utils/plotting.jl index 3f182dd21..29f764a71 100644 --- a/src/utils/plotting.jl +++ b/src/utils/plotting.jl @@ -23,7 +23,8 @@ no `filename` is given, plot will simply display. function plot_comp_graph(m::Model, filename::Union{Nothing, String} = nothing) graph = comp_graph(m.md) - names = map(i -> get_prop(graph, i, :name), vertices(graph)) + paths = map(i -> get_prop(graph, i, :path), vertices(graph)) + names = map(path -> path.names[end], paths) plot = gplot(graph, nodelabel=names, nodesize=6, nodelabelsize=6) if filename !== nothing diff --git a/test/dependencies/run_dependency_tests.jl b/test/dependencies/run_dependency_tests.jl index 5833e46e1..79c6e2877 100644 --- a/test/dependencies/run_dependency_tests.jl +++ b/test/dependencies/run_dependency_tests.jl @@ -2,7 +2,7 @@ using Pkg packages_to_test = [ ("https://github.com/anthofflab/MimiRICE2010.jl.git", "v2.0.3", "MimiRICE2010"), - ("https://github.com/fund-model/MimiFUND.jl.git", "v3.11.5", "MimiFUND") + ("https://github.com/fund-model/MimiFUND.jl.git", "v3.11.8", "MimiFUND") ] for (pkg_url, pkg_rev, pkg_name) in packages_to_test diff --git a/test/mcs/test_defmcs.jl b/test/mcs/test_defmcs.jl index 78d6444ef..fdfed77e2 100644 --- a/test/mcs/test_defmcs.jl +++ b/test/mcs/test_defmcs.jl @@ -8,8 +8,7 @@ using CSVFiles using Test -using Mimi: reset_compdefs, modelinstance, compinstance, - get_var_value, OUTER, INNER, ReshapedDistribution +using Mimi: modelinstance, compinstance, get_var_value, OUTER, INNER, ReshapedDistribution using CSVFiles: load diff --git a/test/mcs/test_defmcs_sobol.jl b/test/mcs/test_defmcs_sobol.jl index 4232ed93e..a76355caf 100644 --- a/test/mcs/test_defmcs_sobol.jl +++ b/test/mcs/test_defmcs_sobol.jl @@ -40,7 +40,6 @@ sd = @defsim begin save(grosseconomy.K, grosseconomy.YGROSS, emissions.E, emissions.E_Global) end -Mimi.reset_compdefs() include("../../examples/tutorial/02-two-region-model/main.jl") m = model diff --git a/test/mcs/test_reshaping.jl b/test/mcs/test_reshaping.jl index d52f60643..5b1371fd4 100644 --- a/test/mcs/test_reshaping.jl +++ b/test/mcs/test_reshaping.jl @@ -7,10 +7,8 @@ using DelimitedFiles using Test -using Mimi: reset_compdefs, modelinstance, compinstance, - get_var_value, OUTER, INNER, ReshapedDistribution +using Mimi: modelinstance, compinstance, get_var_value, OUTER, INNER, ReshapedDistribution -reset_compdefs() include("test-model/test-model.jl") using .TestModel m = create_model() diff --git a/test/runtests.jl b/test/runtests.jl index 69a45a7ee..de784fb95 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,14 +1,14 @@ using Mimi using Test -# reduce the chatter during testing -Mimi.set_defcomp_verbosity(false) - @testset "Mimi" begin @info("test_main.jl") include("test_main.jl") + @info("test_composite.jl") + include("test_composite.jl") + @info("test_main_variabletimestep.jl") include("test_main_variabletimestep.jl") diff --git a/test/test_adder.jl b/test/test_adder.jl index 03c80cdc3..1744e5977 100644 --- a/test/test_adder.jl +++ b/test/test_adder.jl @@ -3,11 +3,6 @@ module TestAdder using Mimi using Test -import Mimi: - reset_compdefs - -reset_compdefs() - ############################################ # adder component without a different name # ############################################ diff --git a/test/test_clock.jl b/test/test_clock.jl index dc0092c73..bfe7b2aff 100644 --- a/test/test_clock.jl +++ b/test/test_clock.jl @@ -5,9 +5,7 @@ using Test import Mimi: AbstractTimestep, FixedTimestep, VariableTimestep, Clock, timestep, time_index, - advance, reset_compdefs - -reset_compdefs() + advance t_f = FixedTimestep{1850, 10, 3000}(1) c_f = Clock{FixedTimestep}(1850, 10, 3000) diff --git a/test/test_components.jl b/test/test_components.jl index fae7dff8d..bad57e1d5 100644 --- a/test/test_components.jl +++ b/test/test_components.jl @@ -4,24 +4,21 @@ using Mimi using Test import Mimi: - reset_compdefs, compdefs, compdef, compkeys, hascomp, first_period, - last_period, compmodule, compname, numcomponents, - dim_keys, dim_values, dimensions - -reset_compdefs() + compdefs, compdef, compkeys, has_comp, first_period, + last_period, compmodule, compname, compinstance, dim_keys, dim_values my_model = Model() # Try running model with no components @test length(compdefs(my_model)) == 0 -@test numcomponents(my_model) == 0 +@test length(my_model) == 0 @test_throws ErrorException run(my_model) # Now add several components to the module @defcomp testcomp1 begin var1 = Variable(index=[time]) par1 = Parameter(index=[time]) - + """ Test docstring. """ @@ -33,7 +30,7 @@ end @defcomp testcomp2 begin var1 = Variable(index=[time]) par1 = Parameter(index=[time]) - + function run_timestep(p, v, d, t) v.var1[t] = p.par1[t] end @@ -43,15 +40,12 @@ end var1 = Variable(index=[time]) par1 = Parameter(index=[time]) cbox = Variable(index=[time, 5]) # anonymous dimension - + function run_timestep(p, v, d, t) v.var1[t] = p.par1[t] end end -# Can't add component before setting time dimension -@test_throws ErrorException add_comp!(my_model, testcomp1) - # Start building up the model set_dimension!(my_model, :time, 2015:5:2110) add_comp!(my_model, testcomp1) @@ -82,16 +76,18 @@ comps = collect(compdefs(my_model)) # Test compdefs, compdef, compkeys, etc. @test comps == collect(compdefs(my_model.md)) @test length(comps) == 3 -@test testcomp3 == comps[3] +@test compdef(my_model, :testcomp3).comp_id == comps[3].comp_id +@test_throws KeyError compdef(my_model, :testcomp4) #this component does not exist @test [compkeys(my_model.md)...] == [:testcomp1, :testcomp2, :testcomp3] -@test hascomp(my_model.md, :testcomp1) == true && hascomp(my_model.md, :testcomp4) == false +@test has_comp(my_model.md, :testcomp1) == true +@test has_comp(my_model.md, :testcomp4) == false @test compmodule(testcomp3) == Main.TestComponents @test compname(testcomp3) == :testcomp3 -@test numcomponents(my_model) == 3 +@test length(my_model) == 3 add_comp!(my_model, testcomp3, :testcomp3_v2) -@test numcomponents(my_model) == 4 +@test length(my_model) == 4 #------------------------------------------------------------------------------ @@ -101,39 +97,43 @@ add_comp!(my_model, testcomp3, :testcomp3_v2) @defcomp testcomp1 begin var1 = Variable(index=[time]) par1 = Parameter(index=[time]) - + function run_timestep(p, v, d, t) v.var1[t] = p.par1[t] end end -# 1. Test resetting the time dimension without explicit first/last values +# 1. Test resetting the time dimension without explicit first/last values -cd = testcomp1 +cd = testcomp1 @test cd.first === nothing # original component definition's first and last values are unset @test cd.last === nothing m = Model() set_dimension!(m, :time, 2001:2005) add_comp!(m, testcomp1, :C) # Don't set the first and last values here -cd = m.md.comp_defs[:C] # Get the component definition in the model -@test cd.first === nothing # First and last values should still be nothing because they were not explicitly set -@test cd.last === nothing +cd = compdef(m.md, :C) # Get the component definition in the model + +# These tests are not valid in the composite world... +#@test cd.first === nothing # First and last values should still be nothing because they were not explicitly set +#@test cd.last === nothing set_param!(m, :C, :par1, zeros(5)) Mimi.build(m) # Build the model -ci = m.mi.components[:C] # Get the component instance +ci = compinstance(m, :C) # Get the component instance @test ci.first == 2001 # The component instance's first and last values should match the model's index @test ci.last == 2005 set_dimension!(m, :time, 2005:2020) # Reset the time dimension -cd = m.md.comp_defs[:C] # Get the component definition in the model -@test cd.first === nothing # First and last values should still be nothing -@test cd.last === nothing +cd = compdef(m.md, :C) # Get the component definition in the model + +# These tests are not valid in the composite world... +#@test cd.first === nothing # First and last values should still be nothing +#@test cd.last === nothing update_param!(m, :par1, zeros(16); update_timesteps=true) Mimi.build(m) # Build the model -ci = m.mi.components[:C] # Get the component instance +ci = compinstance(m, :C) # Get the component instance @test ci.first == 2005 # The component instance's first and last values should match the model's index @test ci.last == 2020 @@ -142,26 +142,38 @@ ci = m.mi.components[:C] # Get the component instance m = Model() set_dimension!(m, :time, 2000:2100) -add_comp!(m, testcomp1, :C; first=2010, last=2090) # Give explicit first and last values for the component -cd = m.md.comp_defs[:C] # Get the component definition in the model -@test cd.first == 2010 # First and last values are defined in the comp def because they were explicitly given -@test cd.last == 2090 + +@test_logs( + (:warn, "add_comp!: Keyword arguments 'first' and 'last' are currently disabled."), + add_comp!(m, testcomp1, :C; first=2010, last=2090) # Give explicit first and last values for the component +) + +cd = compdef(m.md, :C) # Get the component definition in the model + +# first and last are disabled currently +# @test cd.first == 2010 # First and last values are defined in the comp def because they were explicitly given +# @test cd.last == 2090 + +# Verify that they didn't change +#@test cd.first === nothing +#@test cd.last === nothing + +set_dimension!(m, :time, 2010:2090) set_param!(m, :C, :par1, zeros(81)) Mimi.build(m) # Build the model -ci = m.mi.components[:C] # Get the component instance +ci = compinstance(m, :C) # Get the component instance @test ci.first == 2010 # The component instance's first and last values are the same as in the comp def @test ci.last == 2090 set_dimension!(m, :time, 2000:2200) # Reset the time dimension -cd = m.md.comp_defs[:C] # Get the component definition in the model -@test cd.first == 2010 # First and last values should still be the same -@test cd.last == 2090 +cd = compdef(m.md, :C) # Get the component definition in the model +# @test cd.first == 2010 # First and last values should still be the same +# @test cd.last == 2090 Mimi.build(m) # Build the model -ci = m.mi.components[:C] # Get the component instance -@test ci.first == 2010 # The component instance's first and last values are the same as the comp def -@test ci.last == 2090 - +ci = compinstance(m, :C) # Get the component instance +# @test ci.first == 2010 # The component instance's first and last values are the same as the comp def +# @test ci.last == 2090 end #module diff --git a/test/test_components_ordering.jl b/test/test_components_ordering.jl index a2e7f4888..95e055f6a 100644 --- a/test/test_components_ordering.jl +++ b/test/test_components_ordering.jl @@ -1,8 +1,6 @@ using Mimi using Test -reset_compdefs() - my_model = Model() #Testing that you cannot add two components of the same name diff --git a/test/test_composite.jl b/test/test_composite.jl new file mode 100644 index 000000000..575564702 --- /dev/null +++ b/test/test_composite.jl @@ -0,0 +1,166 @@ +module TestComposite + +using Test +using Mimi + +import Mimi: + ComponentId, ComponentPath, DatumReference, ComponentDef, AbstractComponentDef, CompositeComponentDef, + Binding, ModelDef, build, time_labels, compdef, find_comp + + +@defcomp Comp1 begin + par_1_1 = Parameter(index=[time]) # external input + var_1_1 = Variable(index=[time]) # computed + foo = Parameter() + + function run_timestep(p, v, d, t) + v.var_1_1[t] = p.par_1_1[t] + end +end + +@defcomp Comp2 begin + par_2_1 = Parameter(index=[time]) # connected to Comp1.var_1_1 + par_2_2 = Parameter(index=[time]) # external input + var_2_1 = Variable(index=[time]) # computed + foo = Parameter() + + function run_timestep(p, v, d, t) + v.var_2_1[t] = p.par_2_1[t] + p.foo * p.par_2_2[t] + end +end + +@defcomp Comp3 begin + par_3_1 = Parameter(index=[time]) # connected to Comp2.var_2_1 + var_3_1 = Variable(index=[time]) # external output + foo = Parameter(default=30) + + function run_timestep(p, v, d, t) + # @info "Comp3 run_timestep" + v.var_3_1[t] = p.par_3_1[t] * 2 + end +end + +@defcomp Comp4 begin + par_4_1 = Parameter(index=[time]) # connected to Comp2.var_2_1 + var_4_1 = Variable(index=[time]) # external output + foo = Parameter(default=300) + + function run_timestep(p, v, d, t) + # @info "Comp4 run_timestep" + v.var_4_1[t] = p.par_4_1[t] * 2 + end +end + +m = Model() +set_dimension!(m, :time, 2005:2020) + +@defcomposite A begin + component(Comp1) + component(Comp2) + + foo1 = Comp1.foo + foo2 = Comp2.foo + + # Should accomplish the same as calling + # connect_param!(m, "/top/A/Comp2:par_2_1", "/top/A/Comp1:var_1_1") + # after the `@defcomposite top ...` + Comp2.par_2_1 = Comp1.var_1_1 + Comp2.par_2_2 = Comp1.var_1_1 +end + +@defcomposite B begin + component(Comp3) # bindings=[foo => bar, baz => [1 2 3; 4 5 6]]) + component(Comp4) + + foo3 = Comp3.foo + foo4 = Comp4.foo +end + +@defcomposite top begin + component(A) + + fooA1 = A.foo1 + fooA2 = A.foo2 + + # TBD: component B isn't getting added to mi + component(B) + foo3 = B.foo3 + foo4 = B.foo4 +end + +# We have created the following composite structure: +# +# top +# / \ +# A B +# / \ / \ +# 1 2 3 4 + +top_ref = add_comp!(m, top, nameof(top)) +top_comp = compdef(top_ref) + +md = m.md + +@test find_comp(md, :top) == top_comp + +# +# Test various ways to access sub-components +# +c1 = find_comp(md, ComponentPath(:top, :A, :Comp1)) +@test c1.comp_id == Comp1.comp_id + +c2 = md[:top][:A][:Comp2] +@test c2.comp_id == Comp2.comp_id + +c3 = find_comp(md, "/top/B/Comp3") +@test c3.comp_id == Comp3.comp_id + +set_param!(m, "/top/A/Comp1:foo", 1) +set_param!(m, "/top/A/Comp2:foo", 2) + +# 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, "/top/B/Comp3:foo", 10) +set_param!(m, "/top/B/Comp4:foo", 20) + +set_param!(m, "/top/A/Comp1", :par_1_1, collect(1:length(time_labels(md)))) + +# connect_param!(m, "/top/A/Comp2:par_2_1", "/top/A/Comp1:var_1_1") +# connect_param!(m, "/top/A/Comp2:par_2_2", "/top/A/Comp1:var_1_1") +connect_param!(m, "/top/B/Comp3:par_3_1", "/top/A/Comp2:var_2_1") +connect_param!(m, "/top/B/Comp4:par_4_1", "/top/B/Comp3:var_3_1") + +build(m) + +run(m) + +# +# TBD +# +# 1. Create parallel structure of exported vars/pars in Instance hierarchy? +# - Perhaps just a dict mapping local name to a component path under mi, to where the var actually exists +# 2. Be able to connect to the leaf version of vars/pars or by specifying exported version below the compdef +# given as first arg to connect_param!(). +# 3. set_param!() should work with relative path from any compdef. +# 4. set_param!() stores external_parameters in the parent object, creating namespace conflicts between comps. +# Either store these in the leaf or store them with a key (comp_name, param_name) + +mi = m.mi + +@test mi[:top][:A][:Comp2, :par_2_2] == collect(1.0:16.0) +@test mi["/top/A/Comp2", :par_2_2] == collect(1.0:16.0) + +@test mi["/top/A/Comp2", :var_2_1] == collect(3.0:3:48.0) +@test mi["/top/A/Comp1", :var_1_1] == collect(1.0:16.0) +@test mi["/top/B/Comp4", :par_4_1] == collect(6.0:6:96.0) + +end # module + +m = TestComposite.m +md = m.md +top = Mimi.find_comp(md, :top) +A = Mimi.find_comp(top, :A) +comp1 = Mimi.find_comp(A, :Comp1) + +nothing diff --git a/test/test_connectorcomp.jl b/test/test_connectorcomp.jl index aab7e557f..ba16595a7 100644 --- a/test/test_connectorcomp.jl +++ b/test/test_connectorcomp.jl @@ -3,27 +3,27 @@ module TestConnectorComp using Mimi using Test -import Mimi: - reset_compdefs, compdef - -reset_compdefs() +import Mimi: compdef, compdefs @defcomp Long begin x = Parameter(index=[time]) end +late_start = 2005 + @defcomp Short begin a = Parameter() b = Variable(index=[time]) function run_timestep(p, v, d, t) - v.b[t] = p.a * t.t + if gettime(t) >= late_start + v.b[t] = p.a * t.t + end end end years = 2000:2010 -late_start = 2005 -dim = Mimi.Dimension(years) +year_dim = Mimi.Dimension(years) #------------------------------------------------------------------------------ @@ -33,7 +33,7 @@ dim = Mimi.Dimension(years) model1 = Model() set_dimension!(model1, :time, years) -add_comp!(model1, Short; first=late_start) +add_comp!(model1, Short) #; first=late_start) add_comp!(model1, Long) set_param!(model1, :Short, :a, 2.) connect_param!(model1, :Long, :x, :Short, :b, zeros(length(years))) @@ -41,7 +41,7 @@ connect_param!(model1, :Long, :x, :Short, :b, zeros(length(years))) run(model1) @test length(components(model1.mi)) == 3 # ConnectorComp is added to the list of components in the model isntance -@test length(model1.md.comp_defs) == 2 # The ConnectorComp shows up in the model instance but not the model definition +@test length(compdefs(model1.md)) == 2 # The ConnectorComp shows up in the model instance but not the model definition b = model1[:Short, :b] x = model1[:Long, :x] @@ -50,13 +50,14 @@ x = model1[:Long, :x] @test length(b) == length(years) @test length(x) == length(years) -@test all(ismissing, b[1:dim[late_start]-1]) -@test all(iszero, x[1:dim[late_start]-1]) +@test all(ismissing, b[1:year_dim[late_start]-1]) + +#@test all(iszero, x[1:year_dim[late_start]-1]) +@test all(ismissing, x[1:year_dim[late_start]-1]) # Test the values are right after the late start -@test b[dim[late_start]:end] == - x[dim[late_start]:end] == - [2 * i for i in 1:(years[end]-late_start + 1)] +@test b[year_dim[late_start]:end] == x[year_dim[late_start]:end] +@test b[year_dim[late_start]:end] == collect(year_dim[late_start]:year_dim[years[end]]) * 2.0 @test Mimi.datum_size(model1.md, Mimi.compdef(model1.md, :Long), :x) == (length(years),) @@ -75,7 +76,7 @@ early_last = 2010 model2 = Model() set_dimension!(model2, :time, years_variable) -add_comp!(model2, Short; last=early_last) +add_comp!(model2, Short) #; last=early_last) add_comp!(model2, Long) set_param!(model2, :Short, :a, 2.) connect_param!(model2, :Long, :x, :Short, :b, zeros(length(years_variable))) @@ -83,7 +84,7 @@ connect_param!(model2, :Long, :x, :Short, :b, zeros(length(years_variable))) run(model2) @test length(components(model2.mi)) == 3 -@test length(model2.md.comp_defs) == 2 # The ConnectorComp shows up in the model instance but not the model definition +@test length(compdefs(model2.md)) == 2 # The ConnectorComp shows up in the model instance but not the model definition b = model2[:Short, :b] x = model2[:Long, :x] @@ -92,13 +93,16 @@ x = model2[:Long, :x] @test length(b) == length(years_variable) @test length(x) == length(years_variable) -@test all(ismissing, b[dim_variable[early_last]+1 : end]) -@test all(iszero, x[dim_variable[early_last]+1 : end]) +# +# These are no longer correct since add_comp! ignores first and last keywords +# +# @test all(ismissing, b[dim_variable[early_last]+1 : end]) +# @test all(iszero, x[dim_variable[early_last]+1 : end]) -# Test the values are right after the late start -@test b[1 : dim_variable[early_last]] == - x[1 : dim_variable[early_last]] == - [2 * i for i in 1:dim_variable[early_last]] +# # Test the values are right after the late start +# @test b[1 : dim_variable[early_last]] == +# x[1 : dim_variable[early_last]] == +# [2 * i for i in 1:dim_variable[early_last]] #------------------------------------------------------------------------------ @@ -129,7 +133,7 @@ regions = [:A, :B] model3 = Model() set_dimension!(model3, :time, years) set_dimension!(model3, :regions, regions) -add_comp!(model3, Short_multi; first=late_start) +add_comp!(model3, Short_multi) #; first=late_start) add_comp!(model3, Long_multi) set_param!(model3, :Short_multi, :a, [1,2]) connect_param!(model3, :Long_multi, :x, :Short_multi, :b, zeros(length(years), length(regions))) @@ -137,7 +141,7 @@ connect_param!(model3, :Long_multi, :x, :Short_multi, :b, zeros(length(years), l run(model3) @test length(components(model3.mi)) == 3 -@test length(model3.md.comp_defs) == 2 # The ConnectorComp shows up in the model instance but not the model definition +@test length(compdefs(model3.md)) == 2 # The ConnectorComp shows up in the model instance but not the model definition b = model3[:Short_multi, :b] x = model3[:Long_multi, :x] @@ -146,14 +150,18 @@ x = model3[:Long_multi, :x] @test size(b) == (length(years), length(regions)) @test size(x) == (length(years), length(regions)) -@test all(ismissing, b[1:dim[late_start]-1, :]) -@test all(iszero, x[1:dim[late_start]-1, :]) +# +# No longer correct without first/last keywords +# +# @test all(ismissing, b[1:year_dim[late_start]-1, :]) +# @test all(iszero, x[1:year_dim[late_start]-1, :]) -# Test the values are right after the late start -@test b[dim[late_start]:end, :] == - x[dim[late_start]:end, :] == - [[i + 1 for i in 1:(years[end]-late_start + 1)] [i + 2 for i in 1:(years[end]-late_start + 1)]] +# # Test the values are right after the late start +# late_yr_idxs = year_dim[late_start]:year_dim[end] +# @test b[late_yr_idxs, :] == x[year_dim[late_start]:end, :] + +# @test b[late_yr_idxs, :] == [[i + 1 for i in late_yr_idxs] [i + 2 for i in late_yr_idxs]] #------------------------------------------------------------------------------ # 4. Test where the short component starts late and ends early @@ -164,7 +172,7 @@ first, last = 2002, 2007 model4 = Model() set_dimension!(model4, :time, years) set_dimension!(model4, :regions, regions) -add_comp!(model4, Short_multi; first=first, last=last) +add_comp!(model4, Short_multi) #; first=first, last=last) add_comp!(model4, Long_multi) set_param!(model4, :Short_multi, :a, [1,2]) @@ -173,7 +181,7 @@ connect_param!(model4, :Long_multi=>:x, :Short_multi=>:b, zeros(length(years), l run(model4) @test length(components(model4.mi)) == 3 -@test length(model4.md.comp_defs) == 2 # The ConnectorComp shows up in the model instance but not the model definition +@test length(compdefs(model4.md)) == 2 # The ConnectorComp shows up in the model instance but not the model definition b = model4[:Short_multi, :b] x = model4[:Long_multi, :x] @@ -182,16 +190,19 @@ x = model4[:Long_multi, :x] @test size(b) == (length(years), length(regions)) @test size(x) == (length(years), length(regions)) -@test all(ismissing, b[1:dim[first]-1, :]) -@test all(ismissing, b[dim[last]+1:end, :]) -@test all(iszero, x[1:dim[first]-1, :]) -@test all(iszero, x[dim[last]+1:end, :]) +# +# No longer correct without first/last keywords +# +# @test all(ismissing, b[1:year_dim[first]-1, :]) +# @test all(ismissing, b[year_dim[last]+1:end, :]) +# @test all(iszero, x[1:year_dim[first]-1, :]) +# @test all(iszero, x[year_dim[last]+1:end, :]) # Test the values are right after the late start -@test b[dim[first]:dim[last], :] == - x[dim[first]:dim[last], :] == - [[i + 1 for i in 1:(years[end]-late_start + 1)] [i + 2 for i in 1:(years[end]-late_start + 1)]] - +yr_idxs = year_dim[first]:year_dim[last] +@test b[yr_idxs, :] == x[yr_idxs, :] +#@test b[yr_idxs, :] == [[i + 1 for i in 1:(years[end]-late_start + 1)] [i + 2 for i in 1:(years[end]-late_start + 1)]] +@test b[yr_idxs, :] == [[i + 1 for i in yr_idxs] [i + 2 for i in yr_idxs]] #------------------------------------------------------------------------------ # 5. Test errors with backup data @@ -201,17 +212,17 @@ late_start_long = 2002 model5 = Model() set_dimension!(model5, :time, years) -add_comp!(model5, Short; first = late_start) -add_comp!(model5, Long; first = late_start_long) # starts later as well, so backup data needs to match this size +add_comp!(model5, Short) # ; first = late_start) +add_comp!(model5, Long) #; first = late_start_long) # starts later as well, so backup data needs to match this size set_param!(model5, :Short, :a, 2) # A. test wrong size (needs to be length of component, not length of model) -@test_throws ErrorException connect_param!(model5, :Long=>:x, :Short=>:b, zeros(length(years))) +# @test_throws ErrorException connect_param!(model5, :Long=>:x, :Short=>:b, zeros(length(years))) @test_throws ErrorException connect_param!(model4, :Long_multi=>:x, :Short_multi=>:b, zeros(length(years), length(regions)+1)) # test case with >1 dimension # B. test no backup data provided -@test_throws ErrorException connect_param!(model5, :Long=>:x, :Short=>:b) # Error because no backup data provided +# @test_throws ErrorException connect_param!(model5, :Long=>:x, :Short=>:b) # Error because no backup data provided #------------------------------------------------------------------------------ @@ -229,9 +240,9 @@ end model6 = Model() set_dimension!(model6, :time, years) -add_comp!(model6, foo, :Long) -add_comp!(model6, foo, :Short; first=late_start) -connect_param!(model6, :Short=>:par, :Long=>:var) +add_comp!(model6, foo, :Long; rename=[:var => :long_foo]) +add_comp!(model6, foo, :Short; rename=[:var => :short_foo]) #, first=late_start) +connect_param!(model6, :Short => :par, :Long => :var) set_param!(model6, :Long, :par, years) run(model6) @@ -244,8 +255,8 @@ short_var = model6[:Short, :var] @test short_par == years # The parameter has values instead of `missing` for years when this component doesn't run, # because they are coming from the longer component that did run -@test all(ismissing, short_var[1:dim[late_start]-1]) -@test short_var[dim[late_start]:end] == years[dim[late_start]:end] +# @test all(ismissing, short_var[1:year_dim[late_start]-1]) +@test short_var[year_dim[late_start]:end] == years[year_dim[late_start]:end] end #module diff --git a/test/test_datum_storage.jl b/test/test_datum_storage.jl index de7c73be8..2b6151ac3 100644 --- a/test/test_datum_storage.jl +++ b/test/test_datum_storage.jl @@ -3,10 +3,16 @@ module TestDatumStorage using Mimi using Test +comp_first = 2003 +comp_last = 2008 + @defcomp foo begin v = Variable(index = [time]) function run_timestep(p, v, d, ts) - v.v[ts] = gettime(ts) + # implement "short component" via time checking + if comp_first <= gettime(ts) <= comp_last + v.v[ts] = gettime(ts) + end end end @@ -15,6 +21,7 @@ end v = Variable(index = [time, region]) function run_timestep(p, v, d, ts) # v.v[ts, 1:end] = gettime(ts) + for d in d.region v.v[ts, d] = gettime(ts) end @@ -23,9 +30,9 @@ end years = 2001:2010 regions = [:A, :B] -comp_first = 2003 -comp_last = 2008 +nyears = length(years) +nregions = length(regions) #------------------------------------------------------------------------------ # 1. Single dimension case, fixed timesteps @@ -33,11 +40,14 @@ comp_last = 2008 m = Model() set_dimension!(m, :time, years) -add_comp!(m, foo, first=comp_first, last=comp_last) +@test_logs( + (:warn, "add_comp!: Keyword arguments 'first' and 'last' are currently disabled."), + add_comp!(m, foo, first=comp_first, last=comp_last) +) run(m) v = m[:foo, :v] -@test length(v) == length(years) # Test that the array allocated for variable v is the full length of the time dimension +@test length(v) == nyears # Test that the array allocated for variable v is the full length of the time dimension # Test that the missing values were filled in before/after the first/last values for (i, y) in enumerate(years) @@ -54,20 +64,39 @@ end m2 = Model() +@defcomp baz begin + region = Index() + v = Variable(index = [time, region]) + function run_timestep(p, v, d, ts) + # v.v[ts, 1:end] = gettime(ts) + + # implement "short component" via time checking + if comp_first <= gettime(ts) <= comp_last + for d in d.region + v.v[ts, d] = gettime(ts) + end + end + end +end + set_dimension!(m2, :time, years) set_dimension!(m2, :region, regions) -add_comp!(m2, bar, first=comp_first, last=comp_last) + +@test_logs( + (:warn, "add_comp!: Keyword arguments 'first' and 'last' are currently disabled."), + add_comp!(m2, baz, first=comp_first, last=comp_last) +) run(m2) -v2 = m2[:bar, :v] -@test size(v2) == (length(years), length(regions)) # Test that the array allocated for variable v is the full length of the time dimension +v2 = m2[:baz, :v] +@test size(v2) == (nyears, nregions) # Test that the array allocated for variable v is the full length of the time dimension # Test that the missing values were filled in before/after the first/last values for (i, y) in enumerate(years) if y < comp_first || y > comp_last - [@test ismissing(v2[i, j]) for j in 1:length(regions)] + [@test ismissing(v2[i, j]) for j in 1:nregions] else - [@test v2[i, j]==y for j in 1:length(regions)] + [@test v2[i, j]==y for j in 1:nregions] end end @@ -77,19 +106,34 @@ end #------------------------------------------------------------------------------ years_variable = [2000:2004..., 2005:5:2030...] -last = 2010 +foo2_first = 2003 +foo2_last = 2010 m = Model() set_dimension!(m, :time, years_variable) -add_comp!(m, foo, first=comp_first, last=last) + +@defcomp foo2 begin + v = Variable(index = [time]) + function run_timestep(p, v, d, ts) + # implement "short component" via time checking + if foo2_first <= gettime(ts) <= foo2_last + v.v[ts] = gettime(ts) + end + end +end + +@test_logs( + (:warn, "add_comp!: Keyword arguments 'first' and 'last' are currently disabled."), + add_comp!(m, foo2, first=foo2_first, last=foo2_last) +) run(m) -v = m[:foo, :v] +v = m[:foo2, :v] @test length(v) == length(years_variable) # Test that the array allocated for variable v is the full length of the time dimension # Test that the missing values were filled in before/after the first/last values for (i, y) in enumerate(years_variable) - if y < comp_first || y > last + if y < foo2_first || y > foo2_last @test ismissing(v[i]) else @test v[i] == y @@ -102,22 +146,38 @@ end m2 = Model() +buz_first = 2003 +buz_last = 2010 + +@defcomp buz begin + region = Index() + v = Variable(index = [time, region]) + function run_timestep(p, v, d, ts) + # v.v[ts, 1:end] = gettime(ts) + + # implement "short component" via time checking + if buz_first <= gettime(ts) <= buz_last + for d in d.region + v.v[ts, d] = gettime(ts) + end + end + end +end + set_dimension!(m2, :time, years_variable) set_dimension!(m2, :region, regions) -add_comp!(m2, bar, first=comp_first, last=last) - +add_comp!(m2, buz) run(m2) -v2 = m2[:bar, :v] -@test size(v2) == (length(years_variable), length(regions)) # Test that the array allocated for variable v is the full length of the time dimension +v2 = m2[:buz, :v] +@test size(v2) == (length(years_variable), nregions) # Test that the array allocated for variable v is the full length of the time dimension # Test that the missing values were filled in before/after the first/last values for (i, y) in enumerate(years_variable) - if y < comp_first || y > last - [@test ismissing(v2[i, j]) for j in 1:length(regions)] + if y < buz_first || y > buz_last + [@test ismissing(v2[i, j]) for j in 1:nregions] else - [@test v2[i, j]==y for j in 1:length(regions)] + [@test v2[i, j]==y for j in 1:nregions] end end - end # module \ No newline at end of file diff --git a/test/test_defcomposite.jl b/test/test_defcomposite.jl new file mode 100644 index 000000000..7d9ec9ab0 --- /dev/null +++ b/test/test_defcomposite.jl @@ -0,0 +1,79 @@ +module TestDefComposite + +using Test +using Mimi +using MacroTools + +import Mimi: ComponentPath, build, @defmodel + +@defcomp Comp1 begin + par_1_1 = Parameter(index=[time]) # external input + var_1_1 = Variable(index=[time]) # computed + foo = Parameter() + + function run_timestep(p, v, d, t) + v.var_1_1[t] = p.par_1_1[t] + end +end + +@defcomp Comp2 begin + par_2_1 = Parameter(index=[time]) # connected to Comp1.var_1_1 + par_2_2 = Parameter(index=[time]) # external input + var_2_1 = Variable(index=[time]) # computed + foo = Parameter() + + function run_timestep(p, v, d, t) + v.var_2_1[t] = p.par_2_1[t] + p.foo * p.par_2_2[t] + end +end + +@defcomposite A begin + component(Comp1) + component(Comp2) + + # imports + bar = Comp1.par_1_1 + foo2 = Comp2.foo + + # linked imports + # foo = Comp1.foo, Comp2.foo + + foo1 = Comp1.foo + foo2 = Comp2.foo + + # connections + Comp2.par_2_1 = Comp1.var_1_1 + Comp2.par_2_2 = Comp1.var_1_1 +end + + +# doesn't work currently +# @defmodel m begin +# index[time] = 2005:2020 +# component(A) + +# A.foo1 = 10 +# A.foo2 = 4 +# end + +m = Model() +years = 2005:2020 +set_dimension!(m, :time, years) +add_comp!(m, A) + +set_param!(m, "/A/Comp1", :par_1_1, 2:2:2*length(years)) + +a = m.md[:A] +set_param!(a, :Comp1, :foo, 10) +set_param!(a, :Comp2, :foo, 4) # TBD: why does this overwrite the 10 above?? + +build(m) +run(m) + +end # module + +m = TestDefComposite.m +A = TestDefComposite.A +md = m.md + +nothing diff --git a/test/test_dependencies.jl b/test/test_dependencies.jl new file mode 100644 index 000000000..182c1fd24 --- /dev/null +++ b/test/test_dependencies.jl @@ -0,0 +1,81 @@ +using Pkg +Pkg.add("InfoZIP") +Pkg.add("ExcelReaders") +Pkg.add("DataFrames") +Pkg.add("CSVFiles") +Pkg.add("CSV") +Pkg.add("StatsBase") +Pkg.add("Distributions") + +using Mimi +using InfoZIP + +function isa_url(x) + return startswith(x, "https:") +end + +#list of URLs of branches of packages to test +dependencies = [ + "https://github.com/fund-model/fund/archive/1768edf12aaaac3a41bbea081d5b51299121f993.zip", + "https://github.com/anthofflab/mimi-rice-2010.jl/archive/2b5996b0a0c8be92290991f045c43af425c5a9c8.zip" +] + +function run_dependency_tests(dependencies=dependencies) + #list of failed tests to build as you go + errors = [] + #make a temporary directory to run the tests in + tmp_path = joinpath(@__DIR__,"tmp_testing/") + mkdir(tmp_path) + + #loop through each dependent package + for d in dependencies + if isa_url(d) + zip_name = chomp(basename(d)) + zip_file_path = joinpath(tmp_path, zip_name) + download(d, zip_file_path) + InfoZIP.unzip(zip_file_path, tmp_path) + rm(zip_file_path) + #find the name of the unzipped package (this only works if the zip archive only has one directory, the package) + package_name = readdir(tmp_path)[1] + file_path = string(tmp_path, package_name) + else + package_name = basename(d) + file_path = d + end + + #first check for mimitests.jl, if not found default to runtests.jl + if "mimitests.jl" in readdir(string(file_path, "/test/")) + process = string(file_path, "/test/mimitests.jl") + else + process = string(file_path, "/test/runtests.jl") + end + + #test the package + try + run(`$(Sys.BINDIR)/julia $process`) + catch e + append!(errors, [(package_name, e)]) + end + #delete current package before testing next one (if it was a downloaded package) + if isa_url(d) + rm(joinpath(tmp_path, package_name), recursive=true) + end + end + + #remove the temporary directory + rm(tmp_path, recursive=true) + + #report the errors that occurred + num_errors = length(errors) + error_message = "Failed tests: $num_errors" + + for (package_name, error) in errors + error_message = string(error_message, "\n", "error in $package_name:", error) + end + + if num_errors > 0 + error(error_message) + else + @info "All dependency tests passed." + end +end diff --git a/test/test_dimensions.jl b/test/test_dimensions.jl index 94aca5c90..ce081c563 100644 --- a/test/test_dimensions.jl +++ b/test/test_dimensions.jl @@ -4,10 +4,8 @@ using Mimi using Test import Mimi: - AbstractDimension, RangeDimension, Dimension, key_type, - reset_compdefs - -reset_compdefs() + compdef, AbstractDimension, RangeDimension, Dimension, key_type, first_period, last_period, + ComponentReference dim_varargs = Dimension(:foo, :bar, :baz) # varargs dim_vec = Dimension([:foo, :bar, :baz]) # Vector @@ -93,29 +91,31 @@ end m = Model() set_dimension!(m, :time, 2000:2100) -@test_throws ErrorException add_comp!(m, foo2; first = 2005, last = 2105) # Can't add a component longer than a model -add_comp!(m, foo2; first = 2005, last = 2095) -# Test that foo's time dimension is unchanged +# First and last have been disabled... +#@test_throws ErrorException add_comp!(m, foo2; first = 2005, last = 2105) # Can't add a component longer than a model + @test_logs( - # (:warn, "Redefining dimension :time"), - set_dimension!(m, :time, 1990:2200) + (:warn, "add_comp!: Keyword arguments 'first' and 'last' are currently disabled."), + foo2_ref = add_comp!(m, foo2; first = 2005, last = 2095) ) -@test m.md.comp_defs[:foo2].first == 2005 -@test m.md.comp_defs[:foo2].last == 2095 + +foo2_ref = ComponentReference(m, :foo2) +my_foo2 = compdef(foo2_ref) + +# First and last have been disabled... +# Can't set time more narrowly than components are defined as +# @test_throws ErrorException set_dimension!(m, :time, 1990:2200) +# @test first_period(my_foo2) == 2005 +# @test last_period(my_foo2) == 2095 # Test parameter connections -@test_throws ErrorException set_param!(m, :foo2, :x, 1990:2200) # too long -set_param!(m, :foo2, :x, 2005:2095) # Shouldn't throw an error +# @test_throws ErrorException set_param!(m, :foo2, :x, 1990:2200) # too long +# set_param!(m, :foo2, :x, 2005:2095) # Shouldn't throw an error -# Test that foo's time dimension is updated -@test_logs( - # (:warn, "Redefining dimension :time"), - # (:warn, "Resetting foo2 component's first timestep to 2010"), - # (:warn, "Resetting foo2 component's last timestep to 2050"), - set_dimension!(m, :time, 2010:2050) -) -@test m.md.comp_defs[:foo2].first == 2010 -@test m.md.comp_defs[:foo2].last == 2050 +set_dimension!(m, :time, 2010:2050) + +@test first_period(m.md) == 2010 +@test last_period(m.md) == 2050 end #module diff --git a/test/test_explorer_model.jl b/test/test_explorer_model.jl index 3f82e3f98..1ab039305 100644 --- a/test/test_explorer_model.jl +++ b/test/test_explorer_model.jl @@ -5,10 +5,7 @@ using VegaLite using Electron import Mimi: - dataframe_or_scalar, _spec_for_item, menu_item_list, getdataframe, - reset_compdefs, dimensions - -reset_compdefs() + dataframe_or_scalar, _spec_for_item, menu_item_list, getdataframe, dimensions @defcomp MyComp begin a = Parameter(index=[time, regions]) diff --git a/test/test_explorer_sim.jl b/test/test_explorer_sim.jl index dfeed6af2..d51b31a50 100644 --- a/test/test_explorer_sim.jl +++ b/test/test_explorer_sim.jl @@ -8,7 +8,7 @@ using Query using CSVFiles import Mimi: - _spec_for_sim_item, menu_item_list, getdataframe, reset_compdefs, get_sim_results + _spec_for_sim_item, menu_item_list, getdataframe, get_sim_results # Get the example include("mcs/test-model-2/two-region-model.jl") diff --git a/test/test_getdataframe.jl b/test/test_getdataframe.jl index 4dd058b72..0e4bb0ab2 100644 --- a/test/test_getdataframe.jl +++ b/test/test_getdataframe.jl @@ -3,11 +3,6 @@ module TestGetDataframe using Mimi using Test -import Mimi: - reset_compdefs, _load_dataframe - -reset_compdefs() - #------------------------------------------------------------------------------ # 1. Test with 1 dimension #------------------------------------------------------------------------------ @@ -18,32 +13,40 @@ model1 = Model() var1 = Variable(index=[time]) par1 = Parameter(index=[time]) par_scalar = Parameter() - + function run_timestep(p, v, d, t) v.var1[t] = p.par1[t] end end +late_first = 2030 +early_last = 2100 + @defcomp testcomp2 begin var2 = Variable(index=[time]) par2 = Parameter(index=[time]) - + function run_timestep(p, v, d, t) - v.var2[t] = p.par2[t] + if late_first <= gettime(t) <= early_last # apply time constraints in the component + v.var2[t] = p.par2[t] + end end end years = collect(2015:5:2110) -late_first = 2030 -early_last = 2100 set_dimension!(model1, :time, years) add_comp!(model1, testcomp1) set_param!(model1, :testcomp1, :par1, years) set_param!(model1, :testcomp1, :par_scalar, 5.) -add_comp!(model1, testcomp2; first = late_first, last = early_last) -set_param!(model1, :testcomp2, :par2, late_first:5:early_last) +@test_logs( + (:warn, "add_comp!: Keyword arguments 'first' and 'last' are currently disabled."), + add_comp!(model1, testcomp2; first = late_first, last = early_last) +) + +@test_throws ErrorException set_param!(model1, :testcomp2, :par2, late_first:5:early_last) +set_param!(model1, :testcomp2, :par2, years) # Test running before model built @test_throws ErrorException df = getdataframe(model1, :testcomp1, :var1) @@ -59,30 +62,36 @@ run(model1) # Regular getdataframe df = getdataframe(model1, :testcomp1=>:var1, :testcomp1=>:par1, :testcomp2=>:var2, :testcomp2=>:par2) + dim = Mimi.dimension(model1, :time) @test df.var1 == df.par1 == years @test all(ismissing, df.var2[1 : dim[late_first]-1]) -@test all(ismissing, df.par2[1 : dim[late_first]-1]) +#@test all(ismissing, df.par2[1 : dim[late_first]-1]) @test df.var2[dim[late_first] : dim[early_last]] == df.par2[dim[late_first] : dim[early_last]] == late_first:5:early_last @test all(ismissing, df.var2[dim[years[end]] : dim[early_last]]) @test all(ismissing, df.par2[dim[years[end]] : dim[early_last]]) # Test trying to load an item into an existing dataframe where that item key already exists -@test_throws ErrorException _load_dataframe(model1, :testcomp1, :var1, df) +@test_throws UndefVarError _load_dataframe(model1, :testcomp1, :var1, df) #------------------------------------------------------------------------------ # 2. Test with > 2 dimensions #------------------------------------------------------------------------------ +stepsize = 5 +years = collect(2015:stepsize:2110) +regions = [:reg1, :reg2] +rates = [0.025, 0.05] + +nyears = length(years) +nregions = length(regions) +nrates = length(rates) @defcomp testcomp3 begin par3 = Parameter(index=[time, regions, rates]) var3 = Variable(index=[time]) end -regions = [:reg1, :reg2] -rates = [0.025, 0.05] - # A. Simple case where component has same time length as model model2 = Model() @@ -91,8 +100,8 @@ set_dimension!(model2, :time, years) set_dimension!(model2, :regions, regions) set_dimension!(model2, :rates, rates) -data = Array{Int}(undef, length(years), length(regions), length(rates)) -data[:] = 1:(length(years) * length(regions) * length(rates)) +data = Array{Int}(undef, nyears, nregions, nrates) +data[:] = 1:(nyears * nregions * nrates) add_comp!(model2, testcomp3) set_param!(model2, :testcomp3, :par3, data) @@ -106,24 +115,44 @@ df2 = getdataframe(model2, :testcomp3, :par3) @test_throws ErrorException getdataframe(model2, Pair(:testcomp3, :par3), Pair(:testcomp3, :var3)) -# B. Test with shorter time than model +# B. Test with shorter time than model model3 = Model() set_dimension!(model3, :time, years) set_dimension!(model3, :regions, regions) set_dimension!(model3, :rates, rates) -add_comp!(model3, testcomp3; first = late_first, last = early_last) -par3 = Array{Float64}(undef, length(late_first:5:early_last), length(regions), length(rates)) -par3[:] = 1:(length(late_first:5:early_last) * length(regions) * length(rates)) +dim = Mimi.dimension(model3, :time) + +late_first = 2030 +early_last = 2100 + +@test_logs( + (:warn, "add_comp!: Keyword arguments 'first' and 'last' are currently disabled."), + add_comp!(model3, testcomp3; first = late_first, last = early_last) +) + +indices = collect(late_first:stepsize:early_last) +nindices = length(indices) + +valid_indices = collect(dim[late_first]:dim[early_last]) +nvalid = length(valid_indices) + +par3 = Array{Union{Missing,Float64}}(undef, nyears, nregions, nrates) +par3[:] .= missing + +par3[valid_indices, :, :] = 1:(nindices * nregions * nrates) set_param!(model3, :testcomp3, :par3, par3) run(model3) -df3 = getdataframe(model3, :testcomp3=>:par3) -@test size(df3) == (length(rates)*length(regions)*length(years), 4) +df3 = getdataframe(model3, :testcomp3 => :par3) +@test size(df3) == (nrates * nregions * nyears, 4) # Test that times outside the component's time span are padded with `missing` values -@test all(ismissing, df3.par3[1 : (length(rates)*length(regions)*(dim[late_first]-1))]) -@test all(ismissing, df3.par3[end - (length(rates)*length(regions)*(dim[end]-dim[early_last]))+1: end]) +@test all(ismissing, df3.par3[1 : (nrates * nregions * (dim[late_first] - 1))]) + +nmissing = (Int((years[end] - early_last) / stepsize) * nregions * nrates - 1) + +@test all(ismissing, df3.par3[end - nmissing : end]) end #module diff --git a/test/test_getindex.jl b/test/test_getindex.jl index 63b18f4ce..18a0a1747 100644 --- a/test/test_getindex.jl +++ b/test/test_getindex.jl @@ -3,11 +3,6 @@ module TestGetIndex using Mimi using Test -import Mimi: - reset_compdefs - -reset_compdefs() - my_model = Model() #Testing that you cannot add two components of the same name diff --git a/test/test_getindex_variabletimestep.jl b/test/test_getindex_variabletimestep.jl index e25aaeb79..3fd3e5ec1 100644 --- a/test/test_getindex_variabletimestep.jl +++ b/test/test_getindex_variabletimestep.jl @@ -3,11 +3,6 @@ module TestGetIndex_VariableTimestep using Mimi using Test -import Mimi: - reset_compdefs - -reset_compdefs() - my_model = Model() #Testing that you cannot add two components of the same name diff --git a/test/test_main.jl b/test/test_main.jl index dfc37b63f..74fcef462 100644 --- a/test/test_main.jl +++ b/test/test_main.jl @@ -4,12 +4,10 @@ using Test using Mimi import Mimi: - reset_compdefs, reset_variables, @defmodel, + reset_variables, @defmodel, variable, variable_names, external_param, build, compdefs, dimension, compinstance -reset_compdefs() - @defcomp foo1 begin index1 = Index() diff --git a/test/test_main_variabletimestep.jl b/test/test_main_variabletimestep.jl index 32dba20e6..6e6d4c42b 100644 --- a/test/test_main_variabletimestep.jl +++ b/test/test_main_variabletimestep.jl @@ -4,11 +4,9 @@ using Test using Mimi import Mimi: - reset_compdefs, reset_variables, @defmodel, + reset_variables, @defmodel, variable, variable_names, external_param, build, - compdef, compdefs, dimensions, dimension, compinstance - -reset_compdefs() + compdef, compdefs, dimension, compinstance @defcomp foo1 begin index1 = Index() diff --git a/test/test_marginal_models.jl b/test/test_marginal_models.jl index 2b8c80135..e730e9d7f 100644 --- a/test/test_marginal_models.jl +++ b/test/test_marginal_models.jl @@ -3,11 +3,6 @@ module TestMarginalModels using Mimi using Test -import Mimi: - reset_compdefs - -reset_compdefs() - @defcomp compA begin varA = Variable(index=[time]) parA = Parameter(index=[time]) diff --git a/test/test_metainfo.jl b/test/test_metainfo.jl index 4ffd0ec39..56521028f 100644 --- a/test/test_metainfo.jl +++ b/test/test_metainfo.jl @@ -4,9 +4,7 @@ using Test using Mimi import Mimi: - compdef, reset_compdefs, first_period, last_period - -reset_compdefs() + compdef, compname, compmodule, first_period, last_period, variable_names @defcomp ch4forcing1 begin c_N2Oconcentration = Parameter(index=[time],unit="ppbv") @@ -37,15 +35,25 @@ end component(ch4forcing1, ch4forcing2) # add another one with a different name end +c0 = ch4forcing1 +@test compmodule(c0) == TestMetaInfo +@test compname(c0) == :ch4forcing1 +@test nameof(c0) == :ch4forcing1 + +# These are deepcopies of c0 that are added to test_model c1 = compdef(test_model, :ch4forcing1) c2 = compdef(test_model, :ch4forcing2) -@test c1 == compdef(test_model, :ch4forcing1) +@test c1.comp_id == ch4forcing1.comp_id +@test_throws KeyError compdef(test_model, :missingcomp) + +@test variable_names(c1) == variable_names(c0) @test_throws KeyError compdef(test_model, :missingcomp) -@test c2.comp_id.module_name == Main.TestMetaInfo -@test c2.comp_id.comp_name == :ch4forcing1 -@test c2.name == :ch4forcing2 +@test compmodule(c2) == Main.TestMetaInfo +#@test compmodule(c2) == :TestMetaInfo +@test compname(c2) == :ch4forcing1 +@test nameof(c2) == :ch4forcing2 vars = Mimi.variable_names(c2) @test length(vars) == 3 diff --git a/test/test_metainfo_variabletimestep.jl b/test/test_metainfo_variabletimestep.jl index b0caedea6..8fe34fe42 100644 --- a/test/test_metainfo_variabletimestep.jl +++ b/test/test_metainfo_variabletimestep.jl @@ -4,9 +4,7 @@ using Test using Mimi import Mimi: - compdef, reset_compdefs, first_period, last_period - -reset_compdefs() + compdef, first_period, last_period, compmodule, compname @defcomp ch4forcing1 begin c_N2Oconcentration = Parameter(index=[time],unit="ppbv") @@ -40,11 +38,15 @@ end c1 = compdef(test_model, :ch4forcing1) c2 = compdef(test_model, :ch4forcing2) -@test c1 == ch4forcing1 +@test compmodule(c2) == TestMetaInfo_VariableTimestep + +# TBD: old tests; might still work +@test c1.comp_id == ch4forcing1.comp_id +@test c2.comp_id == ch4forcing1.comp_id @test_throws KeyError compdef(test_model, :missingcomp) -@test c2.comp_id.module_name == Main.TestMetaInfo_VariableTimestep -@test c2.comp_id.comp_name == :ch4forcing1 +@test compmodule(c2) == Main.TestMetaInfo_VariableTimestep +@test compname(c2) == :ch4forcing1 @test c2.name == :ch4forcing2 vars = Mimi.variable_names(c2) diff --git a/test/test_model_structure.jl b/test/test_model_structure.jl index 91821896f..98c14d35b 100644 --- a/test/test_model_structure.jl +++ b/test/test_model_structure.jl @@ -5,13 +5,10 @@ module TestModelStructure using Test using Mimi -import Mimi: - connect_param!, unconnected_params, set_dimension!, - reset_compdefs, numcomponents, get_connections, internal_param_conns, dim_count, - modeldef, modelinstance, compdef, getproperty, setproperty!, dimension, - dimensions, compdefs - -reset_compdefs() +import Mimi: + connect_param!, unconnected_params, set_dimension!, build, + get_connections, internal_param_conns, dim_count, dim_names, + modeldef, modelinstance, compdef, getproperty, setproperty!, dimension, compdefs @defcomp A begin varA::Int = Variable(index=[time]) @@ -45,8 +42,9 @@ end m = Model() +# TBD: This is not necessarily an error with composites. # make sure you can't add a component before setting time dimension -@test_throws ErrorException add_comp!(m, A) +# @test_throws ErrorException add_comp!(m, A) set_dimension!(m, :time, 2015:5:2100) @@ -59,19 +57,22 @@ connect_param!(m, :A, :parA, :C, :varC) unconn = unconnected_params(m) @test length(unconn) == 1 -@test unconn[1] == (:C, :parC) + +c = compdef(m, :C) +@test unconn[1] == (c.comp_path, :parC) connect_param!(m, :C => :parC, :B => :varB) @test_throws ErrorException add_comp!(m, C, after=:A, before=:B) -@test numcomponents(m.md) == 3 +@test length(m.md) == 3 @test length(internal_param_conns(m)) == 2 -@test get_connections(m, :A, :incoming)[1].src_comp_name == :C +c = compdef(m, :C) +@test get_connections(m, :A, :incoming)[1].src_comp_path == c.comp_path @test length(get_connections(m, :B, :incoming)) == 0 -@test get_connections(m, :B, :outgoing)[1].dst_comp_name == :C +@test get_connections(m, :B, :outgoing)[1].dst_comp_path == c.comp_path @test length(get_connections(m, :A, :all)) == 1 @@ -108,17 +109,17 @@ time = dimension(m, :time) a = collect(keys(time)) @test all([a[i] == 2010 + 5*i for i in 1:18]) -@test dimensions(m, :A, :varA)[1] == :time -@test length(dimensions(m, :A, :parA)) == 0 +@test dim_names(m, :A, :varA)[1] == :time +@test length(dim_names(m, :A, :parA)) == 0 ################################ # tests for delete! function # ################################ @test_throws ErrorException delete!(m, :D) -@test length(m.md.internal_param_conns) == 2 +@test length(internal_param_conns(m.md)) == 2 delete!(m, :A) -@test length(m.md.internal_param_conns) == 1 +@test length(internal_param_conns(m.md)) == 1 @test !(:A in compdefs(m)) @test length(compdefs(m)) == 2 @@ -132,7 +133,7 @@ delete!(m, :A) end add_comp!(m, D) -@test_throws ErrorException Mimi.build(m) +@test_throws ErrorException build(m) ########################################## # Test init function # @@ -167,12 +168,7 @@ run(m) @test m[:E, :varE] == 10 # run for just one timestep, so init sets the value here -# This results in 2 warnings, so we test for both. -@test_logs( - # (:warn, "Redefining dimension :time"), - # (:warn, "Resetting E component's last timestep to 2015"), - set_dimension!(m, :time, [2015]) -) +set_dimension!(m, :time, [2015]) run(m) @test m[:E, :varE] == 1 diff --git a/test/test_model_structure_variabletimestep.jl b/test/test_model_structure_variabletimestep.jl index 7379a7f61..eed91f696 100644 --- a/test/test_model_structure_variabletimestep.jl +++ b/test/test_model_structure_variabletimestep.jl @@ -5,17 +5,15 @@ module TestModelStructure_VariableTimestep using Test using Mimi -import Mimi: - connect_param!, unconnected_params, set_dimension!, - reset_compdefs, numcomponents, get_connections, internal_param_conns, dim_count, - compdef, getproperty, setproperty!, dimension, dimensions, compdefs - -reset_compdefs() +import Mimi: + connect_param!, unconnected_params, set_dimension!, has_comp, + get_connections, internal_param_conns, dim_count, + dim_names, compdef, getproperty, setproperty!, dimension, compdefs @defcomp A begin varA::Int = Variable(index=[time]) parA::Int = Parameter() - + function run_timestep(p, v, d, t) v.varA[t] = p.parA end @@ -49,11 +47,26 @@ last_A = 2150 m = Model() set_dimension!(m, :time, years) -@test_throws ErrorException add_comp!(m, A, last = 2210) -@test_throws ErrorException add_comp!(m, A, first = 2010) +# first and last are now disabled +# @test_throws ErrorException add_comp!(m, A, last = 2210) +# @test_throws ErrorException add_comp!(m, A, first = 2010) + +@test_logs( + (:warn, "add_comp!: Keyword arguments 'first' and 'last' are currently disabled."), + add_comp!(m, A, last = 2210) +) + +# remove the comp we just added so later tests succeed +delete!(m, :A) +@test has_comp(m, :A) == false + @test_throws ArgumentError add_comp!(m, A, after=:B) # @test_throws ErrorException add_comp!(m, A, after=:B) -add_comp!(m, A, first = first_A, last = last_A) #test specific last and first + +@test_logs( + (:warn, "add_comp!: Keyword arguments 'first' and 'last' are currently disabled."), + add_comp!(m, A, first = first_A, last = last_A) #test specific last and first +) add_comp!(m, B, before=:A) @@ -64,19 +77,22 @@ connect_param!(m, :A, :parA, :C, :varC) unconn = unconnected_params(m) @test length(unconn) == 1 -@test unconn[1] == (:C, :parC) +c = compdef(m, :C) +@test unconn[1] == (c.comp_path, :parC) connect_param!(m, :C => :parC, :B => :varB) @test_throws ErrorException add_comp!(m, C, after=:A, before=:B) -@test numcomponents(m.md) == 3 +@test length(m.md) == 3 @test length(internal_param_conns(m)) == 2 -@test get_connections(m, :A, :incoming)[1].src_comp_name == :C +c = compdef(m, :C) +@test get_connections(m, :A, :incoming)[1].src_comp_path == c.comp_path @test length(get_connections(m, :B, :incoming)) == 0 -@test get_connections(m, :B, :outgoing)[1].dst_comp_name == :C +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 @@ -109,17 +125,17 @@ time = dimension(m, :time) a = collect(keys(time)) @test all([a[i] == years[i] for i in 1:28]) -@test dimensions(m, :A, :varA)[1] == :time -@test length(dimensions(m, :A, :parA)) == 0 +@test dim_names(m, :A, :varA)[1] == :time +@test length(dim_names(m, :A, :parA)) == 0 ################################ # tests for delete! function # ################################ @test_throws ErrorException delete!(m, :D) -@test length(m.md.internal_param_conns) == 2 +@test length(internal_param_conns(m.md)) == 2 delete!(m, :A) -@test length(m.md.internal_param_conns) == 1 +@test length(internal_param_conns(m.md)) == 1 @test !(:A in compdefs(m)) @test length(compdefs(m)) == 2 diff --git a/test/test_mult_getdataframe.jl b/test/test_mult_getdataframe.jl index 71516b2bc..d794f92ac 100644 --- a/test/test_mult_getdataframe.jl +++ b/test/test_mult_getdataframe.jl @@ -4,11 +4,6 @@ using Mimi using NamedArrays using Test -import Mimi: - reset_compdefs - -reset_compdefs() - ##################################### # LARGER MULTIREGIONAL TEST (2/3) # ##################################### diff --git a/test/test_parameter_labels.jl b/test/test_parameter_labels.jl index 3df662274..9ffcad632 100644 --- a/test/test_parameter_labels.jl +++ b/test/test_parameter_labels.jl @@ -4,11 +4,6 @@ using Mimi using NamedArrays using Test -import Mimi: - reset_compdefs - -reset_compdefs() - ############################################ # BASIC TEST - use NamedArrays (1/3) # ############################################ diff --git a/test/test_parametertypes.jl b/test/test_parametertypes.jl index 52a83434e..e66a5e1fd 100644 --- a/test/test_parametertypes.jl +++ b/test/test_parametertypes.jl @@ -4,10 +4,8 @@ using Mimi using Test import Mimi: - external_params, external_param, TimestepMatrix, TimestepVector, ArrayModelParameter, - ScalarModelParameter, FixedTimestep, reset_compdefs - -reset_compdefs() + external_params, external_param, TimestepMatrix, TimestepVector, + ArrayModelParameter, ScalarModelParameter, FixedTimestep # # Test that parameter type mismatches are caught @@ -52,6 +50,8 @@ end # Check that explicit number type for model works as expected numtype = Float32 +arrtype = Union{Missing, numtype} + m = Model(numtype) set_dimension!(m, :time, 2000:2100) @@ -78,11 +78,11 @@ extpars = external_params(m) @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}, numtype, 1} -@test typeof(extpars[:b].values) == TimestepVector{FixedTimestep{2000, 1}, numtype} -@test typeof(extpars[:c].values) == Array{numtype, 1} +@test typeof(extpars[:a].values) == TimestepMatrix{FixedTimestep{2000, 1}, arrtype, 1} +@test typeof(extpars[:b].values) == TimestepVector{FixedTimestep{2000, 1}, arrtype} +@test typeof(extpars[:c].values) == Array{arrtype, 1} @test typeof(extpars[:d].value) == numtype -@test typeof(extpars[:e].values) == Array{numtype, 1} +@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 @@ -101,9 +101,9 @@ update_param!(m, :d, 5) # should work, will convert to float update_param!(m, :e, [4,5,6,7]) @test length(extpars) == 8 -@test typeof(extpars[:a].values) == TimestepMatrix{FixedTimestep{2000, 1}, numtype, 1} +@test typeof(extpars[:a].values) == TimestepMatrix{FixedTimestep{2000, 1}, arrtype, 1} @test typeof(extpars[:d].value) == numtype -@test typeof(extpars[:e].values) == Array{numtype, 1} +@test typeof(extpars[:e].values) == Array{arrtype, 1} #------------------------------------------------------------------------------ @@ -122,18 +122,18 @@ end m = Model() set_dimension!(m, :time, 2000:2002) -add_comp!(m, MyComp2; first=2000, last=2002) +add_comp!(m, MyComp2) # ; first=2000, last=2002) set_param!(m, :MyComp2, :x, [1, 2, 3]) -@test_logs( - # (:warn, "Redefining dimension :time"), - # (:warn, "Resetting MyComp2 component's first timestep to 2001"), - set_dimension!(m, :time, 2001:2003) -) +# N.B. `first` and `last` are now disabled. +# Can't move last beyond last for a component +# @test_throws ErrorException set_dimension!(m, :time, 2001:2003) + +set_dimension!(m, :time, 2001:2002) update_param!(m, :x, [4, 5, 6], update_timesteps = false) -x = external_param(m, :x) -@test x.values isa Mimi.TimestepArray{Mimi.FixedTimestep{2000, 1, LAST} where LAST, Float64, 1} +x = external_param(m.md, :x) +@test x.values isa Mimi.TimestepArray{Mimi.FixedTimestep{2000, 1, LAST} where LAST, Union{Missing,Float64}, 1} @test x.values.data == [4., 5., 6.] # TBD: this fails, but I'm not sure how it's supposed to behave. It says: # (ERROR: BoundsError: attempt to access 3-element Array{Float64,1} at index [4]) @@ -141,10 +141,10 @@ x = external_param(m, :x) # @test m[:MyComp2, :y][1] == 5 # 2001 # @test m[:MyComp2, :y][2] == 6 # 2002 -update_param!(m, :x, [2, 3, 4], update_timesteps = true) -x = external_param(m, :x) -@test x.values isa Mimi.TimestepArray{Mimi.FixedTimestep{2001, 1, LAST} where LAST, Float64, 1} -@test x.values.data == [2., 3., 4.] +update_param!(m, :x, [2, 3], update_timesteps = true) +x = external_param(m.md, :x) +@test x.values isa Mimi.TimestepArray{Mimi.FixedTimestep{2001, 1, LAST} where LAST, Union{Missing,Float64}, 1} +@test x.values.data == [2., 3.] run(m) @test m[:MyComp2, :y][1] == 2 # 2001 @test m[:MyComp2, :y][2] == 3 # 2002 @@ -154,26 +154,26 @@ run(m) m = Model() set_dimension!(m, :time, [2000, 2005, 2020]) -add_comp!(m, MyComp2; first=2000, last=2020) -set_param!(m, :MyComp2, :x, [1, 2, 3]) @test_logs( - # (:warn, "Redefining dimension :time"), - # (:warn, "Resetting MyComp2 component's first timestep to 2005"), - set_dimension!(m, :time, [2005, 2020, 2050]) + (:warn, "add_comp!: Keyword arguments 'first' and 'last' are currently disabled."), + add_comp!(m, MyComp2; first=2000, last=2020) ) +set_param!(m, :MyComp2, :x, [1, 2, 3]) + +set_dimension!(m, :time, [2005, 2020, 2050]) update_param!(m, :x, [4, 5, 6], update_timesteps = false) -x = external_param(m, :x) -@test x.values isa Mimi.TimestepArray{Mimi.VariableTimestep{(2000, 2005, 2020)}, Float64, 1} +x = external_param(m.md, :x) +@test x.values isa Mimi.TimestepArray{Mimi.VariableTimestep{(2000, 2005, 2020)}, Union{Missing,Float64}, 1} @test x.values.data == [4., 5., 6.] #run(m) #@test m[:MyComp2, :y][1] == 5 # 2005 #@test m[:MyComp2, :y][2] == 6 # 2020 update_param!(m, :x, [2, 3, 4], update_timesteps = true) -x = external_param(m, :x) -@test x.values isa Mimi.TimestepArray{Mimi.VariableTimestep{(2005, 2020, 2050)}, Float64, 1} +x = external_param(m.md, :x) +@test x.values isa Mimi.TimestepArray{Mimi.VariableTimestep{(2005, 2020, 2050)}, Union{Missing,Float64}, 1} @test x.values.data == [2., 3., 4.] run(m) @test m[:MyComp2, :y][1] == 2 # 2005 @@ -186,15 +186,12 @@ m = Model() set_dimension!(m, :time, [2000, 2005, 2020]) add_comp!(m, MyComp2) set_param!(m, :MyComp2, :x, [1, 2, 3]) -@test_logs( - # (:warn, "Redefining dimension :time"), - # (:warn, "Resetting MyComp2 component's first timestep to 2005"), - set_dimension!(m, :time, [2005, 2020, 2050]) -) + +set_dimension!(m, :time, [2005, 2020, 2050]) update_params!(m, Dict(:x=>[2, 3, 4]), update_timesteps = true) -x = external_param(m, :x) -@test x.values isa Mimi.TimestepArray{Mimi.VariableTimestep{(2005, 2020, 2050)}, Float64, 1} +x = external_param(m.md, :x) +@test x.values isa Mimi.TimestepArray{Mimi.VariableTimestep{(2005, 2020, 2050)}, Union{Missing,Float64}, 1} @test x.values.data == [2., 3., 4.] run(m) @test m[:MyComp2, :y][1] == 2 # 2005 @@ -209,21 +206,17 @@ set_dimension!(m, :time, 2000:2002) # length 3 add_comp!(m, MyComp2) set_param!(m, :MyComp2, :x, [1, 2, 3]) -@test_logs( - # (:warn, "Redefining dimension :time"), - set_dimension!(m, :time, 1999:2003) # length 5 -) +set_dimension!(m, :time, 1999:2003) # length 5 @test_throws ErrorException update_param!(m, :x, [2, 3, 4, 5, 6], update_timesteps = false) update_param!(m, :x, [2, 3, 4, 5, 6], update_timesteps = true) -x = external_param(m, :x) -@test x.values isa Mimi.TimestepArray{Mimi.FixedTimestep{1999, 1, LAST} where LAST, Float64, 1} +x = external_param(m.md, :x) +@test x.values isa Mimi.TimestepArray{Mimi.FixedTimestep{1999, 1, LAST} where LAST, Union{Missing,Float64}, 1} @test x.values.data == [2., 3., 4., 5., 6.] run(m) @test m[:MyComp2, :y] == [2., 3., 4., 5., 6.] - # 5. Test all the warning and error cases @defcomp MyComp3 begin @@ -244,22 +237,19 @@ set_param!(m, :MyComp3, :z, 0) @test_throws ErrorException update_param!(m, :x, [1, 2, 3, 4]) # Will throw an error because size @test_throws ErrorException update_param!(m, :y, [10, 15], update_timesteps=true) # Not a timestep array update_param!(m, :y, [10, 15]) -@test external_param(m, :y).values == [10., 15.] +@test external_param(m.md, :y).values == [10., 15.] @test_throws ErrorException update_param!(m, :z, 1, update_timesteps=true) # Scalar parameter update_param!(m, :z, 1) -@test external_param(m, :z).value == 1 +@test external_param(m.md, :z).value == 1 # Reset the time dimensions -@test_logs( - # (:warn, "Redefining dimension :time"), - # (:warn, "Resetting MyComp3 component's first timestep to 2005"), - set_dimension!(m, :time, 2005:2007) -) +set_dimension!(m, :time, 2005:2007) + update_params!(m, Dict(:x=>[3,4,5], :y=>[10,20], :z=>0), update_timesteps=true) # Won't error when updating from a dictionary -@test external_param(m, :x).values isa Mimi.TimestepArray{Mimi.FixedTimestep{2005,1},Float64,1} -@test external_param(m, :x).values.data == [3.,4.,5.] -@test external_param(m, :y).values == [10.,20.] -@test external_param(m, :z).value == 0 +@test external_param(m.md, :x).values isa Mimi.TimestepArray{Mimi.FixedTimestep{2005,1},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 end #module diff --git a/test/test_plotting.jl b/test/test_plotting.jl index 8b326a160..dcaf6f7a7 100644 --- a/test/test_plotting.jl +++ b/test/test_plotting.jl @@ -3,10 +3,7 @@ module TestPlotting using Mimi using Test -import Mimi: - reset_compdefs - -reset_compdefs() +using Mimi: plot_comp_graph @defcomp LongComponent begin x = Parameter(index=[time]) @@ -31,8 +28,8 @@ m = Model() set_dimension!(m, :time, 2000:3000) nsteps = Mimi.dim_count(m.md, :time) -add_comp!(m, ShortComponent; first=2100) -add_comp!(m, LongComponent; first=2000) +add_comp!(m, ShortComponent) #; first=2100) +add_comp!(m, LongComponent) #; first=2000) set_param!(m, :ShortComponent, :a, 2.) set_param!(m, :LongComponent, :y, 1.) diff --git a/test/test_references.jl b/test/test_references.jl index ec22af634..7a753321b 100644 --- a/test/test_references.jl +++ b/test/test_references.jl @@ -4,9 +4,7 @@ using Test using Mimi import Mimi: - reset_compdefs, getproperty, @defmodel - -reset_compdefs() + getproperty, @defmodel @defcomp Foo begin input = Parameter() @@ -28,7 +26,7 @@ end @defmodel m begin - index[time] = [1] + index[time] = [1, 2] component(Foo) component(Bar) diff --git a/test/test_replace_comp.jl b/test/test_replace_comp.jl index ab23bfe2e..bf00cccd2 100644 --- a/test/test_replace_comp.jl +++ b/test/test_replace_comp.jl @@ -3,9 +3,7 @@ module TestReplaceComp using Test using Mimi import Mimi: - reset_compdefs - -reset_compdefs() + compdefs, compname, compdef, comp_id, external_param_conns, external_params @defcomp X begin x = Parameter(index = [time]) @@ -43,15 +41,13 @@ end y = Variable() # different variable dimensions end - - # 1. Test scenario where the replacement works m = Model() set_dimension!(m, :time, 2000:2005) add_comp!(m, X) # Original component X set_param!(m, :X, :x, zeros(6)) -replace_comp!(m, X_repl, :X) # Successfully replaced by X_repl +replace_comp!(m, X_repl, :X) # Replace X with X_repl 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 @@ -66,7 +62,8 @@ add_comp!(m, X, :second) connect_param!(m, :second => :x, :first => :y) # Make an internal connection with a parameter with a time dimension @test_throws ErrorException replace_comp!(m, bad1, :second) # Cannot make reconnections because :x in bad1 has different dimensions replace_comp!(m, bad1, :second, reconnect = false) # Can replace without reconnecting -@test m.md.comp_defs[:second].comp_id.comp_name == :bad1 # Successfully replaced +second = compdef(m, :second) +@test second.comp_id.comp_name == :bad1 # Successfully replaced # 3. Test bad internal outgoing variable @@ -78,7 +75,8 @@ add_comp!(m, X, :second) connect_param!(m, :second => :x, :first => :y) # Make an internal connection from a variable with a time dimension @test_throws ErrorException replace_comp!(m, bad2, :first) # Cannot make reconnections because bad2 does not have a variable :y replace_comp!(m, bad2, :first, reconnect = false) # Can replace without reconnecting -@test m.md.comp_defs[:first].comp_id.comp_name == :bad2 # Successfully replaced +first = compdef(m, :first) +@test first.comp_id.comp_name == :bad2 # Successfully replaced # 4. Test bad external parameter name @@ -94,9 +92,9 @@ set_param!(m, :X, :x, zeros(6)) # Set external parameter for replace_comp!(m, bad3, :X) ) -@test m.md.comp_defs[:X].comp_id.comp_name == :bad3 # The replacement was still successful -@test length(m.md.external_param_conns) == 0 # The external paramter connection was removed -@test length(m.md.external_params) == 1 # The external parameter still exists +@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 # 5. Test bad external parameter dimensions @@ -125,17 +123,19 @@ add_comp!(m, X) @test_throws ErrorException replace_comp!(m, X_repl, :Z) # Component Z does not exist in the model, cannot be replaced -# 8. Test original postion placement functionality +# 8. Test original position placement functionality m = Model() set_dimension!(m, :time, 2000:2005) add_comp!(m, X, :c1) add_comp!(m, X, :c2) add_comp!(m, X, :c3) -replace_comp!(m, X_repl, :c3) # test replacing the last component -@test collect(values(m.md.comp_defs))[3].comp_id.comp_name == :X_repl + +replace_comp!(m, X_repl, :c3) # test replacing the last component +@test compdef(m, :c3).comp_id == X_repl.comp_id + replace_comp!(m, X_repl, :c2) # test replacing not the last one -@test collect(values(m.md.comp_defs))[2].comp_id.comp_name == :X_repl +@test compdef(m, :c2).comp_id == X_repl.comp_id end # module \ No newline at end of file diff --git a/test/test_show.jl b/test/test_show.jl new file mode 100644 index 000000000..83e526bc4 --- /dev/null +++ b/test/test_show.jl @@ -0,0 +1,111 @@ +module TestShow + +using Test +using Mimi +import Mimi: + compdefs, compdef, ComponentId, MimiStruct, ParameterDef + +@defcomp X begin + x = Parameter(index = [time]) + y = Variable(index = [time]) + function run_timestep(p, v, d, t) + v.y[t] = 1 + end +end + +function test_show(obj, expected::AbstractString) + buf = IOBuffer() + show(buf, obj) + @test String(take!(buf)) == expected +end + +function showstr(obj) + buf = IOBuffer() + show(buf, obj) + String(take!(buf)) +end + +compid = ComponentId(TestShow, :something) +test_show(compid, "") + +struct Foo <: MimiStruct + a::Dict + b::Int + c::Float64 +end + +foo = Foo(Dict((:x=>10, :y=>20)), 4, 44.4) +# Use typeof(Foo) so it works in test mode and using include(), which results in Main.TestShow.Foo. +# test_show(foo, "$(typeof(foo))\n a: Dict{Symbol,Int64}\n y => 20\n x => 10\n b: 4\n c: 44.4") + +# (:name, :datatype, :dim_names, :description, :unit, :default) +p = ParameterDef(:v1, Float64, [:time], "description string", "Mg C", 101) +# test_show(p, "ParameterDef\n name: :v1\n datatype: Float64\n dim_names: Symbol[:time]\n description: \"description string\"\n unit: \"Mg C\"\n default: 101") + +p = ParameterDef(:v1, Float64, [:time], "", "", nothing) +# test_show(p, "ParameterDef\n name: :v1\n datatype: Float64\n dim_names: Symbol[:time]\n default: nothing") + +m = Model() +set_dimension!(m, :time, 2000:2005) +add_comp!(m, X) # Original component X +set_param!(m, :X, :x, zeros(6)) + +expected = """ +Model + md: ModelDef(##anonymous#) + comp_id: + variables: OrderedCollections.OrderedDict{Symbol,VariableDef} + parameters: OrderedCollections.OrderedDict{Symbol,ParameterDef} + dim_dict: OrderedCollections.OrderedDict{Symbol,Union{Nothing, Dimension}} + time => [2000, 2001, 2002, 2003, 2004, 2005] + first: nothing + last: nothing + is_uniform: true + comps_dict: OrderedCollections.OrderedDict{Symbol,AbstractComponentDef} + X => ComponentDef(X) + comp_id: + variables: OrderedCollections.OrderedDict{Symbol,VariableDef} + y => VariableDef(y::Number) + 1: time + parameters: OrderedCollections.OrderedDict{Symbol,ParameterDef} + x => ParameterDef(x::Number) + 1: time + default: nothing + dim_dict: OrderedCollections.OrderedDict{Symbol,Union{Nothing, Dimension}} + time => nothing + first: nothing + last: nothing + is_uniform: true + 1: ExternalParameterConnection + comp_name: :X + param_name: :x + external_param: :x + external_params: Dict{Symbol,ModelParameter} + x => ArrayModelParameter{TimestepArray{FixedTimestep{2000,1,LAST} where LAST,Float64,1}} + values: TimestepArray{FixedTimestep{2000,1,LAST} where LAST,Float64,1} + 1: 0.0 + 2: 0.0 + 3: 0.0 + 4: 0.0 + ... + 6: 0.0 + 1: time + sorted_comps: nothing + number_type: Float64 + mi: nothing""" # ignore (most) whitespace + +# Quote regex special characters +# Modified from https://github.com/JuliaLang/julia/issues/6124 +function quotemeta(s::AbstractString) + res = replace(s, r"([()[\]{}?*\$.&~#=!<>|:])" => s"\\\1") + replace(res, "\0" => "\\0") +end + +re = quotemeta(expected) + +# remove the number in the gensym since it changes each run +output = replace(showstr(m), r"(##anonymous#)\d+" => s"\1") + +@test match(Regex(re), output) !== nothing + +end # module diff --git a/test/test_timesteparrays.jl b/test/test_timesteparrays.jl index f93849206..15748a314 100644 --- a/test/test_timesteparrays.jl +++ b/test/test_timesteparrays.jl @@ -5,9 +5,7 @@ using Test import Mimi: FixedTimestep, VariableTimestep, TimestepVector, TimestepMatrix, next_timestep, hasvalue, - isuniform, first_period, last_period, first_and_step, reset_compdefs - -reset_compdefs() + isuniform, first_period, last_period, first_and_step a = collect(reshape(1:16,4,4)) @@ -240,7 +238,7 @@ m = Model() set_dimension!(m, :time, years) add_comp!(m, foo, :first) add_comp!(m, foo, :second) -connect_param!(m, :second=>:par, :first=>:var) +connect_param!(m, :second => :par, :first => :var) set_param!(m, :first, :par, 1:length(years)) @test_throws MissingException run(m) diff --git a/test/test_timesteps.jl b/test/test_timesteps.jl index ab52dffa7..81cc8bd25 100644 --- a/test/test_timesteps.jl +++ b/test/test_timesteps.jl @@ -6,11 +6,9 @@ using Test import Mimi: AbstractTimestep, FixedTimestep, VariableTimestep, TimestepVector, TimestepMatrix, TimestepArray, next_timestep, hasvalue, is_first, is_last, - gettime, getproperty, Clock, time_index, get_timestep_array, reset_compdefs, + gettime, getproperty, Clock, time_index, get_timestep_array, is_timestep, is_time -reset_compdefs() - #------------------------------------------------------------------------------ # Test basic timestep functions and Base functions for Fixed Timestep #------------------------------------------------------------------------------ @@ -68,7 +66,10 @@ t3 = VariableTimestep{years}(42) t4 = next_timestep(t3) @test is_timestep(t4, 43) -@test is_time(t4, 2106) #note here that this comes back to an assumption made in variable timesteps that we may assume the next step is 1 year + +# note here that this comes back to an assumption made in variable +# timesteps that we may assume the next step is 1 year +@test is_time(t4, 2106) @test_throws ErrorException t_next = t4 + 1 @test_throws ErrorException next_timestep(t4) @@ -108,11 +109,12 @@ first_foo = 2005 m = Model() set_dimension!(m, :time, years) +# first and last disabled # test that you can only add components with first/last within model's time index range -@test_throws ErrorException add_comp!(m, Foo; first=1900) -@test_throws ErrorException add_comp!(m, Foo; last=2100) +# @test_throws ErrorException add_comp!(m, Foo; first=1900) +# @test_throws ErrorException add_comp!(m, Foo; last=2100) -foo = add_comp!(m, Foo; first=first_foo) # offset for foo +foo = add_comp!(m, Foo) # DISABLED: first=first_foo) # offset for foo bar = add_comp!(m, Bar) set_param!(m, :Foo, :inputF, 5.) @@ -123,9 +125,10 @@ run(m) @test length(m[:Foo, :output]) == length(years) @test length(m[:Bar, :output]) == length(years) -dim = Mimi.Dimension(years) -foo_output = m[:Foo, :output][dim[first_foo]:dim[years[end]]] -for i in 1:6 +yr_dim = Mimi.Dimension(years) +idxs = yr_dim[first_foo]:yr_dim[years[end]] +foo_output = m[:Foo, :output] +for i in idxs @test foo_output[i] == 5+i end @@ -149,20 +152,17 @@ set_dimension!(m, :time, 2000:2009) vector = ones(5) matrix = ones(3,2) -t_vector= get_timestep_array(m.md, Float64, 1, 1, vector) +t_vector = get_timestep_array(m.md, Float64, 1, 1, vector) t_matrix = get_timestep_array(m.md, Float64, 2, 1, matrix) @test typeof(t_vector) <: TimestepVector @test typeof(t_matrix) <: TimestepMatrix -#try with variable timestep -@test_logs( - # (:warn, "Redefining dimension :time"), - set_dimension!(m, :time, [2000:1:2004; 2005:2:2016]) -) +# try with variable timestep +set_dimension!(m, :time, [2000:1:2004; 2005:2:2009]) -t_vector= get_timestep_array(m.md, Float64, 1, 1, vector) +t_vector = get_timestep_array(m.md, Float64, 1, 1, vector) t_matrix = get_timestep_array(m.md, Float64, 2, 2, matrix) @test typeof(t_vector) <: TimestepVector @@ -185,7 +185,7 @@ end m2 = Model() set_dimension!(m2, :time, years) bar = add_comp!(m2, Bar) -foo2 = add_comp!(m2, Foo2, first=first_foo) +foo2 = add_comp!(m2, Foo2) # , first=first_foo) set_param!(m2, :Bar, :inputB, collect(1:length(years))) @@ -195,7 +195,7 @@ connect_param!(m2, :Foo2, :inputF, :Bar, :output) run(m2) -foo_output2 = m2[:Foo2, :output][dim[first_foo]:dim[years[end]]] +foo_output2 = m2[:Foo2, :output][yr_dim[first_foo]:yr_dim[years[end]]] for i in 1:6 @test foo_output2[i] == (i+5)^2 end @@ -218,11 +218,12 @@ years = 2000:2010 m3 = Model() set_dimension!(m3, :time, years) -add_comp!(m3, Foo, first=2005) +add_comp!(m3, Foo) #, first=2005) add_comp!(m3, Bar2) set_param!(m3, :Foo, :inputF, 5.) connect_param!(m3, :Bar2, :inputB, :Foo, :output, zeros(length(years))) + run(m3) @test length(m3[:Foo, :output]) == 11 diff --git a/test/test_tmp.jl b/test/test_tmp.jl new file mode 100644 index 000000000..7bc25a9d6 --- /dev/null +++ b/test/test_tmp.jl @@ -0,0 +1,44 @@ +module Tmp + +using Test +using Mimi +import Mimi: + compdefs, compdef, external_param_conns + +@defcomp X begin + x = Parameter(index = [time]) + y = Variable(index = [time]) + function run_timestep(p, v, d, t) + v.y[t] = 1 + end +end + +@defcomp X_repl begin + x = Parameter(index = [time]) + y = Variable(index = [time]) + function run_timestep(p, v, d, t) + v.y[t] = 2 + end +end + +m = Model() +set_dimension!(m, :time, 2000:2005) +add_comp!(m, X, exports=[:x => :z]) # Original component X +add_comp!(m, X_repl) +set_param!(m, :X, :x, zeros(6)) + +if false + run(m) + @test m[:X, :y] == ones(6) + + replace_comp!(m, X_repl, :X) + 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 + +end # module + +using Mimi +m = Tmp.m diff --git a/test/test_tools.jl b/test/test_tools.jl index 9bca207a0..b60885ea1 100644 --- a/test/test_tools.jl +++ b/test/test_tools.jl @@ -4,15 +4,13 @@ using Test using Mimi import Mimi: - getproperty, reset_compdefs + getproperty, pretty_string -reset_compdefs() - -#utils: prettify -@test Mimi.prettify("camelCaseBasic") == Mimi.prettify(:camelCaseBasic) == "Camel Case Basic" -@test Mimi.prettify("camelWithAOneLetterWord") == Mimi.prettify(:camelWithAOneLetterWord) == "Camel With A One Letter Word" -@test Mimi.prettify("snake_case_basic") == Mimi.prettify(:snake_case_basic) == "Snake Case Basic" -@test Mimi.prettify("_snake__case__weird_") == Mimi.prettify(:_snake__case__weird_) == "Snake Case Weird" +#utils: pretty_string +@test pretty_string("camelCaseBasic") == pretty_string(:camelCaseBasic) == "Camel Case Basic" +@test pretty_string("camelWithAOneLetterWord") == pretty_string(:camelWithAOneLetterWord) == "Camel With A One Letter Word" +@test pretty_string("snake_case_basic") == pretty_string(:snake_case_basic) == "Snake Case Basic" +@test pretty_string("_snake__case__weird_") == pretty_string(:_snake__case__weird_) == "Snake Case Weird" #utils: interpolate stepsize = 2 # N.B. ERROR: cannot assign variable Base.step from module Main diff --git a/test/test_units.jl b/test/test_units.jl index efaccdd83..1d3164e2d 100644 --- a/test/test_units.jl +++ b/test/test_units.jl @@ -3,9 +3,7 @@ module TestUnits using Test using Mimi -import Mimi: verify_units, connect_param!, ComponentReference, @defmodel, reset_compdefs - -reset_compdefs() +import Mimi: verify_units, connect_param!, ComponentReference, @defmodel # Try directly using verify_units @test verify_units("kg", "kg") diff --git a/test/test_variables_model_instance.jl b/test/test_variables_model_instance.jl index f24b9b6b5..7b281d361 100644 --- a/test/test_variables_model_instance.jl +++ b/test/test_variables_model_instance.jl @@ -4,10 +4,10 @@ using Mimi using Test import Mimi: - reset_compdefs, variable_names, compinstance, get_var_value, get_param_value, - set_param_value, set_var_value, dim_count, dim_key_dict, dim_value_dict, compdef - -reset_compdefs() + variable_names, compinstance, get_var_value, get_param_value, + set_param_value, set_var_value, dim_count, compdef, + LeafComponentInstance, AbstractComponentInstance, ComponentDef, TimestepArray, + ComponentInstanceParameters, ComponentInstanceVariables my_model = Model() @@ -36,39 +36,35 @@ run(my_model) mi = my_model.mi md = modeldef(mi) ci = compinstance(mi, :testcomp1) -cdef = compdef(ci) +cdef = compdef(md, ci.comp_path) citer = components(mi) @test typeof(md) == Mimi.ModelDef && md == mi.md -@test typeof(ci) <: Mimi.ComponentInstance && ci == mi.components[:testcomp1] -@test typeof(cdef) <: Mimi.ComponentDef && cdef == compdef(ci.comp_id) -@test name(ci) == :testcomp1 -@test typeof(citer) <: Base.ValueIterator && length(citer) == 1 && eltype(citer) == Mimi.ComponentInstance +@test typeof(ci) <: LeafComponentInstance && ci == compinstance(mi, :testcomp1) +@test typeof(cdef) <: ComponentDef && cdef.comp_id == ci.comp_id +@test ci.comp_name == :testcomp1 +@test typeof(citer) <: Base.ValueIterator && length(citer) == 1 && eltype(citer) <: AbstractComponentInstance #test convenience functions that can be called with name symbol param_value = get_param_value(ci, :par1) -@test typeof(param_value)<: Mimi.TimestepArray +@test typeof(param_value)<: TimestepArray @test_throws ErrorException get_param_value(ci, :missingpar) var_value = get_var_value(ci, :var1) @test_throws ErrorException get_var_value(ci, :missingvar) -@test typeof(var_value) <: Mimi.TimestepArray +@test typeof(var_value) <: TimestepArray params = parameters(mi, :testcomp1) params2 = parameters(mi, :testcomp1) -@test typeof(params) <: Mimi.ComponentInstanceParameters +@test typeof(params) <: ComponentInstanceParameters @test params == params2 vars = variables(mi, :testcomp1) vars2 = variables(ci) -@test typeof(vars) <: Mimi.ComponentInstanceVariables +@test typeof(vars) <: ComponentInstanceVariables @test vars == vars2 @test dim_count(mi, :time) == 20 -key_dict = dim_key_dict(mi) -value_dict = dim_value_dict(mi) -@test [key_dict[:time]...] == [2015:5:2110...] && length(key_dict) == 1 -@test value_dict[:time] == [1:1:20...] && length(value_dict) == 1 end #module diff --git a/wip/export_all.jl b/wip/export_all.jl index 75bd1b63e..ac2885038 100644 --- a/wip/export_all.jl +++ b/wip/export_all.jl @@ -3,10 +3,10 @@ # macro import_all(pkg) function ok_to_import(symbol) - ! (symbol in (:eval, :show, :include) || string(symbol)[1] == '#') + ! (symbol in (:eval, :show, :include, :name) || string(symbol)[1] == '#') end - symbols = Iterators.filter(ok_to_import, names(eval(pkg), all=true)) + symbols = Iterators.filter(ok_to_import, names(getproperty(@__MODULE__, pkg), all=true)) symlist = join(symbols, ",") return Meta.parse("import $pkg: $symlist") end diff --git a/wip/getprop_exploration.jl b/wip/getprop_exploration.jl deleted file mode 100644 index c49acd316..000000000 --- a/wip/getprop_exploration.jl +++ /dev/null @@ -1,40 +0,0 @@ - -module test - -struct ComponentInstanceParameters{T <: NamedTuple} - nt::T -end - -struct ComponentInstanceVariables{T <: NamedTuple} - nt::T -end - -struct ComponentInstance{V <: NamedTuple, P <: NamedTuple} - vars::ComponentInstanceVariables{V} - params::ComponentInstanceParameters{P} -end - -function ComponentInstanceParameters(names, types, values) - NT = NamedTuple{names, types} - ComponentInstanceParameters{NT}(NT(values)) -end - -@inline function Base.getproperty(obj::ComponentInstanceParameters{T}, name::Symbol) where {T} - nt = getfield(obj, :nt) - return fieldtype(T, name) <: Ref ? getproperty(nt, name)[] : getproperty(nt, name) -end - - -using BenchmarkTools - -ci = ComponentInstanceParameters((a=1., b=2.)) - -foo(ci) = ci.a + ci.b - -println("@btime ci.a + ci.b") -@btime foo($ci) - -function run_timestep(p, v, d, t) -end - -end # module