Skip to content

Mathematical Expressions

Back to Building Models

Rates and formula parameters are written as expressions, such as "k1 * A * B / total". This page lists everything an expression can contain: operators, variables, functions, and the security limits. The engine checks each expression against this language, compiles it to machine code (Cranelift) once, and reuses it at every step.

Quick path

from commol import Aggregate, ModelBuilder, Simulation

model = (
    ModelBuilder(name="Expressions")
    .add_bin(id="A", name="State A")
    .add_bin(id="B", name="State B")
    .add_parameter(id="k1", value=0.3)
    .add_parameter(id="k1_periodic", value="k1 * (1 + 0.3 * sin(2 * pi * step / 365))")
    .add_transition(id="t_ab", source=["A"], target=["B"], rate="k1_periodic * A * B / total")
    .add_transition(id="t_ba", source=["B"], target=["A"], rate="if(step > 30, 0.05, 0)")
    .build(
        Aggregate(
            bin_amounts={"A": 990, "B": 10},
        ),
        engine="DifferenceEquations",
    )
)

results = Simulation(model).run(num_steps=60)
Where Example Evaluated
Transition rate "k1 * A * B / total" For every flow, at every step
Formula parameter value "k1 * 2" At every step, in declaration order, before the rates
Stratified rate rate "k_g1 * A" Like a transition rate
Calibration constraint expression "k1 - k2" See Model Calibration

The variables a rate names decide whether it is absolute or proportional; see Absolute and proportional rates.

Operators

Operator Meaning Example Value
+, - Addition, subtraction, negation "k1 - k2", "-k1"
*, / Multiplication, division "k1 * B / total"
% Remainder "step % 7" 0 to 6
** or ^ Power "2 * 3 ** 4" 162
( ) Grouping "(2 + 3) * 4" 20
>, <, >=, <=, ==, != Comparison, only inside if "if(step > 30, k1, 0)"
! Not, only inside if "if(!(step > 30), k1, 0)"

Precedence, from high to low: parentheses, power, the sign -, multiplication, division and remainder, addition and subtraction. So "2 ** 3 * 4" is 32, "2 + 3 * 4" is 14 and "-2 ** 2" is -4. Operators of one level apply from the left: "8 - 4 - 2" is 2 and "8 / 4 * 2" is 4. A chain of powers applies from the right, as in Python: "2 ** 3 ** 2" is "2 ** (3 ** 2)", 512.

A comparison gives a boolean, not a number, so it cannot be multiplied: "k1 * (step > 30)" is rejected. Write "if(step > 30, k1, 0)". Rates do not accept && or ||; nest if calls instead:

in_window = "if(step >= 10, if(step <= 20, 0.05, 0), 0)"   # 0.05 for steps 10 to 20
outside = "if(step < 10, 0.05, if(step > 20, 0.05, 0))"    # 0.05 outside that window

Variables

An id starts with a letter or _ and can hold digits after it, such as e1 or c1star. A number such as 1e5 or 2.5E-3 is a number: its exponent is never a variable.

Variable Value
A parameter id The parameter's value at this step
A bin id, without stratifications The bin's amount
A compartment name, such as A_g1 That compartment's amount
A bin id, with stratifications The total of all its compartments
total The total: the sum of every compartment, accumulators excluded
neighbors_<bin>, neighbors In a transition rate only: what the neighbors of a unit hold, of one bin or of every bin. The structure says who they are; a rate that reads them and no compartment is proportional. See Neighbors
t The current position on the axis. A difference model reads it at each step, so there it equals step; a differential model reads it anywhere
step The step that holds the current position, floor(t): 0, 1, 2, … It is constant on each unit interval of a differential model; see Time inside a step
pi, e π and e

Subgroup totals

With stratifications, the engine also provides sums of compartments, the subgroup totals. Category suffixes must follow the order in which the stratifications were declared. commol_rs.model.subgroup_total_names (in crates/commol-model/src/builder.rs) lists them for a process.

Variable Sum of Example (group then type)
total_<cat>… Every compartment with these categories total_g1, total_t1, total_g1_t1
<bin> Every compartment of the bin A
<bin>_<cat>… The bin's compartments with these categories A_g1, A_t1; with every category, the compartment itself (A_g1_t1)
from commol import ModelBuilder

def value_of(expression: str) -> float:
    """The value of an expression at step 0, read through a formula parameter."""
    model = (
        ModelBuilder(name="Variables")
        .add_bin(id="A", name="State A")
        .add_bin(id="B", name="State B")
        .add_stratification(id="group", categories=["g1", "g2"])
        .add_stratification(id="type", categories=["t1", "t2"])
        .add_parameter(id="probe", value=expression)
        .add_transition(id="probe_flow", source=[], target=["B"], rate="probe")
        .build(
            Aggregate(
                bin_amounts={"A": 600, "B": 400},
                stratification_fractions={
                    "group": {"g1": 0.25, "g2": 0.75},
                    "type": {"t1": 0.5, "t2": 0.5},
                },
            ),
            engine="DifferenceEquations",
        )
    )
    results = Simulation(model).run(num_steps=1)
    inflow = sum(series[1] - series[0] for series in results.values())
    return inflow / 4  # the probe flows into each of the 4 B compartments

print(value_of("total + 0"))        # 1000.0
print(value_of("total_g1 + 0"))     # 250.0  = 1000 x 0.25
print(value_of("total_g1_t1 + 0"))  # 125.0  = 1000 x 0.25 x 0.5
print(value_of("A + 0"))        # 600.0
print(value_of("A_g1 + 0"))     # 150.0  = 600 x 0.25
print(value_of("A_t1 + 0"))     # 300.0  = 600 x 0.5
Pitfall Result
Suffixes out of declaration order, as in A_t1_g1 Error: the variable is not found
total_… out of order, as in total_t1_g1 build fails and lists the valid total_… names
A combination that a conditional stratification never creates build fails for total_…; <bin>_… is not found

Functions

Group Functions
Trigonometric sin, cos, tan, asin, acos, atan, atan2(y, x)
Hyperbolic sinh, cosh, tanh, asinh, acosh, atanh
Exponential and logarithm exp, log (natural), ln (natural), log10, log2
Power and root sqrt, cbrt, pow(x, y), hypot(x, y)
Comparison min(a, b, ...), max(a, b, ...) (two arguments or more), abs
Rounding floor, ceil, round (halves round away from zero: round(2.5) is 3, round(-2.5) is -3)
Conditional if(condition, value_if_true, value_if_false)

No other function is in the language: erf(x) or trunc(x) is rejected.

For rates that change along the axis, AxisPattern builds tested expressions for pulses, windows, ramps and sinusoids.

Common patterns

patterns = {
    "product of two amounts": "k1 * A * B",
    "frequency dependent": "k1 * A * B / total",
    "saturation": "k1 * B / (1 + B)",
    "linear growth along the axis": "0.1 + 0.001 * step",
    "exponential decay along the axis": "k1 * exp(-0.01 * step)",
    "cycle of 365 steps": "k1 * (1 + 0.3 * sin(2 * pi * step / 365))",
    "never negative": "max(0, k1 - 0.001 * step)",
    "capped above a threshold": "k1 * min(1, 100 / max(B, 1))",
    "only after step 30": "if(step > 30, k1, 0)",
    "safe division": "k1 * B / max(total, 1)",
}

To keep a rate readable, move part of it into a formula parameter: add_parameter(id="k1_periodic", value="k1 * (1 + 0.3 * sin(2 * pi * step / 365))") and then rate="k1_periodic * A * B / total".

Rejected expressions

The engine checks each expression when it is added: a transition rate, a stratified rate, a formula parameter, a calibration constraint or an axis pattern. An expression outside the language of this page fails with a ValueError that names the expression and its first part outside the language: an unknown function, a call with the wrong number of arguments, && or ||, or a comparison or ! outside the condition of an if.

try:
    ModelBuilder(name="Rejected").add_parameter(id="k2", value="k1 * (step > 30)")
except ValueError as error:
    print(error)  # ... Parameter 'k2': Formula 'k1 * (step > 30)': the comparison '>' ...

When the model is built, the engine checks the variables each rate and each formula parameter reads. A variable that no parameter, bin, compartment, subgroup total or engine variable defines fails the build, with a message that names the transition or the parameter, the expression, the undefined variables, and the parameters and bins of the model: Undefined variables in transition 'bc' rate 'k9 * B': k9. Available parameters: k1, k2. Available bins: A, B, C. A category id, such as g1, is not a variable: write total_g1 for its compartments.

Security

Every expression is also checked before it reaches the engine (commol.utils.security.validate_expression_security). An expression that is not plain mathematics is rejected when the transition is added:

try:
    ModelBuilder(name="Unsafe").add_bin(id="A", name="State A").add_transition(
        id="t", source=["A"], target=[], rate="__import__('os')"
    )
except ValueError as error:
    print("rejected")
Limit (SecurityConfig) Value
Expression length 10000 characters
Nesting depth 10 levels
Function calls 500
Variable name length 50 characters
Numeric literal 1e12
Complexity score: the nodes of the tree of the expression (numbers, variables, operators and calls) 1000

The engine has one limit of its own, which no configuration changes: a formula nests at most 100 levels deep. Each pair of parentheses, each call, each sign and each power is one level; a sum or a product of any number of terms is one. A deeper formula fails with it nests deeper than 100 levels of parentheses, calls, signs and powers.

Next steps

To learn Read
Ready-made rates that change along the axis Axis Patterns
How rates become flows Transitions
Running a model Running Simulations