/
defcomposite.jl
300 lines (242 loc) · 11 KB
/
defcomposite.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
using MacroTools
# splitarg produces a tuple for each arg of the form (arg_name, arg_type, slurp, default)
_arg_name(arg_tup) = arg_tup[1]
_arg_type(arg_tup) = arg_tup[2]
_arg_slurp(arg_tup) = arg_tup[3]
_arg_default(arg_tup) = arg_tup[4]
function _typecheck(obj, expected_type, msg)
obj isa expected_type || error("$msg must be a $expected_type; got $(typeof(obj)): $obj")
end
"""
parse_dotted_symbols(expr)
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[]
ex = expr
while @capture(ex, left_.right_) && right isa Symbol
push!(syms, right)
ex = left
end
if ex isa Symbol
push!(syms, ex)
else
return nothing
end
syms = reverse(syms)
datum_name = pop!(syms)
return ComponentPath(syms), datum_name
end
#
# Convert @defcomposite "shorthand" statements into Mimi API calls
#
function _parse(expr)
result = nothing
if @capture(expr, newname_ = Component(compname_, args__)) ||
@capture(expr, Component(compname_, args__))
valid_keys = (:first, :last)
# 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
#assign newname
newname = (newname === nothing ? compname : newname)
# handle keyword arguments
keyargs = []
for arg in args
@capture(arg, keywd_ = value_) # keyword arguments
if keywd in valid_keys
push!(keyargs, arg)
else
error("Unrecognized Component keyword '$keywd'; must be 'first' or 'last")
end
end
result = :(Mimi.add_comp!(obj, $compname, $(QuoteNode(newname)); $(keyargs...)))
elseif @capture(expr, localparname_ = Parameter(args__))
valid_keys = (:default, :description, :unit)
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, :(obj[$(QuoteNode(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, ex)
Define a Mimi CompositeComponentDef `cc_name` with the expressions in `ex`. Expressions
are all shorthand for longer-winded API calls, and include the following:
p = Parameter(...)
v = Variable(varname)
local_name = Component(name)
Component(name) # equivalent to `name = Component(name)`
connect(...)
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.
"""
macro defcomposite(cc_name, ex)
@capture(ex, exprs__)
calling_module = __module__
# @info "defcomposite calling module: $calling_module"
stmts = [_parse(expr) for expr in exprs]
result = :(
let cc_id = Mimi.ComponentId($calling_module, $(QuoteNode(cc_name))),
obj = Mimi.CompositeComponentDef(cc_id)
global $cc_name = obj
$(stmts...)
Mimi.import_params!(obj)
$cc_name
end
)
return esc(result)
end
"""
import_params!(obj::AbstractCompositeComponentDef)
Imports all unconnected parameters below the given composite `obj` by adding references
to these parameters in `obj`.
N.B. This is also called at the end of code emitted by @defcomposite.
"""
function import_params!(obj::AbstractCompositeComponentDef)
unconn = unconnected_params(obj)
# Check for unresolved parameter name collisions.
# Users must explicitly define any parameters that come from multiple subcomponents.
all_names = [ref.datum_name for ref in unconn]
unique_names = unique(all_names)
_map = Dict([name => count(isequal(name), all_names) for name in unique_names])
non_unique = [name for (name, val) in _map if val>1]
isempty(non_unique) || error("Cannot build composite :$(obj.name). There are unresolved parameter name collisions from subcomponents for the following parameter names: $(join(non_unique, ", ")).")
for param_ref in unconn
name = param_ref.datum_name
haskey(obj, name) && error("Cannot build composite :$(obj.name). Failed to auto-import parameter :$name from component :$(param_ref.comp_name), this name has already been defined in the composite component's namespace.")
obj[name] = CompositeParameterDef(obj, param_ref)
end
end
# Helper function for finding any field collisions for parameters that want to be joined
function _find_collisions(fields, pairs::Vector{Pair{T, Symbol}}) where T
collisions = Symbol[]
pardefs = [comp.namespace[param_name] for (comp, param_name) in pairs]
for f in fields
subcomponent_set = Set([getproperty(pardef, f) for pardef in pardefs])
length(subcomponent_set) > 1 && push!(collisions, f)
end
return collisions
end
# `kwargs` contains the keywords specified by the user when defining the composite parameter in @defcomposite.
# If the user does not provide a value for one or any of the possible fields, this function looks at the fields
# of the subcomponents' parameters to use, but errors if any of them are in conflict.
# Note that :dim_names and :datatype can't be specified at the composite level, but must match from the subcomponents.
function _resolve_composite_parameter_kwargs(obj::AbstractCompositeComponentDef, kwargs::Dict{Symbol, Any}, pairs::Vector{Pair{T, Symbol}}, parname::Symbol) where T <: AbstractComponentDef
fields = (:default, :description, :unit, :dim_names, :datatype)
collisions = _find_collisions(fields, pairs)
# Create a new dictionary of resolved values to return
new_kwargs = Dict{Symbol, Any}()
for f in fields
try
new_kwargs[f] = kwargs[f] # Get the user specified value for this field if there is one
catch e
# If the composite definition does not specify a value, then need to look to subcomponents and resolve or error
if f in collisions
error("Cannot build composite parameter :$parname, subcomponents have conflicting values for the \"$f\" field.")
else
compdef, curr_parname = pairs[1]
pardef = compdef[curr_parname]
new_kwargs[f] = getproperty(pardef, f)
end
end
end
return new_kwargs
end
# Helper function for detecting whether a specified datum has already been imported or connected
function _is_connected(obj::AbstractCompositeComponentDef, comp_name::Symbol, datum_name::Symbol)
for (k, item) in obj.namespace
if isa(item, AbstractCompositeParameterDef)
for ref in item.refs
if ref.comp_name == comp_name && ref.datum_name == datum_name
return true
end
end
elseif isa(item, AbstractCompositeVariableDef)
ref = item.ref
if ref.comp_name == comp_name && ref.datum_name == datum_name
return true
end
end
end
return false
# cannot use the following, because all parameters haven't bubbled up yet
# return UnnamedReference(comp_name, datum_name) in unconnected_params(obj)
end
# This function creates a CompositeParameterDef in the CompositeComponentDef obj
function import_param!(obj::AbstractCompositeComponentDef, localname::Symbol,
pairs::Pair...; kwargs...)
print_pairs = [(comp.comp_id, name) for (comp, name) in pairs]
# @info "import_param!($(obj.comp_id), :$localname, $print_pairs)"
for (comp, pname) in pairs
if comp == :* # wild card
error("Got wildcard component specification (*) 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")
_is_connected(obj, compname, pname) &&
error("Duplicate import of $(comp.name).$pname")
end
end
new_kwargs = _resolve_composite_parameter_kwargs(obj, Dict{Symbol, Any}(kwargs), collect(pairs), localname)
obj[localname] = CompositeParameterDef(localname, pathof(obj), collect(pairs), new_kwargs)
end
"""
Import a variable from the given subcomponent
"""
function _import_var!(obj::AbstractCompositeComponentDef, localname::Symbol,
path::ComponentPath, vname::Symbol)
if haskey(obj, localname)
error("Cannot 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] = CompositeVariableDef(localname, pathof(obj), comp, vname)
end
nothing