Declarations and Configuration
Cropbox has two declaration macros. @system defines model structure; @config describes values applied to that structure.
| Macro | Result | Use it for |
|---|---|---|
@system | a subtype of System | variables, equations, behaviors, and composition |
@config | one Config or a vector of them | parameters, scenario patches, sweeps, and factorial designs |
Neither macro runs a simulation. A system type is a reusable specification, and a configuration is reusable input. instance, simulate, or visualize brings the two together.
@system
@system Name[{patches...}][(mixins...)] [<: Supertype] begin
# variable declarations...
endThe block may be omitted when the new system only combines mixins:
@system CropModel(Weather, Phenology, Growth, Controller)@system parses the declarations, combines mixins from left to right, checks behavior tags and dependencies, and generates the Julia system type and update methods. A later mixin or local declaration can replace an earlier public variable. Always inspect a heavily composed result with look.
The variable grammar is:
name[(dependencies...; explicit_arguments...)][: alias] [=> body]
~ [behavior][::static_type|<:dynamic_type][(tags...)]Read DSL Syntax for every field in this grammar and Behaviors and Tags for accepted behavior options. The generated type is ordinary Julia in the sense that it can be passed to functions and used in type annotations, but its fields are Cropbox states and must follow the Cropbox update lifecycle.
Config
A Config is an ordered mapping from system name to parameter names and values. The following parameter-list forms are equivalent:
@config Model => (:rate => 2, :initial => 3)
@config Model => (rate = 2, initial = 3)Config() and @config() create an empty configuration. A value can be read with config[:Model][:rate]; length, iteration, and equality follow the usual Julia collection conventions. Combine configurations with @config base + patch. Treat a completed configuration as experiment input rather than mutating its nested mapping in place. Build a later patch and merge it when a value changes.
Parameter values may be scalars, arrays, tables, functions, or other objects accepted by the declaration. A value is not expanded merely because it is a collection; expansion happens only through the @config sweep syntax.
missing means “no configured override” during parameter construction, so Cropbox falls back to the declaration body. It is not stored as the parameter value. For a preserve(optional) declaration whose intentional value is absent, configure nothing instead.
System keys and validation
Prefer the system type when it is available:
@config Model => :rate => 2Cropbox then checks the short name or alias against parameters declared by Model and applies the declared unit to a plain number. An explicitly unitful value is converted when its dimension is compatible. A symbol such as :Model or a string path such as "Model.rate" is normalized by name but cannot offer the same early validation. Name-based keys are still useful for external files or code that runs before the model package is loaded.
The special key :0 means the executable root system. It is useful in generic components and old workshop material whose root type is not known in advance. Prefer the actual root type in maintained code because it is searchable and validated.
Merge order
Commas and + merge configurations from left to right. Later values win:
base = @config Model => (rate = 1, initial = 2)
patch = @config Model => :rate => 3
combined = @config base + patchThis rule also applies when the same parameter appears twice in one tuple. A useful ordering is package defaults, cultivar or site settings, experiment settings, and finally the treatment patch.
One-factor expansion
Prefix ! creates one configuration for each value in an iterable:
rates = @config !(Model => :rate => [1, 2, 3])It returns Vector{Config} even when the iterable has one value. A literal vector creates a hand-written scenario list without expansion:
scenarios = @config [
Model => (rate = 1, initial = 0),
Model => (rate = 3, initial = 10),
]Pass either result as configs, not as config.
Factorial expansion
Infix * creates a Cartesian product of factors:
design = @config (
Model => :rate => [1, 2]
) * (
Model => :initial => [0, 10]
)The example produces four configurations in a stable nested-product order. Parentheses matter because Julia's pair and arithmetic operators have different precedence. Use ! for one varying factor and * when every combination is intended.
parameters
parameters is the safest way to discover the configuration surface before writing a patch:
parameters(SystemType;
alias = false,
recursive = false,
exclude = (),
scope = nothing,
)
parameters(instance;
alias = false,
recursive = false,
exclude = (),
)The type form evaluates dependency-free defaults; a default that depends on another variable is reported as missing. The instance form reports the current configured values. recursive=true follows nested system types; exclude prevents unwanted infrastructure or already visited types from being included. scope controls where a type form evaluates constants and defaults and normally needs no override.
Aliases are convenient for reading, but short declared names are usually more stable in saved configuration. Check both when adapting an older notebook.
Configuration consumers
| Consumer | Configuration behavior |
|---|---|
instance | one config |
simulate | one config, many configs, or a shared config plus patches |
visualize | one base config plus method-specific sweeps or groups |
evaluate | either config or configs, not both |
calibrate | either config or complete environment configs, not both |
options is separate from configuration. It passes constructor resources such as an external geometric container. Prefer configuration for declared model parameters so the experiment can be inspected and saved.
API docstrings
Cropbox.Config — Type
ConfigContains a set of configuration for systems. Configuration for a system contains parameter values.
Examples
julia> @config :S => :a => 1
Config for 1 system:
S
a = 1See also: @config
Cropbox.@system — Macro
@system name[{patches..}][(mixins..)] [<: type] [decl] -> Type{<:System}Declare a new system called name with new variables declared in decl block using a custom syntax. The resultant system is subtype of System or a custom type. mixins allows reusing specification of existing systems to be pasted into the declaration of new system. patches may provide type substitution and/or constant definition needed for advanced use.
Variable
name[(args..; kwargs..)][: alias] [=> expr] [~ [state][::type|<:type][(tags..)]]name: variable name; usually short abbreviation.args: automatically bound depending variableskwargs: custom bound depending variables; used bycallandintegrate.alias: alternative name; long description.expr: state-specific code snippet; use begin-end block for multiple statements.type: internal data type; default is Float64 for many, but not all, variables.tags: state-specific options;unit,min/max, etc.
States
hold: marks a placeholder for variable shared between mixins.wrap: passes a state variable to other fucnction as is with no unwrapping its value.advance: manages a time-keeping variable;timeandtickfromClock.preserve: keeps initially assigned value with no further updates; constants, parameters.tabulate: makes a two dimensional table with named keys; i.e. partitioning table.interpolate: makes a curve function interpolated with discrete values; i.e. soil characteristic curve.track: evaluates variable expression as is for each update.remember: keeps tracking variable until a certain condition is met; essentiallytrackturning intopreserve.provide: manages a table of time-series in DataFrame.drive: fetches the current value from a time-series; maybe supplied byprovide.call: defines a partial function bound with some variables.integrate: calculates integral using Gaussian method; not for time domain.accumulate: emulates integration of rate variable over time; essentially Euler method.capture: calculates difference between integration for each time step.flag: sets a boolean flag; essentiallytrack::Bool.produce: attaches a new instance of system dynamically constructed; i.e. root structure growth.bisect: solves nonlinear equation using bisection method; i.e. gas-exchange model coupling.solve: solves polynomial equation symbolically; i.e. quadratic equations in photosynthesis model.
Examples
julia> @system S(Controller) begin
a => 1 ~ preserve(parameter)
b(a) ~ accumulate
end
SCropbox.@config — Macro
@config c.. -> Config | Vector{Config}Construct a set or multiple sets of configuration.
A basic unit of configuration for a system S is represented by a pair in the form of S => pv. System name S is expressed in a symbol. If actual type of system is used, its name will be automatically converted to a symbol.
A parameter name and corresponding value is then represented by another pair in the form of p => v. When specifiying multiple parameters, a tuple of pairs like (p1 => v1, p2 => v2) or a named tuple like (p1 = v1, p2 = v2) can be used. Parameter name must be a symbol and should indicate a variable declared with parameter tag as often used by preserve state variable. For example, :S => (:a => 1, :b => 2) has the same meaning as S => (a = 1, b = 2) in the same scope.
Configurations for multiple systems can be concatenated by a tuple. Multiple elements in c separated by commas implicitly forms a tuple. For example, :S => (:a => 1, :b => 2), :T => :x => 1 represents a set of configuration for two systems S and T with some parameters. When the same names of system or variable appears again during concatenation, it will be overriden by later ones in an order appeared in a tuple. For example, :S => :a => 1, :S => :a => 2 results into :S => :a => 2. Instead of commas, + operator can be used in a similar way as (:S => :a => 1) + (:S => :a => 2). Note parentheses placed due to operator precedence.
When multiple sets of configurations are needed, as in configs for simulate, a vector of Config is used. This macro supports some convenient ways to construct a vector by composing simpler configurations. Prefix operator ! allows expansion of any iterable placed in the configuration value. Infix operator * allows multiplication of a vector of configurations with another vector or a single configuration to construct multiple sets of configurations. For example, !(:S => :a => 1:2) is expanded into two sets of separate configurations [:S => :a => 1, :S => :a => 2]. (:S => :a => 1:2) * (:S => :b => 0) is multiplied into [:S => (a = 1, b = 0), :S => (a = 2, b = 0)].
Examples
julia> @config :S => (:a => 1, :b => 2)
Config for 1 system:
S
a = 1
b = 2julia> @config :S => :a => 1, :S => :a => 2
Config for 1 system:
S
a = 2julia> @config !(:S => :a => 1:2)
2-element Vector{Config}:
<Config>
<Config>julia> @config (:S => :a => 1:2) * (:S => :b => 0)
2-element Vector{Config}:
<Config>
<Config>Cropbox.parameters — Function
parameters(S; <keyword arguments>) -> ConfigExtract a list of parameters defined for system S.
Arguments
S::Type{<:System}: type of system to be inspected.
Keyword Arguments
alias=false: show alias instead of parameter name.recursive=false: extract parameters from other systems declared inS.exclude=(): systems excluded in recurisve search.scope=nothing: evaluation scope; default isS.name.module.
Examples
julia> @system S(Controller) begin
a: aaa => 1 ~ preserve(parameter)
end;
julia> parameters(S)
Config for 1 system:
S
a = 1
julia> parameters(S; alias=true)
Config for 1 system:
S
aaa = 1
julia> parameters(S; recursive=true)
Config for 3 systems:
Clock
init = 0 hr
step = 1 hr
Context
S
a = 1
julia> parameters(S; recursive=true, exclude=(Context,))
Config for 1 system:
S
a = 1parameters(s; <keyword arguments>) -> ConfigExtract a list of parameters from an existing instance of system s.
Arguments
s::System: instance of system to be inspected.
Keyword Arguments
alias=false: show alias instead of parameter name.recursive=false: extract parameters from other systems declared inS.exclude=(): systems excluded in recurisve search.
Examples
julia> @system S(Controller) begin
a: aaa => 1 ~ preserve(parameter)
end;
julia> s = instance(S; config = :S => :a => 2);
julia> parameters(s)
Config for 1 system:
S
a = 2.0
julia> parameters(s; alias = true)
Config for 1 system:
S
aaa = 2.0
julia> parameters(s; recursive = true)
Config for 3 systems:
Clock
init = 0.0 hr
step = 1.0 hr
Context
S
a = 2.0
julia> parameters(s; recursive = true, exclude = (Context,))
Config for 1 system:
S
a = 2.0