diff --git a/Project.toml b/Project.toml index c82cc93c6..df5c959ac 100644 --- a/Project.toml +++ b/Project.toml @@ -58,7 +58,7 @@ ProgressMeter = "1.2" StatsBase = "0.32" StringBuilders = "0.2" TableTraits = "0.4.1, 1" -VegaLite = "1, 2.0" +VegaLite = "1" julia = "1.2" [extras] diff --git a/contrib/test_all_models.jl b/contrib/test_all_models.jl index 79c9b8c17..f839d9eb0 100644 --- a/contrib/test_all_models.jl +++ b/contrib/test_all_models.jl @@ -12,16 +12,16 @@ packages_to_test = [ - "MimiDICE2010", - "MimiDICE2013", - "MimiRICE2010", - "MimiFUND", - "MimiPAGE2009", - "MimiDICE2016" => ("https://github.com/AlexandrePavlov/MimiDICE2016.jl", "master"), - "MimiSNEASY" => ("https://github.com/anthofflab/MimiSNEASY.jl", "master"), - "MimiFAIR" => ("https://github.com/anthofflab/MimiFAIR.jl", "master"), - "MimiMAGICC" => ("https://github.com/anthofflab/MimiMAGICC.jl", "master"), - "MimiHECTOR" => ("https://github.com/anthofflab/MimiHECTOR.jl", "master") + "MimiDICE2010" => ("https://github.com/anthofflab/MimiDICE2010.jl", "mimi-next"), + "MimiDICE2013" => ("https://github.com/lrennels/MimiDICE2013.jl.git", "mimi-next"), + "MimiRICE2010" => ("https://github.com/lrennels/mimi-rice-2010.jl", "mimi-next"), + "MimiFUND" => ("https://github.com/fund-model/MimiFUND.jl", "mimi-next"), + "MimiPAGE2009" => ("https://github.com/fund-model/MimiFUND.jl", "mimi-next"), + # "MimiDICE2016" => ("https://github.com/AlexandrePavlov/MimiDICE2016.jl", "master"), + # "MimiSNEASY" => ("https://github.com/anthofflab/MimiSNEASY.jl", "master"), + # "MimiFAIR" => ("https://github.com/anthofflab/MimiFAIR.jl", "master"), + # "MimiMAGICC" => ("https://github.com/anthofflab/MimiMAGICC.jl", "master"), + # "MimiHECTOR" => ("https://github.com/anthofflab/MimiHECTOR.jl", "master") ] using Pkg @@ -30,7 +30,7 @@ mktempdir() do folder_name pkg_that_errored = [] Pkg.activate(folder_name) - Pkg.develop(PackageSpec(path=joinpath(homedir(), ".julia", "dev", "Mimi"))) + Pkg.develop(PackageSpec(path=joinpath(@__DIR__, ".."))) Pkg.add([i isa Pair ? PackageSpec(url=i[2][1], rev=i[2][2]) : PackageSpec(i) for i in packages_to_test]) diff --git a/docs/src/internals/composite_components.md b/docs/src/internals/composite_components.md index c1f6dc07a..e9a1ae545 100644 --- a/docs/src/internals/composite_components.md +++ b/docs/src/internals/composite_components.md @@ -1,82 +1,183 @@ # Composite Components -## Goals +## Overview -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. +This document describes the core data structures used to implement in Mimi 1.0. -## Major elements -This suggests three types of elements: +Prior versions of Mimi supported only "flat" models, i.e., with one level of components. The current version supports mulitple layers of components, with some components being "final" or leaf components, and others being "composite" components which themselves contain other leaf or composite components. This approach allows for a cleaner organization of complex models, and allows the construction of building blocks that can be re-used in multiple models. -1. `LeafComponent(Def|Instance)` -- equivalent to the Mimi v0.4 `Component(Def|Instance)` concept. +To the degree possible, composite components are designed to operate the same as leaf components, though there are necessarily differences: -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. Leaf components are defined using the macro `@defcomp`, while composites are defined using `@defcomposite`. Each macro supports syntax and semantics specific to the type of component. See below for more details on these macros. -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. +1. Leaf composites support user-defined `run_timestep()` functions, whereas composites have a built-in `run_timestep()` function that iterates over its subcomponents and calls their `run_timestep()` function. The `init()` function is handled analogously. -## Implementation Notes +### Classes.jl -### Model +Most of the core data structures are defined using the `Classes.jl` package, which was developed for Mimi, but separated out as a generally useful julia package. The main features of `Classes` are: -* A `Model` will be defined using the `@defmodel` macro. +1. Classes can subclass other classes, thereby inheriting the same list of fields as a starting point, which can then be extended with further fields. -* 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!`. +1. A type hierarchy is defined automatically that allows classes and subclasses to be referenced with a single type. In short, if you define a class `Foo`, an abstract type called `AbstractFoo` is defined, along with the concrete class `Foo`. If you subclass `Foo` (say with the class `Bar`), then `AbstractBar` will be a subtype of `AbstractFoo`, allowing methods to be defined that operate on both the superclass and subclass. See the Classes.jl documentation for further details. -* 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. +For example, in Mimi, `ModelDef` is a subclass of `CompositeComponentDef`, which in turn is a subclass of `ComponentDef`. Thus, methods can be written with arguments typed `x::ComponentDef` to operate on leaf components only, or `x::AbstractCompositeComponentDef` to operate on composites and `ModelDef`, or as `x::AbstractComponentDef` to operate on all three concrete types. -* 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.) +## Core types -![MetaComponent Schematic](../figs/Mimi-model-schematic-v3.png) +These are defined in `types/core.jl`. +1. `MimiStruct` and `MimiClass` -### MetaComponent +All structs and classes in Mimi are derived from these abstract types, which allows us to identify Mimi-defined items when writing `show()` methods. -* 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. +1. `ComponentId` -* 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. + To identify components, `@defcomp` creates a variable with the name of + the component whose value is an instance of this type. The definition is: -* 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`. + ```julia + struct ComponentId <: MimiStruct + module_obj::Union{Nothing, Module} + comp_name::Symbol + end + ``` -## Other Notes +1. `ComponentPath` -* 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: + A `ComponentPath` identifies the path from one or more composites to any component, using an `NTuple` of symbols. Since component names are unique at the composite level, the sequence of names through a component hierarchy uniquely identifies a component in that hierarchy. - * Aggregate from the bottom up building `_firsts` and `_lasts` in each `MetaComponentInstance` holding the values for its sub-components. + ```julia + struct ComponentPath <: MimiStruct + names::NTuple{N, Symbol} where N + end + ``` - * Also store the `MetaComponentInstance`'s own summary `first` and `last` which are just `min(firsts)` and `max(lasts)`, respectively. +## Model Definition -* Currently, the `run()` function creates a vector of `Clock` instances, corresponding to each model component. I see two options here: +Models are composed of two separate structures, which we refer to as the "definition" side and the "instance" or "instantiated" side. The definition side is operated on by the user via the `@defcomp` and `@defcomposite` macros, and the public API. - 1. Extend the current approach to have each `MetaComponentInstance` hold a vector of `Clock` instances for its sub-components. +The instantiated model can be thought of as a "compiled" version of the model definition, with its data structures oriented toward run-time efficiency. It is constructed by Mimi in the `build()` function, which is called by the `run()` function. - 2. Store a `Clock` instance with each `MetaComponentInstance` or `LeafComponentInstance` and provide a recursive method to reset all clocks. +The public API sets a flag whenever the user modifies the model definition, and the instance is rebuilt before it is run if the model definition has changed. Otherwise, the model instance is re-run. +The model definition is constructed from the following elements. -### Other stuff +### Leaf components -* 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. +1. `DatumDef` -* Unit conversion components should be simple "multiplier" components that are bound with specific conversion factors, conceptually like a "closure" on a component. + This is a superclass holding elements common to `VariableDef` and `ParameterDef`, including the `ComponentPath` to the component in which the datum is defined, the data type, and dimension definitions. `DatumDef` subclasses are stored only in leaf components. -* 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. +1. `VariableDef <: DatumDef` -* If > 1 component exports parameters of the same name, it's an error. At least one comp must rename. + This class adds no new fields. It exists to differentiate variables from parameters. -* David suggested making composite comps immutable, generating a new one each time a change is made. (Why not just have the CompositeComponentInstance be immutable?) +1. `ParameterDef <: DatumDef` -## Notes from 9/21/18 Meeting + This class adds only a "default value" field to the `DatumDef`. Note that functions defined to operate on the `AbstractDatumDef` type work for both variable and parameter definitions. -``` -@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 +1. `ComponentDef` + +Instances of `ComponentDef` are defined using `@defcomp`. Their internal `namespace` +dictionary can hold both `VariableDef` and `ParameterDef` instances. + +### Composite components + +Composite components provide a single component-like interface to an arbitrarily complex set +of components (both leaf and composite components). + +1. `DatumReference` + + This abstract class serves as a superclass for `ParameterDefReference`, and + `VariableDefReference`. + +1. `ParameterDefReference`, and `VariableDefReference` + + These are used in composite components to store references to `ParameterDef` and `VariableDef` instances defined in leaf components. (They are conceptually like symbolic links in a + file system.) Whereas a `VariableDef` or `ParameterDef` can appear in a leaf + component, references to these may appear in any number of composite components. + + "Importing" a parameter or variable from a sub-component defines a reference to that + datum in a leaf component. Note that if a composite imports a datum from another + composite, a reference to the leaf datum is stored in each case. That is, we don't + store references to references. + +1. `CompositeComponentDef <: ComponentDef` + + Instances of `CompositeComponentDef` are defined using `@defcomposite`. Their internal `namespace` dictionary can hold instances of `ComponentDef`, `CompositeComponentDef`, `VariableDefReference` and `ParameterDefReference`. + Composite components also record internal parameter connections. + +1. `ModelDef <: CompositeComponentDef` + + A `ModelDef` is a top-level composite that also stores external parameters and a list + of external parameter connections. + +### Parameter Connections + +Parameters hold values defined exogneously to the model ("external" parameters) or to the +component ("internal" parameters). + +1. `InternalParameterConnection` + +Internal parameters are defined by connecting a parameter in one component to a variable +in another component. This struct holds the names and `ComponentPath`s of the parameter +and variable, and other information such as the "backup" data source. At build time, +internal parameter connections result in direct references from the parameter to the +storage allocated for the variable. + +1. `ExternalParameterConnection` + +Values that are exogenous to the model are defined in external parameters whose values are +assigned using the public API function `set_param!()`, or by setting default values in +`@defcomp` or `@defcomposite`, in which case, the default values are assigned via an +internal call to `set_param!()`. + +External connections are stored in the `ModelDef`, along with the actual `ModelParameter`s, +which may be scalar values or arrays, as described below. +1. `ModelParameter` +This is an abstract type that is the supertype of both `ScalarModelParameter{T}` and +`ArrayModelParameter{T}`. These two parameterized types are used to store values set +for external model parameters. + +## Instantiated Models + + + +1. `ComponentInstanceData` + +This is the supertype for variables and parameters in component instances. + +```julia +@class ComponentInstanceData{NT <: NamedTuple} <: MimiClass begin + nt::NT + comp_paths::Vector{ComponentPath} # records the origin of each datum end -``` \ No newline at end of file +``` + +1. `ComponentInstanceParameters` + +1. `ComponentInstanceVariables` + +1. `ComponentInstance` + +1. `LeafComponentInstance <: ComponentInstance` + +1. `CompositeComponentInstance <: ComponentInstance` + + The `run_timestep()` method of a `ComponentInstance` simply calls the `run_timestep()` + method of each of its sub-components in dependency order. + +1. `ModelInstance <: CompositeComponentInstance` + + +## User-facing Classes + +1. `Model` + +The `Model` class contains the `ModelDef`, and after the `build()` function is called, a `ModelInstance` that can be run. The API for `Model` delegates many calls to either its top-level `ModeDef` or `ModelInstance`, while providing additional functionality including running a Monte Carlo simulation. + +1. `ComponentReference` + +1. `VariableReference` diff --git a/examples/01-onecomponent.jl b/examples/01-onecomponent.jl index e72bd0a64..3f28c40c8 100644 --- a/examples/01-onecomponent.jl +++ b/examples/01-onecomponent.jl @@ -18,16 +18,11 @@ using Mimi end - # Create a model uses the component # ================================= -@defmodel begin - - m = Model() - - component(component1) -end +m = Model() +add_comp!(m, component1) # Run model # ========= diff --git a/src/core/_preliminary.jl b/src/core/_preliminary.jl deleted file mode 100644 index 7590fd9de..000000000 --- a/src/core/_preliminary.jl +++ /dev/null @@ -1,42 +0,0 @@ -# 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 5960ec6fa..b504696c8 100644 --- a/src/core/build.jl +++ b/src/core/build.jl @@ -1,5 +1,6 @@ connector_comp_name(i::Int) = Symbol("ConnectorComp$i") + # Return the datatype to use for instance variables/parameters function _instance_datatype(md::ModelDef, def::AbstractDatumDef) dtype = def.datatype == Number ? number_type(md) : def.datatype @@ -76,19 +77,7 @@ function _instantiate_component_vars(md::ModelDef, comp_def::ComponentDef) return ComponentInstanceVariables(names, types, values, paths) end -# 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[] - - 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 - -function _combine_exported_pars(comp_def::AbstractCompositeComponentDef, par_dict::Dict{Tuple{ComponentPath, Symbol}, Any}) +function _combine_exported_pars(comp_def::AbstractCompositeComponentDef) names = Symbol[] values = Any[] paths = repeat(Any[comp_def.comp_path], length(names)) @@ -96,64 +85,54 @@ function _combine_exported_pars(comp_def::AbstractCompositeComponentDef, par_dic return ComponentInstanceParameters(names, types, values, paths) end -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) +function _instantiate_vars(md::ModelDef) + vdict = Dict{ComponentPath, Any}() + recurse(md, cd -> vdict[cd.comp_path] = _instantiate_component_vars(md, cd); leaf_only=true) + return vdict end - -# 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 - -# Do nothing if called on a leaf component -_collect_params(comp_def::ComponentDef, var_dict, par_dict) = nothing - -# 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 +# Collect all parameters with connections to allocated variable storage +function _collect_params(md::ModelDef, var_dict::Dict{ComponentPath, Any}) # @info "Collecting params for $(comp_def.comp_id)" # Iterate over connections to create parameters, referencing storage in vars - for ipc in internal_param_conns(comp_def) + conns = [] + recurse(md, cd -> append!(conns, internal_param_conns(cd)); composite_only=true) + + pdict = Dict{Tuple{ComponentPath, Symbol}, Any}() + + for conn in conns + ipc = dereferenced_conn(md, conn) + # @info "src_comp_path: $(ipc.src_comp_path)" src_vars = var_dict[ipc.src_comp_path] + # @info "src_vars: $src_vars, name: $(ipc.src_var_name)" 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 + pdict[(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(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)" + for epc in external_param_conns(md) + param = external_param(md, epc.external_param) + pdict[(epc.comp_path, epc.param_name)] = (param isa ScalarModelParameter ? param : value(param)) + # @info "external conn: $(pathof(epc)).$(nameof(epc)) => $(param)" end # Make the external parameter connections for the hidden ConnectorComps. # Connect each :input2 to its associated backup value. - for (i, backup) in enumerate(comp_def.backups) - conn_comp = compdef(comp_def, connector_comp_name(i)) + backups = [] + recurse(md, cd -> append!(backups, cd.backups); composite_only=true) + + for (i, backup) in enumerate(backups) + conn_comp = compdef(md, connector_comp_name(i)) conn_path = conn_comp.comp_path - param = external_param(comp_def, backup) - par_dict[(conn_path, :input2)] = (param isa ScalarModelParameter ? param : value(param)) + param = external_param(md, backup) + pdict[(conn_path, :input2)] = (param isa ScalarModelParameter ? param : value(param)) end + + return pdict end function _instantiate_params(comp_def::ComponentDef, par_dict::Dict{Tuple{ComponentPath, Symbol}, Any}) @@ -168,7 +147,7 @@ function _instantiate_params(comp_def::ComponentDef, par_dict::Dict{Tuple{Compon end function _instantiate_params(comp_def::AbstractCompositeComponentDef, par_dict::Dict{Tuple{ComponentPath, Symbol}, Any}) - _combine_exported_pars(comp_def, par_dict) + _combine_exported_pars(comp_def) end # Return a built leaf or composite LeafComponentInstance @@ -198,10 +177,57 @@ function _build(comp_def::AbstractCompositeComponentDef, return CompositeComponentInstance(comps, comp_def, time_bounds) end +""" + _set_defaults!(md::ModelDef) + +Look for default values for any unset parameters and set those values. The +depth-first search starts stores results in a dict, so higher-level settings +(i.e., closer to ModelDef in the hierarchy) overwrite lower-level ones. +""" +function _set_defaults!(md::ModelDef) + not_set = unconnected_params(md) + isempty(not_set) && return + + d = Dict() + + function _store_defaults(obj) + if obj isa AbstractCompositeComponentDef + for ref in obj.defaults + d[(pathof(ref), nameof(ref))] = ref.default + end + else + for param in parameters(obj) + if param.default !== nothing + d[(pathof(obj), nameof(param))] = param.default + end + end + end + end + recurse(md, _store_defaults) + + for ref in not_set + param = dereference(ref) + path = pathof(param) + name = nameof(param) + key = (path, name) + value = get(d, key, missing) + if value !== missing + # @info "Setting param :$name to default $value" + set_param!(md, name, value) + end + end +end + function _build(md::ModelDef) + # moved to build(m), below + # import_params!(md) + # @info "_build(md)" add_connector_comps!(md) + # moved to build(m), below + # _set_defaults!(md) + # check if all parameters are set not_set = unconnected_params(md) @@ -210,28 +236,28 @@ function _build(md::ModelDef) error("Cannot build model; the following parameters are not set:\n $params") end - var_dict = Dict{ComponentPath, Any}() # collect all var defs and - par_dict = Dict{Tuple{ComponentPath, Symbol}, Any}() # store par values as we go + vdict = _instantiate_vars(md) + pdict = _collect_params(md, vdict) - _instantiate_vars(md, var_dict) - _collect_params(md, var_dict, par_dict) - - # @info "var_dict: $var_dict" - # @info "par_dict: $par_dict" + # @info "vdict: $vdict" + # @info "pdict: $pdict" t = dimension(md, :time) time_bounds = (firstindex(t), lastindex(t)) propagate_time!(md, t) - ci = _build(md, var_dict, par_dict, time_bounds) + ci = _build(md, vdict, pdict, time_bounds) mi = ModelInstance(ci, md) return mi end function build(m::Model) - # fix paths and propagate imports - fix_comp_paths!(m.md) + # import any unconnected params into ModelDef + import_params!(m.md) + + # apply defaults to unset parameters + _set_defaults!(m.md) # Reference a copy in the ModelInstance to avoid changes underfoot md = deepcopy(m.md) diff --git a/src/core/connections.jl b/src/core/connections.jl index cc0bd6dfe..bef4bc52f 100644 --- a/src/core/connections.jl +++ b/src/core/connections.jl @@ -18,7 +18,10 @@ function disconnect_param!(obj::AbstractCompositeComponentDef, comp_def::Abstrac 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) + + if obj isa ModelDef + filter!(x -> !(x.comp_path == path && x.param_name == param_name), obj.external_param_conns) + end dirty!(obj) end @@ -101,9 +104,15 @@ end Connect a parameter `param_name` in the component `comp_name` of composite `obj` to the external parameter `ext_param_name`. """ -function connect_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, param_name::Symbol, ext_param_name::Symbol; +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) + connect_param!(obj, comp_def, param_name, ext_param_name, check_labels=check_labels) +end + +function connect_param!(obj::AbstractCompositeComponentDef, comp_def::ComponentDef, + param_name::Symbol, ext_param_name::Symbol; check_labels::Bool=true) ext_param = external_param(obj, ext_param_name) if ext_param isa ArrayModelParameter && check_labels @@ -159,14 +168,16 @@ function connect_param!(obj::AbstractCompositeComponentDef, # Check that the backup data is the right size 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)).") + 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 = 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 + # convert number type and, if it's a NamedArray, convert to Array + backup = convert(Array{Union{Missing, number_type(obj)}}, backup) first = first_period(obj, dst_comp_def) T = eltype(backup) @@ -220,7 +231,8 @@ Try calling: error("Units of $src_comp_path:$src_var_name do not match $dst_comp_path:$dst_par_name.") end - conn = InternalParameterConnection(src_comp_path, src_var_name, dst_comp_path, dst_par_name, ignoreunits, backup_param_name, offset=offset) + 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 @@ -266,6 +278,7 @@ function split_datum_path(obj::AbstractCompositeComponentDef, s::AbstractString) return (ComponentPath(obj, elts[1]), Symbol(elts[2])) end +# TBD: Deprecated? """ 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 @@ -280,63 +293,79 @@ function connect_param!(obj::AbstractCompositeComponentDef, dst::AbstractString, backup; ignoreunits=ignoreunits, offset=offset) end +""" + param_ref(md::ModelDef, conn::InternalParameterConnection) -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)) +Return a ParameterDefReference to the parameter indicated in the given +connection. Used for comparing connections to parameters to find those +that are (un)connected. +""" +function param_ref(obj::AbstractCompositeComponentDef, conn::InternalParameterConnection) + comp = find_comp(obj, conn.dst_comp_path) + comp !== nothing || error("Can't find $(conn.dst_comp_path) from $(obj.comp_id)") + return datum_reference(comp, conn.dst_par_name) +end - append!(connected, union(ext_set_params, int_set_params)) +function param_ref(md::ModelDef, conn::ExternalParameterConnection) + comp = find_comp(md, pathof(conn)) + comp !== nothing || error("Can't find $(pathof(conn)) from $(md.comp_id)") + return datum_reference(comp, nameof(conn)) end """ - connected_params(obj::AbstractCompositeComponentDef) + dereferenced_conn(obj::AbstractCompositeComponentDef, + conn::InternalParameterConnection) -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)`. +Convert an InternalParameterConnection with datum references to one referring +only to leaf component pars and vars. """ -function connected_params(obj::AbstractCompositeComponentDef) - connected = ParamVector() - _collect_connected_params(obj, connected) - return connected +function dereferenced_conn(obj::AbstractCompositeComponentDef, + conn::InternalParameterConnection) + comp = find_comp(obj, conn.dst_comp_path) + comp !== nothing || error("Can't find $(conn.dst_comp_path) from $(obj.comp_id)") + pref = datum_reference(comp, conn.dst_par_name) + + comp = find_comp(obj, conn.src_comp_path) + comp !== nothing || error("Can't find $(conn.src_comp_path) from $(obj.comp_id)") + vref = datum_reference(comp, conn.src_var_name) + + ipc = InternalParameterConnection(pathof(vref), nameof(vref), pathof(pref), nameof(pref), + conn.ignoreunits, conn.backup; offset=conn.offset) + return ipc 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. + connection_refs(obj::AbstractCompositeComponentDef) + +Return a vector of ParameterDefReferences to parameters with connections. +The references are always to the original Parameter definition in a leaf +component. """ -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 +function connection_refs(obj::AbstractCompositeComponentDef) + refs = ParameterDefReference[] + root = get_root(obj) + # @info "root of $(obj.comp_id) is $(root.comp_id))" -function _collect_unconnected_params(obj::AbstractCompositeComponentDef, connected::ParamVector, unconnected::ParamVector) - for comp_def in compdefs(obj) - _collect_unconnected_params(comp_def, connected, unconnected) + function _add_conns(obj::AbstractCompositeComponentDef) + append!(refs, [param_ref(root, conn) for conn in obj.internal_param_conns]) end + + recurse(obj, _add_conns; composite_only=true) + + if obj isa ModelDef + append!(refs, [param_ref(obj, epc) for epc in external_param_conns(obj)]) + end + return refs end """ unconnected_params(obj::AbstractCompositeComponentDef) -Return a list of tuples (comp_path, param_name) of parameters -that have not been connected to a value anywhere within `obj`. +Return a list of ParameterDefReferences to parameters that have not been connected +to a value. """ function unconnected_params(obj::AbstractCompositeComponentDef) - unconnected = ParamVector() - connected = connected_params(obj) - - _collect_unconnected_params(obj, connected, unconnected) - return unconnected + return setdiff(leaf_params(obj), connection_refs(obj)) end """ @@ -347,7 +376,9 @@ to some other component to a value from a dictionary `parameters`. This method a the dictionary keys are strings that match the names of unset parameters in the model. """ function set_leftover_params!(md::ModelDef, parameters::Dict{T, Any}) where T - for (comp_path, param_name) in unconnected_params(md) + for param_ref in unconnected_params(md) + comp_path = pathof(param_ref) + param_name = nameof(param_ref) comp_def = compdef(md, comp_path) comp_name = nameof(comp_def) @@ -379,16 +410,20 @@ function add_internal_param_conn!(obj::AbstractCompositeComponentDef, conn::Inte dirty!(obj) end +# +# These should all take ModelDef instead of AbstractCompositeComponentDef as 1st argument +# + # Find external param conns for a given comp -function external_param_conns(obj::AbstractCompositeComponentDef, comp_path::ComponentPath) +function external_param_conns(obj::ModelDef, comp_path::ComponentPath) return filter(x -> x.comp_path == comp_path, external_param_conns(obj)) end -function external_param_conns(obj::AbstractCompositeComponentDef, comp_name::Symbol) +function external_param_conns(obj::ModelDef, comp_name::Symbol) return external_param_conns(obj, ComponentPath(obj.comp_path, comp_name)) end -function external_param(obj::AbstractCompositeComponentDef, name::Symbol; missing_ok=false) +function external_param(obj::ModelDef, name::Symbol; missing_ok=false) haskey(obj.external_params, name) && return obj.external_params[name] missing_ok && return nothing @@ -396,25 +431,26 @@ function external_param(obj::AbstractCompositeComponentDef, name::Symbol; missin error("$name not found in external parameter list") end -function add_external_param_conn!(obj::AbstractCompositeComponentDef, conn::ExternalParameterConnection) +function add_external_param_conn!(obj::ModelDef, conn::ExternalParameterConnection) push!(obj.external_param_conns, conn) dirty!(obj) end -function set_external_param!(obj::AbstractCompositeComponentDef, name::Symbol, value::ModelParameter) +function set_external_param!(obj::ModelDef, name::Symbol, value::ModelParameter) # if haskey(obj.external_params, name) # @warn "Redefining external param :$name in $(obj.comp_path) from $(obj.external_params[name]) to $value" # end obj.external_params[name] = value dirty!(obj) + return value end -function set_external_param!(obj::AbstractCompositeComponentDef, name::Symbol, value::Number; +function set_external_param!(obj::ModelDef, name::Symbol, value::Number; param_dims::Union{Nothing,Array{Symbol}} = nothing) set_external_scalar_param!(obj, name, value) end -function set_external_param!(obj::AbstractCompositeComponentDef, name::Symbol, +function set_external_param!(obj::ModelDef, name::Symbol, value::Union{AbstractArray, AbstractRange, Tuple}; param_dims::Union{Nothing,Array{Symbol}} = nothing) ti = get_time_index_position(param_dims) @@ -430,51 +466,51 @@ function set_external_param!(obj::AbstractCompositeComponentDef, name::Symbol, end """ - set_external_array_param!(obj::AbstractCompositeComponentDef, + set_external_array_param!(obj::ModelDef, name::Symbol, value::TimestepVector, dims) Add a one dimensional time-indexed array parameter indicated by `name` and `value` to the composite `obj`. In this case `dims` must be `[:time]`. """ -function set_external_array_param!(obj::AbstractCompositeComponentDef, +function set_external_array_param!(obj::ModelDef, name::Symbol, value::TimestepVector, dims) param = ArrayModelParameter(value, [:time]) # must be :time set_external_param!(obj, name, param) end """ - set_external_array_param!(obj::AbstractCompositeComponentDef, + set_external_array_param!(obj::ModelDef, name::Symbol, value::TimestepMatrix, dims) Add a multi-dimensional time-indexed array parameter `name` with value `value` to the composite `obj`. In this case `dims` must be `[:time]`. """ -function set_external_array_param!(obj::AbstractCompositeComponentDef, +function set_external_array_param!(obj::ModelDef, name::Symbol, value::TimestepArray, dims) param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims) set_external_param!(obj, name, param) end """ - set_external_array_param!(obj::AbstractCompositeComponentDef, + set_external_array_param!(obj::ModelDef, name::Symbol, value::AbstractArray, dims) Add an array type parameter `name` with value `value` and `dims` dimensions to the composite `obj`. """ -function set_external_array_param!(obj::AbstractCompositeComponentDef, +function set_external_array_param!(obj::ModelDef, name::Symbol, value::AbstractArray, dims) param = ArrayModelParameter(value, dims === nothing ? Vector{Symbol}() : dims) set_external_param!(obj, name, param) end """ - set_external_scalar_param!(obj::AbstractCompositeComponentDef, name::Symbol, value::Any) + set_external_scalar_param!(obj::ModelDef, name::Symbol, value::Any) Add a scalar type parameter `name` with the value `value` to the composite `obj`. """ -function set_external_scalar_param!(obj::AbstractCompositeComponentDef, name::Symbol, value::Any) - p = ScalarModelParameter(value) - set_external_param!(obj, name, p) +function set_external_scalar_param!(obj::ModelDef, name::Symbol, value::Any) + param = ScalarModelParameter(value) + set_external_param!(obj, name, param) end """ @@ -590,7 +626,7 @@ end function add_connector_comps!(obj::AbstractCompositeComponentDef) conns = internal_param_conns(obj) - i = 1 # counter to track the number of connector comps added + i = 1 # counter to track the number of connector comps added for comp_def in compdefs(obj) comp_name = nameof(comp_def) @@ -615,7 +651,7 @@ function add_connector_comps!(obj::AbstractCompositeComponentDef) conn_comp_def = (num_dims == 1 ? Mimi.ConnectorCompVector : Mimi.ConnectorCompMatrix) conn_comp_name = connector_comp_name(i) # generate a new name i += 1 # increment connector comp counter - + # Add the connector component before the user-defined component that required it conn_comp = add_comp!(obj, conn_comp_def, conn_comp_name, before=comp_name) conn_path = conn_comp.comp_path @@ -633,6 +669,7 @@ function add_connector_comps!(obj::AbstractCompositeComponentDef) # add a connection between ConnectorComp and the external backup data add_external_param_conn!(obj, ExternalParameterConnection(conn_path, :input2, conn.backup)) + # TBD: first/last stuff may be deprecated 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)) @@ -644,30 +681,3 @@ function add_connector_comps!(obj::AbstractCompositeComponentDef) return nothing end - -""" -propagate_params!( - obj::AbstractCompositeComponentDef; - names::Union{Nothing,Vector{Symbol}}=nothing) - -Exports all unconnected parameters below the given composite `obj` by adding references -to these parameters in `obj`. If `names` is not `nothing`, only the given names are -exported into `obj`. - -This is called automatically by `build!()`, but it can be -useful for developers of composites as well. - -TBD: should we just call this at the end of code emitted by @defcomposite? -""" -function propagate_params!( - obj::AbstractCompositeComponentDef; - names::Union{Nothing,Vector{Symbol}}=nothing) - - # returns a Vector{ParamPath}, which are Tuple{ComponentPath, Symbol} - for (path, name) in unconnected_params(obj) - comp = compdef(obj, path) - if names === nothing || name in names - obj[name] = datum_reference(comp, name) - end - end -end diff --git a/src/core/defcomposite.jl b/src/core/defcomposite.jl index a5fb68e27..5a6b3c4fb 100644 --- a/src/core/defcomposite.jl +++ b/src/core/defcomposite.jl @@ -1,75 +1,60 @@ using MacroTools +# From 1/16/2020 meeting +# +# c1 = Component(A) +# Component(B) # equiv B = Component(B) +# +# x3 = Parameter(a.p1, a.p2, b.p3, default=3, description="asflijasef", visibility=:private) +# +# This creates external param x3, and connects b.p3 and ANY parameter in any child named p1 to it +# AND now no p1 in any child can be connected to anything else. Use Not from the next if you want +# an exception for that +# x3 = Parameter(p1, b.p3, default=3, description="asflijasef", visibility=:private) +# +# x3 = Parameter(p1, p2, Not(c3.p1), b.p3, default=3, description="asflijasef", visibility=:private) +# +# connect(B.p2, c1.v4) +# connect(B.p3, c1.v4) +# +# x2 = Parameter(c2.x2, default=35) +# +# BUBBLE UP PHASE +# +# for p in unique(unbound_parameters) +# x1 = Parameter(c1.x1) +# end +# +# if any(unbound_parameter) then error("THIS IS WRONG") +# +# +# Expressions to parse in @defcomposite: +# +# 1. name_ = Component(compname_) +# 2. Component(compname_) => (compname = Component(compname_)) +# 3. pname_ = Parameter(args__) # args can be: pname, comp.pname, or keyword=value +# 4. connect(a.param, b.var) +# +# +# @defcomposite should just emit all the same API calls one could make manually +# + # 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 +function _typecheck(obj, expected_type, msg) + obj isa expected_type || error("$msg must be a $expected_type; got $(typeof(obj)): $obj") 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 +""" + parse_dotted_symbols(expr) -# Convert an expr like `a.b.c.d` to `[:a, :b, :c, :d]` +Parse and expression like `a.b.c.d` and return the tuple `(ComponentPath(:a, :b, :c), :d)`, +or `nothing` if the expression is not a series of dotted symbols. +""" function parse_dotted_symbols(expr) global Args = expr syms = Symbol[] @@ -83,155 +68,231 @@ function parse_dotted_symbols(expr) 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 + datum_name = pop!(syms) + return ComponentPath(syms), datum_name +end + +# +# Convert @defcomposite "shorthand" statements into Mimi API calls +# +function _parse(expr) + valid_keys = (:default, :description, :visability, :unit) + result = nothing + + if @capture(expr, newname_ = Component(compname_)) || + @capture(expr, Component(compname_)) + # check newname is nothing or Symbol, compname is Symbol + _typecheck(compname, Symbol, "Referenced component name") + + if newname !== nothing + _typecheck(newname, Symbol, "Local name for component name") + end + + newname = (newname === nothing ? compname : newname) + result = :(Mimi.add_comp!(obj, $compname, $(QuoteNode(newname)))) + + elseif @capture(expr, localparname_ = Parameter(args__)) + regargs = [] + keyargs = [] + + for arg in args + if @capture(arg, keywd_ = value_) + if keywd in valid_keys + push!(keyargs, arg) + else + error("Unrecognized Parameter keyword '$keywd'; must be one of $valid_keys") + end + + elseif @capture(arg, (cname_.pname_ | pname_)) + cname = (cname === nothing ? :(:*) : cname) # wildcard + push!(regargs, :($cname => $(QuoteNode(pname)))) + end + end + result = :(Mimi.import_param!(obj, $(QuoteNode(localparname)), $(regargs...); + $(keyargs...))) + + elseif @capture(expr, localvarname_ = Variable(datum_expr_)) + if ((tup = parse_dotted_symbols(datum_expr)) === nothing) + error("In @defcomposite's Variable(x), x must a Symbol or ", + "a dotted series of Symbols. Got :($datum_expr)") + end + comppath, varname = tup + # @info "Variable: $comppath, :$varname" + _typecheck(localvarname, Symbol, "Local variable name") + _typecheck(comppath, ComponentPath, "The referenced component") + _typecheck(varname, Symbol, "Name of referenced variable") + + # import from the added copy of the component, not the template -- thus + # the lookup of obj[varcomp]. + result = :(Mimi._import_var!(obj, $(QuoteNode(localvarname)), $comppath, + $(QuoteNode(varname)))) + + elseif @capture(expr, connect(parcomp_.parname_, varcomp_.varname_)) + # raise error if parameter is already bound + result = :(Mimi.connect_param!(obj, + $(QuoteNode(parcomp)), $(QuoteNode(parname)), + $(QuoteNode(varcomp)), $(QuoteNode(varname)); + # allow_overwrite=false # new keyword to implement + )) + else + error("Unrecognized composite statement: $expr") + end + return result end +# TBD: finish documenting this! """ 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: +Define a Mimi CompositeComponentDef `cc_name` with the expressions in `ex`. Expressions +are all shorthand for longer-winded API calls, and include the following: - component(comp_name, local_name; - bindings=[list Pair{Symbol, Symbol or Number or Array of Numbers}]) + p = Parameter(...) + v = Variable(varname) + local_name = Component(name) + Component(name) # equivalent to `name = Component(name)` + connect(...) -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. +Variable names are 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. +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 = [] + @capture(ex, exprs__) 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)) + # @info "defcomposite calling module: $calling_module" - # distinguish imports, e.g., :(EXP_VAR = CHILD_COMP1.COMP2.VAR3), - # from connections, e.g., :(COMP1.PAR2 = COMP2.COMP5.VAR2) + stmts = [_parse(expr) for expr in exprs] - # 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)) + result = :( + let cc_id = Mimi.ComponentId($calling_module, $(QuoteNode(cc_name))), + obj = Mimi.CompositeComponentDef(cc_id) - elseif @capture(elt, left_ = right_) + global $cc_name = obj + $(stmts...) + Mimi.import_params!(obj) + $cc_name + end + ) + return esc(result) +end - 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" +""" + import_params!(obj::AbstractCompositeComponentDef; + names::Union{Nothing,Vector{Symbol}}=nothing) - # 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") +Imports all unconnected parameters below the given composite `obj` by adding references +to these parameters in `obj`. - src = parse_dotted_symbols(right) - src === nothing && error("Expected dot-delimited sequence of symbols, got $right") +NOT IMPLEMENTED: If `names` is not `nothing`, only the given names ar imported. - push!(conns, (dst, src)) - # @info "connection: $dst = $src" +This is called automatically by `build!()`, but it can be useful for developers of +composites as well. - else - error("Unrecognized expression on left hand side of '=' in @defcomposite: $elt") - end - else - error("Unrecognized element in @defcomposite: $elt") +N.B. This is also called at the end of code emitted by @defcomposite. +""" +function import_params!(obj::AbstractCompositeComponentDef; + names::Union{Nothing,Vector{Symbol}}=nothing) + + unconn = unconnected_params(obj) + params = parameters(obj) + + # remove imported params from list of unconnected params + unconn = setdiff(unconn, params) + # filter!(param_ref -> !(param_ref in params), unconn) + + # verify that all explicit names are importable + if names !== nothing + unconn_names = [nameof(param_ref) for param_ref in unconn] + unknown = setdiff(names, unconn_names) + if ! isempty(unknown) + @error "Can't import names $unknown as these are not unconnected params" end end - # @info "imports: $imports" - # @info " $(length(imports)) elements" - # global IMP = imports + for param_ref in unconn + # @info "importing $param_ref to $(obj.comp_id)" + name = nameof(param_ref) + if names === nothing || name in names + obj[name] = param_ref + end + end +end - result = :( - let conns = $conns, - imports = $imports, +# Return the local name of an already-imported parameter, or nothing if not found +function _find_param_ref(obj, dr) + for (name, param_ref) in param_dict(obj) + # @info "Comparing refs $param_ref == $dr" + if param_ref == dr + # @info "Found prior import to $dr named $name" + return name + end + end + nothing +end - cc_id = Mimi.ComponentId($calling_module, $(QuoteNode(cc_name))) +function import_param!(obj::AbstractCompositeComponentDef, localname::Symbol, + pairs::Pair...; kwargs...) - global $cc_name = Mimi.CompositeComponentDef(cc_id, $(QuoteNode(cc_name)), $comps, $__module__) + print_pairs = [(comp.comp_id, name) for (comp, name) in pairs] + # @info "import_param!($(obj.comp_id), :$localname, $print_pairs)" - # @info "Defining composite $cc_id" + for (comp, pname) in pairs - function _store_in_ns(refs, local_name) - isempty(refs) && return + if comp == :* # wild card + @info "Got wildcard for param $pname (Not yet implemented)" + else + compname = nameof(comp) + has_comp(obj, compname) || + error("_import_param!: $(obj.comp_id) has no element named $compname") - 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 + newcomp = obj[compname] - # 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 + dr = datum_reference(newcomp, pname) + old_name = _find_param_ref(obj, dr) - _store_in_ns(var_refs, local_name) - _store_in_ns(par_refs, local_name) + # TBD: :allow_overwrite is not yet passed from @defcomposite + key = :allow_overwrite + if old_name === nothing || (haskey(kwargs, key) && kwargs[key]) + # import the parameter from the given component + obj[localname] = dr = datum_reference(newcomp, pname) + # @info "import_param! created dr $dr" + else + error("Duplicate import of $dr as $localname, already imported as $old_name. ", + "To allow duplicates, use Parameter($(nameof(comp)).$pname; :$key=True)") end - 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) + if haskey(kwargs, :default) + root = get_root(obj) + ref = ParameterDefReference(pname, root, pathof(newcomp), kwargs[:default]) + save_default!(obj, ref) end - - # propagates unconnected parameters - Mimi.propagate_params!($cc_name) - - $cc_name end - ) + end +end - # @info "defcomposite:\n$result" - return esc(result) +""" +Import a variable from the given subcomponent +""" +function _import_var!(obj::AbstractCompositeComponentDef, localname::Symbol, + path::ComponentPath, vname::Symbol) + if haskey(obj, localname) + error("Can't import variable; :$localname already exists in component $(obj.comp_id)") + end + + comp = @or(find_comp(obj, path), error("$path not found from component $(obj.comp_id)")) + obj[localname] = datum_reference(comp, vname) end nothing diff --git a/src/core/defmodel.jl b/src/core/defmodel.jl index 6ab2d9865..4e722f47e 100644 --- a/src/core/defmodel.jl +++ b/src/core/defmodel.jl @@ -22,7 +22,7 @@ macro defmodel(model_name, ex) global $model_name = Model() end ) - + # helper function used in loop below function addexpr(expr) let_block = result.args[end].args @@ -36,7 +36,7 @@ macro defmodel(model_name, ex) 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)) + 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) @@ -54,19 +54,15 @@ macro defmodel(model_name, ex) expr = :(Mimi.connect_param!($model_name, $(QuoteNode(dst_comp)), $(QuoteNode(dst_name)), - $(QuoteNode(src_comp)), $(QuoteNode(src_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. diff --git a/src/core/defs.jl b/src/core/defs.jl index 4535602f5..e6b61c963 100644 --- a/src/core/defs.jl +++ b/src/core/defs.jl @@ -113,7 +113,7 @@ 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. +# N.B. only composites hold other comps in the namespace. components(obj::AbstractCompositeComponentDef) = filter(istype(AbstractComponentDef), obj.namespace) param_dict(obj::ComponentDef) = filter(istype(ParameterDef), obj.namespace) @@ -129,6 +129,7 @@ Return an iterator of the parameter definitions (or references) for `comp_def`. """ parameters(obj::AbstractComponentDef) = values(param_dict(obj)) +parameters(comp_id::ComponentId) = parameters(compdef(comp_id)) """ variables(comp_def::AbstractComponentDef) @@ -136,25 +137,35 @@ parameters(obj::AbstractComponentDef) = values(param_dict(obj)) 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` +""" +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 +""" +Get a named element from the namespace of `obj` and verify its type. +""" function _ns_get(obj::AbstractComponentDef, name::Symbol, T::DataType) - haskey(obj.namespace, name) || error("Item :$name was not found in component $(obj.comp_path)") + if ! haskey(obj.namespace, name) + error("Item :$name was not found in component $(obj.comp_path)") + end item = obj[name] item isa T || error(":$name in component $(obj.comp_path) is a $(typeof(item)); expected type $T") return item end +""" +Save a value to a component's namespace. Allow replacement of existing values for a key +only with items of the same type; otherwise an error is thrown. +""" 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) @@ -181,7 +192,9 @@ end 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) +function datum_reference(comp::AbstractCompositeComponentDef, datum_name::Symbol) + _ns_get(comp, datum_name, AbstractDatumReference) +end function Base.setindex!(comp::AbstractCompositeComponentDef, value::CompositeNamespaceElement, key::Symbol) _save_to_namespace(comp, key, value) @@ -199,7 +212,7 @@ end step_size(values::Vector{Int}) = (length(values) > 1 ? values[2] - values[1] : 1) # -# TBD: should these be defined as methods of CompositeComponentDef? +# TBD: should these be defined as methods of CompositeComponentDef, i.e., not for leaf comps # function step_size(obj::AbstractComponentDef) keys = time_labels(obj) @@ -335,15 +348,18 @@ parameter_names(comp_def::AbstractComponentDef) = collect(keys(param_dict(comp_d parameter(obj::ComponentDef, name::Symbol) = _ns_get(obj, name, ParameterDef) -parameter(obj::AbstractCompositeComponentDef, name::Symbol) = _ns_get(obj, name, ParameterDefReference) +parameter(obj::AbstractCompositeComponentDef, + name::Symbol) = dereference(_ns_get(obj, name, ParameterDefReference)) -parameter(obj::AbstractCompositeComponentDef, comp_name::Symbol, param_name::Symbol) = parameter(compdef(obj, comp_name), param_name) +parameter(obj::AbstractCompositeComponentDef, comp_name::Symbol, + param_name::Symbol) = parameter(compdef(obj, comp_name), param_name) parameter(dr::ParameterDefReference) = parameter(compdef(dr), nameof(dr)) 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) +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) @@ -353,7 +369,7 @@ end """ parameter_dimensions(obj::AbstractComponentDef, param_name::Symbol) -Return the names of the dimensions of parameter `param_name` exposed in the component +Return the names of the dimensions of parameter `param_name` exposed in the component definition indicated by `obj`. """ function parameter_dimensions(obj::AbstractComponentDef, param_name::Symbol) @@ -375,104 +391,234 @@ function parameter_dimensions(obj::AbstractComponentDef, comp_name::Symbol, para return parameter_dimensions(compdef(obj, comp_name), param_name) end +# +# TBD: handling for importing/joining a set of parameters into a composite +# +# """ +# new_set_param!(m::ModelDef, param_name::Symbol, value) + +# Search all model components, find all unbound parameters named `param_name`, create +# external parameter called `param_name`, set its value to `value`, and create +# connections from the external unbound parameters named `param_name` to the new +# external parameter. If a prior external parameter `param_name` is found, raise error. +# """ +# function new_set_param!(md::ModelDef, param_name::Symbol, value) +# comps = comps_with_unbound_param(md, param_name) +# new_set_param!(md, comps, param_name, value) +# end + +# function new_set_param!(md::ModelDef, comp_list::Vector{ComponentDef}, +# param_name::Symbol, value) +# pairs = [pathof(cd) => param_name for cd in comp_list] +# new_set_param!(md, pairs, param_name, value) +# end + +# function new_set_param!(md::ModelDef, pairs::Vector{Pair{ComponentPath, Symbol}}, +# ext_param_name::Symbol, value) +# if length(pairs) > 0 +# param = set_external_param!(md, ext_param_name, value) + +# for (comp_path, param_name) in pairs +# connect_param!(md, comp_path, param_name, ext_param_name) +# end +# end +# return nothing +# end + +""" +Find and return a vector of tuples containing references to a ComponentDef and +a ParameterDef for all instances of parameters with name `param_name`, below the +composite `obj`. If none are found, an empty vector is returned. +""" +function find_params(obj::AbstractCompositeComponentDef, param_name::Symbol) + found = Vector{Tuple{ComponentDef, ParameterDef}}() + + for (name, compdef) in components(obj) + items = find_params(compdef, param_name) + append!(found, items) + end + + return found +end + +function find_params(obj::ComponentDef, param_name::Symbol) + namespace = ns(obj) + if haskey(namespace, param_name) && (item = namespace[param_name]) isa ParameterDef + return [(obj, item)] + end + + return [] +end + +""" + recurse(obj::AbstractComponentDef, f::Function, args...; + composite_only=false, depth_first=true) + +Generalized recursion functions for walking composite structures. The function `f` +is called with the first argument a leaf or composite component, followed by any +other arguments passed before the semi-colon delimiting keywords. Set `composite_only` +to `true` if the function should be called only on composites, or `leaf_only` to +call `f` only on leaf ComponentDefs. + +By default, the recursion is depth-first; set `depth_first=false`, to walk the +structure breadth-first. + +For example, to collect all parameters defined in leaf ComponentDefs, do this: + + `params = [] + recurse(md, obj->append!(params, parameters(obj)); leaf_only=true)` +""" +function recurse(obj::AbstractCompositeComponentDef, f::Function, args...; + composite_only=false, leaf_only=false, depth_first=true) + + if (! leaf_only && ! depth_first) + f(obj, args...) + end + + for child in compdefs(obj) + recurse(child, f, args...; + composite_only=composite_only, leaf_only=leaf_only, + depth_first=depth_first) + end + + if (! leaf_only && depth_first) + f(obj, args...) + end + + nothing +end + +# Same thing, but for leaf ComponentDefs +function recurse(obj::ComponentDef, f::Function, args...; + composite_only=false, leaf_only=false, depth_first=true) + + composite_only || f(obj, args...) + nothing +end + +function leaf_params(obj::AbstractCompositeComponentDef) + params = [] + recurse(obj, x->append!(params, parameters(x)); leaf_only=true) + + root = get_root(obj) + return [ParameterDefReference(nameof(param), root, pathof(param)) for param in params] +end + """ - set_param!(obj::AbstractCompositeComponentDef, comp_path::ComponentPath, - value_dict::Dict{Symbol, Any}, param_names) + set_param!(md::ModelDef, comp_name::Symbol, + value_dict::Dict{Symbol, Any}, param_names) Call `set_param!()` for each name in `param_names`, retrieving the corresponding value from `value_dict[param_name]`. """ -function set_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, value_dict::Dict{Symbol, Any}, param_names) +function set_param!(md::ModelDef, 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) + set_param!(md, comp_name, value_dict, param_name) end end """ - set_param!(obj::AbstractCompositeComponentDef, comp_path::ComponentPath, param_name::Symbol, + set_param!(md::ModelDef, comp_path::ComponentPath, param_name::Symbol, value_dict::Dict{Symbol, Any}, dims=nothing) Call `set_param!()` with `param_name` and a value dict in which `value_dict[param_name]` references the value of parameter `param_name`. """ -function set_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, value_dict::Dict{Symbol, Any}, +function set_param!(md::ModelDef, 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) + set_param!(md, comp_name, param_name, value, dims) end -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) +# May be deprecated +function set_param!(md::ModelDef, comp_path::ComponentPath, + param_name::Symbol, value, dims=nothing) + # @info "set_param!($(md.comp_id), $comp_path, $param_name, $value)" + comp_def = find_comp(md, comp_path) + @or(comp_def, error("Component with path $comp_path not found")) + # set_param!(comp.parent, nameof(comp), param_name, value, dims) + set_param!(md, comp_def, param_name, value, dims) end -""" - set_param!(obj::AbstractCompositeComponentDef, 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 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) +function set_param!(md::ModelDef, comp_name::Symbol, param_name::Symbol, value, dims=nothing) + comp_def = compdef(md, comp_name) + @or(comp_def, error("Top-level component with name $comp_name not found")) + set_param!(md, comp_def, param_name, value, dims) end """ - 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 ":"). +Compare two items, each of which must be either a ParameterDef or ParameterDefReference, +to see if they refer to the same ultimate parameter. """ -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) +function _is_same_param(p1::Union{ParameterDef, ParameterDefReference}, + p2::Union{ParameterDef, ParameterDefReference}) + p1 = (p1 isa ParameterDefReference ? dereference(p1) : p1) + p2 = (p2 isa ParameterDefReference ? dereference(p2) : p2) + return p1 == p2 end -""" - set_param!(obj::AbstractCompositeComponentDef, param_name::Symbol, value, dims=nothing) +function set_param!(md::ModelDef, comp_def::AbstractComponentDef, param_name::Symbol, + value, dims=nothing) + has_parameter(comp_def, param_name) || + error("Can't find parameter :$param_name in component $(pathof(comp_def))") -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 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.") + if ! has_parameter(md, param_name) + import_param!(md, param_name, comp_def => param_name) + + elseif ! _is_same_param(md[param_name], comp_def[param_name]) + error("Can't import parameter :$param_name; ModelDef has another item with this name") end - param_ref = obj[param_name] - set_param!(obj, param_ref.comp_path, param_ref.name, value, dims=dims) + + set_param!(md, param_name, value, dims) end """ - set_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, name::Symbol, value, dims=nothing) + set_param!(md::ModelDef, param_name::Symbol, value, dims=nothing) + +Set the value of a parameter exposed in `md` by following the ParameterDefReference. If +not found in the local namespace, import it if there is only one such parameter in md'same +children, and it is not yet bound. Otherwise raise an error. -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. +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!(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 +function set_param!(md::ModelDef, param_name::Symbol, value, dims=nothing) + # search immediate subcomponents for this parameter + found = [comp for (compname, comp) in components(md) if has_parameter(comp, param_name)] + + if ! has_parameter(md, param_name) + count = length(found) + if count >= 1 + comp = found[1] + # @info "Found one child with $param_name to auto-import: $(comp.comp_id)" + import_param!(md, param_name, comp => param_name) + + # elseif count > 1 + # # error("Can't set parameter :$param_name; found in multiple components") + else # count == 0 + error("Can't set parameter :$param_name; not found in ModelDef or children") + end + end + if value isa NamedArray dims = dimnames(value) end if dims !== nothing - check_parameter_dimensions(obj, value, dims, param_name) + check_parameter_dimensions(md, value, dims, param_name) end - comp_def = compdef(obj, comp_name) - comp_param_dims = parameter_dimensions(comp_def, param_name) - num_dims = length(comp_param_dims) + param_ref = md[param_name] + comp_def = compdef(param_ref) + param = dereference(param_ref) + param_dims = dim_names(param) + num_dims = length(param_dims) - param = parameter(comp_def, param_name) data_type = param.datatype - dtype = Union{Missing, (data_type == Number ? number_type(obj) : data_type)} + dtype = Union{Missing, (data_type == Number ? number_type(md) : data_type)} - if length(comp_param_dims) > 0 + if num_dims > 0 # convert the number type and, if NamedArray, convert to Array if dtype <: AbstractArray @@ -481,28 +627,30 @@ function set_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, param # 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.") + error("Mismatched data size for a set parameter call: dimension :$param_name", + " in $(pathof(comp_def)) has $num_dims dimensions; indicated value", + " has $value_dims dimensions.") end value = convert(Array{dtype, num_dims}, value) end - ti = get_time_index_position(obj, comp_name, param_name) + ti = get_time_index_position(param_dims) - if ti != nothing # there is a time dimension + if ti !== nothing # there is a time dimension T = eltype(value) if num_dims == 0 values = value else # 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 = first_period(md, comp_def) first === nothing && @warn "set_param!: first === nothing" - if isuniform(obj) - stepsize = step_size(obj) + if isuniform(md) + stepsize = step_size(md) values = TimestepArray{FixedTimestep{first, stepsize}, T, num_dims, ti}(value) else - times = time_labels(obj) + times = time_labels(md) # 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) @@ -512,16 +660,21 @@ function set_param!(obj::AbstractCompositeComponentDef, comp_name::Symbol, param values = value end - set_external_array_param!(obj, param_name, values, comp_param_dims) + set_external_array_param!(md, param_name, values, param_dims) else # scalar parameter case value = convert(dtype, value) - set_external_scalar_param!(obj, param_name, value) + set_external_scalar_param!(md, param_name, value) end # 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) + if length(found) == 1 + connect_param!(md, comp_def, nameof(param_ref), param_name) + else + for comp_def in found + connect_param!(md, comp_def, param_name, param_name) + end + end nothing end @@ -577,10 +730,10 @@ unit(obj::ParameterDefReference) = parameter(obj).unit """ variable_dimensions(obj::AbstractCompositeComponentDef, comp_path::ComponentPath, var_name::Symbol) -Return the names of the dimensions of variable `var_name` exposed in the composite -component definition indicated by`obj` along the component path `comp_path`. The -`comp_path` is of type `Mimi.ComponentPath` with the single field being an NTuple -of symbols describing the relative (to a composite) or absolute (relative to ModelDef) +Return the names of the dimensions of variable `var_name` exposed in the composite +component definition indicated by`obj` along the component path `comp_path`. The +`comp_path` is of type `Mimi.ComponentPath` with the single field being an NTuple +of symbols describing the relative (to a composite) or absolute (relative to ModelDef) path through composite nodes to specific composite or leaf node. """ function variable_dimensions(obj::AbstractCompositeComponentDef, comp_path::ComponentPath, var_name::Symbol) @@ -591,7 +744,7 @@ end """ variable_dimensions(obj::AbstractCompositeComponentDef, comp::Symbol, var_name::Symbol) -Return the names of the dimensions of variable `var_name` exposed in the composite +Return the names of the dimensions of variable `var_name` exposed in the composite component definition indicated by `obj` for the component `comp`, which exists in a flat model. """ @@ -602,9 +755,9 @@ end """ variable_dimensions(obj::AbstractCompositeComponentDef, comp::Symbol, var_name::Symbol) -Return the names of the dimensions of variable `var_name` exposed in the composite -component definition indicated by `obj` along the component path `comp_path`. The -`comp_path` is a tuple of symbols describing the relative (to a composite) or +Return the names of the dimensions of variable `var_name` exposed in the composite +component definition indicated by `obj` along the component path `comp_path`. The +`comp_path` is a tuple of symbols describing the relative (to a composite) or absolute (relative to ModelDef) path through composite nodes to specific composite or leaf node. """ function variable_dimensions(obj::AbstractCompositeComponentDef, comp_path::NTuple{N, Symbol}, var_name::Symbol) where N @@ -614,7 +767,7 @@ end """ variable_dimensions(obj::AbstractComponentDef, name::Symbol) -Return the names of the dimensions of variable `name` exposed in the component definition +Return the names of the dimensions of variable `name` exposed in the component definition indicated by `obj`. """ function variable_dimensions(obj::AbstractComponentDef, name::Symbol) @@ -717,6 +870,10 @@ function _insert_comp!(obj::AbstractCompositeComponentDef, comp_def::AbstractCom _set_comps!(obj, new_comps) end + # adjust paths to include new parent + # @info "_insert_comp: fixing comp path" + _fix_comp_path!(comp_def, obj) + # @info "parent obj comp_path: $(printable(obj.comp_path))" # @info "inserted comp's path: $(comp_def.comp_path)" dirty!(obj) @@ -746,6 +903,7 @@ function _find_var_par(parent::AbstractCompositeComponentDef, comp_def::Abstract end if has_parameter(comp_def, datum_name) + @info "Calling ParameterDefReference($datum_name, $(root.comp_id), $path)" return ParameterDefReference(datum_name, root, path) end @@ -768,12 +926,26 @@ function propagate_time!(obj::AbstractComponentDef, t::Dimension) end end +# Save the default value for a parameter, which is applied, if needed, at build time. +function save_default!(obj::AbstractCompositeComponentDef, comp::AbstractComponentDef, + param::ParameterDef) + root = get_root(obj) + path = pathof(comp) + name = nameof(param) + save_default!(obj, ParameterDefReference(name, root, path, param.default)) +end + +function save_default!(obj::AbstractCompositeComponentDef, ref::ParameterDefReference) + push!(obj.defaults, ref) + nothing +end + """ add_comp!( - obj::AbstractCompositeComponentDef, + obj::AbstractCompositeComponentDef, comp_def::AbstractComponentDef, - comp_name::Symbol=comp_def.comp_id.comp_name; - before::NothingSymbol=nothing, + comp_name::Symbol=comp_def.comp_id.comp_name; + before::NothingSymbol=nothing, after::NothingSymbol=nothing, rename::NothingPairList=nothing ) @@ -783,14 +955,12 @@ added at the end of the list unless one of the keywords `before` or `after` is s 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`. """ -function add_comp!( - obj::AbstractCompositeComponentDef, - comp_def::AbstractComponentDef, - comp_name::Symbol=comp_def.comp_id.comp_name; - before::NothingSymbol=nothing, - after::NothingSymbol=nothing, - rename::NothingPairList=nothing -) +function add_comp!(obj::AbstractCompositeComponentDef, + comp_def::AbstractComponentDef, + comp_name::Symbol=comp_def.comp_id.comp_name; + before::NothingSymbol=nothing, + after::NothingSymbol=nothing, + rename::NothingPairList=nothing) # TBD: rename is not yet implemented # Check if component being added already exists has_comp(obj, comp_name) && error("Cannot add two components of the same name ($comp_name)") @@ -817,8 +987,7 @@ function add_comp!( 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) + save_default!(obj, comp_def, param) end end end @@ -829,10 +998,10 @@ end """ add_comp!( - obj::AbstractCompositeComponentDef, + obj::AbstractCompositeComponentDef, comp_id::ComponentId, comp_name::Symbol=comp_id.comp_name; - before::NothingSymbol=nothing, + before::NothingSymbol=nothing, after::NothingSymbol=nothing, rename::NothingPairList=nothing ) @@ -840,26 +1009,23 @@ end 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. + +[Not yet implemented:] The optional argument `rename` can be a list of pairs indicating `original_name => imported_name`. """ -function add_comp!( - obj::AbstractCompositeComponentDef, - comp_id::ComponentId, - comp_name::Symbol=comp_id.comp_name; - before::NothingSymbol=nothing, - after::NothingSymbol=nothing, - rename::NothingPairList=nothing -) +function add_comp!(obj::AbstractCompositeComponentDef, + comp_id::ComponentId, + comp_name::Symbol=comp_id.comp_name; kwargs...) # println("Adding component $comp_id as :$comp_name") - add_comp!(obj, compdef(comp_id), comp_name, before=before, after=after, rename=rename) + add_comp!(obj, compdef(comp_id), comp_name; kwargs...) end """ replace_comp!( - obj::AbstractCompositeComponentDef, + obj::AbstractCompositeComponentDef, comp_id::ComponentId, comp_name::Symbol=comp_id.comp_name; - before::NothingSymbol=nothing, + before::NothingSymbol=nothing, after::NothingSymbol=nothing, reconnect::Bool=true ) @@ -871,15 +1037,12 @@ added with the same first and last values, unless the keywords `first` or `last` 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. """ -function replace_comp!( - obj::AbstractCompositeComponentDef, - comp_id::ComponentId, - comp_name::Symbol=comp_id.comp_name; - before::NothingSymbol=nothing, - after::NothingSymbol=nothing, - reconnect::Bool=true -) - +function replace_comp!(obj::AbstractCompositeComponentDef, + comp_id::ComponentId, + comp_name::Symbol=comp_id.comp_name; + before::NothingSymbol=nothing, + after::NothingSymbol=nothing, + reconnect::Bool=true) if ! has_comp(obj, comp_name) error("Cannot replace '$comp_name'; component not found in model.") end diff --git a/src/core/instances.jl b/src/core/instances.jl index 6170b9585..911bdc524 100644 --- a/src/core/instances.jl +++ b/src/core/instances.jl @@ -268,7 +268,7 @@ end function Base.run(mi::ModelInstance, ntimesteps::Int=typemax(Int), dimkeys::Union{Nothing, Dict{Symbol, Vector{T} where T <: DimensionKeyTypes}}=nothing) - if (ncomps = length(components(mi))) == 0 + if length(components(mi)) == 0 error("Cannot run the model: no components have been created.") end diff --git a/src/core/model.jl b/src/core/model.jl index 66ddde3df..24ed666fc 100644 --- a/src/core/model.jl +++ b/src/core/model.jl @@ -59,7 +59,7 @@ component parameter should only be calculated for the second timestep and beyond @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; +@delegate connect_param!(m::Model, dst::AbstractString, src::AbstractString, backup::Union{Nothing, Array}=nothing; ignoreunits::Bool=false, offset::Int=0) => md @@ -83,12 +83,11 @@ 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 +# TBD: these may not be needed as delegators @delegate set_external_param!(m::Model, name::Symbol, value::ModelParameter) => md -@delegate set_external_param!(m::Model, name::Symbol, value::Number; - param_dims::Union{Nothing,Array{Symbol}} = nothing) => md - -@delegate set_external_param!(m::Model, name::Symbol, value::Union{AbstractArray, AbstractRange, Tuple}; +@delegate set_external_param!(m::Model, name::Symbol, + value::Union{Number, AbstractArray, AbstractRange, Tuple}; param_dims::Union{Nothing,Array{Symbol}} = nothing) => md @delegate add_internal_param_conn!(m::Model, conn::InternalParameterConnection) => md @@ -122,12 +121,12 @@ model definition. """ add_comp!( m::Model, comp_id::ComponentId, comp_name::Symbol=comp_id.comp_name; - before::NothingSymbol=nothing, - after::NothingSymbol=nothing, + before::NothingSymbol=nothing, + after::NothingSymbol=nothing, rename::NothingPairList=nothing ) -Add the component indicated by `comp_id` to the model indicated by `m`. The component is added +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`. @@ -140,8 +139,8 @@ end """ add_comp!( m::Model, comp_def::AbstractComponentDef, comp_name::Symbol=comp_id.comp_name; - before::NothingSymbol=nothing, - after::NothingSymbol=nothing, + before::NothingSymbol=nothing, + after::NothingSymbol=nothing, rename::NothingPairList=nothing ) @@ -157,8 +156,8 @@ end """ replace_comp!( m::Model, comp_id::ComponentId, comp_name::Symbol=comp_id.comp_name; - before::NothingSymbol=nothing, - after::NothingSymbol=nothing, + before::NothingSymbol=nothing, + after::NothingSymbol=nothing, reconnect::Bool=true ) @@ -342,6 +341,7 @@ that they match the model's index labels. """ @delegate set_param!(m::Model, comp_name::Symbol, param_name::Symbol, value, dims=nothing) => md +# Deprecated? """ set_param!(m::Model, path::AbstractString, param_name::Symbol, value, dims=nothing) @@ -351,6 +351,7 @@ relative to `m.md`, which at the top of the hierarchy, produces the same result """ @delegate set_param!(m::Model, path::AbstractString, param_name::Symbol, value, dims=nothing) => md +# Deprecated? """ set_param!(m::Model, path::AbstractString, value, dims=nothing) @@ -365,8 +366,10 @@ Set the value of a parameter exposed in the ModelDef (m.md). """ @delegate set_param!(m::Model, param_name::Symbol, value, dims=nothing) => md +# Deprecated? @delegate set_param!(m::Model, comp_path::ComponentPath, param_name::Symbol, value, dims=nothing) => md +@delegate import_params!(m::Model) => md """ run(m::Model) diff --git a/src/core/paths.jl b/src/core/paths.jl index 2bcb36fb1..ffecac76b 100644 --- a/src/core/paths.jl +++ b/src/core/paths.jl @@ -24,58 +24,61 @@ Base.joinpath(p1::ComponentPath, other...) = joinpath(joinpath(p1, other[1]), ot _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. +For composites, also update the component paths for all 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 + parent_path = pathof(parent) 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) + # do same recursively + for grandchild in compdefs(child) + # @info "recursively fixing comp path: child: $(pathof(child)), grandchild: $(pathof(grandchild))" + _fix_comp_path!(grandchild, child) + end # 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) + src_path = ComponentPath(child_path, tail(conn.src_comp_path)) + dst_path = ComponentPath(child_path, tail(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) + 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) + for (name, ref) in ns(child) + if ref isa AbstractDatumReference + T = typeof(ref) + # @info "parent_path: $parent_path ref.comp_path: $(ref.comp_path)" + ref_comp = find_comp(parent, pathof(ref)) + child[name] = new_ref = T(ref.name, root, pathof(ref_comp)) + # @info "new path: $(new_ref.comp_path)" + end end - for cd in compdefs(child) - _fix_comp_path!(cd, child) + defaults = child.defaults + for (i, ref) in enumerate(defaults) + ref_comp = find_comp(parent, pathof(ref)) + defaults[i] = ParameterDefReference(ref.name, root, pathof(ref_comp), ref.default) end + else for datum in [variables(child)..., parameters(child)...] + # @info "Resetting leaf IPC from $(datum.comp_path) to $child_path" datum.comp_path = child_path end end @@ -86,7 +89,7 @@ end 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 +DatumReferences in the namespace. For leaf components, we also update the ComponentPath for ParameterDefs and VariableDefs. """ function fix_comp_paths!(md::AbstractModelDef) @@ -166,7 +169,7 @@ end find_comp(dr::AbstractDatumReference) = find_comp(dr.root, dr.comp_path) -find_comp(cr::ComponentReference) = find_comp(cr.parent, cr.comp_path) +find_comp(cr::AbstractComponentReference) = find_comp(parent(cr), pathof(cr)) """ Return the relative path of `descendant` if is within the path of composite `ancestor` or diff --git a/src/core/references.jl b/src/core/references.jl index 12657a488..275281045 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.parent, ref.comp_path, name, value) + set_param!(parent(ref), pathof(ref), name, value) end """ - set_param!(ref.parent, ref.comp_name, name, value) + Base.setindex!(ref::ComponentReference, value, name::Symbol) Set a component parameter as `reference[symbol] = value`. """ function Base.setindex!(ref::ComponentReference, value, name::Symbol) - set_param!(ref.parent, ref.comp_path, name, value) + set_param!(parent(ref), pathof(ref), name, value) end """ @@ -22,40 +22,58 @@ 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.parent, dst.comp_path, dst_name, src.comp_path, src_name) + connect_param!(parent(dst), pathof(dst), dst_name, pathof(src), src_name) end """ connect_param!(dst::ComponentReference, src::ComponentReference, name::Symbol) - + 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.parent, dst.comp_path, name, src.comp_path, name) + connect_param!(parent(dst), pathof(dst), name, pathof(src), name) end - """ - Base.getindex(comp_ref::ComponentReference, var_name::Symbol) + Base.getindex(ref::ComponentReference, name::Symbol) -Get a variable reference as `comp_ref[var_name]`. +Get a sub-comp, parameter, or variable reference as `ref[name]`. """ -function Base.getindex(comp_ref::ComponentReference, var_name::Symbol) - VariableReference(comp_ref, var_name) +function Base.getindex(ref::ComponentReference, name::Symbol) + VariableReference(ref, name) +end + +# Methods to convert components, params, and vars to references +# for use with getproperty() chaining. +function _make_reference(obj::AbstractComponentDef, comp_ref::ComponentReference) + return ComponentReference(parent(obj), nameof(obj)) +end + +function _make_reference(obj::VariableDef, comp_ref::ComponentReference) + return VariableReference(comp_ref, obj.name) +end + +function _make_reference(obj::ParameterDef, comp_ref::ComponentReference) + return ParameterReference(comp_ref, obj.name) +end + +function Base.getproperty(ref::ComponentReference, name::Symbol) + comp = find_comp(ref) + return _make_reference(getfield(comp, name), ref) # might be ref to comp, var, or param 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] + return head(pathof(ref1)) == head(pathof(ref2)) end """ Base.setindex!(comp_ref::ComponentReference, var_ref::VariableReference, var_name::Symbol) - + Connect two components as `comp_ref[var_name] = var_ref`. """ -function Base.setindex!(comp_ref::ComponentReference, var_ref::VariableReference, var_name::Symbol) - _same_composite(comp_ref, var_ref)|| error("Can't connect variables defined in different composite trees") +function Base.setindex!(comp_ref::ComponentReference, var_ref::VariableReference, vname::Symbol) + _same_composite(comp_ref, var_ref) || error("Can't connect variables defined in different composite trees") - connect_param!(comp_ref.parent, comp_ref.comp_path, var_name, var_ref.comp_path, var_ref.var_name) + connect_param!(parent(comp_ref), pathof(comp_ref), vname, pathof(var_ref), var_name(var_ref)) end diff --git a/src/core/show.jl b/src/core/show.jl index 1e5eca90e..c8667caab 100644 --- a/src/core/show.jl +++ b/src/core/show.jl @@ -188,8 +188,13 @@ function show(io::IO, obj::AbstractComponentDef) end end -function show(io::IO, obj::AbstractDatumReference) - print(io, nameof(typeof(obj)), "(name=:$(obj.name) path=$(obj.comp_path))") +function show(io::IO, obj::VariableDefReference) + print(io, "VariableDefReference(name=:$(obj.name) path=$(obj.comp_path))") +end + +function show(io::IO, obj::ParameterDefReference) + default = printable(obj.default) + print(io, "ParameterDefReference(name=:$(obj.name) path=$(obj.comp_path) default=$default)") end function show(io::IO, obj::ModelInstance) diff --git a/src/core/types/core.jl b/src/core/types/core.jl index 9692d5c4b..7d86b8d7f 100644 --- a/src/core/types/core.jl +++ b/src/core/types/core.jl @@ -47,6 +47,9 @@ ComponentPath(path1::ComponentPath, path2::ComponentPath) = ComponentPath(path1. ComponentPath(::Nothing, name::Symbol) = ComponentPath(name) +# Convert path string like "/foo/bar/baz" to ComponentPath(:foo, :bar, :baz) +ComponentPath(path::AbstractString) = ComponentPath([Symbol(s) for s in split(path, "/") if s != ""]...) + const ParamPath = Tuple{ComponentPath, Symbol} # @@ -87,4 +90,3 @@ end mutable struct RangeDimension{T <: DimensionRangeTypes} <: AbstractDimension range::T end - diff --git a/src/core/types/defs.jl b/src/core/types/defs.jl index f546dd6e8..449c8db0e 100644 --- a/src/core/types/defs.jl +++ b/src/core/types/defs.jl @@ -27,6 +27,8 @@ Base.nameof(obj::AbstractNamedObj) = obj.name unit::String end +Base.pathof(obj::AbstractDatumDef) = obj.comp_path + @class mutable VariableDef <: DatumDef @class mutable ParameterDef <: DatumDef begin @@ -65,9 +67,11 @@ end 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.comp_path = nothing # this is set in add_comp!() and ModelDef() + self.comp_path = ComponentPath(name) # 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 @@ -84,7 +88,7 @@ end ns(obj::AbstractComponentDef) = obj.namespace comp_id(obj::AbstractComponentDef) = obj.comp_id -pathof(obj::AbstractComponentDef) = obj.comp_path +Base.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 @@ -97,7 +101,6 @@ 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 @@ -108,22 +111,54 @@ end comp_path::ComponentPath end -@class ParameterDefReference <: DatumReference +Base.pathof(dr::AbstractDatumReference) = dr.comp_path + +# +# TBD: rather than store an array of these, perhaps this class should store +# multiple references, since there is only one name and (at most) one default +# value. It might look like this: +# +# @class DatumReference <: NamedObj +# +# struct UnnamedReference +# root::AbstractComponentDef +# comp_path::ComponentPath +# end +# +# @class ParameterDefReference <: DatumReference begin +# name::Symbol is inherited from NamedObj +# default::Any # allows defaults set in composites +# refs::Vector{UnnammedReference} +# end +# +# DatumReference would become simpler, with no new fields beyond those in NamedObj, +# with the root and comp_path moved into VariableDefReference instead. +# +# @class VariableDefReference <: DatumReference begin +# name::Symbol is inherited from NamedObj +# unnamed_ref::UnnamedReference +# end +# + +@class ParameterDefReference <: DatumReference begin + default::Any # allows defaults set in composites +end + +function ParameterDefReference(name::Symbol, root::AbstractComponentDef, + comp_path::ComponentPath) + return ParameterDefReference(name, root, comp_path, nothing) +end @class VariableDefReference <: DatumReference -function _dereference(ref::AbstractDatumReference) +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}} +# convert(::Type{VariableDef}, ref::VariableDefReference) = dereference(ref) +# convert(::Type{ParameterDef}, ref::ParameterDefReference) = dereference(ref) # Define which types can appear in the namespace dict for leaf and composite compdefs global const LeafNamespaceElement = AbstractDatumDef @@ -131,15 +166,14 @@ global const CompositeNamespaceElement = Union{AbstractComponentDef, AbstractDat 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} + # store default values at the composite level until ModelDef is built + defaults::Vector{ParameterDefReference} + sorted_comps::Union{Nothing, Vector{Symbol}} function CompositeComponentDef(comp_id::Union{Nothing, ComponentId}=nothing) @@ -152,11 +186,9 @@ global const NamespaceElement = Union{LeafNamespaceElement, CompositeNa 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.defaults = Vector{ParameterDefReference}() self.sorted_comps = nothing end end @@ -180,9 +212,6 @@ end add_backup!(obj::AbstractCompositeComponentDef, backup) = push!(obj.backups, backup) 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 @@ -195,16 +224,28 @@ ComponentPath(obj::AbstractCompositeComponentDef, path::AbstractString) = comp_p ComponentPath(obj::AbstractCompositeComponentDef, names::Symbol...) = ComponentPath(obj.comp_path.names..., names...) @class mutable ModelDef <: CompositeComponentDef begin + external_param_conns::Vector{ExternalParameterConnection} + external_params::Dict{Symbol, ModelParameter} + 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 + + ext_conns = Vector{ExternalParameterConnection}() + ext_params = Dict{Symbol, ModelParameter}() + + # N.B. @class-generated method + return ModelDef(self, ext_conns, ext_params, number_type, false) end end +external_param_conns(md::ModelDef) = md.external_param_conns + +external_params(md::ModelDef) = md.external_params + # # Reference types offer a more convenient syntax for interrogating Components. # @@ -215,8 +256,12 @@ end comp_path::ComponentPath end +# Define access methods via getfield() since we override dot syntax +Base.parent(comp_ref::AbstractComponentReference) = getfield(comp_ref, :parent) +Base.pathof(comp_ref::AbstractComponentReference) = getfield(comp_ref, :comp_path) + function ComponentReference(parent::AbstractComponentDef, name::Symbol) - return ComponentReference(parent, ComponentPath(parent.comp_path, name)) + return ComponentReference(parent, ComponentPath(pathof(parent), name)) end # A container for a variable within a component, to improve connect_param! aesthetics, @@ -224,3 +269,5 @@ end @class VariableReference <: ComponentReference begin var_name::Symbol end + +var_name(comp_ref::VariableReference) = getfield(comp_ref, :var_name) diff --git a/src/core/types/instances.jl b/src/core/types/instances.jl index 3e29f1f27..5e5c58ef0 100644 --- a/src/core/types/instances.jl +++ b/src/core/types/instances.jl @@ -49,7 +49,7 @@ 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. -# All values in the dictionary are vectors of Ints, except the `:time` value, which is a +# All values in the dictionary are vectors of Ints, except the `:time` value, which is a # vector of AbstractTimesteps, so that `d.time` returns values that can be used for indexing # into timestep arrays. struct DimValueDict <: MimiStruct @@ -86,8 +86,8 @@ Base.getproperty(obj::DimValueDict, property::Symbol) = getfield(obj, :dict)[pro # 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 - + end + function ComponentInstance(comp_def::AbstractComponentDef, time_bounds::Tuple{Int,Int}, name::Symbol=nameof(comp_def)) @@ -96,7 +96,7 @@ Base.getproperty(obj::DimValueDict, property::Symbol) = getfield(obj, :dict)[pro end end -@class mutable LeafComponentInstance{TV <: ComponentInstanceVariables, +@class mutable LeafComponentInstance{TV <: ComponentInstanceVariables, TP <: ComponentInstanceParameters} <: ComponentInstance begin variables::TV # TBD: write functions to extract these from type instead of storing? parameters::TP @@ -108,7 +108,7 @@ end vars::TV, pars::TP, time_bounds::Tuple{Int,Int}, name::Symbol=nameof(comp_def)) where - {TV <: ComponentInstanceVariables, + {TV <: ComponentInstanceVariables, TP <: ComponentInstanceParameters} # superclass initializer @@ -123,6 +123,11 @@ end # CompositeComponentInstances use a standard method that just loops over inner components. # TBD: use FunctionWrapper here? function get_func(name) + + # + # TBD: since this class is no longer a superclass of CompositeComponentInstance + # this test should be unnecessary. Double-check this though... + # if is_composite(self) return nothing end @@ -136,8 +141,6 @@ end 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") @@ -161,7 +164,7 @@ end # These can be called on CompositeComponentInstances and ModelInstances compdef(obj::AbstractComponentInstance) = compdef(comp_id(obj)) -pathof(obj::AbstractComponentInstance) = obj.comp_path +Base.pathof(obj::AbstractComponentInstance) = obj.comp_path first_period(obj::AbstractComponentInstance) = obj.first last_period(obj::AbstractComponentInstance) = obj.last @@ -176,33 +179,6 @@ function _comp_instance_vars_pars(comp_def::AbstractCompositeComponentDef, 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])) diff --git a/src/core/types/params.jl b/src/core/types/params.jl index eef2b8275..da70b8b04 100644 --- a/src/core/types/params.jl +++ b/src/core/types/params.jl @@ -77,4 +77,7 @@ 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 +end + +Base.pathof(obj::ExternalParameterConnection) = obj.comp_path +Base.nameof(obj::ExternalParameterConnection) = obj.param_name diff --git a/test/dependencies/run_dependency_tests.jl b/test/dependencies/run_dependency_tests.jl index c959201de..ba6688688 100644 --- a/test/dependencies/run_dependency_tests.jl +++ b/test/dependencies/run_dependency_tests.jl @@ -1,8 +1,8 @@ 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.9", "MimiFUND") + ("https://github.com/lrennels/mimi-rice-2010.jl.git", "mimi-next", "MimiRICE2010"), + ("https://github.com/fund-model/MimiFUND.jl.git", "mimi-next", "MimiFUND") ] for (pkg_url, pkg_rev, pkg_name) in packages_to_test diff --git a/test/runtests.jl b/test/runtests.jl index fbf63b6df..ae8003585 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,8 +2,8 @@ using Pkg # We need these for the doctests. We install them before we load any # package so that we don't run into precompile problems -Pkg.add(PackageSpec(url="https://github.com/fund-model/MimiFUND.jl", rev="master")) -Pkg.add(PackageSpec(url="https://github.com/anthofflab/MimiDICE2010.jl", rev="master")) +Pkg.add(PackageSpec(url="https://github.com/fund-model/MimiFUND.jl", rev="mimi-next")) +Pkg.add(PackageSpec(url="https://github.com/anthofflab/MimiDICE2010.jl", rev="mimi-next")) using Mimi import Electron diff --git a/test/test_composite.jl b/test/test_composite.jl index 575564702..2902699e4 100644 --- a/test/test_composite.jl +++ b/test/test_composite.jl @@ -4,9 +4,9 @@ using Test using Mimi import Mimi: - ComponentId, ComponentPath, DatumReference, ComponentDef, AbstractComponentDef, CompositeComponentDef, - Binding, ModelDef, build, time_labels, compdef, find_comp - + ComponentId, ComponentPath, DatumReference, ComponentDef, AbstractComponentDef, + CompositeComponentDef, ModelDef, build, time_labels, compdef, find_comp, + import_params! @defcomp Comp1 begin par_1_1 = Parameter(index=[time]) # external input @@ -55,37 +55,43 @@ m = Model() set_dimension!(m, :time, 2005:2020) @defcomposite A begin - component(Comp1) - component(Comp2) + Component(Comp1) + Component(Comp2) + + foo1 = Parameter(Comp1.foo) + foo2 = Parameter(Comp2.foo) - foo1 = Comp1.foo - foo2 = Comp2.foo + var_2_1 = Variable(Comp2.var_2_1) - # 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 + connect(Comp2.par_2_1, Comp1.var_1_1) + connect(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) + Component(Comp3) + Component(Comp4) + + foo3 = Parameter(Comp3.foo) + foo4 = Parameter(Comp4.foo) - foo3 = Comp3.foo - foo4 = Comp4.foo + var_3_1 = Variable(Comp3.var_3_1) end @defcomposite top begin - component(A) + Component(A) - fooA1 = A.foo1 - fooA2 = A.foo2 + fooA1 = Parameter(A.foo1) + fooA2 = Parameter(A.foo2) # TBD: component B isn't getting added to mi - component(B) - foo3 = B.foo3 - foo4 = B.foo4 + Component(B) + foo3 = Parameter(B.foo3) + foo4 = Parameter(B.foo4) + + var_3_1 = Variable(B.Comp3.var_3_1) + + connect(B.par_3_1, A.var_2_1) + connect(B.par_4_1, B.var_3_1) end # We have created the following composite structure: @@ -115,37 +121,21 @@ c2 = md[:top][:A][:Comp2] 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) +set_param!(m, :fooA1, 1) +set_param!(m, :fooA2, 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, :foo3, 10) +set_param!(m, :foo4, 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") +set_param!(m, :par_1_1, collect(1:length(time_labels(md)))) 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) @@ -155,12 +145,27 @@ mi = m.mi @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) +# +# Test joining external params. +# +m2 = Model() +set_dimension!(m2, :time, 2005:2020) + +@defcomposite top2 begin + Component(Comp1) + Component(Comp2) + + connect(Comp2.par_2_1, Comp1.var_1_1) + connect(Comp2.par_2_2, Comp1.var_1_1) +end + +top2_ref = add_comp!(m2, top2, nameof(top2)) + 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) +using Mimi +m2 = TestComposite.m2 +md2 = m2.md +top2 = Mimi.find_comp(md2, :top2) nothing diff --git a/test/test_composite_simple.jl b/test/test_composite_simple.jl new file mode 100644 index 000000000..de1fa70c7 --- /dev/null +++ b/test/test_composite_simple.jl @@ -0,0 +1,64 @@ +#module TestCompositeSimple + +using Test +using Mimi + +import Mimi: + ComponentId, ComponentPath, DatumReference, ComponentDef, AbstractComponentDef, + CompositeComponentDef, ModelDef, build, time_labels, compdef, find_comp, + import_params! + +@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 + 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 + end +end + +@defcomposite A begin + Component(Comp1) + Component(Comp2) + + foo1 = Parameter(Comp1.foo) + foo2 = Parameter(Comp2.foo, default=100) + + connect(Comp2.par_2_1, Comp1.var_1_1) +end + +@defcomposite Top begin + Component(A) + + fooA1 = Parameter(A.foo1) + fooA2 = Parameter(A.foo2) +end + +m = Model() +md = m.md + +set_dimension!(m, :time, 2005:2020) + +add_comp!(m, Top) +top = md[:Top] + +set_param!(m, :Top, :fooA1, 10) + +import_params!(m) +set_param!(m, :par_1_1, 1:16) + +build(m) +run(m) + +#end diff --git a/test/test_composite_simple2.jl b/test/test_composite_simple2.jl new file mode 100644 index 000000000..d6b23dee1 --- /dev/null +++ b/test/test_composite_simple2.jl @@ -0,0 +1,58 @@ +#module TestCompositeSimple + +using Test +using Mimi + +import Mimi: + ComponentId, ComponentPath, DatumReference, ComponentDef, AbstractComponentDef, + CompositeComponentDef, ModelDef, build, time_labels, compdef, find_comp, + import_params! + +@defcomp Leaf begin + p1 = Parameter() + v1 = Variable() + + function run_timestep(p, v, d, t) + v.v1 = p.p1 + end +end + +@defcomposite Intermediate begin + Component(Leaf) + v1 = Variable(Leaf.v1) +end + +@defcomposite Top begin + Component(Intermediate) + v = Variable(Intermediate.v1) + p = Parameter(Intermediate.p1) + connect(Intermediate.p1, Intermediate.v1) +end + + +m = Model() +md = m.md +set_dimension!(m, :time, 2005:2020) + +add_comp!(m, Top) + +top = md[:Top] +inter = top[:Intermediate] +leaf = inter[:Leaf] + +# +# Two ways to set a value in a subcomponent of the ModelDef: +# +use_import = false +if use_import + # import into model and reference it there + import_params!(m) + set_param!(m, :p, 10) +else + # or, reference :p in Top, + set_param!(m, :Top, :p, 10) +end + +build(m) +run(m) +#end diff --git a/test/test_defcomposite.jl b/test/test_defcomposite.jl index 7d9ec9ab0..2bd305d08 100644 --- a/test/test_defcomposite.jl +++ b/test/test_defcomposite.jl @@ -4,7 +4,7 @@ using Test using Mimi using MacroTools -import Mimi: ComponentPath, build, @defmodel +import Mimi: ComponentPath, build, import_params! @defcomp Comp1 begin par_1_1 = Parameter(index=[time]) # external input @@ -28,45 +28,35 @@ end end @defcomposite A begin - component(Comp1) - component(Comp2) + Component(Comp1) + Component(Comp2) # imports - bar = Comp1.par_1_1 - foo2 = Comp2.foo + bar = Parameter(Comp1.par_1_1) + foo2 = Parameter(Comp2.foo) # linked imports - # foo = Comp1.foo, Comp2.foo - - foo1 = Comp1.foo - foo2 = Comp2.foo + foo = Parameter(Comp1.foo, Comp2.foo) # connections - Comp2.par_2_1 = Comp1.var_1_1 - Comp2.par_2_2 = Comp1.var_1_1 + connect(Comp2.par_2_1, Comp1.var_1_1) + connect(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)) +#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?? +import_params!(m.md) # so we can set params at top-level +set_param!(m, :par_1_1, 2:2:2*length(years)) + build(m) run(m) diff --git a/test/test_explorer_sim.jl b/test/test_explorer_sim.jl index ba74fd29d..b779137d9 100644 --- a/test/test_explorer_sim.jl +++ b/test/test_explorer_sim.jl @@ -7,7 +7,7 @@ using Distributions using Query using CSVFiles -import Mimi: +import Mimi: _spec_for_sim_item, menu_item_list, getdataframe, get_sim_results # Get the example @@ -37,11 +37,12 @@ sd = @defsim begin Region3 => Uniform(0.10, 0.20)] sampling(LHSData, corrlist=[(:name1, :name2, 0.7), (:name1, :name3, 0.5)]) - + # indicate which parameters to save for each model run. Specify # a parameter name or [later] some slice of its data, similar to the # assignment of RVs, above. - save(grosseconomy.K, grosseconomy.YGROSS, grosseconomy.share_var, grosseconomy.depk_var, emissions.E, emissions.E_Global) + save(grosseconomy.K, grosseconomy.YGROSS, grosseconomy.share_var, grosseconomy.depk_var, + emissions.E, emissions.E_Global) end si = run(sd, m, N) @@ -49,7 +50,8 @@ results_output_dir = tempdir() si_disk = run(sd, m, N; results_output_dir = results_output_dir, results_in_memory = false) ## 1. Specs and Menu -pairs = [(:grosseconomy, :K), (:grosseconomy, :YGROSS), (:grosseconomy, :share_var), (:grosseconomy, :depk_var), (:emissions, :E), (:emissions, :E_Global)] +pairs = [(:grosseconomy, :K), (:grosseconomy, :YGROSS), (:grosseconomy, :share_var), + (:grosseconomy, :depk_var), (:emissions, :E), (:emissions, :E_Global)] for (comp, var) in pairs results = get_sim_results(si, comp, var) diff --git a/test/test_main_variabletimestep.jl b/test/test_main_variabletimestep.jl index 77da9b47d..c374e95c2 100644 --- a/test/test_main_variabletimestep.jl +++ b/test/test_main_variabletimestep.jl @@ -3,9 +3,9 @@ module TestMain_VariableTimestep using Test using Mimi -import Mimi: - reset_variables, @defmodel, - variable, variable_names, external_param, build, +import Mimi: + reset_variables, @defmodel, + variable, variable_names, external_param, build, compdef, compdefs, dimension, compinstance @defcomp foo1 begin diff --git a/test/test_model_structure.jl b/test/test_model_structure.jl index 012840465..cba4c7f81 100644 --- a/test/test_model_structure.jl +++ b/test/test_model_structure.jl @@ -55,11 +55,11 @@ add_comp!(m, C, after=:B) connect_param!(m, :A, :parA, :C, :varC) -unconn = unconnected_params(m) -@test length(unconn) == 1 - +unconns = unconnected_params(m) +@test length(unconns) == 1 c = compdef(m, :C) -@test unconn[1] == (c.comp_path, :parC) +uconn = unconns[1] +@test (pathof(uconn), nameof(uconn)) == (c.comp_path, :parC) connect_param!(m, :C => :parC, :B => :varB) diff --git a/test/test_model_structure_variabletimestep.jl b/test/test_model_structure_variabletimestep.jl index 2e28a0b24..da9429f70 100644 --- a/test/test_model_structure_variabletimestep.jl +++ b/test/test_model_structure_variabletimestep.jl @@ -7,7 +7,7 @@ using Mimi import Mimi: connect_param!, unconnected_params, set_dimension!, has_comp, - get_connections, internal_param_conns, dim_count, + get_connections, internal_param_conns, dim_count, dim_names, compdef, getproperty, setproperty!, dimension, compdefs @defcomp A begin @@ -63,10 +63,11 @@ add_comp!(m, C, after=:B) # test a later first than model connect_param!(m, :A, :parA, :C, :varC) -unconn = unconnected_params(m) -@test length(unconn) == 1 +unconns = unconnected_params(m) +@test length(unconns) == 1 c = compdef(m, :C) -@test unconn[1] == (c.comp_path, :parC) +uconn = unconns[1] +@test (pathof(uconn), nameof(uconn)) == (c.comp_path, :parC) connect_param!(m, :C => :parC, :B => :varB) diff --git a/test/test_parametertypes.jl b/test/test_parametertypes.jl index 4e96b6d38..d45f3c7d2 100644 --- a/test/test_parametertypes.jl +++ b/test/test_parametertypes.jl @@ -3,9 +3,9 @@ module TestParameterTypes using Mimi using Test -import Mimi: - external_params, external_param, TimestepMatrix, TimestepVector, - ArrayModelParameter, ScalarModelParameter, FixedTimestep +import Mimi: + external_params, external_param, TimestepMatrix, TimestepVector, + ArrayModelParameter, ScalarModelParameter, FixedTimestep, build, import_params! # # Test that parameter type mismatches are caught @@ -52,7 +52,7 @@ eval(expr) # Just a deprecation warning for v0.10, then will change to error in g = Parameter{Int}(default=10.0) # value should be Int despite Float64 default h = Parameter(default=10) # should be "numtype", despite Int default j::Int = Parameter(index = [regions]) - + function run_timestep(p, v, d, t) end end @@ -74,10 +74,13 @@ set_param!(m, :MyComp, :e, [1,2,3,4]) set_param!(m, :MyComp, :f, reshape(1:16, 4, 4)) set_param!(m, :MyComp, :j, [1,2,3]) +build(m) # applies defaults, creating external params extpars = external_params(m) +# TBD: These are not (yet) external params. Defaults are applied at build time. @test isa(extpars[:a], ArrayModelParameter) @test isa(extpars[:b], ArrayModelParameter) + @test isa(extpars[:c], ArrayModelParameter) @test isa(extpars[:d], ScalarModelParameter) @test isa(extpars[:e], ArrayModelParameter) @@ -85,6 +88,7 @@ extpars = external_params(m) @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{arrtype, 1} @@ -96,6 +100,7 @@ extpars = external_params(m) @test_throws ErrorException update_param!(m, :a, 5) # expects an array @test_throws ErrorException update_param!(m, :a, ones(101)) # wrong size @test_throws ErrorException update_param!(m, :a, fill("hi", 101, 3)) # wrong type + update_param!(m, :a, Array{Int,2}(zeros(101, 3))) # should be able to convert from Int to Float @test_throws ErrorException update_param!(m, :d, ones(5)) # wrong type; should be scalar @@ -107,6 +112,7 @@ update_param!(m, :e, [4,5,6,7]) @test length(extpars) == 9 @test typeof(extpars[:a].values) == TimestepMatrix{FixedTimestep{2000, 1}, arrtype, 1} + @test typeof(extpars[:d].value) == numtype @test typeof(extpars[:e].values) == Array{arrtype, 1} @@ -115,8 +121,8 @@ update_param!(m, :e, [4,5,6,7]) # Test updating TimestepArrays with update_param! #------------------------------------------------------------------------------ -@defcomp MyComp2 begin - x=Parameter(index=[time]) +@defcomp MyComp2 begin + x=Parameter(index=[time]) y=Variable(index=[time]) function run_timestep(p,v,d,t) v.y[t]=p.x[t] @@ -187,7 +193,7 @@ m = Model() set_dimension!(m, :time, [2000, 2005, 2020]) add_comp!(m, MyComp2) set_param!(m, :MyComp2, :x, [1, 2, 3]) - + set_dimension!(m, :time, [2005, 2020, 2050]) update_params!(m, Dict(:x=>[2, 3, 4]), update_timesteps = true) @@ -220,12 +226,12 @@ run(m) # 5. Test all the warning and error cases -@defcomp MyComp3 begin +@defcomp MyComp3 begin regions=Index() x=Parameter(index=[time]) # One timestep array parameter y=Parameter(index=[regions]) # One non-timestep array parameter z=Parameter() # One scalar parameter -end +end m = Model() # Build the model set_dimension!(m, :time, 2000:2002) # Set the time dimension diff --git a/test/test_timesteparrays.jl b/test/test_timesteparrays.jl index 86b39e8a4..8160d89c6 100644 --- a/test/test_timesteparrays.jl +++ b/test/test_timesteparrays.jl @@ -4,7 +4,7 @@ using Mimi using Test import Mimi: - FixedTimestep, VariableTimestep, TimestepVector, TimestepMatrix, TimestepArray, next_timestep, hasvalue, + FixedTimestep, VariableTimestep, TimestepVector, TimestepMatrix, TimestepArray, next_timestep, hasvalue, isuniform, first_period, last_period, first_and_step function reset_time_val(arr, vals::Array{T, 1}) where T @@ -29,11 +29,17 @@ end @test first_and_step([2,3,4]) == (2,1) @test first_and_step([1:2:10...]) == (1,2) +# shorthand for stuff used a lot +idx1 = TimestepIndex(1) +idx2 = TimestepIndex(2) +idx3 = TimestepIndex(3) +idx4 = TimestepIndex(4) + #------------------------------------------------------------------------------ -# 1. Test TimestepVector - Fixed Timestep +# 1. Test TimestepVector - Fixed Timestep #------------------------------------------------------------------------------ -#1a. test constructor, lastindex, and length (with both +#1a. test constructor, lastindex, and length (with both # matching years and mismatched years) x = TimestepVector{FixedTimestep{2000, 1}, Int}([9, 10, 11, 12]) @@ -53,13 +59,13 @@ x[:] = temp_dim_val reset_time_val(x, time_dim_val) # TimestepValue and TimestepIndex Indexing -@test x[TimestepIndex(1)] == time_dim_val[1] -@test x[TimestepIndex(1) + 1] == time_dim_val[2] -@test x[TimestepIndex(4)] == time_dim_val[4] +@test x[idx1] == time_dim_val[1] +@test x[idx1 + 1] == time_dim_val[2] +@test x[idx4] == time_dim_val[4] @test_throws ErrorException x[TimestepIndex(5)] -x[TimestepIndex(1)] = temp_dim_val[1] -@test x[TimestepIndex(1)] == temp_dim_val[1] +x[idx1] = temp_dim_val[1] +@test x[idx1] == temp_dim_val[1] reset_time_val(x, time_dim_val) @test x[TimestepValue(2000)] == time_dim_val[1] @@ -76,7 +82,7 @@ reset_time_val(x, time_dim_val) t = FixedTimestep{2001, 1, 3000}(1) @test hasvalue(x, t) -@test !hasvalue(x, FixedTimestep{2000, 1, 2012}(10)) +@test !hasvalue(x, FixedTimestep{2000, 1, 2012}(10)) @test x[t] == 10 t2 = next_timestep(t) @@ -93,12 +99,12 @@ reset_time_val(x, time_dim_val) # Deprecated int indexing should still run @test x[3] == time_dim_val[3] -x[3] = temp_dim_val[3] -@test x[3] == temp_dim_val[3] +x[TimestepIndex(3)] = temp_dim_val[3] +@test x[TimestepIndex(3)] == temp_dim_val[3] reset_time_val(x, time_dim_val) #------------------------------------------------------------------------------ -# 2. Test TimestepVector - Variable Timestep +# 2. Test TimestepVector - Variable Timestep #------------------------------------------------------------------------------ years = (2000, 2005, 2015, 2025) @@ -117,13 +123,13 @@ x[:] = temp_dim_val reset_time_val(x, time_dim_val) # TimestepValue and TimestepIndex Indexing -@test x[TimestepIndex(1)] == time_dim_val[1] -@test x[TimestepIndex(1) + 1] == time_dim_val[2] -@test x[TimestepIndex(4)] == time_dim_val[4] +@test x[idx1] == time_dim_val[1] +@test x[idx1 + 1] == time_dim_val[2] +@test x[idx4] == time_dim_val[4] @test_throws ErrorException x[TimestepIndex(5)] -x[TimestepIndex(1)] = temp_dim_val[1] -@test x[TimestepIndex(1)] == temp_dim_val[1] +x[idx1] = temp_dim_val[1] +@test x[idx1] == temp_dim_val[1] reset_time_val(x, time_dim_val) @test x[TimestepValue(2000)] == time_dim_val[1] @@ -142,8 +148,8 @@ reset_time_val(x, time_dim_val) y2 = Tuple([2005:5:2010; 2015:10:3000]) t = VariableTimestep{y2}() -@test hasvalue(x, t) -@test !hasvalue(x, VariableTimestep{years}(time_dim_val[2])) +@test hasvalue(x, t) +@test !hasvalue(x, VariableTimestep{years}(time_dim_val[2])) @test x[t] == time_dim_val[2] t2 = next_timestep(t) @@ -158,22 +164,21 @@ x[t3] = temp_dim_val[1] @test x[t3] == temp_dim_val[1] reset_time_val(x, time_dim_val) -# Deprecated int indexing should still run -@test x[3] == time_dim_val[3] -x[3] = temp_dim_val[3] -@test x[3] == temp_dim_val[3] +@test x[TimestepIndex(3)] == time_dim_val[3] +x[TimestepIndex(3)] = temp_dim_val[3] +@test x[TimestepIndex(3)] == temp_dim_val[3] reset_time_val(x, time_dim_val) #------------------------------------------------------------------------------ -# 3. Test TimestepMatrix - Fixed Timestep +# 3. Test TimestepMatrix - Fixed Timestep #------------------------------------------------------------------------------ for ti = 1:2 - #3a. test constructor (with both matching years + #3a. test constructor (with both matching years # and mismatched years) - y = TimestepMatrix{FixedTimestep{2000, 1}, Int, ti}(collect(reshape(1:8, 4, 2))) + y = TimestepMatrix{FixedTimestep{2000, 1}, Int, ti}(collect(reshape(1:8, 4, 2))) z = TimestepMatrix{FixedTimestep{2000, 2}, Int, ti}(collect(reshape(1:8, 4, 2))) time_dim_val = collect(reshape(1:8, 4, 2)) @@ -199,18 +204,18 @@ for ti = 1:2 # TimestepValue and TimestepIndex Indexing if ti == 1 - @test y[TimestepIndex(1), 1] == time_dim_val[1,1] - @test y[TimestepIndex(1), 2] == time_dim_val[1,2] - @test y[TimestepIndex(1) + 1, 1] == time_dim_val[2,1] - @test y[TimestepIndex(4), 2] == time_dim_val[4,2] + @test y[idx1, 1] == time_dim_val[1,1] + @test y[idx1, 2] == time_dim_val[1,2] + @test y[idx1 + 1, 1] == time_dim_val[2,1] + @test y[idx4, 2] == time_dim_val[4,2] @test_throws ErrorException y[TimestepIndex(5), 2] - y[TimestepIndex(1), 1] = temp_dim_val[1] - @test y[TimestepIndex(1), 1] == temp_dim_val[1] + y[idx1, 1] = temp_dim_val[1] + @test y[idx1, 1] == temp_dim_val[1] reset_time_val(y, time_dim_val) - @test y[TimestepValue(2000), 1] == time_dim_val[1, 1] - @test y[TimestepValue(2000), 2] == time_dim_val[1, 2] + @test y[TimestepValue(2000), 1] == time_dim_val[1, 1] + @test y[TimestepValue(2000), 2] == time_dim_val[1, 2] @test y[TimestepValue(2001), :] == time_dim_val[2, :] @test y[TimestepValue(2000; offset = 1), 1] == time_dim_val[2,1] @test y[TimestepValue(2000) + 1, 1] == time_dim_val[2,1] @@ -221,17 +226,17 @@ for ti = 1:2 @test y[TimestepValue(2000), 1] == temp_dim_val[1] reset_time_val(y, time_dim_val) else - @test y[1, TimestepIndex(1)] == time_dim_val[1,1] - @test y[2, TimestepIndex(1)] == time_dim_val[2, 1] - @test y[1, TimestepIndex(1) + 1] == time_dim_val[1, 2] + @test y[1, idx1] == time_dim_val[1,1] + @test y[2, idx1] == time_dim_val[2, 1] + @test y[1, idx1 + 1] == time_dim_val[1, 2] @test y[2, TimestepIndex(2)] == time_dim_val[2,2] @test_throws ErrorException y[2, TimestepIndex(3)] - y[1, TimestepIndex(1)] = temp_dim_val[1] - @test y[1, TimestepIndex(1)] == temp_dim_val[1] + y[1, idx1] = temp_dim_val[1] + @test y[1, idx1] == temp_dim_val[1] reset_time_val(y, time_dim_val) - @test y[1, TimestepValue(2000)] == time_dim_val[1,1] + @test y[1, TimestepValue(2000)] == time_dim_val[1,1] @test y[2, TimestepValue(2000)] == time_dim_val[2,1] @test y[:, TimestepValue(2001)] == time_dim_val[:,2] @test y[1, TimestepValue(2000; offset = 1)] == time_dim_val[1,2] @@ -246,7 +251,7 @@ for ti = 1:2 # AbstractTimestep Indexing t = FixedTimestep{2001, 1, 3000}(1) - @test hasvalue(y, t, 1) + @test hasvalue(y, t, 1) @test !hasvalue(y, FixedTimestep{2000, 1, 3000}(10), 1) if ti == 1 @@ -280,7 +285,7 @@ for ti = 1:2 else @test y[1, t] == time_dim_val[1,2] @test y[2, t] == time_dim_val[2,2] - + t2 = FixedTimestep{2000, 1, 2005}(1) @test y[1, t2] == time_dim_val[1,1] @test y[2, t2] == time_dim_val[2,1] @@ -299,24 +304,23 @@ for ti = 1:2 @test z[2, t2] == time_dim_val[2,2] end - # Deprecated int indexing should still run - @test y[1,2] == time_dim_val[1,2] - @test z[1,2] == time_dim_val[1,2] - y[1,2] = temp_dim_val[1] - z[1,2] = temp_dim_val[1] - @test y[1,2] == z[1,2] == temp_dim_val[1] + @test y[idx1, 2] == time_dim_val[1,2] + @test z[idx1, 2] == time_dim_val[1,2] + y[idx1, 2] = temp_dim_val[1] + z[idx1, 2] = temp_dim_val[1] + @test y[idx1, 2] == z[idx1, 2] == temp_dim_val[1] reset_time_val(y, time_dim_val) reset_time_val(z, time_dim_val) end #------------------------------------------------------------------------------ -# 4. Test TimestepMatrix - Variable Timestep +# 4. Test TimestepMatrix - Variable Timestep #------------------------------------------------------------------------------ for ti = 1:2 - #4a. test constructor (with both matching years + #4a. test constructor (with both matching years # and mismatched years) if ti == 1 @@ -325,7 +329,6 @@ for ti = 1:2 years = (2000, 2005) end y = TimestepMatrix{VariableTimestep{years}, Int, ti}(collect(reshape(1:8, 4, 2))) - y = TimestepMatrix{VariableTimestep{years}, Int, ti}(collect(reshape(1:8, 4, 2))) time_dim_val = collect(reshape(1:8, 4, 2)) temp_dim_val = collect(reshape(100:107, 4, 2)) @@ -350,14 +353,14 @@ for ti = 1:2 # TimestepValue and TimestepIndex Indexing if ti == 1 - @test y[TimestepIndex(1), 1] == time_dim_val[1,1] - @test y[TimestepIndex(1), 2] == time_dim_val[1,2] - @test y[TimestepIndex(1) + 1, 1] == time_dim_val[2,1] - @test y[TimestepIndex(4), 2] == time_dim_val[4,2] + @test y[idx1, 1] == time_dim_val[1,1] + @test y[idx1, 2] == time_dim_val[1,2] + @test y[idx1 + 1, 1] == time_dim_val[2,1] + @test y[idx4, 2] == time_dim_val[4,2] @test_throws ErrorException y[TimestepIndex(5),2] - y[TimestepIndex(1), 1] = temp_dim_val[1,1] - @test y[TimestepIndex(1), 1] == temp_dim_val[1,1] + y[idx1, 1] = temp_dim_val[1,1] + @test y[idx1, 1] == temp_dim_val[1,1] reset_time_val(y, time_dim_val) @test y[TimestepValue(2000), 1] == time_dim_val[1,1] @@ -373,14 +376,14 @@ for ti = 1:2 @test y[TimestepValue(2015), 1] == temp_dim_val[3,1] reset_time_val(y, time_dim_val) else - @test y[1, TimestepIndex(1)] == time_dim_val[1,1] - @test y[2, TimestepIndex(1)] == time_dim_val[2,1] - @test y[1, TimestepIndex(1) + 1] == time_dim_val[1,2] + @test y[1, idx1] == time_dim_val[1,1] + @test y[2, idx1] == time_dim_val[2,1] + @test y[1, idx1 + 1] == time_dim_val[1,2] @test y[2, TimestepIndex(2)] == time_dim_val[2,2] @test_throws ErrorException y[2, TimestepIndex(3)] - y[1, TimestepIndex(1)] = temp_dim_val[1,1] - @test y[1, TimestepIndex(1)] == temp_dim_val[1,1] + y[1, idx1] = temp_dim_val[1,1] + @test y[1, idx1] == temp_dim_val[1,1] reset_time_val(y, time_dim_val) @test y[1, TimestepValue(2000)] == time_dim_val[1,1] @@ -399,8 +402,8 @@ for ti = 1:2 # AbstractTimestep Indexing t = VariableTimestep{Tuple([2005:5:2010; 2015:10:3000])}() if ti == 1 - @test hasvalue(y, t, time_dim_val[1]) - @test !hasvalue(y, VariableTimestep{years}(10)) + @test hasvalue(y, t, time_dim_val[1]) + @test !hasvalue(y, VariableTimestep{years}(10)) @test y[t,1] == time_dim_val[2,1] @test y[t,2] == time_dim_val[2,2] @@ -419,8 +422,8 @@ for ti = 1:2 reset_time_val(y, time_dim_val) else - @test hasvalue(y, t, time_dim_val[1]) - @test !hasvalue(y, VariableTimestep{years}(10)) + @test hasvalue(y, t, time_dim_val[1]) + @test !hasvalue(y, VariableTimestep{years}(10)) @test y[1,t] == time_dim_val[1,2] @test y[2,t] == time_dim_val[2,2] @@ -432,10 +435,9 @@ for ti = 1:2 reset_time_val(y, time_dim_val) end - # Deprecated int indexing should still run - @test y[1,2] == time_dim_val[1,2] - y[1,2] = temp_dim_val[1,2] - @test y[1,2] == temp_dim_val[1,2] + @test y[idx1,2] == time_dim_val[1,2] + y[idx1,2] = temp_dim_val[1,2] + @test y[idx1,2] == temp_dim_val[1,2] reset_time_val(y, time_dim_val) end @@ -453,18 +455,31 @@ for ti = 1:2 temp_dim_val = collect(reshape(100:163, 4, 4, 4)) # Using a colon in the time dimension - @test arr_fixed[:,1,1] == arr_variable[:,1,1] == time_dim_val[:,1,1] - @test arr_fixed[:,2,3] == arr_variable[:,2,3] == time_dim_val[:,2,3] - arr_fixed[:,1,1] = temp_dim_val[:,1,1] - arr_variable[:,1,1] = temp_dim_val[:,1,1] - @test arr_fixed[:,1,1] == arr_variable[:,1,1] == temp_dim_val[:,1,1] - arr_fixed[:,:,2] = temp_dim_val[:,:,2] - arr_variable[:,:,2] = temp_dim_val[:,:,2] - @test arr_fixed[:,:,2] == arr_variable[:,:,2] == temp_dim_val[:,:,2] - arr_fixed[:,:,:] = temp_dim_val - arr_variable[:,:,:] = temp_dim_val - @test arr_fixed[:,:,:] == arr_variable[:,:,:] == temp_dim_val[:,:,:] - + if ti == 1 + @test arr_fixed[:,1,1] == arr_variable[:,1,1] == time_dim_val[:,1,1] + @test arr_fixed[:,2,3] == arr_variable[:,2,3] == time_dim_val[:,2,3] + arr_fixed[:,1,1] = temp_dim_val[:,1,1] + arr_variable[:,1,1] = temp_dim_val[:,1,1] + @test arr_fixed[:,1,1] == arr_variable[:,1,1] == temp_dim_val[:,1,1] + arr_fixed[:,:,2] = temp_dim_val[:,:,2] + arr_variable[:,:,2] = temp_dim_val[:,:,2] + @test arr_fixed[:,:,2] == arr_variable[:,:,2] == temp_dim_val[:,:,2] + arr_fixed[:,:,:] = temp_dim_val + arr_variable[:,:,:] = temp_dim_val + @test arr_fixed[:,:,:] == arr_variable[:,:,:] == temp_dim_val[:,:,:] + else + @test arr_fixed[1,:,1] == arr_variable[1,:,1] == time_dim_val[1,:,1] + @test arr_fixed[2,:,3] == arr_variable[2,:,3] == time_dim_val[2,:,3] + arr_fixed[1,:,1] = temp_dim_val[1,:,1] + arr_variable[1,:,1] = temp_dim_val[1,:,1] + @test arr_fixed[1,:,1] == arr_variable[1,:,1] == temp_dim_val[1,:,1] + arr_fixed[:,:,2] = temp_dim_val[:,:,2] + arr_variable[:,:,2] = temp_dim_val[:,:,2] + @test arr_fixed[:,:,2] == arr_variable[:,:,2] == temp_dim_val[:,:,2] + arr_fixed[:,:,:] = temp_dim_val + arr_variable[:,:,:] = temp_dim_val + @test arr_fixed[:,:,:] == arr_variable[:,:,:] == temp_dim_val[:,:,:] + end reset_time_val(arr_fixed, time_dim_val) reset_time_val(arr_variable, time_dim_val) @@ -473,25 +488,25 @@ for ti = 1:2 # Indexing with single TimestepIndex if ti == 1 - @test arr_fixed[TimestepIndex(1), 1, 1] == arr_variable[TimestepIndex(1), 1, 1] == time_dim_val[1,1,1] + @test arr_fixed[idx1, 1, 1] == arr_variable[idx1, 1, 1] == time_dim_val[1,1,1] @test arr_fixed[TimestepIndex(3), 3, 3] == arr_variable[TimestepIndex(3), 3, 3] == time_dim_val[3,3,3] - arr_fixed[TimestepIndex(1), 1, 1] = temp_dim_val[1,1,1] - arr_variable[TimestepIndex(1), 1, 1] = temp_dim_val[1,1,1] - @test arr_fixed[TimestepIndex(1), 1, 1] == arr_variable[TimestepIndex(1), 1, 1] == temp_dim_val[1,1,1] + arr_fixed[idx1, 1, 1] = temp_dim_val[1,1,1] + arr_variable[idx1, 1, 1] = temp_dim_val[1,1,1] + @test arr_fixed[idx1, 1, 1] == arr_variable[idx1, 1, 1] == temp_dim_val[1,1,1] reset_time_val(arr_fixed, time_dim_val) reset_time_val(arr_variable, time_dim_val) - @test_throws ErrorException arr_fixed[TimestepIndex(1)] - @test_throws ErrorException arr_variable[TimestepIndex(1)] + @test_throws ErrorException arr_fixed[idx1] + @test_throws ErrorException arr_variable[idx1] # Indexing with Array{TimestepIndex, N} @test arr_fixed[TimestepIndex.([1,3]), 1, 1] == time_dim_val[[1,3], 1, 1] @test arr_variable[TimestepIndex.([2,4]), 1, 1] == time_dim_val[[2,4], 1, 1] # Indexing with Array{TimestepIndex, N} created by Colon syntax - @test arr_fixed[TimestepIndex(1):TimestepIndex(3), 1, 1] == time_dim_val[[1:3...], 1, 1] - @test arr_fixed[TimestepIndex(1):2:TimestepIndex(3), 1, 1] == time_dim_val[[1:2:3...], 1, 1] + @test arr_fixed[idx1:TimestepIndex(3), 1, 1] == time_dim_val[[1:3...], 1, 1] + @test arr_fixed[idx1:2:TimestepIndex(3), 1, 1] == time_dim_val[[1:2:3...], 1, 1] # Indexing with single TimestepValue @test arr_fixed[TimestepValue(2000), 1, 1] == arr_variable[TimestepValue(2000), 1, 1] == time_dim_val[1,1,1] @@ -503,7 +518,7 @@ for ti = 1:2 reset_time_val(arr_fixed, time_dim_val) reset_time_val(arr_variable, time_dim_val) - # Indexing with Array{TimestepValue, N} + # Indexing with Array{TimestepValue, N} @test arr_fixed[TimestepValue.([2000, 2010]), 1, 1] == time_dim_val[[1,3],1,1] @test arr_variable[TimestepValue.([2000, 2005, 2025]), 1, 1] == time_dim_val[[1,2,4],1,1] @@ -515,14 +530,20 @@ for ti = 1:2 reset_time_val(arr_fixed, time_dim_val) reset_time_val(arr_variable, time_dim_val) + @test arr_fixed[idx1,2,3] == time_dim_val[1,2,3] + @test arr_variable[idx1,2,3] == time_dim_val[1,2,3] + arr_fixed[idx1,2,3] = temp_dim_val[1,2,3] + arr_variable[idx1,2,3] = temp_dim_val[1,2,3] + @test arr_fixed[idx1,2,3] == arr_variable[idx1,2,3] == temp_dim_val[1,2,3] + else - @test arr_fixed[1, TimestepIndex(1), 1] == arr_variable[1, TimestepIndex(1), 1] == time_dim_val[1,1,1] + @test arr_fixed[1, idx1, 1] == arr_variable[1, idx1, 1] == time_dim_val[1,1,1] @test arr_fixed[3, TimestepIndex(3), 3] == arr_variable[3, TimestepIndex(3), 3] == time_dim_val[3,3,3] - arr_fixed[1, TimestepIndex(1), 1] = temp_dim_val[1,1,1] - arr_variable[1, TimestepIndex(1), 1] = temp_dim_val[1,1,1] - @test arr_fixed[1, TimestepIndex(1), 1] == arr_variable[1, TimestepIndex(1), 1] == temp_dim_val[1,1,1] + arr_fixed[1, idx1, 1] = temp_dim_val[1,1,1] + arr_variable[1, idx1, 1] = temp_dim_val[1,1,1] + @test arr_fixed[1, idx1, 1] == arr_variable[1, idx1, 1] == temp_dim_val[1,1,1] reset_time_val(arr_fixed, time_dim_val) reset_time_val(arr_variable, time_dim_val) @@ -531,8 +552,8 @@ for ti = 1:2 @test arr_variable[1, TimestepIndex.([2,4]), 1] == time_dim_val[1, [2,4], 1] # Indexing with Array{TimestepIndex, N} created by Colon syntax - @test arr_fixed[1, TimestepIndex(1):TimestepIndex(3), 1] == time_dim_val[1, [1:3...], 1] - @test arr_fixed[1, TimestepIndex(1):2:TimestepIndex(3), 1] == time_dim_val[1, [1:2:3...], 1] + @test arr_fixed[1, idx1:TimestepIndex(3), 1] == time_dim_val[1, [1:3...], 1] + @test arr_fixed[1, idx1:2:TimestepIndex(3), 1] == time_dim_val[1, [1:2:3...], 1] # Indexing with single TimestepValue @test arr_fixed[1, TimestepValue(2000), 1] == arr_variable[1, TimestepValue(2000), 1] == time_dim_val[1,1,1] @@ -544,7 +565,7 @@ for ti = 1:2 reset_time_val(arr_fixed, time_dim_val) reset_time_val(arr_variable, time_dim_val) - # Indexing with Array{TimestepValue, N} + # Indexing with Array{TimestepValue, N} @test arr_fixed[1, TimestepValue.([2000, 2010]), 1] == time_dim_val[1, [1,3],1] @test arr_variable[1, TimestepValue.([2000, 2005, 2025]), 1] == time_dim_val[1,[1,2,4],1] arr_fixed[1, TimestepValue.([2000, 2010]), 1] = temp_dim_val[1, [1,3],1] @@ -554,14 +575,13 @@ for ti = 1:2 reset_time_val(arr_fixed, time_dim_val) reset_time_val(arr_variable, time_dim_val) - end - # Deprecated int indexing should still run - @test arr_fixed[1,2,3] == time_dim_val[1,2,3] - @test arr_variable[1,2,3] == time_dim_val[1,2,3] - arr_fixed[1,2,3] = temp_dim_val[1,2,3] - arr_variable[1,2,3] = temp_dim_val[1,2,3] - @test arr_fixed[1,2,3] == arr_variable[1,2,3] == temp_dim_val[1,2,3] + @test arr_fixed[1,idx2,3] == time_dim_val[1,2,3] + @test arr_variable[1,idx2,3] == time_dim_val[1,2,3] + arr_fixed[1,idx2,3] = temp_dim_val[1,2,3] + arr_variable[1,idx2,3] = temp_dim_val[1,2,3] + @test arr_fixed[1,idx2,3] == arr_variable[1,idx2,3] == temp_dim_val[1,2,3] + end reset_time_val(arr_fixed, time_dim_val) reset_time_val(arr_variable, time_dim_val) @@ -573,14 +593,14 @@ time_dim_val = collect(reshape(1:64, 4, 4, 4)) x_years = Tuple(2000:5:2015) #fixed y_years = Tuple([2000:5:2005; 2015:10:2025]) #variable -x_vec = TimestepVector{FixedTimestep{2000, 5}, Int}(time_dim_val[:,1,1]) +x_vec = TimestepVector{FixedTimestep{2000, 5}, Int}(time_dim_val[:,1,1]) x_mat = TimestepMatrix{FixedTimestep{2000, 5}, Int, 1}(time_dim_val[:,:,1]) -y_vec = TimestepVector{VariableTimestep{y_years}, Int}(time_dim_val[:,2,2]) +y_vec = TimestepVector{VariableTimestep{y_years}, Int}(time_dim_val[:,2,2]) y_mat = TimestepMatrix{VariableTimestep{y_years}, Int, 1}(time_dim_val[:,:,2]) -@test first_period(x_vec) == first_period(x_mat) == x_years[1] +@test first_period(x_vec) == first_period(x_mat) == x_years[1] @test first_period(y_vec) == first_period(y_mat) == y_years[1] -@test last_period(x_vec) == last_period(x_mat) == x_years[end] +@test last_period(x_vec) == last_period(x_mat) == x_years[end] @test last_period(y_vec) == last_period(y_mat) == y_years[end] @test size(x_vec) == size(y_vec) == (4,) @@ -597,25 +617,37 @@ y_mat = TimestepMatrix{VariableTimestep{y_years}, Int, 1}(time_dim_val[:,:,2]) #------------------------------------------------------------------------------ @defcomp foo begin - par = Parameter(index=[time]) - var = Variable(index=[time]) + par1 = Parameter(index=[time]) + var1 = Variable(index=[time]) function run_timestep(p, v, d, t) if is_last(t) - v.var[t] = 0 + v.var1[t] = 0 else - v.var[t] = p.par[t+1] # This is where the error will be thrown, if connected to an internal variable that has not yet been computed. + v.var1[t] = p.par1[t+1] # This is where the error will be thrown, if connected to an internal variable that has not yet been computed. end - end -end + end +end + +@defcomp bar begin + par2 = Parameter(index=[time]) + var2 = Variable(index=[time]) + function run_timestep(p, v, d, t) + if is_last(t) + v.var2[t] = 0 + else + v.var2[t] = p.par2[t+1] # This is where the error will be thrown, if connected to an internal variable that has not yet been computed. + end + end +end years = 2000:2010 m = Model() set_dimension!(m, :time, years) add_comp!(m, foo, :first) -add_comp!(m, foo, :second) -connect_param!(m, :second => :par, :first => :var) -set_param!(m, :first, :par, 1:length(years)) +add_comp!(m, bar, :second) +connect_param!(m, :second => :par2, :first => :var1) +set_param!(m, :first, :par1, 1:length(years)) @test_throws MissingException run(m) diff --git a/test/test_timesteps.jl b/test/test_timesteps.jl index 3502bd489..19ad2f994 100644 --- a/test/test_timesteps.jl +++ b/test/test_timesteps.jl @@ -4,19 +4,18 @@ using Mimi using Test import Mimi: - AbstractTimestep, FixedTimestep, VariableTimestep, TimestepVector, - TimestepMatrix, TimestepArray, next_timestep, hasvalue, getproperty, Clock, + AbstractTimestep, FixedTimestep, VariableTimestep, TimestepVector, + TimestepMatrix, TimestepArray, next_timestep, hasvalue, getproperty, Clock, time_index, get_timestep_array #------------------------------------------------------------------------------ -# Test basic timestep functions and Base functions for Fixed Timestep +# Test basic timestep functions and Base functions for Fixed Timestep #------------------------------------------------------------------------------ t1 = FixedTimestep{1850, 10, 3000}(1) @test is_first(t1) # is_first and is_timestep still run but are deprecated; will error in v1.0 and should remove these two tests then -@test is_timestep(t1, 1) -@test is_time(t1, 1850) @test t1 == TimestepIndex(1) +@test t1 == TimestepValue(1850) @test TimestepIndex(1) == t1 # test both ways because to test both method definitions @test t1 == TimestepValue(1850) @test TimestepValue(1850) == t1 # test both ways because to test both method definitions @@ -47,7 +46,7 @@ t4 = next_timestep(t3) #------------------------------------------------------------------------------ -# Test basic timestep functions and Base functions for Variable Timestep +# Test basic timestep functions and Base functions for Variable Timestep #------------------------------------------------------------------------------ years = Tuple([2000:1:2024; 2025:5:2105]) @@ -78,9 +77,9 @@ t3 = VariableTimestep{years}(42) t4 = next_timestep(t3) @test t4 == TimestepIndex(43) -# note here that this comes back to an assumption made in variable +# note here that this comes back to an assumption made in variable # timesteps that we may assume the next step is 1 year for the final year in TIMES -@test t4 == TimestepValue(2106) +@test t4 == TimestepValue(2106) @test_throws ErrorException t_next = t4 + 1 @test_throws ErrorException next_timestep(t4) @@ -105,7 +104,7 @@ step = 2 @test TimestepIndex(2) - 1 == TimestepIndex(1) #------------------------------------------------------------------------------ -# Test a model with components with different offsets +# Test a model with components with different offsets #------------------------------------------------------------------------------ # we'll have Bar run from 2000 to 2010 @@ -114,7 +113,7 @@ step = 2 @defcomp Foo begin inputF = Parameter() output = Variable(index=[time]) - + function run_timestep(p, v, d, ts) v.output[ts] = p.inputF + ts.t end @@ -123,7 +122,7 @@ end @defcomp Bar begin inputB = Parameter(index=[time]) output = Variable(index=[time]) - + function run_timestep(p, v, d, ts) if ts < TimestepValue(2005) v.output[ts] = p.inputB[ts] @@ -200,13 +199,13 @@ t_matrix = get_timestep_array(m.md, Float64, 2, 2, matrix) #------------------------------------------------------------------------------ -# Now build a model with connecting components +# Now build a model with connecting components #------------------------------------------------------------------------------ @defcomp Foo2 begin inputF = Parameter(index=[time]) output = Variable(index=[time]) - + function run_timestep(p, v, d, ts) v.output[ts] = p.inputF[ts] end @@ -221,7 +220,7 @@ set_param!(m2, :Bar, :inputB, collect(1:length(years))) # TBD: Connecting components with different "first" times creates a mismatch # in understanding how to translate the index back to a year. -connect_param!(m2, :Foo2, :inputF, :Bar, :output) +connect_param!(m2, :Foo2, :inputF, :Bar, :output) run(m2) @@ -231,13 +230,13 @@ for i in 1:6 end #------------------------------------------------------------------------------ -# Connect them in the other direction +# Connect them in the other direction #------------------------------------------------------------------------------ @defcomp Bar2 begin inputB = Parameter(index=[time]) output = Variable(index=[time]) - + function run_timestep(p, v, d, ts) v.output[ts] = p.inputB[ts] * ts.t end diff --git a/test/test_tools.jl b/test/test_tools.jl index b60885ea1..ddcd1b50b 100644 --- a/test/test_tools.jl +++ b/test/test_tools.jl @@ -21,7 +21,7 @@ ts = 10 @defcomp Foo begin input = Parameter() intermed = Variable(index=[time]) - + function run_timestep(p, v, d, t) v.intermed[t] = p.input end @@ -30,7 +30,7 @@ end @defcomp Bar begin intermed = Parameter(index=[time]) output = Variable(index=[time]) - + function run_timestep(p, v, d, t) v.output[t] = p.intermed[t] end @@ -42,8 +42,11 @@ foo = add_comp!(m, Foo) bar = add_comp!(m, Bar) foo[:input] = 3.14 -bar[:intermed] = foo[:intermed] + +# The connection via references is broken... +# bar[:intermed] = m.md[:Foo][:intermed] +connect_param!(m, :Bar, :intermed, :Foo, :intermed) run(m) -end #module \ No newline at end of file +end #module diff --git a/test/test_units.jl b/test/test_units.jl index 1d3164e2d..25f7996b1 100644 --- a/test/test_units.jl +++ b/test/test_units.jl @@ -23,19 +23,15 @@ end input = Parameter(unit="kg") end -@defmodel m begin - index[time] = [1] - component(Foo) - component(Bar) - component(Baz) -end - -foo = ComponentReference(m, :Foo) -bar = ComponentReference(m, :Bar) -baz = ComponentReference(m, :Baz) +m = Model() +set_dimension!(m, :time, [1]) +foo = add_comp!(m, Foo) +bar = add_comp!(m, Bar) +baz = add_comp!(m, Baz) # Check that we cannot connect foo and bar... @test_throws ErrorException bar[:input] = foo[:output] +#@test_throws ErrorException connect_param!(m, :Bar, :input, :Foo, :output, ignoreunits=false) # ...unless we pass ignoreunits=true connect_param!(m, :Bar, :input, :Foo, :output, ignoreunits=true) @@ -43,4 +39,4 @@ connect_param!(m, :Bar, :input, :Foo, :output, ignoreunits=true) # But we can connect foo and baz baz[:input] = foo[:output] -end # module \ No newline at end of file +end # module