Skip to content

Units

Back to Building Models

Commol checks that every rate of a built Model has the right units. To see the equations the engine simulates, with their units, see Describing a Model.

Quick path

from commol import Aggregate, ModelBuilder

model = (
    ModelBuilder(name="Model with Units", bin_unit="item", step_unit="day")
    .add_bin(id="A", name="State A")
    .add_bin(id="B", name="State B")
    .add_bin(id="C", name="State C")
    .add_parameter(id="k1", value=0.5, unit="1/day")
    .add_parameter(id="k2", value=0.1, unit="1/day")
    .add_transition(id="t_ab", source=["A"], target=["B"], rate="k1 * A * B / total")
    .add_transition(id="t_bc", source=["B"], target=["C"], rate="k2 * B")
    .build(
        Aggregate(
            bin_amounts={"A": 900, "B": 100, "C": 0},
        ),
        engine="DifferenceEquations",
    )
)

model.check_unit_consistency(verbose=True)  # Unit consistency check passed successfully.

Unit checking

Model.check_unit_consistency(verbose=False) checks that every flow has the unit <bin unit>/<step unit>, every impulse the unit <bin unit>, and that every function receives a plain number. It checks the models of every engine alike: in each, a rate is per unit of the axis, and only what its value means differs (see What a rate means). It reads the flows the engine plans (transition_flows), so a proportional rate is checked as its flow, the rate times its source: k2 in 1/day from a bin in item gives item/day. The bin unit of a flow is the unit of the bin it leaves, or of the bin it enters when it has no source; so a category transition in a model of bins in item and kg moves item/day in one bin and kg/day in the other. In such a model total, a subgroup total total_<categories> and neighbors add amounts of different units, so they have none, and a rate that reads one fails the check; read the bins or the compartments instead. A rate that reads the neighbors of a unit is checked as the sums they are. It raises UnitConsistencyError (from commol.utils.equations) and lists every transition that fails.

What needs a unit

Item How it gets a unit
Bins ModelBuilder(bin_unit=...), or add_bin(unit=...) for one bin. Required: the check names every bin without one.
total, total_<category>… The bin unit, automatically
Constant parameters add_parameter(unit=...). Required: the check raises if one is missing.
Formula parameters Inferred from the formula; a unit is optional
t The step unit, automatically: it is a position on the axis
step, pi, e Dimensionless, automatically: step counts increments
Unit Use for
"1/day", "1/week", "1/step" Rates
"dimensionless" Ratios, fractions, amplitudes, function arguments
Any word that pint does not define, such as "item" A unit of its own dimension; Commol registers it

A unit means what pint defines, for time as for any other quantity: month, year and fortnight are units of time, but quarter is a unit of mass and decade is a number. A word that pint does not define, such as mo, becomes a unit of its own dimension, which no other unit converts to.

The step unit

The engine converts no value between units. So one step has one unit, and every rate must use it (UnitChecker._step_unit in py-commol/commol/context/_model/units.py).

The axis of a model is its independent variable: the quantity the model steps along or differentiates over. Time is one case; a length, a mass or any other quantity is another. So the step unit may have any single dimension. Declare it with ModelBuilder(..., step_unit="day") (Model.step_unit); check_step_unit accepts one unit of one dimension with exponent 1, such as day or meter, and rejects 1/day or meter**2. A model that declares no step unit has the step unit step, the default increment of any axis, of its own dimension [step]. The check never guesses the step unit from the parameters.

Model Step unit Flows must be
step_unit="day" day <bin unit>/day: a rate in 1/d passes, a rate in 1/week or 1/quarter fails
step_unit="meter" meter <bin unit>/meter
No step_unit, rates in 1/step or none step <bin unit>/step
No step_unit, rates in 1/day step Fails; the error says to declare step_unit

Two names of one unit, such as d and day, are one unit. A flow must have exactly the expected unit: item/week does not match item/day, because the engine would read a weekly rate as a daily one.

along_a_line = (
    ModelBuilder(name="Along a line", bin_unit="item", step_unit="meter")
    .add_bin(id="A", name="State A")
    .add_bin(id="B", name="State B")
    .add_parameter(id="k1", value=0.01, unit="1/meter")
    .add_transition(id="t_ab", source=["A"], target=["B"], rate="k1 * A")
    .build(
        Aggregate(
            bin_amounts={"A": 1000, "B": 0},
        ),
        engine="DifferentialEquations",
    )
)
along_a_line.check_unit_consistency(verbose=True)  # Unit consistency check passed successfully.

Model.axis_units() converts values in units of the step dimension to steps, and gives the rates the engine reads; see Units of the axis.

Positions and numbers in a rate

t is a position on the axis, so it has the step unit: with step_unit="day", k * t * A passes when k is in 1/day**2. step counts increments and is dimensionless.

When a rate adds a number to a quantity, subtracts it or compares it, the number has the unit of that quantity, as the engine reads it: if(t >= 10, k, 0) compares t with 10 days, and t - 3 is 3 days before t. The two branches of if and the arguments of min and max must share a unit, and a written 0 takes the unit of the other branch. A number that a function computes, such as exp(2), stays dimensionless.

Impulses

An impulse moves an amount at once, so its rate must have the bin unit: p * A with p in "dimensionless" is an amount of <bin unit>; k * A with k in "1/day" is a flow and fails with expected '<bin unit>'.

What the check catches

from commol.utils.equations import UnitConsistencyError

wrong_units = (
    ModelBuilder(name="Wrong units", bin_unit="item", step_unit="day")
    .add_bin(id="A", name="State A")
    .add_bin(id="B", name="State B")
    .add_parameter(id="k1", value=0.5, unit="day")  # should be "1/day"
    .add_parameter(id="k2", value=0.1, unit="1/day")
    .add_transition(id="t_ab", source=["A"], target=["B"], rate="k1 * A * B / total")
    .add_transition(id="t_ba", source=["B"], target=["A"], rate="k2 * sin(B) * B")
    .build(
        Aggregate(
            bin_amounts={"A": 900, "B": 100},
        ),
        engine="DifferenceEquations",
    )
)

try:
    wrong_units.check_unit_consistency()
except UnitConsistencyError as error:
    print(error)
Error Cause
Unit mismatch: equation '…' has unit 'day * item' but expected 'item/day' A rate with the wrong unit
Cannot convert from 'item' to 'dimensionless' A function such as sin, exp or log received a quantity with a unit
The following constant parameters are missing units: … A constant parameter has no unit
The following bins have no unit: … A bin has no unit (set bin_unit)
The model declares no step unit, so it is 'step' Rates in a unit such as 1/day without step_unit; see The step unit
Variable '…' … has no defined unit A variable of a rate has no unit

To pass a quantity to a function, make it dimensionless first, for example sin(2 * pi * step / 365) or exp(-decay * step) with decay in "dimensionless", or sin(2 * pi * t / period) with period in the step unit.

Dates on the axis

An axis of time can start at a calendar moment: ModelBuilder(origin=...) takes a datetime.date (read as its midnight) or a datetime.datetime with no time zone. Model.axis_units() then converts between dates and positions, and gives the positions where calendar days and weeks end.

import calendar
from datetime import date, datetime

dated = (
    ModelBuilder(name="Dated", step_unit="hour", origin=date(2020, 1, 1))
    .add_bin(id="A", name="State A")
    .add_bin(id="B", name="State B")
    .add_parameter(id="k1", value=0.01)
    .add_transition(id="t_ab", source=["A"], target=["B"], rate="k1 * A")
    .build(Aggregate(bin_amounts={"A": 1000, "B": 0}), engine="DifferenceEquations")
)
axis = dated.axis_units()

print(axis.position_of(date(2020, 1, 2)))         # 24.0
print(axis.position_of(datetime(2020, 1, 1, 6)))  # 6.0
print(axis.moment_of(36))                         # 2020-01-02 12:00:00
print(axis.day_ends(6, 72))                       # [24.0, 48.0, 72.0]
# 2020-01-01 is a Wednesday: the first week that starts on Sunday ends on 5 January.
print(axis.week_ends(0, 400, first_weekday=calendar.SUNDAY))  # [96.0, 264.0]
Method Gives
position_of(moment) (moment - origin) in steps; negative before the origin.
moment_of(position) origin + position steps, a datetime with no time zone. The inverse of position_of.
day_ends(start, end) The positions of the midnights in (start, end].
week_ends(start, end, first_weekday) The positions in (start, end] where a week starts, at midnight of first_weekday: 0 (Monday) to 6 (Sunday), as date.weekday() and calendar.SUNDAY number them.

A period ends where the next one starts, so its end is the midnight that starts the next day or week. The range (start, end] is the range of a window: a period that ends at start is not in it, and the first end can close a partial period that starts before start. The number of a period (a week number of a national rule) is not part of the axis: label a period from its end with your own function.

Rules of the origin, each a ValueError:

Rule Why
A conversion needs an origin. An axis without one has no dates.
The moment has no time zone. A step has no time zone, so (moment - origin) / step stays exact through a change of summer time. Convert an aware moment with moment.astimezone(zone).replace(tzinfo=None).
The step unit is a unit of time of a whole number of days, or a day divided by a whole number, such as hour, day or week. month and year are units of a fixed length (a month is 1/12 of a year, about 30.44 days), not the length of a calendar month or year, so their positions would not fall on calendar dates.

With an origin, results.plot.series() labels its ticks with dates when it has no position_label.

Next steps

To learn Read
The equations the engine simulates Describing a Model
What a formula can contain Mathematical Expressions
Running the model Running Simulations