Configure Models and Scenarios

Configuration keeps equations separate from experimental conditions. A Cropbox configuration maps a system key and parameter name to a value.

Inspect configurable values first

using Cropbox
using DataFrames

@system Experiment(Controller) begin
    rate: growth_rate     => 1 ~ preserve(parameter, u"g/hr")
    initial: initial_mass => 2 ~ preserve(parameter, u"g")
    mass(rate)                 ~ accumulate(init = initial, u"g")
end

parameters(Experiment)

Config for 1 system:

Experiment
rate=1 g hr^-1
initial=2 g

Use alias=true for long names and recursive=true to include parameters from embedded systems. exclude can remove infrastructure systems from a recursive listing.

parameters(Experiment; alias = true, recursive = true, exclude = (Context,))

Config for 1 system:

Experiment
growth_rate=1 g hr^-1
initial_mass=2 g

Set one or more parameters

These forms are equivalent:

c1 = @config Experiment => (:rate => 2, :initial => 3)
c2 = @config Experiment => (rate = 2, initial = 3)
c1 == c2
true

Cropbox also accepts a symbol such as :Experiment as the system key. Prefer the system type when it is available in scope: Experiment must resolve to a real Julia binding, and Cropbox immediately validates the parameter name and unit against that system's declarations. A symbol key defers those checks until the configuration is applied to a model. Symbols remain useful for configurations loaded from text formats or assembled dynamically before the model package is loaded. Inspect the accepted surface with parameters(Experiment).

Values without units are interpreted in the unit declared by the variable. Supplying explicit compatible quantities is clearer when configurations cross files or packages.

Sample stochastic parameters with ±

mean ± standard_deviation

Cropbox interprets this value as a Normal draw when a state is initialized. Units may be attached to the entire expression:

config = @config RootType => (
    growth_rate = (6.0 ± 0.6)u"cm/d",
    angle = 70 ± 10,
)

s = instance(RootModel; config, seed = 1)

This is a sampling specification, not a general uncertainty-propagation number. Once sampled, the state contains one ordinary value. Use seed for a reproducible draw, and use an explicit seed loop when independent replicates are needed. The CropRootBox tutorial shows this pattern in a dynamic root model.

Merge configurations

Later values override earlier values.

base      = @config Experiment => (rate = 1, initial = 2)
treatment = @config Experiment => :rate => 3
combined  = @config base + treatment

Config for 1 system:

Experiment
rate=3 g hr^-1
initial=2 g

Use this ordering consistently: package defaults, site or cultivar defaults, experiment settings, and finally the smallest treatment patch.

Expand a parameter sweep

Prefix ! expands one iterable into separate configurations.

rates = @config base + !(Experiment => :rate => 1:3)
length(rates)
3

A literal vector is useful when scenarios are not generated from one range:

explicit_rates = @config [
    Experiment => :rate => 1,
    Experiment => :rate => 3,
]
length(explicit_rates)
2

Pass the result as configs, not config.

simulate(Experiment;
    configs = rates,
    stop = 2u"hr",
    target = :mass,
    meta = :Experiment,
)
9×4 DataFrame
Rowtimemassrateinitial
Quantity…Quantity…Quantity…Quantity…
10//1 hr2.0 g1 g hr^-12 g
21//1 hr3.0 g1 g hr^-12 g
32//1 hr4.0 g1 g hr^-12 g
40//1 hr2.0 g2 g hr^-12 g
51//1 hr4.0 g2 g hr^-12 g
62//1 hr6.0 g2 g hr^-12 g
70//1 hr2.0 g3 g hr^-12 g
81//1 hr5.0 g3 g hr^-12 g
92//1 hr8.0 g3 g hr^-12 g

Build factorial combinations

* forms the Cartesian product of configuration patches.

design = @config (
    Experiment => :rate    => [1, 2]
) * (
    Experiment => :initial => [0, 10]
)
length(design)
4

The values associated with * are treated as collections to expand. Use ! when only one factor is needed.

Configure infrastructure

Built-in systems are configured like model systems.

@config (
    Clock => :step => 1u"d",
    Calendar => :init => ZonedDateTime(2025, 1, 1, tz"UTC"),
)

The special system key :0 targets the root controller name. It is useful in generic workshop code but an explicit system name is easier to maintain in a published model.

Supply tables and external values

Parameters are not limited to scalars. provide(parameter) commonly receives a DataFrame through configuration, while a system with an extern variable may receive an object through constructor options instead.

config = @config Weather => :data => weather
s = instance(RootArchitecture; config, options = (; box = container))

Configuration is declarative input. options are constructor keywords and may carry resources that are not normal model parameters. The distinction matters for reproducibility: store configuration when possible, and document every external option explicitly.

Normalize units at data boundaries

unitfy reads compatible units and simple type annotations from DataFrame column names. This pattern is useful for workshop data and CSV files whose headers carry their schema.

raw = DataFrame(
    "time (d)" => 0:2,
    "mass (g)" => [1, 3, 7],
)

typed = unitfy(raw)
(names = names(typed), time = typed.time, mass = typed.mass)
(names = ["time", "mass"], time = Unitful.Quantity{Int64, 𝐓, Unitful.FreeUnits{(d,), 𝐓, nothing}}[0 d, 1 d, 2 d], mass = Unitful.Quantity{Int64, 𝐌, Unitful.FreeUnits{(g,), 𝐌, nothing}}[1 g, 3 g, 7 g])

deunitfy(typed) removes quantities and writes unit annotations back into column names. Use it only at an output boundary that requires plain values; keep quantities inside model calculations. A provide declaration performs the same column-name interpretation by default through autounit=true.

Avoid ambiguous combinations

  • Use config for one scenario and configs for a collection. With simulate, both may be supplied deliberately: config becomes the shared base and each entry in configs is applied as a later patch.
  • evaluate and calibrate instead treat config and configs as mutually exclusive. Merge a shared base into every entry before passing configs to those functions.
  • Do not combine configs with the simulate(parameters=...) shortcut; those are alternative ways to generate scenario collections.
  • Confirm that a target declaration has the parameter tag; otherwise a matching configuration entry cannot replace it.
  • Check the system key, short variable name, and alias with parameters when a value appears to be ignored.