Skip to content

Dynamic Expressions

Many model parameters accept “dynamic expressions” which are mathematical expressions that may include data references, constants, model results, inbuilt variables.

Basic Usage

The simplest dynamic expressions are constants, data references, model result references.

Constant expressions

evap = 5.0
rain = 2.5

Data References

evap = data.climate.by_name.evaporation
rain = data.rainfall.by_name.value

Click here to find out more about data references.

Model Result References

evap = data.climate.by_name.evaporation
rain = data.rainfall.by_name.value

Click here to find out more about model result references.

Arithmetic

Dynamic expressions accept standard mathematical operators: +, -, *, /, ^

evap = 1.2 * data.climate.by_name.evaporation
rain = 0.2 * data.rainfall.by_name.site_a + 0.8 * data.rainfall.by_name.site_b

observed = data.price_of_bananas.by_name.discounted ^ 2 + 10

Conditional Logic

Use if(condition, true_value, false_value) for conditional expressions.

# Summer evaporation is higher
evap = if(data.month > 10, data.summer.by_index.1, data.winter.by_index.1)

# Apply seasonal adjustment
rain = data.rainfall.by_index.1 * if(data.season.by_index.1 == 1, 1.2, 0.8)

# Clamp negative values to zero
inflow = if(data.raw_flows.by_index.0 < 0, 0, data.raw_flows.by_index.0)

Comparison operators: >, <, >=, <=, ==, !=

Common Functions

You can also use common mathematical functions.

# Take the greater of two values
evap = max(data.evap.by_name.observed, data.evap.by_name.modelled)

# Ensure non-negative
inflow = max(data.flow.by_name.inflow - data.flow.by_name.loss, 0)

# Absolute value
diff = abs(observed - modeled)

# Power and square root
area = sqrt(area_squared)
volume = pow(radius, 3) * 3.14159

# Trigonometric
angle = sin(time * 0.1) * amplitude

# Complex example
evap = if(temp > 30,
    evap_high * 1.3,
    if(temp > 20, 
          data.evap_medium, 
          data.evap_low * 0.8))

Available functions:

Function Arguments Description
if 3 Conditional: if(condition, true_val, false_val)
max 2+ Maximum of values
min 2+ Minimum of values
sum 1+ Sum of values
mean 1+ Arithmetic mean of values
abs 1 Absolute value
sqrt 1 Square root
pow 2 Power: pow(base, exponent)
exp 1 Exponential (e^x)
ln 1 Natural logarithm
log10 1 Base-10 logarithm
log2 1 Base-2 logarithm
sin 1 Sine
cos 1 Cosine
tan 1 Tangent
asin 1 Arcsine
acos 1 Arccosine
atan 1 Arctangent
atan2 2 Two-argument arctangent: atan2(y, x)
floor 1 Round down
ceil 1 Round up
round 1 Round to nearest
sign 1 Sign (-1, 0, or 1)
infill 2 Infill a missing value: infill(x, value) is value when x is NaN, else x unchanged
clamp 3 Constrain to a range: clamp(x, lo, hi)
is_leap_year 1 1 in a Gregorian leap year, else 0: is_leap_year(sim.year)
month_at 1 Month (1-12) at the current date + n days — the pattern month an order placed today arrives in
days_in_month_at 1 Leap-aware days in the month at the current date + n days
moving_sum 3 Sum over the last n steps: moving_sum(x, n, default)
moving_mean 3 Mean over the last n steps
moving_min 3 Minimum over the last n steps
moving_max 3 Maximum over the last n steps
moving_sum_years 3 Sum over the last n_years water years: moving_sum_years(x, n_years, wy_month)
moving_min_years 3 Minimum over the last n_years water years
moving_max_years 3 Maximum over the last n_years water years
moving_sum_months 2 Sum over the last n_months calendar months: moving_sum_months(x, n_months)
moving_min_months 2 Minimum over the last n_months calendar months
moving_max_months 2 Maximum over the last n_months calendar months
moving_sum_days 2 Sum over the last n_days calendar days: moving_sum_days(x, n_days)
moving_min_days 2 Minimum over the last n_days calendar days
moving_max_days 2 Maximum over the last n_days calendar days
sum_since 2 Sum of x since a reset condition last fired
min_since 2 Minimum of x since reset
max_since 2 Maximum of x since reset
count_since 2 Steps on which a condition held since reset
steps_since 1 Steps elapsed since reset (0 on a reset step)
latch 3 Sample x when a condition holds, else hold the last sample: latch(x, condition, init)

Two names are deliberately absent. There is no log: write the explicit ln or log10. And there is no avg or average: the function is mean, named for the specific statistic ("average" is the family that also contains the median and mode).

The calendar pair (month_at, days_in_month_at) exists for order-ahead pattern lookups — orders are placed lag days before delivery, so the demand pattern belongs to the arrival month:

order = annual * table.pattern(month_at(3)) / days_in_month_at(3)

Negative offsets look back. The engine owns the calendar: the offset date is computed exactly, so year boundaries and leap-February need no hand-rolled logic and any offset length works.

Lookup Tables

Named tables defined in [table.*] sections can be called like functions — table.my_table(x) for 1D interpolation, table.my_table(x, y) for 2D.

flow = table.rating(node.reach_5.dsflow)
release = table.pump_rating(sim.month, node.dam.volume)

Click here to find out more about lookup tables.

Program Blocks

A value can be a { ... } program block: statements terminated by ;, then a bare final expression whose value is the block's value. Statements are local assignments and assertions.

pond_demand = {
    target = table.monthly_demand(sim.month);
    recent = moving_mean(node.headwater.ds_1, 30, 0.0);
    assert(target >= 0);
    min(target, recent * const.demand_fraction)
    }
  • The final line must be a bare expression with no ; — a terminated final line is a load error, never a silent default.
  • Locals are bare lowercase names, private to their block, and must be assigned above their first use. They cannot take the name of any inbuilt function or reserved word.
  • assert(cond) stops the run — naming the statement, node, and timestep — when cond is 0 or NaN. NaN is exactly the case you most want caught.
  • A block is only legal as the entire value.

Temporal Functions

These functions remember earlier timesteps. Their state advances exactly once per timestep, unconditionally — a moving_mean inside an untaken if branch still updates, so its value depends only on the series it watches, never on which branches past evaluations took.

Moving windowsmoving_sum(x, n, default) and friends compute over the last n steps. n and default must be plain numbers (state is sized when the model loads): default pre-fills the window, so the statistic is well-defined from the very first step.

recent_flow = moving_mean(node.gauge_1.dsflow, 30, 0.0)

Annual, monthly, and daily windowsmoving_sum_years(x, wy_month, n_years) and friends work differently: x is bucketed by calendar period first (one running total per water year, month, or day), and the statistic is reported over the last n buckets, not the last n steps. There's no default — a bucket not yet reached simply contributes nothing. moving_*_years takes an anchor month, wy_month (1-12): a new water year starts the first time the month reaches it. moving_*_months/ moving_*_days need no anchor — a new bucket starts every calendar month/day.

# Trailing 3-year total, water year starting 1 July
three_yr_diversion = moving_sum_years(node.town.diversion, 3, 7)

# Trailing 12-month mean
rolling_12mo_total = moving_sum_months(node.gauge_1.dsflow, 12)

moving_min_years/max (and their monthly/daily equivalents) suppress NaN — a NaN input never disturbs the tracked extremum, matching plain moving_min/max. moving_sum_years (and the monthly/daily sums) still poison on NaN, like plain moving_sum: a real gap in the data should make that year's total suspect, not vanish quietly. This matters if you build a value with if(cond, x, 0.0 / 0.0) to make it count only on certain steps (the usual way to say "only this branch counts" in a side-effect-free expression language, since there's no bare nan literal outside the [offset, default] position) — that composes straight into moving_max_years, but needs infill(..., 0) to compose into moving_sum_years. infill substitutes the value rather than dropping the element, so the fill is stated at the call site:

daily_total = if(sim.new_day, moving_sum_days(node.gauge_1.dsflow, 1), 0.0 / 0.0)
peak_daily_total_wy = moving_max_years(daily_total, 5, 7)
sum_daily_totals_wy = moving_sum_years(infill(daily_total, 0), 5, 7)

Event windows — the *_since family accumulates since a reset condition last fired, and the last argument is always the reset condition. On the step the reset fires, the accumulator clears first and that step's contribution is then included — on 1 July, "usage this water year" equals that day's usage, not zero. The start of the run counts as a reset.

used_wy = sum_since(node.town.diversion, sim.new_month && sim.month == 7)
dry_spell = steps_since(node.gauge.dsflow > const.low_flow_threshold)
spill_days = count_since(node.dam.ds_1_spill > 0, sim.new_year)

sum_since/*_since and the calendar flags deliberately carry no water-year setting of their own — the boundary is an expression written where it's used (or named once per model in a user-defined function), because the water year varies from valley to valley. moving_*_years above is the one deliberate exception: a trailing multi-year window needs more than a reset condition can express, so it takes wy_month directly.

User-Defined Functions

Functions defined in the [fn] section are called with the fn. prefix:

order = fn.net_demand(data.town.by_name.population, sim.day_of_year)

Model Variables

Values published by [var.*] blocks are read like any series, including with the offset syntax:

release = min(this.order, var.accounting.headroom)
prev = var.accounting.headroom[-1, 0.0]

Notes

  • Expressions are evaluated once per timestep

  • Simple constants and data references are optimised for performance

  • Whitespace is ignored: a+b and a + b are equivalent

  • Function names are not case sensitive when called; names you define (functions, parameters, var blocks and keys) are strictly lowercase

  • Nothing you name may collide with an inbuilt function, a temporal function, or a reserved word (assert, this) — the load error names the clash

Referencing Input DataReferencing Model ResultsSimulation Context VarsConstantsTables