Examples — the behaviour spec¶
Every model here is a specification, and not a demonstration. A Modelica model already says what the system does. Put a small file of expected behaviour beside it, and the pair becomes an acceptance test that no layer of the toolchain can quietly break.
examples/
library/*.mo small component packages: Electrical, Translational, Thermal,
Rotational, Machines, Blocks
library/*.py the same packages through the Python front end
models/*.mo one self-contained example model each
models/*.py the same model through the Python front end
models/*.toml what that model must do — the spec
tests/test_examples.py reads the corpus and runs it. A stage that is not implemented yet
reports as skipped and not as failed. The suite is therefore a live progress meter: as
lang, ir and sim land, skips turn into passes, and no spec is edited.
Written twice, on purpose¶
Every example exists in both front ends, under the same stem. RCCircuit.mo and
RCCircuit.py are the same model, and the suite asserts that they say the same thing. That
pair is the only thing that keeps modelica.build and modelica.lang from drifting apart.
class RCCircuit(Model):
"""A capacitor charging through a resistor from a 1 V source"""
source = Electrical.ConstantVoltage(V=1)
r = Electrical.Resistor(R=100)
c = Electrical.Capacitor(C=0.01, v=mod(start=0, fixed=True))
ground = Electrical.Ground()
def equations(self):
yield connect(self.source.p, self.r.p)
yield connect(self.r.n, self.c.p)
yield connect(self.c.n, self.source.n)
yield connect(self.source.n, self.ground.p)
Four conventions apply.
- A library twin defines its classes and ends with
PACKAGE = package("Electrical", __name__, "..."). The name isPACKAGEand notElectrical, which avoids anElectrical.Electricalon import. - A model twin is one class named after the file, so
getattr(module, path.stem)finds it. - The examples are not an installed package. A model that uses a library adds the sibling
directory to
sys.pathat the top of the file.import Electricalthen reads exactly like the Modelicaimport. - Run a twin directly to print its Modelica:
python examples/models/RCCircuit.py.
Today the comparison is on normalised text. Blank lines are dropped, and numeric literals
go through repr(float(...)) so that 1e4 and 10000.0 agree. Once the comparison of
flat models as values replaces it, the check becomes the one actually worth having.
The spec format¶
TOML, one file per model, named after it. An absent key asserts nothing. Write only what you are willing to commit to.
summary = "One line: what the model is."
teaches = ["connect", "flow variables"] # what a reader should learn from it
requires = ["Electrical"] # library packages to load first ([] if none)
[parse]
classes = ["RCCircuit"] # top-level classes the file must yield
[flatten]
model = "RCCircuit" # the class to instantiate
states = ["c.v"] # continuous states, after index reduction
index = 1 # differentiation index of the flat DAE
balanced = true # #equations == #unknowns
equations = 14 # optional, only where the count is unambiguous
[simulate]
stop = 5.0
tolerance = 1e-8
method = "LSODA" # optional; any scipy `solve_ivp` method, default "Radau"
# A value at a point in time. `source` says where the number came from:
# "analytic" — a closed form, written out in a comment
# "reference" — scipy at rtol/atol 1e-12, which is a golden trajectory
# "measured" — read off real hardware; see `RoastingMachine`
[[simulate.point]]
t = 1.0
values = { "c.v" = 0.632120558829 }
rtol = 1e-6
source = "analytic"
# Something that must hold at every output point: an expression, and either a value it
# equals or bounds it stays within. `after` restricts the check to t >= after.
[[simulate.invariant]]
expr = "y1 + y2 + y3"
value = 1.0
atol = 1e-8
# Expected state events (zero crossings), in order.
[simulate.events]
times = [0.451523641437, 0.813742554587]
atol = 1e-6
Drawing one¶
A model of a mechanism has a picture that a line plot cannot show. For those the spec
carries an optional [animate] section: bodies at their coordinates, rods and springs
between them, the path a bob sweeps, and a panel underneath for the one scalar worth
watching. The coordinates are Modelica expressions over the model's own variables, on the
same parse_expression and emit_expr road the invariants take, so x2 means the model's
x2.
[animate]
stop = 40.0 # optional; defaults to [simulate].stop
fps = 30
speed = 1.0 # seconds of model time per second of video
title = "..."
width = 6.6 # inches; the height follows the plane, never given
[[animate.part]] # kind = rod | spring | mass | pivot | track
kind = "spring"
from = ["0", "0"] # `at` instead of `from`/`to` for mass and pivot
to = ["xp", "yp"]
coils = 14
[[animate.part]]
kind = "mass"
at = ["x2", "y2"]
trace = 4.0 # seconds of path kept behind it
color = 1 # index into the palette; 0 if omitted
[[animate.plot]]
expr = "energy"
label = "total energy (J)"
ylim = [-35.5, -33.5] # worth fixing for a conserved quantity — see below
python examples/animate.py <Model> writes <Model>.mp4. --out x.gif writes a GIF
instead. --still 24.0 writes one frame at that instant, which is what you want while you
write a scene. It needs matplotlib, which is in the docs extra, and for .mp4 it needs
ffmpeg. The library itself will never depend on either.
Two things are easy to get wrong, and both now default the right way. Leave xlim and
ylim out unless you have a reason: the frame is computed from everything the run visits,
and a guess clips a chaotic path. And fix ylim on a panel that shows a conserved
quantity: left alone, the axis zooms into the last digit of the integrator, and a line that
is flat to one part in 10⁹ is drawn as violent noise. That is the opposite of the claim.
Seeing one¶
Every model in the corpus draws. A picture is often the fastest way to check that a model says what you meant.
python -m modelica.diagram examples/models/RLCCircuit.mo examples/library/Electrical.mo > rlc.svg
python -m modelica.diagram examples/models/DCMotor.mo examples/library/Electrical.mo \
examples/library/Rotational.mo examples/library/Machines.mo --depth 2 > motor.svg
--view incidence draws the equations against the variables they contain, and
--view blocks draws the order the solver runs in. tests/test_diagram.py draws every
model in the corpus on every run, and it asserts that no box names a component the flat
model does not have.
The models¶
They are ordered the way the toolchain was built. Each one needs everything that the ones above it needed, plus exactly one new capability.
| Model | The new capability it forces | Reference |
|---|---|---|
NewtonCooling |
der(), parameters, one state, one equation |
analytic |
RCCircuit |
connect(), flow, extends, a component library |
analytic |
TwoBodyHeat |
connectors in a second domain, and a conservation invariant | analytic |
RLCCircuit |
two states, and a coupled linear system | scipy |
MassSpringDamper |
the same physics through the connectors of another domain | scipy |
DrainingTank |
if in an equation, which makes a state event; sqrt |
analytic |
BouncingBall |
when, reinit and pre: repeated events, and a state set again |
analytic |
RoastingMachine |
time in an equation, which makes time events and a state event; a parameter bound to other parameters; a model of the sensor |
a real machine |
Pendulum |
a constraint equation, which makes index 3: Pantelides and dummy derivatives | scipy |
TwoInertias |
index 3 again, but the two candidate state sets are the same shape, so the choice is a tie-break | analytic |
HeatBar |
a declared shape, a for equation and subscripts: the method of lines, written by hand |
matrix exponential |
AtwoodPendulum |
five coupled degrees of freedom, which leave a 4×4 implicit block | scipy and energy |
Robertson |
stiffness: a stiff solver is mandatory, and explicit Runge-Kutta dies | scipy |
VanDerPol |
a relaxation limit cycle: step size control over three orders of magnitude | scipy |
PIControl |
input and output connectors: a signal has a direction, and a loop |
scipy |
DCMotor |
two domains at once, and a component that holds components | scipy |
Robertson and VanDerPol are not physical-modelling examples. They are the classic
Hairer-Wanner stiff test problems, and they are here to hold the solver honest.
AtwoodPendulum is the one whose reference nobody can read by eye. The trajectory is
chaotic, so a golden point five seconds in says something about the arithmetic of this
toolchain and nothing about the physics. What checks the model is its own total energy,
written out as a variable and asserted flat. A computer algebra system produced the
equations and nobody can proofread them by reading. The invariant is what proofreads them,
and it caught a singular mass matrix in the first draft.
PIControl and DCMotor are the two newest. Each one was added to make the toolchain do
something nobody had asked it to do.
PIControl is a block diagram and not a physical network. Its connectors carry an input
Real or an output Real instead of a potential and a flow, so a connection is directed:
it says which end computes the number. Three things came out of adding it. The flattener
compared connectors by class name, which made connect(source.y, error.u1) illegal;
Modelica compares them by the variables they carry, and an output joined to an input is the
ordinary case. The flattener gained the opposite check at the same time, because two
outputs on one wire really is an error and the message should say which two. And the corpus
harness turned out to read the after key on an invariant, exactly as this page documents
it, and then ignore it. "The loop settles on the setpoint" was quietly being asserted of
the transient as well.
DCMotor is the first example where a component holds components. Machines.Motor has a
winding, a coil, the converter and the rotor inside it, and three connectors on its
boundary that are wired from inside and from outside. It is also the first model that lives
in two domains at once. EMF joins them: it turns current into torque and speed into
voltage, so it carries one connector of each kind. It found a bug that nothing before it
could have found. A modification written on an inner component, winding(R = Rw), was not
given the path of the enclosing instance, so the binding named a variable that did not
exist. At the top level that prefix is empty and nothing moves, which is exactly why every
earlier example passed. Adding it also moved ref() into modelica.build, where the
second caller proved that it belonged.
RoastingMachine is the one example whose reference is neither a closed form nor a solver.
It is a Loring S15 that roasted five batches of coffee on 2026-07-30, logged over Modbus
every 1.5 s. The model carries the gas trace of the first batch, and the spec checks it
against what the bean probe of the machine actually read: 1.67 °C rms over the whole batch,
and 1.24 °C after the first two minutes. The machine parameters were fitted against all
five batches at once. Each batch kept its own gas, its own probe reading at charge, and its
own machine temperature at charge.
It is also the example that models its own instrument. One of the four states is the
thermocouple. The thermocouple has heat capacity, so it lags. At charge it has been hanging
in 217 °C recirculating air, and it is then buried in 13.8 kg of coffee at 20 °C. The dive
that follows is the turning point that every roaster reads, and it is a property of the
probe as much as of the beans. That state is what lets the spec check the roast from t =
0 instead of skipping the first ninety seconds. It also prints a number that nobody has
otherwise: two minutes in, the coffee runs about 8 K ahead of the display.
The batch was also weighed, which is what makes the drying model worth anything. The weighing overturned the physics rather than tuning it. Gate the evaporation at boiling — water leaves when water boils — and no rate reproduces the measured 15 % mass loss: force it to remove the right mass, and the curve comes apart. Sweep the onset downward and the fit and the mass balance improve together, monotonically, all the way to ambient. A coffee bean is porous, and it gives up bound water by diffusion for as long as it is warm. The spec carries that table, and it carries the reason the finding was nearly missed: with the machine parameters held fixed, no drying law works, because those parameters had already absorbed the missing heat.
Adding one¶
- Write the
.mo. Keep it short and physically real. Write no synthetic equations. - Write the
.pytwin, with the same stem. - Work out the reference. Use a closed form if one exists, and otherwise scipy at
rtol=1e-12. Say which one insource. If the model is of something that exists, and you can get a log off it, thenmeasuredbeats both. Carry areferencepoint as well, because degrees of slack cannot catch a regression. - Write the
.toml. Assert only what you are sure of. - Run
make test. The corpus guard checks that the set is well formed, and the twin check runs at once, even while the stages that would simulate it are still skipped. - Draw it:
python -m modelica.diagram examples/models/<Model>.mo <the libraries it uses>. A wrong connection is easier to see than to read.