Rumoca Handbook
Rumoca is a Modelica compiler, simulator, and code generation toolkit written in Rust. It takes equation-based models of physical systems and turns them into simulations you can run from the command line, VS Code, or a browser — or into code for other ecosystems such as Python (SymPy, JAX, CasADi), C, Rust, and FMI.
What is Modelica?
Modelica is an open, equation-based language for modeling physical systems. Instead of writing step-by-step simulation code, you declare the equations that govern your system and let the compiler decide how to solve them.
The example below is a damped spring-mass system. It is live: edit the code, press ▶ Simulate to integrate it right here in your browser, or Show DAE to see the equation system the compiler produces. The editor has the same syntax highlighting, completion, and error checking as the Rumoca VS Code extension, powered by the same compiler running in WebAssembly.
model SpringMassDamper "Mass on a spring with viscous damping"
parameter Real m = 1.0 "Mass [kg]";
parameter Real k = 4.0 "Spring constant [N/m]";
parameter Real c = 0.4 "Damping coefficient [N.s/m]";
Real x(start = 1.0) "Position [m]";
Real v(start = 0.0) "Velocity [m/s]";
equation
der(x) = v;
m * der(v) = -k * x - c * v;
annotation(experiment(StopTime = 20.0));
end SpringMassDamper;
Note what you did not have to write: no integration loop, no state vector
bookkeeping, no event logic. der(x) means the time derivative of x, and
the compiler transforms the equations into a form a numerical solver can
integrate.
What Rumoca Gives You
| Capability | Where to read more |
|---|---|
| Compile and simulate Modelica models | Quick Start, Running Simulations |
Repeatable scenario files (rum.toml) for simulation and codegen | Scenario Files |
| Interactive, human-in-the-loop simulation with browser 3D viewers | Interactive Simulation |
| Code generation to SymPy, JAX, CasADi, C, Rust, FMI, and more | Targets and Templates |
| IDE support: diagnostics, completion, hover, run buttons | VS Code Extension |
| Formatter and linter for Modelica source | Formatter and Linter |
| Full compiler in WebAssembly | Web Playground |
| Structural analysis and debugging of models | Inspecting and Debugging Models |
The Normal Workflow
- Write or open a Modelica model (
.mofile). - Configure any external Modelica package roots, such as the Modelica Standard Library (MSL).
- Run a direct command (
rumoca sim model.mo) or a colocatedrum.tomlscenario (rumoca sim -c rum.toml). - Inspect results in the CLI, VS Code, the browser viewer, or generated target output.
Project Status
Rumoca is in active development. It compiles and simulates a growing subset of Modelica, validated continuously against the Modelica Standard Library, but it is not yet a complete replacement for mature tools such as OpenModelica or Dymola. See Language Support Status for an honest description of what works today.
How This Book Is Organized
- Getting Started installs Rumoca and walks you through your first model.
- The Modelica Language explains equation-based modeling and what Rumoca supports.
- Tools covers the CLI, VS Code extension, playground, formatter, and linter.
- Simulation covers direct runs, scenario files, solvers, interactive simulation, and debugging.
- Code Generation covers built-in and custom targets.
- Examples points at runnable examples, including live in-browser ones.
Developers who want to understand or modify the compiler itself should read the companion Rumoca Internals book.
Installation
Rumoca is distributed from GitHub Releases: prebuilt binaries, Python wheels, the VS Code extension, and WASM assets. It is not published to crates.io.
Prebuilt Binaries (Recommended)
Linux and macOS:
curl --proto '=https' --tlsv1.2 -LsSf https://raw.githubusercontent.com/cognipilot/rumoca/main/install/install.sh | bash
Install a specific version, and optionally the rumoca-lsp language server:
curl --proto '=https' --tlsv1.2 -LsSf https://raw.githubusercontent.com/cognipilot/rumoca/main/install/install.sh | bash -s -- --version v0.8.0 --with-lsp
Windows PowerShell uses install/install.ps1 from the same directory.
The installer places binaries in ~/.local/bin by default; override with
--bin-dir <path>. Check the result with:
rumoca --version
VS Code Extension
Install Rumoca Modelica from the
VS Code marketplace.
The extension bundles its own rumoca-lsp server, so it works without a
separate compiler install. See VS Code Extension.
Python Package
pip install rumoca
The Python package exposes the compiler for scripting and notebook use.
Shell Completions
rumoca completions bash > ~/.local/share/bash-completion/completions/rumoca
Other shells (zsh, fish, …) are supported; run rumoca completions --help.
From Source
Install the Rust toolchain selected by rust-toolchain.toml, then build the
workspace:
git clone https://github.com/CogniPilot/rumoca
cd rumoca
cargo build --workspace
For interactive simulation, prefer release builds:
cargo run -p rumoca --release -- --help
The repository includes an xtask developer CLI used by CI and local
development (cargo xtask verify quick, cargo xtask vscode test, …). It is
documented in the
Rumoca Internals book and
CONTRIBUTING.md.
Modelica Library Dependencies
The repository examples use pinned Modelica dependencies declared in
examples/modelica_dependencies.toml. Fetch them with:
cargo xtask repo modelica-deps ensure
This downloads the Modelica Standard Library (MSL) and the CogniPilot
Modelica Models (CMM) into target/. The repository’s committed VS Code
settings point at those directories with workspace-relative paths, so the
examples work as soon as the download finishes. See
Using Modelica Libraries for how library lookup
works in general.
Quick Start
This page assumes rumoca is on your PATH
(Installation). If you are working from a source
checkout, replace rumoca with cargo run -p rumoca --release --.
Simulate a Model Directly
Save this as Ball.mo:
model Ball
Real x(start=10);
Real v(start=1);
parameter Real g = 9.81;
equation
der(x) = v;
der(v) = -g;
when x < 0 then
reinit(v, -0.8*pre(v));
end when;
end Ball;
Then run:
rumoca sim Ball.mo --t-end 10
Rumoca compiles the model, simulates to t = 10 s, and writes an HTML report
(Ball_results.html by default) with interactive plots of every variable.
The model name is inferred from the file; pass --model to pick a specific
class.
Useful options:
rumoca sim Ball.mo --model Ball --t-end 10 --solver rk-like --dt 0.01 -o ball.html
Use --source-root for packages that are not in the same source tree:
rumoca sim my_model.mo \
--model MyPackage.MyModel \
--source-root target/msl/ModelicaStandardLibrary-4.1.0/Modelica\ 4.1.0
Run a Scenario
The preferred repeatable workflow is a colocated rum.toml scenario file
that records the model, solver, plots, and viewer settings in one place:
rumoca sim -c examples/simulation/rum.ball.toml
Interactive examples use the same command shape:
rumoca sim -c examples/interactive/quadrotor/rum.acro.toml
Generate a commented starter scenario and validate it without running:
rumoca sim init > rum.toml
rumoca sim check -c rum.toml
See Scenario Files for the full format.
Compile or Generate Code
List the built-in code generation targets:
rumoca targets
Render a target:
rumoca compile examples/models/SympyDecay.mo \
--model SympyDecay \
--target sympy \
--output /tmp/sympy_decay
Dump an intermediate representation of the compiler instead:
rumoca compile Ball.mo --emit dae-mo # the DAE as Modelica source
rumoca compile Ball.mo --emit solve-json # the solver IR as JSON
Next Steps
- Build a model from scratch in Your First Model.
- Learn the scenario format in Scenario Files.
- Explore the full CLI reference.
Your First Model
This tutorial builds a small physical model from scratch and explains each piece of Modelica syntax as it appears. The code blocks are live: you can edit and simulate them directly on this page.
A Cooling Cup of Coffee
Newton’s law of cooling says the temperature T of an object approaches the
ambient temperature T_amb at a rate proportional to their difference:
model Coffee "Newton's law of cooling"
parameter Real T_amb = 20.0 "Ambient temperature [degC]";
parameter Real tau = 300.0 "Cooling time constant [s]";
Real T(start = 90.0) "Coffee temperature [degC]";
equation
der(T) = (T_amb - T) / tau;
annotation(experiment(StopTime = 1800.0));
end Coffee;
Press ▶ Simulate and watch the temperature decay toward 20 °C.
Reading the model line by line:
model Coffee "..."— declares a class namedCoffee. The string after the name is a description; tools display it but it has no effect on the equations.parameter Real T_amb = 20.0— a parameter is fixed during a simulation but adjustable between runs. Try changing it and re-running.Real T(start = 90.0)— a continuous variable.startgives its initial value. Becauseder(T)appears in an equation,Tis a state: the solver integrates it through time.der(T) = (T_amb - T) / tau— an equation, not an assignment. You could equally write(T_amb - T) / tau = der(T); the compiler decides how to solve the system.annotation(experiment(StopTime = 1800.0))— the standard Modelica way to store default simulation settings with the model. The live editors on these pages and the web playground honor it; native CLI runs currently use--t-end(default 1.0 s) or the[sim]section of a scenario file instead.
Adding a Second State
Models grow by adding variables and equations — one equation per unknown. Here is the same idea applied to a mass hanging on a spring, which needs two states (position and velocity):
model HangingMass
parameter Real m = 0.5 "Mass [kg]";
parameter Real k = 20.0 "Spring constant [N/m]";
parameter Real c = 0.5 "Damping [N.s/m]";
parameter Real g = 9.81;
Real x(start = 0.0) "Displacement below natural length [m]";
Real v(start = 0.0);
Real f_spring "Spring force [N]";
equation
f_spring = -k * x;
der(x) = v;
m * der(v) = f_spring - c * v + m * g;
annotation(experiment(StopTime = 5.0));
end HangingMass;
f_spring is an algebraic variable: it has no der(), so the compiler
solves it from its equation at every step instead of integrating it. Mixing
differential and algebraic equations like this is what makes the system a
DAE (differential-algebraic equation system) — press Show DAE to see the
sorted system Rumoca produces.
Running It Natively
Save the model as HangingMass.mo and run:
rumoca sim HangingMass.mo --t-end 5
The HTML report HangingMass_results.html plots all variables.
Recording the Run as a Scenario
Once a model has settings worth keeping — solver, plots, output paths —
record them in a rum.toml scenario next to the model:
[rumoca]
version = "1"
task = "simulate"
[model]
file = "HangingMass.mo"
name = "HangingMass"
[sim]
t_end = 5.0
solver = "auto"
output = "hanging_mass.html"
rumoca sim -c rum.toml
From here:
- Modeling with Equations explains the language concepts systematically.
- Events and Discrete Behavior adds switching, impacts, and sampled control.
- Scenario Files documents everything a
rum.tomlcan do.
Modeling with Equations
This chapter explains the core ideas of equation-based modeling as Rumoca implements them. It is not a full Modelica tutorial — the Modelica Language Specification is the authority — but it covers what you need to write and read most models.
Equations, Not Assignments
A Modelica equation declares a relationship that must hold at every instant.
The compiler — not you — decides which variable each equation solves for,
in what order, and which variables become integrator states. This is why you
can write m * der(v) = f instead of der(v) := f / m, and why models stay
readable as they grow: adding a component adds equations, and the compiler
re-sorts the whole system.
A model is balanced when it has exactly one equation per unknown. Rumoca checks this during compilation and reports counts when they disagree.
Variability
Every variable has a variability that tells the compiler how it can change:
| Declaration | Meaning |
|---|---|
constant Real c = 2.0 | Fixed forever, usable in types and array sizes |
parameter Real k = 1.0 | Fixed during a run, settable between runs |
discrete Real u | Changes only at events, holds its value between them |
Real x | Continuous-time variable |
input Real f / output Real y | Causal connectors for the model boundary; interactive simulation routes signals to inputs |
States and Initial Values
A continuous variable that appears under der(...) becomes a state. Give
states an initial value with the start attribute:
Real x(start = 1.0);
Variables without der() are algebraic: solved from the equation system
at each step rather than integrated.
A Worked Example
The Van der Pol oscillator shows a nonlinear two-state system. The mu
parameter controls how strongly nonlinear (and numerically stiff) it is —
edit it and re-run:
model VanDerPol "Van der Pol oscillator"
parameter Real mu = 5.0 "Nonlinearity / stiffness";
Real x(start = 2.0);
Real y(start = 0.0);
equation
der(x) = y;
der(y) = mu * (1 - x^2) * y - x;
annotation(experiment(StopTime = 30.0));
end VanDerPol;
Structure: Models, Packages, Extends
Modelica organizes code in classes:
model/block/class— components with equations.package— a namespace holding other classes, usually one per library.record— pure data structures.function— algorithmic code callable from equations.connector— interface definitions used byconnect(...).
Classes compose by instantiation (declaring a component of another class)
and inheritance (extends). Modifications customize an instance in place:
model TwoTanks
Tank tank1(area = 2.0);
Tank tank2(area = 0.5, h(start = 1.0));
equation
connect(tank1.outlet, tank2.inlet);
end TwoTanks;
connect generates equality equations for potential variables and sum-to-zero
equations for flow variables, which is how component-based physical
modeling works without manually wiring every force and current.
What the Compiler Does With Your Equations
When you press run, Rumoca:
- parses and resolves the model and everything it references,
- type-checks and instantiates components with their modifications,
- flattens the class hierarchy and
connectsets into one equation system, - sorts and analyzes the system structurally (matching equations to unknowns, finding simultaneous blocks, tearing algebraic loops),
- hands the prepared system to a numerical solver.
You can watch each stage with rumoca compile --emit <stage> and
--inspect structure — see
Inspecting and Debugging Models. The full
pipeline is documented for contributors in the
Rumoca Internals book.
Events and Discrete Behavior
Physical models often mix continuous dynamics with discrete switching: impacts, controllers that sample, valves that open. Modelica expresses these with events, and the solver locates them precisely instead of stepping blindly past them.
When Equations and reinit
A when equation activates at the instant its condition becomes true. The
classic example is the bouncing ball, which reverses its velocity at impact:
model Ball
Real x(start = 10) "Height [m]";
Real v(start = 1) "Velocity [m/s]";
parameter Real g = 9.81;
parameter Real e = 0.8 "Coefficient of restitution";
equation
der(x) = v;
der(v) = -g;
when x < 0 then
reinit(v, -e * pre(v));
end when;
annotation(experiment(StopTime = 10.0));
end Ball;
when x < 0 then ... end when— fires once at the crossing instant, not continuously while the condition holds.pre(v)— the value ofvimmediately before the event.reinit(v, ...)— restarts the statevfrom a new value.
The solver detects the zero crossing of x, locates the impact time, applies
the reinit, and continues. Run the example and zoom in on the velocity
trace: each bounce is a clean jump, not a smoothed-over spike.
Sampled (Clocked-Style) Control
sample(start, period) fires periodically, which is the standard way to model
a digital controller around a continuous plant:
model SampledControl "Continuous plant with a sampled P controller"
parameter Real kp = 2.0 "Proportional gain";
parameter Real Ts = 0.2 "Sample period [s]";
parameter Real r = 1.0 "Reference";
Real x(start = 0.0) "Plant state";
discrete Real u(start = 0.0) "Held control signal";
equation
der(x) = -x + u;
when sample(0, Ts) then
u = kp * (r - pre(x));
end when;
annotation(experiment(StopTime = 6.0));
end SampledControl;
u is discrete: it changes only when the when fires and is held constant
(zero-order hold) in between. Try shortening Ts toward continuous control,
or raising kp until the loop rings.
Hysteresis with elsewhen
elsewhen chains mutually exclusive switching conditions. A thermostat with
a hysteresis band:
model Thermostat "Bang-bang temperature control with hysteresis"
parameter Real T_set = 21.0 "Setpoint [degC]";
parameter Real band = 1.0 "Hysteresis half-width [degC]";
parameter Real T_amb = 5.0 "Outside temperature [degC]";
parameter Real tau = 600.0 "Thermal time constant [s]";
parameter Real heat = 0.02 "Heater authority [degC/s]";
Real T(start = 15.0) "Room temperature [degC]";
Boolean on(start = true) "Heater state";
equation
der(T) = (T_amb - T) / tau + (if on then heat else 0.0);
when T > T_set + band then
on = false;
elsewhen T < T_set - band then
on = true;
end when;
annotation(experiment(StopTime = 7200.0, Solver = "rk-like"));
end Thermostat;
This model pins Solver = "rk-like" in its experiment annotation: the
explicit solver handles its rapid relay switching robustly, while the default
implicit solver can currently stall on it (see
Solvers and Accuracy).
Conditional Expressions vs Events
An if-expression inside an equation (if on then heat else 0.0) also
generates events at its switch points so the integrator never smears across
a discontinuity. When a discontinuity is harmless and you want to suppress
event handling, Modelica provides noEvent(...); smooth(...) asserts
differentiability.
Things to Keep In Mind
whenbodies relate discrete values; usereinitto restart continuous states.pre(x)is only meaningful for discrete-valued variables or at event instants.- Event-heavy models simulate best with explicit solvers today; pin
Solver = "rk-like"in the experiment annotation or pass--solver rk-like.
Arrays and Discretized PDEs
Modelica has no built-in partial differential equations, but array variables
plus for-equations make method of lines discretizations natural: slice
the spatial domain into cells, give each cell a state, and let the compiler
unroll the equations.
Cooking a Turkey
A turkey in the oven is (approximately!) a sphere heated from the outside —
the 1-D radial heat equation. We split the sphere into N concentric
shells, write an energy balance for each shell, and drive the outer surface
with oven convection and radiation.
Press ▶ Simulate, then press play on the cross-section: colors show the temperature field conducting inward over four hours of cooking. Drag the slider to scrub through time.
model Turkey "Roasting a turkey: 1-D spherical heat equation, method of lines"
parameter Integer N = 10 "Number of radial shells";
final parameter Integer Np1 = N + 1;
parameter Real M = 5.0 "Turkey mass [kg]";
parameter Real rho = 1050.0 "Density [kg/m3]";
parameter Real cp = 3500.0 "Specific heat [J/(kg.K)]";
parameter Real kc = 0.5 "Thermal conductivity [W/(m.K)]";
parameter Real T_oven = 450.0 "Oven temperature [K] (~177 degC)";
parameter Real h = 15.0 "Convective film coefficient [W/(m2.K)]";
parameter Real epsilon = 0.85 "Surface emissivity";
parameter Real sigma = 5.67e-8 "Stefan-Boltzmann constant";
parameter Real pi = 3.14159265359;
parameter Real R = (3.0 * M / (4.0 * pi * rho)) ^ (1.0 / 3.0) "Radius [m]";
parameter Real dr = R / N "Shell thickness [m]";
Real T[N](each start = 277.0) "Shell temperatures [K] (fridge-cold start)";
Real Q_cond[Np1] "Conductive heat flow across shell interfaces [W]";
Real Q_surf "Heat into the surface from the oven [W]";
Real r[Np1] "Interface radii [m]";
Real A_interface[Np1] "Interface areas [m2]";
Real V_shell[N] "Shell volumes [m3]";
Real m_shell[N] "Shell masses [kg]";
equation
for i in 1:Np1 loop
r[i] = (i - 1) * dr;
A_interface[i] = 4.0 * pi * r[i] ^ 2;
end for;
for i in 1:N loop
V_shell[i] = (4.0 / 3.0) * pi * (r[i + 1] ^ 3 - r[i] ^ 3);
m_shell[i] = rho * V_shell[i];
end for;
Q_cond[1] = 0.0 "No flux through the center";
for i in 2:N loop
Q_cond[i] = kc * A_interface[i] * (T[i - 1] - T[i]) / dr;
end for;
Q_cond[Np1] = 0.0;
Q_surf = h * A_interface[Np1] * (T_oven - T[N])
+ epsilon * sigma * A_interface[Np1] * (T_oven ^ 4 - T[N] ^ 4);
for i in 1:N - 1 loop
m_shell[i] * cp * der(T[i]) = Q_cond[i] - Q_cond[i + 1];
end for;
m_shell[N] * cp * der(T[N]) = Q_cond[N] + Q_surf;
annotation(experiment(StopTime = 14400.0, Interval = 60.0));
end Turkey;
The cross-section animation above is itself an editable JavaScript block —
expand it, change the colors or geometry, and re-run ▶ Simulate to see
your version. It receives the simulation results and a small helper API
(api.arrayField, api.makeCanvas, api.addAnimation,
api.addColorbar, api.heatColor, …) from the book’s live runner.
// Draw the turkey cross-section: concentric shells colored by temperature.
const field = api.arrayField(); // T[1..N], sorted by index
const { vMin, vMax } = api.valueRange(field.members);
const T_done = 347; // 74 degC: poultry-safe core temp
const size = 300;
const { ctx2d } = api.makeCanvas(size, size);
const n = field.members.length;
api.addAnimation(times, (frame) => {
ctx2d.clearRect(0, 0, size, size);
const maxR = size / 2 - 8;
// Outermost shell first so inner shells paint on top.
for (let i = n - 1; i >= 0; i--) {
const T = field.members[i].values[frame];
ctx2d.beginPath();
ctx2d.arc(size / 2, size / 2, maxR * ((i + 1) / n), 0, 2 * Math.PI);
ctx2d.fillStyle = api.heatColor((T - vMin) / (vMax - vMin));
ctx2d.fill();
}
ctx2d.beginPath();
ctx2d.arc(size / 2, size / 2, maxR, 0, 2 * Math.PI);
ctx2d.strokeStyle = '#777';
ctx2d.lineWidth = 2;
ctx2d.stroke();
const T_core = field.members[0].values[frame];
const doneness = T_core >= T_done ? ' — done!' : '';
return `t = ${api.formatClock(times[frame])} · core `
+ `${(T_core - 273.15).toFixed(1)} degC${doneness}`;
}, 12000);
api.addColorbar(vMin, vMax, api.heatColor);
The poultry-safe core temperature is 347 K (74 °C / 165 °F). Watch T[1]
(the center) in the plot: with these parameters a 5 kg bird is not done
after four hours at 177 °C — try a hotter oven, a smaller turkey, or a
longer StopTime in the experiment annotation.
What to Notice in the Model
- One state per cell.
Real T[N](each start = 277.0)declaresNstates at once;eachapplies the modification to every element. for-equations are unrolled at compile time. The loop range must be known structurally, which is whyNis aparameter Integer. After flattening, the compiler sees5 * N + 4plain scalar equations — press Show DAE to look at them.- Energy balances, not finite-difference formulas. Writing
m_shell[i] * cp * der(T[i]) = Q_cond[i] - Q_cond[i+1]per shell, with an explicit interface flux array, conserves energy exactly by construction and reads like the physics. (This formulation follows the Dyad turkey demo.) - Mixed boundary condition. The surface shell receives both convection
(
h·A·ΔT) and radiation (ε·σ·A·(T⁴_oven − T⁴)) — theT⁴terms make the system nonlinear, which the solver handles without any special treatment.
2-D Fields: A Vibrating Membrane
The same technique extends to two dimensions with matrix states and nested
for-equations. This is the 2-D wave equation on a square membrane with
clamped edges, started from a Gaussian pluck in the center. The grid
resolution is the N parameter — edit it (try 8 to 20) and re-run:
model Wave2D "2-D wave equation on a square membrane, method of lines"
parameter Integer N = 12 "Grid cells per side";
parameter Real L = 1.0 "Side length [m]";
parameter Real c = 1.0 "Wave speed [m/s]";
parameter Real d = 0.05 "Damping [1/s]";
parameter Real dx = L / (N - 1);
Real u[N, N] "Displacement";
Real w[N, N] "Velocity";
initial equation
for i in 1:N loop
for j in 1:N loop
u[i, j] = exp(-200.0 * (((i - 1) * dx - 0.5 * L) ^ 2
+ ((j - 1) * dx - 0.5 * L) ^ 2));
w[i, j] = 0.0;
end for;
end for;
equation
for i in 1:N loop
for j in 1:N loop
der(u[i, j]) = w[i, j];
end for;
end for;
// Fixed boundary: edges clamped to zero motion.
for i in 1:N loop
der(w[i, 1]) = 0.0;
der(w[i, N]) = 0.0;
end for;
for j in 2:N - 1 loop
der(w[1, j]) = 0.0;
der(w[N, j]) = 0.0;
end for;
// Interior: five-point Laplacian with light damping.
for i in 2:N - 1 loop
for j in 2:N - 1 loop
der(w[i, j]) = c ^ 2 * (u[i + 1, j] + u[i - 1, j] + u[i, j + 1]
+ u[i, j - 1] - 4.0 * u[i, j]) / dx ^ 2
- d * w[i, j];
end for;
end for;
annotation(experiment(StopTime = 2.0, Interval = 0.02, Solver = "rk-like"));
end Wave2D;
The heatmap below is an editable visualization script, like the turkey’s.
api.matrixField() collects the u[i,j] states into a grid; the script
draws each cell, colored by displacement:
// Animate the membrane displacement field u[i,j] as a heatmap.
const field = api.matrixField();
const { vMin, vMax } = api.valueRange(field.members);
const span = Math.max(Math.abs(vMin), Math.abs(vMax)) || 1;
const size = 300;
const { ctx2d } = api.makeCanvas(size, size);
const cell = size / field.rows;
api.addAnimation(times, (frame) => {
for (let i = 1; i <= field.rows; i++) {
for (let j = 1; j <= field.cols; j++) {
const u = field.at(i, j)[frame];
// Map displacement [-span, span] onto the color scale.
ctx2d.fillStyle = api.heatColor(0.5 + 0.5 * (u / span));
ctx2d.fillRect((i - 1) * cell, (j - 1) * cell, cell + 1, cell + 1);
}
}
return `t = ${times[frame].toFixed(1)} s`;
}, 8000);
api.addColorbar(-span, span, api.heatColor);
Notice the cost of resolution: each cell adds two states (u and w), so
N = 20 is an 800-state system. Rumoca compiles and simulates it fine, but
output volume grows as N² — keep Interval coarse enough for the
browser.
2-D Navier–Stokes: Flow over a NACA 2412 Airfoil
The same machinery scales up to fluid dynamics. This example solves the 2-D incompressible Navier–Stokes equations around a NACA 2412 airfoil at an adjustable angle of attack, using two classic tricks that keep the system a pure ODE — exactly what the method of lines wants:
- Artificial compressibility: instead of the pressure-Poisson algebraic
constraint, pressure gets its own fast dynamics
der(q) = -cs² · div(V). The continuity error propagates away as an artificial acoustic wave, and no algebraic loop is needed. - Brinkman penalization: the airfoil is not meshed. Cells inside the
NACA 2412 outline (computed per grid column from the standard camber and
thickness polynomials) get a strong drag term
-sig·V/tauthat drives the velocity to zero, so the flow sees a solid body on a plain Cartesian grid.
The freestream is horizontal and the airfoil itself pitches: each
cell’s coordinates are rotated into the airfoil frame (sc chordwise,
nc chord-normal), so the solid mask turns with the angle of attack the
way a real wind-tunnel model would. aoa is a plain parameter — the
trig stays in the equations rather than being folded into derived
constants, so changing it only re-settles the algebraic mask. That is
what the AoA slider under the animation does: it overrides the
parameter on the already-compiled model and re-runs in milliseconds,
skipping the Modelica→WGSL pipeline entirely. The slider drives the GPU
fast path and freezes while a run is in flight (on the CPU path, edit
aoa in the source instead).
This example defaults the GPU checkbox
on: the compiler’s experimental wgsl-solve backend lowers the system to
WebGPU compute kernels and an in-page RK4 integrator runs them — about
5× faster than the CPU (WASM) path even on a software GPU adapter. If
WebGPU is unavailable the run fails with a clear error instead of
silently falling back (uncheck GPU for the CPU path). GPU v1 runs in
f32 with events and algebraics frozen at their settled initial values,
which is exact here: the mask depends on parameters only, so it is
constant during a run. The run is an impulsive wind-tunnel start:
the field begins at rest and the freestream sweeps in from the inlet and
far-field boundaries. This is the heaviest example in the book
(~4,100 unknowns after unrolling): expect the first run to take a while.
model AirfoilFlow "2-D flow over a NACA 2412: artificial compressibility + penalization"
parameter Integer NX = 30 "Cells along the channel";
parameter Integer NY = 18 "Cells across the channel";
parameter Real Lx = 4.0 "Domain length [chords]";
parameter Real Ly = 1.5 "Domain height [chords]";
parameter Real xle = 1.0 "Leading edge distance from inlet [chords]";
parameter Real aoa = 8.0 "Angle of attack [deg]: the airfoil pitches nose-up";
parameter Real U = 1.0 "Freestream speed (horizontal)";
parameter Real nu = 0.05 "Kinematic viscosity (Re = U/nu = 20)";
parameter Real cs = 3.0 "Artificial-compressibility wave speed";
parameter Real tau = 0.02 "Solid penalization time constant [s]";
parameter Real taub = 0.05 "Boundary relaxation time constant [s]";
parameter Real mc = 0.02 "NACA 2412 max camber";
parameter Real pc = 0.4 "NACA 2412 camber position";
parameter Real tk = 0.12 "NACA 2412 thickness";
parameter Real dx = Lx / NX;
parameter Real dy = Ly / NY;
parameter Real pi = 3.14159265359;
Real u[NX, NY] "x-velocity";
Real v[NX, NY] "y-velocity";
Real q[NX, NY] "pressure / rho";
Real sc[NX, NY] "Chordwise coordinate in the pitched airfoil frame";
Real nc[NX, NY] "Chord-normal coordinate in the pitched airfoil frame";
Real sig[NX, NY] "Solid mask (1 inside the airfoil)";
// States start at rest (default start = 0): an impulsive wind-tunnel
// start where the freestream sweeps in through the boundary relaxation.
// The trig on `aoa` stays in the equations (not in derived parameters)
// so the mask follows the parameter at runtime: changing `aoa` only
// requires re-settling these algebraics, not recompiling.
equation
for i in 1:NX loop
for j in 1:NY loop
sc[i, j] = ((i - 0.5) * dx - xle) * cos(aoa * pi / 180.0)
- ((j - 0.5) * dy - Ly / 2.0) * sin(aoa * pi / 180.0);
nc[i, j] = ((i - 0.5) * dx - xle) * sin(aoa * pi / 180.0)
+ ((j - 0.5) * dy - Ly / 2.0) * cos(aoa * pi / 180.0);
// Inside when 0 <= sc <= 1 and |nc - camber| <= half thickness
// (floored to one grid cell so the coarse mask stays closed).
sig[i, j] = if sc[i, j] >= 0.0 and sc[i, j] <= 1.0
and abs(nc[i, j]
- (if sc[i, j] < pc then mc / pc ^ 2 * (2.0 * pc * sc[i, j] - sc[i, j] ^ 2)
else mc / (1.0 - pc) ^ 2
* ((1.0 - 2.0 * pc) + 2.0 * pc * sc[i, j] - sc[i, j] ^ 2)))
<= max(0.8 * dy,
5.0 * tk * (0.2969 * sqrt(max(sc[i, j], 0.0)) - 0.1260 * sc[i, j]
- 0.3516 * sc[i, j] ^ 2 + 0.2843 * sc[i, j] ^ 3
- 0.1036 * sc[i, j] ^ 4))
then 1.0 else 0.0;
end for;
end for;
// Interior: momentum + artificial-compressibility continuity.
for i in 2:NX - 1 loop
for j in 2:NY - 1 loop
der(u[i, j]) = -u[i, j] * (u[i + 1, j] - u[i - 1, j]) / (2.0 * dx)
- v[i, j] * (u[i, j + 1] - u[i, j - 1]) / (2.0 * dy)
- (q[i + 1, j] - q[i - 1, j]) / (2.0 * dx)
+ nu * ((u[i + 1, j] - 2.0 * u[i, j] + u[i - 1, j]) / dx ^ 2
+ (u[i, j + 1] - 2.0 * u[i, j] + u[i, j - 1]) / dy ^ 2)
- sig[i, j] * u[i, j] / tau;
der(v[i, j]) = -u[i, j] * (v[i + 1, j] - v[i - 1, j]) / (2.0 * dx)
- v[i, j] * (v[i, j + 1] - v[i, j - 1]) / (2.0 * dy)
- (q[i, j + 1] - q[i, j - 1]) / (2.0 * dy)
+ nu * ((v[i + 1, j] - 2.0 * v[i, j] + v[i - 1, j]) / dx ^ 2
+ (v[i, j + 1] - 2.0 * v[i, j] + v[i, j - 1]) / dy ^ 2)
- sig[i, j] * v[i, j] / tau;
der(q[i, j]) = -cs ^ 2 * ((u[i + 1, j] - u[i - 1, j]) / (2.0 * dx)
+ (v[i, j + 1] - v[i, j - 1]) / (2.0 * dy));
end for;
end for;
// Inlet (left): horizontal freestream; pressure zero-gradient.
for j in 1:NY loop
der(u[1, j]) = (U - u[1, j]) / taub;
der(v[1, j]) = (0.0 - v[1, j]) / taub;
der(q[1, j]) = (q[2, j] - q[1, j]) / taub;
// Outlet (right): zero-gradient velocities, reference pressure.
der(u[NX, j]) = (u[NX - 1, j] - u[NX, j]) / taub;
der(v[NX, j]) = (v[NX - 1, j] - v[NX, j]) / taub;
der(q[NX, j]) = (0.0 - q[NX, j]) / taub;
end for;
// Far field (top/bottom): freestream; pressure zero-gradient.
for i in 2:NX - 1 loop
der(u[i, 1]) = (U - u[i, 1]) / taub;
der(v[i, 1]) = (0.0 - v[i, 1]) / taub;
der(q[i, 1]) = (q[i, 2] - q[i, 1]) / taub;
der(u[i, NY]) = (U - u[i, NY]) / taub;
der(v[i, NY]) = (0.0 - v[i, NY]) / taub;
der(q[i, NY]) = (q[i, NY - 1] - q[i, NY]) / taub;
end for;
annotation(experiment(StopTime = 2.5, Interval = 0.0125, Solver = "rk-like"));
end AirfoilFlow;
// Speed heatmap |V(x,y,t)| with the penalized cells in gray and the true
// NACA 2412 contour drawn on top. Grid size is discovered from the result
// names, so editing NX/NY in the model just works.
const cell = new Map();
let NX = 0, NY = 0;
names.forEach((n, k) => {
const m = /^([uvq]|sig)\[(\d+),(\d+)\]$/.exec(n);
if (!m) return;
cell.set(`${m[1]}:${m[2]},${m[3]}`, data[k]);
if (m[1] === 'u') {
NX = Math.max(NX, Number(m[2]));
NY = Math.max(NY, Number(m[3]));
}
});
const speed = (i, j, f) => Math.hypot(
cell.get(`u:${i},${j}`)[f], cell.get(`v:${i},${j}`)[f]);
let vMax = 0;
for (let f = 0; f < times.length; f += 5) {
for (let i = 1; i <= NX; i++) {
for (let j = 1; j <= NY; j++) {
vMax = Math.max(vMax, speed(i, j, f));
}
}
}
// The mask is constant; sample it at the end of the run (algebraic values
// at the very first output point may not be settled yet).
const maskFrame = times.length - 1;
// Geometry for the overlay — keep in sync with the model parameters.
// The slider override (if any) wins so the outline pitches with the mask.
const geo = { Lx: 4.0, Ly: 1.5, xle: 1.0, mc: 0.02, pc: 0.4, tk: 0.12 };
const aoa = api.overrides.aoa ?? 8.0;
const ca = Math.cos(aoa * Math.PI / 180);
const sa = Math.sin(aoa * Math.PI / 180);
const camber = (sc) => sc < geo.pc
? geo.mc / geo.pc ** 2 * (2 * geo.pc * sc - sc ** 2)
: geo.mc / (1 - geo.pc) ** 2 * ((1 - 2 * geo.pc) + 2 * geo.pc * sc - sc ** 2);
const halfThick = (sc) => 5 * geo.tk * (0.2969 * Math.sqrt(sc) - 0.1260 * sc
- 0.3516 * sc ** 2 + 0.2843 * sc ** 3 - 0.1036 * sc ** 4);
const W = 600;
const H = Math.round(W * (geo.Ly / geo.Lx));
const { ctx2d } = api.makeCanvas(W, H);
const cw = W / NX;
const ch = H / NY;
const px = (x) => (x / geo.Lx) * W; // physical x -> canvas
const py = (y) => H - ((y + geo.Ly / 2) / geo.Ly) * H; // physical y -> canvas
// Airfoil-frame point (sc chordwise, h chord-normal) -> physical x/y,
// pitched nose-up by aoa about the leading edge (the model's inverse map).
const fx = (sc, h) => geo.xle + sc * ca + h * sa;
const fy = (sc, h) => -sc * sa + h * ca;
function drawAirfoil() {
ctx2d.beginPath();
for (let k = 0; k <= 60; k++) { // upper surface, LE -> TE
const sc = k / 60;
const h = camber(sc) + halfThick(sc);
const fn = k === 0 ? 'moveTo' : 'lineTo';
ctx2d[fn](px(fx(sc, h)), py(fy(sc, h)));
}
for (let k = 60; k >= 0; k--) { // lower surface, TE -> LE
const sc = k / 60;
const h = camber(sc) - halfThick(sc);
ctx2d.lineTo(px(fx(sc, h)), py(fy(sc, h)));
}
ctx2d.closePath();
ctx2d.fillStyle = '#111';
ctx2d.fill();
ctx2d.strokeStyle = '#fff';
ctx2d.lineWidth = 1;
ctx2d.stroke();
}
api.addAnimation(times, (frame) => {
for (let i = 1; i <= NX; i++) {
for (let j = 1; j <= NY; j++) {
const solid = cell.get(`sig:${i},${j}`)[maskFrame] > 0.5;
ctx2d.fillStyle = solid
? '#666'
: api.heatColor(speed(i, j, frame) / vMax);
// j = 1 is the bottom row: flip the y axis for drawing.
ctx2d.fillRect((i - 1) * cw, (NY - j) * ch, cw + 1, ch + 1);
}
}
drawAirfoil();
return `t = ${times[frame].toFixed(1)} s · max |V| = ${api.formatTick(vMax)}`;
}, 10000);
api.addColorbar(0, vMax, api.heatColor);
// Pitch the airfoil without recompiling: overrides the `aoa` parameter on
// the prepared model, re-settles the mask, and re-runs the GPU integrator.
api.addTuner('aoa', { min: -10, max: 15, step: 1, value: aoa, label: 'AoA °' });
In the animation, the black shape is the true NACA 2412 contour and the gray blocks around it are the penalized grid cells — the body the flow actually feels at this resolution. Watch the impulsive start settle: the stagnation point appears at the leading edge (dark blue), flow accelerates over the upper surface (red), and a slow wake trails downstream. The pressure field (plotted below the animation as the most dynamic states) shows the suction side developing — the lift. Things to try:
- Slide AoA to
0— the wake straightens and the up/down asymmetry mostly disappears (the residual comes from camber, the 2 in 2412), and the re-run skips compilation entirely. - Slide AoA negative — the airfoil visibly pitches nose-down and the suction side flips.
nu = 0.02— less viscosity, sharper wake (dropIntervaland the solver step accordingly if it goes unstable).
Honest caveats: at this grid (~0.1 chord cells) and Reynolds number
(U/nu = 20), this is a qualitative low-Re flow — a teaching
visualization, not an aerodynamic prediction. The thickness polynomial is
floored to one grid cell so the thin profile stays closed, and central
differencing limits how low the viscosity may go. Resolving boundary layers
at flight Reynolds numbers needs orders of magnitude more cells, which is
GPU-backend territory (see the roadmap note below).
Scaling the Resolution
Increase the cell counts for finer fields. Each extra cell adds states and
equations; the structural analysis and solver scale with the system size.
For 1-D problems, tens of cells are usually plenty; for large 2-D/3-D
fields you would generate the Modelica programmatically or move to a
dedicated PDE solver. GPU-accelerated execution of large discretized fields
is on the roadmap — the targets table already includes experimental CUDA
backends (rumoca targets), and the same solve-IR pathway is how a
WebGPU/WGSL backend would land.
Using Modelica Libraries
Real models build on libraries — most importantly the Modelica Standard Library (MSL). This chapter explains how Rumoca finds library code.
Source Roots
A source root is a file or directory added to the compiler’s search path.
When a model references Modelica.Blocks.Continuous.PID, Rumoca resolves
the name through the source roots you configured.
On the command line, pass --source-root (repeatable):
rumoca sim my_model.mo \
--model MyPackage.MyModel \
--source-root target/msl/ModelicaStandardLibrary-4.1.0/Modelica\ 4.1.0 \
--source-root helper.mo
In a rum.toml scenario, use the top-level source_roots key with paths
relative to the scenario file:
source_roots = ["../modelica_libraries"]
In VS Code, set rumoca.sourceRootPaths (see
VS Code Extension).
MODELICAPATH
Rumoca also honors the standard MODELICAPATH environment variable.
Entries (:-separated) are appended after explicit --source-root flags,
so system-wide library installs resolve without per-command flags.
Pinned Dependencies for the Repository Examples
The examples in the Rumoca repository use pinned library versions declared
in examples/modelica_dependencies.toml. Fetch them once:
cargo xtask repo modelica-deps ensure
This downloads MSL and the CogniPilot Modelica Models (CMM) into target/.
The repository’s committed VS Code settings reference those directories with
workspace-relative paths, so simulation and completion work for every
contributor without machine-specific configuration.
Packages and within
Library code is organized as Modelica packages, either as a single .mo
file or as a directory tree with package.mo files. The within clause at
the top of a library file records which package the file belongs to. When you
add a library’s root directory as a source root, all of its classes become
resolvable by their fully qualified names.
Practical Tips
- Prefer pinned library versions over whatever happens to be checked out; scenario files plus pinned paths give reproducible runs.
- Large library trees compile slower in the browser playground than natively; use the native CLI or VS Code for MSL-heavy work.
- If a name does not resolve, check Troubleshooting — the usual cause is a missing or wrong source root.
Language Support Status
Rumoca implements a substantial and growing subset of the Modelica language. This page describes what you can rely on today and where the edges are. The ground truth is the continuously-run Modelica Standard Library (MSL) quality gate, which compiles and simulates MSL models on every change and blocks regressions (documented in the Rumoca Internals book).
What Works Well
- Core equation modeling — models, blocks, packages, records,
functions,
extends, modifications, nested components,connectwith potential/flowsemantics. - Continuous dynamics — DAE compilation with structural analysis: matching, sorting (BLT), algebraic-loop tearing, and dummy-derivative index reduction for many higher-index systems.
- Events —
when/elsewhen,pre,edge,reinit,sample, if-expressions with event generation,assert,terminate. - Arrays — declarations, slicing, and the common array builtins, with scalarization where targets require it.
- Initialization —
startattributes and initial equation handling. - Experiment annotations —
StopTime,StartTime,Tolerance,Interval,Solverare parsed and used by scenario and browser runs.
Known Limitations
- High-index DAEs: index reduction handles many mechanical-style systems, but some structurally singular formulations (for example the Cartesian pendulum with an explicit length constraint) are still rejected with a structurally singular system diagnostic. Reformulate with generalized coordinates, or watch the release notes — this area is under active development.
- Event-heavy models with implicit solvers: the default (
auto) solver currently selects an implicit method that can stall on rapidly switching models. Workaround:--solver rk-likeorannotation(experiment(Solver = "rk-like")). - Arbitrary large libraries: expect better results with explicit examples and pinned packages than with arbitrary unported libraries; the MSL gate tracks exactly which MSL models compile, simulate, and match reference results.
- Direct CLI runs ignore
experimentstop time — pass--t-endor use a scenario file ([sim] t_end). Browser/live runs do honor it.
Checking a Specific Model
The fastest way to find out whether your model is supported is to compile it:
rumoca compile MyModel.mo --emit dae-mo
A clean DAE dump means parsing, resolution, type checking, flattening, and
DAE lowering all succeeded. Then rumoca sim (or --inspect structure)
exercises the structural preparation and the solver. Diagnostics carry
source locations and are designed to name the offending equation or
variable.
If you hit something the compiler should support, please file an issue with the model at https://github.com/CogniPilot/rumoca/issues.
Command-Line Interface
The main binary is rumoca. Every subcommand supports --help; this page
is a guided reference of the surface you will actually use.
rumoca <COMMAND>
Commands:
compile Compile a Modelica file
sim Compile and simulate a model/scenario (subcommands: check, init, bench)
fmt Format Modelica files
lint Lint Modelica files
completions Print shell completion scripts
targets List built-in code generation targets and their capabilities
cache Inspect or prune the shared Rumoca cache
A global --cache-dir <DIR> overrides the on-disk cache root for any
subcommand.
rumoca sim
Direct model run:
rumoca sim path/to/Model.mo --model Package.Model --t-end 10
Scenario run:
rumoca sim -c path/to/rum.toml
| Option | Meaning |
|---|---|
-c, --config <CONFIG> | Run a rum.toml scenario instead of a direct sim |
-m, --model <MODEL> | Main model/class to compile (auto-inferred when omitted) |
--source-root <PATH> | Add a source root (repeatable); MODELICAPATH entries are appended after these |
--solver <SOLVER> | auto, bdf, esdirk34, trbdf2, or rk-like — see Solvers and Accuracy |
--t-end <T_END> | End time. Direct runs default to 1.0; scenario runs use [sim].t_end |
--dt <DT> | Optional fixed output interval; chosen automatically if omitted |
-o, --output <OUTPUT> | Simulation report path (default <MODEL>_results.html) |
--inspect <MODE> | Analyze instead of simulating: structure, eval, jacobian |
--at <NAME=VALUE,...@T> | Evaluation point for --inspect eval|jacobian |
Subcommands:
| Subcommand | Purpose |
|---|---|
rumoca sim check -c rum.toml | Validate a scenario file without running it |
rumoca sim init | Print a fully commented rum.toml starter template |
rumoca sim bench | Benchmark compile, preparation, and hot simulation throughput |
Interactive viewer ports and scene files come from the [transport.http]
section of the scenario, not CLI flags.
rumoca compile
compile separates three concerns into three flags:
--emit <stage>-mo|<stage>-jsondumps an intermediate representation. Stages areast,flat,dae,solve(solvehas no Modelica form, so onlysolve-jsonexists).--target <id|dir|file.jinja>runs code generation: a built-in target id (seerumoca targets), a directory containingtarget.toml, or a raw Jinja template.--phase <ast|flat|dae|solve>picks which IR a raw.jinjatemplate receives (defaultdae). Ignored for built-in and directory targets, which declare their own IR.
rumoca compile Model.mo --emit dae-mo # DAE as Modelica, to stdout
rumoca compile Model.mo --emit solve-json -o out.json # solver IR as JSON
rumoca compile Model.mo --target sympy -o out/ # built-in target
rumoca compile Model.mo --target my.jinja --phase flat
compile shares --model, --source-root, --inspect, and --at with
sim, and adds -v/--verbose for friendly per-phase progress lines.
rumoca fmt and rumoca lint
See Formatter and Linter.
rumoca targets
Prints the built-in code generation targets with their consumed IR stage, generation mode, deployment class, readiness level, and per-feature support columns. See Targets and Templates.
rumoca cache
Compilation artifacts are cached under the platform cache directory.
rumoca cache status # size and entry counts
rumoca cache prune # remove oldest cache files until under a size limit
Both accept --root (or the global --cache-dir) to operate on a
non-default location.
rumoca completions
rumoca completions bash # or zsh, fish, ...
Environment
MODELICAPATH—:-separated library roots, appended after explicit--source-rootflags.
Rumoca deliberately has no behavior-changing RUMOCA_* environment
variables: every knob is a documented CLI flag or scenario key.
VS Code Extension
The Rumoca Modelica extension provides language support (diagnostics, completion, hover, semantic highlighting), simulation commands, scenario settings, and result viewers — all inside VS Code.
Install it from the
marketplace.
It bundles a rumoca-lsp language server, so no separate install is needed.
To use your own server build instead (for compiler development), enable
rumoca.useSystemServer and put rumoca-lsp on PATH.
Modelica Source Roots
Modelica package paths are configured with rumoca.sourceRootPaths.
Repository-relative paths are supported and resolved against the current VS Code workspace root before they are sent to the language server. This makes workspace settings safe to commit:
{
"rumoca.sourceRootPaths": [
"target/msl/ModelicaStandardLibrary-4.1.0/Modelica 4.1.0",
"target/cmm/CMM-v0.0.2"
]
}
The Rumoca repository includes committed settings for common open modes:
| Workspace opened in VS Code | Settings file |
|---|---|
| repository root | .vscode/settings.json |
examples/ | examples/.vscode/settings.json |
examples/interactive/quadrotor/ | examples/interactive/quadrotor/.vscode/settings.json |
Run cargo xtask repo modelica-deps ensure first so those target
directories exist.
Settings Panel
Open Rumoca settings from the toolbar or command palette (Open Rumoca Settings). The settings panel has two source-root sections:
- Workspace Modelica Path edits
rumoca.sourceRootPathsat workspace scope. Use this for MSL, CMM, and other shared packages. - Scenario Source Root Paths edits the active
rum.tomlscenario. Use this for paths that only apply to that one configured run.
The picker stores workspace-relative paths whenever the selected folder is inside the workspace.
Running Models
Run actions operate on rum.toml scenario files. The extension contributes
rum.toml as a TOML language extension, so normal TOML editor features
attach while Rumoca still activates for rum.toml and .mo files. Use
Create Rumoca Scenario to generate a rum.toml for a model; the
runnable source of truth is always the scenario file.
For interactive examples, open the rum.toml scenario, then use Play. For
batch simulation, the results panel opens inside VS Code.
| Command | Purpose |
|---|---|
| Create Rumoca Scenario | Generate a rum.toml next to a model |
| Run Current Rumoca Scenario | Compile and simulate the active scenario |
| Open Scenario Settings | Edit the active scenario’s settings |
| Open Rumoca Settings | Extension settings menu |
| Open Rumoca User Guide | This book |
| Open Rumoca Internals Guide | The contributor book |
| Toggle / Expand All / Collapse All Annotation Expansion | Control inline annotation rendering |
Diagnostics
Compiler and simulation diagnostics are reported to the Problems panel when a source span is available. CLI output uses rich terminal diagnostics; VS Code diagnostics use the raw file/range information without terminal wrappers.
Documentation Access
The user guide and the contributor guide are available directly from the command palette (Open Rumoca User Guide / Open Rumoca Internals Guide), so you do not need to hunt for URLs. The same live-example pages work in any browser.
Web Playground
The browser playground runs the full Rumoca compiler in WebAssembly: a project file tree, Monaco editors with LSP support, simulation with plots, code generation, and package archive loading — no install required.
https://cognipilot.github.io/rumoca/
It is useful for small models, quick experiments, sharing reproductions in bug reports, and demos.
Live Examples in This Book
The runnable code blocks throughout this book (look for the ▶ Simulate button) use the same WASM package as the playground, embedded as focused mini editors:
- the same compiler, solvers, and diagnostics as the native CLI,
- Monaco-based editing with Rumoca’s completion, hover, and error checking,
- inline plots, DAE views, and per-example visualizations.
The first run on a page downloads the WASM compiler; afterwards it is
cached by the browser. Models honor their experiment annotation
(StopTime, Interval, Tolerance, Solver).
Limitations
- Large package trees compile more slowly than native builds.
- Browser storage and worker memory limits matter for full MSL-sized projects.
- Native interactive examples may have more solver/backend options than the browser build.
For larger models or external package development, prefer the native CLI or the VS Code extension.
Formatter and Linter
Rumoca ships a formatter and a linter for Modelica source. Both accept files or directories (defaulting to the current directory) and are also exposed through the VS Code extension and LSP.
rumoca fmt
rumoca fmt # format the current directory in place
rumoca fmt src/ Model.mo # format specific paths
rumoca fmt --check # report differences without writing (CI-friendly)
Profiles
| Profile | Behavior |
|---|---|
dymola | Preserves MSL/Dymola-compatible local whitespace |
canonical | Stricter spacing and indentation defaults |
rumoca fmt --profile canonical
Individual rules can be toggled regardless of profile:
| Flag | Effect |
|---|---|
--indent-size <N> | Spaces per indentation level |
--use-tabs[=true|false] | Tabs instead of spaces |
--normalize-indentation[=true|false] | Normalize structural indentation (enabled by canonical) |
--repair-missing-indentation[=true|false] | Indent only lines that have none |
--normalize-equation-spacing[=true|false] | Normalize spacing inside equations |
--coverage reports how much of the eligible source trivia the formatter
rules cover, without writing changes.
rumoca lint
rumoca lint # lint the current directory
rumoca lint --min-level warning # filter: help | note | warning | error
rumoca lint --warnings-as-errors # CI gating
rumoca lint --disable-rule <id> # repeatable
rumoca lint --max-messages 50
Lint diagnostics use the same rich terminal rendering as compiler diagnostics, with source spans. In VS Code they appear in the Problems panel.
Running Simulations
Rumoca offers two ways to run a simulation. They share the same compiler and runtime; the difference is where the configuration lives.
Direct Runs
Point rumoca sim at a .mo file for quick, one-off runs:
rumoca sim Ball.mo --model Ball --t-end 10 --solver rk-like
Everything is a CLI flag: --t-end (default 1.0), --dt, --solver,
--source-root, --output. The result is an HTML report with interactive
plots of every variable (default <MODEL>_results.html).
Direct runs are great while developing a model. As soon as a run has settings worth repeating, switch to a scenario.
Scenario Runs
A scenario is a rum.toml file colocated with the model it runs:
rumoca sim -c examples/simulation/rum.ball.toml
The scenario records the model file and name, simulation settings, plots,
viewer/transport configuration, and source roots — one runnable thing per
file. This keeps the CLI, VS Code, and the playground aligned: the play
button runs the scenario instead of guessing solver and source roots from a
bare .mo file.
See Scenario Files for the format.
What a Run Produces
- Batch runs write an HTML report with time-series plots of all
variables, simulation details (solver, tolerances, timing), and any
termination message (
terminate(...)in the model). - Interactive runs (scenarios with transports/viewer sections) launch the runtime with a browser viewer and input routing instead — see Interactive Simulation.
Benchmarking
rumoca sim bench measures compile time, preparation time, and hot
simulation throughput separately — useful when you care about iteration
speed on a large model or are comparing solver settings:
rumoca sim bench Ball.mo --model Ball
rumoca sim bench -c rum.toml
Caching
Compilation artifacts are cached under the platform cache directory, so
repeated runs of unchanged models skip recompilation. rumoca cache status
shows usage; rumoca cache prune trims it. --cache-dir overrides the
location for any command.
Scenario Files (rum.toml)
Rumoca scenarios are plain TOML files and the preferred way to run
repeatable simulation and code generation jobs. They follow a filename
convention — rum.toml for the default scenario and rum.<profile>.toml
for named profiles (such as rum.f16.toml or rum.bench.toml) — and live
next to the model they operate on. The filename is the editor/discovery
hook; the required [rumoca] marker section is the authoritative
declaration.
Each scenario describes one runnable thing. That keeps VS Code, the CLI,
and the playground aligned: the play button runs the active scenario instead
of guessing from a .mo file.
Getting Started
rumoca sim init > rum.toml # commented starter template
rumoca sim check -c rum.toml # validate without running
rumoca sim -c rum.toml # run
A Minimal Batch Scenario
[rumoca]
version = "1"
task = "simulate"
[model]
file = "../models/Ball.mo"
name = "Ball"
[sim]
solver = "rk-like"
t_end = 10.0
[[plot.views]]
id = "states_time"
title = "States vs Time"
type = "timeseries"
x = "time"
y = ["x", "v"]
Paths are resolved relative to the rum.toml file.
Section Reference
[rumoca] (required)
The marker section. version = "1" declares the schema version;
task = "simulate" runs the model, task = "codegen" renders a target into
an output directory.
[model] (required)
[model]
file = "MyVehicle.mo" # relative to this scenario
name = "MyVehicle" # top-level class to compile
Use the top-level source_roots key for package dependencies needed by this
scenario:
source_roots = ["../modelica_libraries"]
Workspace-wide library paths (MSL, CMM) belong in editor settings, not in
every scenario; scenario source_roots are for paths specific to this run.
[sim]
[sim]
dt = 0.01 # simulation timestep [s]
t_end = 10.0 # batch/report stop time; interactive runners may ignore it
solver = "auto" # auto | bdf | esdirk34 | trbdf2 | rk-like
output = "results.html"
mode = "realtime" # optional pacing, see below
mode selects runner pacing:
| Mode | Behavior |
|---|---|
as_fast_as_possible | Drain available inputs and run without sleeping |
realtime | Zero-order-hold inputs, sleep to wall-clock dt |
lockstep | Wait for each external packet before stepping |
The default is lockstep when external coupling is configured and
realtime standalone.
[[plot.views]]
Each view adds a plot to the batch report:
[[plot.views]]
id = "states_time"
title = "States vs Time"
type = "timeseries"
x = "time"
y = ["x", "v"]
[transport.*] — interactive viewer and coupling
HTTP and WebSocket transports serve the interactive browser viewer:
[transport.websocket]
port = 8081
[transport.http]
port = 8080
scene = "my_scene.js" # 3D scene, relative to this scenario
UDP is only needed when coupling to an external process:
[transport.udp]
listen = "0.0.0.0:4244"
send = "127.0.0.1:4242"
[external_interface]
command = "/path/to/external-process"
[schema], [receive], [send] — external interface coupling
These three sections are all-or-nothing. Provide them to couple via FlatBuffers over UDP; omit all three for standalone mode (gamepad/keyboard drive the model inputs directly).
[schema]
bfbs = ["/path/to/your_schema.bfbs"]
[receive]
root_type = "your.namespace.MotorOutput"
[receive.route]
"motors.m0" = { to = "stepper:omega_m1", scale = 1100.0 }
"armed" = { to = "local:armed" }
[send]
root_type = "your.namespace.SimInput"
[send.route]
"gyro.x" = { key = "gyro_x" }
[locals] — named persistent runner state
[locals]
throttle = { type = "float", default = 0.0 }
my_flag = { type = "bool", default = false }
Types are "bool", "float", or "array" (with element and len).
default is optional.
[signals], [input] — input routing
Map keyboard, gamepad, and viewer inputs onto model input variables for
interactive runs. The worked interactive examples are the best reference:
examples/interactive/quadrotor/rum.acro.tomlexamples/interactive/rover/rum.toml
Validation
rumoca sim check -c <file> validates structure and paths without running.
The VS Code extension surfaces the same validation when editing scenario
files.
Solvers and Accuracy
Rumoca ships several integration methods behind one --solver flag (CLI),
solver key ([sim] in scenarios), or Solver experiment annotation.
Choosing a Solver
| Solver | Kind | Use for |
|---|---|---|
auto | — | Default; picks a method from the model’s structure |
bdf | Implicit multistep (diffsol) | Stiff systems, smooth DAEs |
esdirk34 | Implicit SDIRK tableau (diffsol) | Stiff DAEs, one-step alternative to BDF |
trbdf2 | Implicit SDIRK tableau (diffsol) | Stiff DAEs |
rk-like | Explicit Runge–Kutta-style | Non-stiff systems, event-heavy models |
Rules of thumb:
- Start with
auto. - Stiff systems — fast and slow dynamics together (chemical kinetics,
stiff mechanical contacts,
mu-large Van der Pol) — need an implicit solver; explicit methods crawl with tiny steps. - Models that switch rapidly (relays, hysteresis controllers) currently run
most robustly with
rk-like; the implicit path can stall with a step size too small error near dense event cascades. Pin it in the model:annotation(experiment(Solver = "rk-like")).
Tolerances and Output Interval
Implicit solvers control local error against relative/absolute tolerances. Sources, in priority order:
- CLI/scenario settings (
--dt,[sim] dt). - The model’s
experimentannotation:ToleranceandIntervalare used by scenario and browser runs. - Runtime defaults.
--dt (or [sim] dt) sets the output interval — and the fixed step for
the explicit runner path. If omitted, the runtime chooses automatically.
Experiment Annotations
annotation(experiment(
StartTime = 0.0,
StopTime = 30.0,
Tolerance = 1e-6,
Interval = 0.01,
Solver = "rk-like"
));
StopTime, StartTime, Tolerance, Interval, and Solver (also
accepted: Algorithm, __Dymola_Algorithm) are parsed from the annotation.
Browser/live runs and the playground honor them fully; native direct CLI
runs currently take end time from --t-end (default 1.0 s) and scenario
runs from [sim].
Events and the Solver
All solvers cooperate with the event system: zero crossings from when
conditions and if-expressions are located, the state is re-initialized
(reinit), and integration restarts cleanly at the event instant. Sampled
events (sample(t0, period)) are scheduled exactly, not detected.
Try It
The Van der Pol oscillator becomes stiff as mu grows. Compare solvers by
editing the annotation — try mu = 1000 with Solver = "bdf" versus
Solver = "rk-like" and watch the run time in the status line:
model VanDerPolStiff
parameter Real mu = 1000.0 "Try 1, 5, 1000";
Real x(start = 2.0);
Real y(start = 0.0);
equation
der(x) = y;
der(y) = mu * (1 - x^2) * y - x;
annotation(experiment(StopTime = 3000.0, Solver = "bdf"));
end VanDerPolStiff;
Interactive Simulation
Interactive simulation runs a model continuously with live inputs — keyboard,
gamepad, browser controls, or an external process — and streams the state to
a browser viewer, including 3D scenes. It uses the same rum.toml scenario
format as batch simulation; the scenario selects the model, solver, viewer,
input routing, and transport ports.
Running the Examples
Quadrotor software-in-the-loop:
cargo run -p rumoca --release -- \
sim -c examples/interactive/quadrotor/rum.acro.toml
Rover:
cargo run -p rumoca --release -- \
sim -c examples/interactive/rover/rum.toml
The CLI prints the HTTP and WebSocket endpoints for the viewer. Open the HTTP URL in a browser if it does not open automatically. Use release builds — interactive simulation is real-time work.
How a Scenario Becomes Interactive
A scenario with only [model] and [sim] produces a batch HTML report.
Adding transports and input/viewer sections launches the interactive runner
instead:
[transport.http]+[transport.websocket]— serve the browser viewer (scene = "my_scene.js"selects the 3D scene file).[locals],[signals]— named runner state and routing from input devices to modelinputvariables.[transport.udp]+[schema]/[receive]/[send]— couple an external process (e.g. a flight controller) over FlatBuffers/UDP.
See Scenario Files for each section.
Pacing
The runtime supports three pacing styles via [sim] mode:
| Mode | Behavior | Use |
|---|---|---|
as_fast_as_possible | No sleeping; drain inputs | Batch-like exploration |
realtime | Sleep to wall-clock dt, zero-order-hold inputs | Human-in-the-loop browser control |
lockstep | Step only when an external packet arrives | External interfaces that own the timing |
Defaults: lockstep when external coupling is configured, otherwise
realtime.
Input Routing
Keyboard, gamepad, and viewer inputs go through the generic Rumoca input
path; the scenario describes which signals drive which model inputs. The
model side is plain Modelica input variables, so the same model runs in
batch (inputs from equations or defaults) and interactively (inputs from
devices) without modification.
Viewer Modes
The built-in results panel is for plots. External web viewers handle 3D scenes and interactive SIL. Both are launched from scenarios, so the same configuration works from the CLI, VS Code, and editor tooling.
Inspecting and Debugging Models
When a model misbehaves — fails to compile, fails to initialize, or produces
wrong dynamics — Rumoca gives you structured views into every stage of the
pipeline. All of these work with both rumoca compile and rumoca sim.
Dump an Intermediate Representation
--emit prints the model as the compiler sees it after each stage:
| Stage | What you see |
|---|---|
ast-mo / ast-json | The parsed, resolved syntax tree |
flat-mo / flat-json | The flattened model: hierarchy and connects expanded |
dae-mo / dae-json | The DAE system: equations partitioned, ready for analysis |
solve-json | The solver IR: sorted, torn, scheduled for execution |
rumoca compile Model.mo --emit flat-mo # to stdout
rumoca compile Model.mo --emit dae-json -o m.json
Reading flat-mo answers “what did my modifications and connects actually
produce?”. Reading dae-mo answers “what equation system is the solver
given?” — the live examples in this book expose the same view through their
Show DAE button.
Structural Analysis
rumoca compile Model.mo --inspect structure
Prints the structural preparation of the system: the matching between equations and unknowns, the block lower-triangular (BLT) ordering, simultaneous (coupled) blocks, and tearing decisions. This is the first place to look when compilation fails with structurally singular system — it names the unmatched equations and unknowns.
Numerical Evaluation at a Point
rumoca sim Model.mo --inspect eval
rumoca sim Model.mo --inspect eval --at "x=1.5,v=0@2.0"
Evaluates all solver values and state derivatives at a point and names any
non-finite results — the fastest way to find the division-by-zero or domain
error behind a NaN. With no --at, it evaluates at the initial state (which
also discovers the state names for you).
The --at syntax is <name=value,...@t>: states by name, unset states keep
their initial values, time after @ (default 0).
Jacobian Analysis
rumoca sim Model.mo --inspect jacobian --at "x=1.0@0"
Prints the dense state Jacobian at a point and flags singular columns and zero pivots — useful for diagnosing initialization failures and stiff or degenerate dynamics.
NaN Tracing
When a simulation fails with a non-finite value, rumoca sim automatically
re-runs with NaN tracing to locate the offending variables, so the
diagnostic names the variable instead of just reporting a solver failure.
Performance
rumoca sim bench Model.mo # compile / prepare / hot-loop timing
rumoca cache status # compilation cache usage
Verbose Compilation
rumoca compile Model.mo --target sympy -o out -v
-v prints friendly [rumoca] Phase ... progress lines, which localizes
slow or failing phases on large models.
Targets and Templates
Rumoca can render a compiled model into other languages and ecosystems:
symbolic math packages, compiled simulation kernels, FMUs, or Modelica
source at any pipeline stage. Code generation is target-directory based: a
target is a target.toml manifest plus Jinja templates, and each target
declares which compiler IR stage it consumes.
Listing Targets
rumoca targets
Built-in targets include:
| Target | IR | Mode | Output |
|---|---|---|---|
sympy | dae | symbolic | SymPy model classes |
jax | dae | symbolic | JAX functions |
casadi-sx / casadi-mx | dae | symbolic | CasADi expressions |
julia-mtk | dae | symbolic | ModelingToolkit.jl |
symforce | dae | symbolic | SymForce, with native AD support |
onnx | dae | symbolic | ONNX graph |
rust-solve / c-solve / embedded-c | solve | compiled | Self-contained simulation kernels |
cuda-c / cuda-nvrtc-solve-jit | solve | compiled/JIT | GPU kernels |
cranelift-solve-jit / mlir | solve | JIT/compiled | In-process execution backends |
fmi2 / fmi3 | solve | packaged | FMU export |
modelica / flat-modelica / dae-modelica / base-modelica | ast/flat/dae | source-transform | Modelica source at each stage |
The rumoca targets table also reports a readiness level (0 = experimental
… 2 = validated) and per-feature support columns (scalarization, matmul,
linear solve, events, AD, …) for each target. Treat the table — not this
page — as the current source of truth.
Rendering a Target
rumoca compile examples/models/SympyDecay.mo \
--model SympyDecay \
--target sympy \
--output /tmp/sympy_decay
--output may be a file or directory depending on what the target renders.
Codegen Scenarios
Like simulations, generation jobs worth repeating belong in a rum.toml
with task = "codegen". Runnable examples live under examples/codegen/
and write into examples/codegen/gen/ (git-ignored):
examples/codegen/rum.ball_jax.toml— built-in JAX targetexamples/codegen/rum.sympy_decay_sympy.toml— built-in SymPy targetexamples/codegen/rum.sympy_decay_standalone_web.toml— custom web targetexamples/codegen/rum.sympy_decay_custom_casadi.toml— raw Jinja template
IR Dumps vs Targets
If what you want is to see a compiler stage rather than generate project
code, use --emit instead of a target — see
Inspecting and Debugging Models.
Custom Targets
When the built-in targets do not fit, write your own. There are two levels:
Raw Jinja Templates
For one-off generation, pass a .jinja file directly. --phase chooses
which IR the template receives (default dae):
rumoca compile Model.mo --target my_template.jinja --phase flat -o out.txt
The template gets the serialized IR as its context. The repository example
examples/codegen/custom_casadi.jinja shows this workflow.
To learn the available fields, dump the matching IR as JSON first:
rumoca compile Model.mo --emit flat-json | head -50
Target Directories (target.toml)
For anything reusable, create a directory containing a target.toml
manifest and the templates it references, then pass the directory:
rumoca compile Model.mo --target path/to/my_target -o out/
The manifest declares which IR stage the target consumes and which templates render which output files. The target — not individual templates — owns the IR choice, so a bundle stays consistent.
The repository ships a complete worked example:
examples/codegen/standalone_web/target.toml renders a standalone HTML page
plus companion JavaScript from one model.
Design Rule: Language Knowledge Lives in Targets
Rumoca’s compiler phases are deliberately target-agnostic: no Rust code
special-cases C, CUDA, Python, or MLIR. Everything language-specific belongs
in target.toml metadata and templates. If a custom target needs
information the IR does not expose, that is a compiler feature request — not
something to hack around in a template.
Examples Overview
Runnable examples live under examples/ in the repository, organized by
what they demonstrate. Each runnable example is driven by a colocated
rum.toml scenario.
| Example | What it shows |
|---|---|
examples/simulation/rum.ball.toml | Small batch simulation with plots |
examples/models/ | Shared Modelica models used by simulation and codegen scenarios |
examples/codegen/rum.ball_jax.toml | Built-in JAX target |
examples/codegen/rum.sympy_decay_sympy.toml | Built-in SymPy target |
examples/codegen/rum.sympy_decay_standalone_web.toml | Custom standalone web target bundle |
examples/codegen/rum.sympy_decay_custom_casadi.toml | Direct raw Jinja template |
examples/interactive/quadrotor/rum.acro.toml | Interactive quadrotor SIL with 3D viewer |
examples/interactive/rover/rum.toml | Interactive rover |
Run any of them the same way:
cargo run -p rumoca --release -- sim -c examples/simulation/rum.ball.toml
Codegen scenarios write generated files under examples/codegen/gen/,
which is ignored by git.
Library Dependencies
Examples that use MSL or the CogniPilot Modelica Models need the pinned packages fetched first:
cargo xtask repo modelica-deps ensure
The repository’s committed VS Code workspace settings point at the fetched
packages for the common open modes (repository root, examples/, and the
quadrotor directory).
In-Browser Examples
The Live Examples page collects runnable models embedded directly in this book — no install needed. The same live blocks appear throughout the guide chapters, including the turkey PDE animation.
Live Examples
Every block on this page runs the real Rumoca compiler in your browser. Edit freely — the editors have Rumoca’s completion, hover, and error checking — then ▶ Simulate to integrate and plot, or Show DAE to see the compiled equation system. Reset restores the original text.
More live examples appear throughout the book: spring-mass-damper, cooling coffee and a hanging mass, events and discrete behavior, the stiff Van der Pol, and the turkey PDE with an animated cross-section.
Exponential Decay
The simplest possible ODE — one state, one parameter:
model SympyDecay
Real x(start = 1);
parameter Real k = 0.5;
equation
der(x) = -k * x;
annotation(experiment(StopTime = 10.0));
end SympyDecay;
Bouncing Ball
Events: a zero crossing detected by the solver, with a state jump:
model Ball
Real x(start = 10) "Height [m]";
Real v(start = 1) "Velocity [m/s]";
parameter Real g = 9.81;
parameter Real e = 0.8 "Coefficient of restitution";
equation
der(x) = v;
der(v) = -g;
when x < 0 then
reinit(v, -e * pre(v));
end when;
annotation(experiment(StopTime = 10.0));
end Ball;
Try e = 1.0 (elastic) or e = 0.5 (dead tennis ball).
Coupled Oscillators
Two masses, three springs — energy sloshes between the modes:
model CoupledOscillators
parameter Real m = 1.0;
parameter Real k = 10.0 "Outer springs";
parameter Real kc = 1.0 "Coupling spring";
Real x1(start = 1.0);
Real v1(start = 0.0);
Real x2(start = 0.0);
Real v2(start = 0.0);
equation
der(x1) = v1;
m * der(v1) = -k * x1 - kc * (x1 - x2);
der(x2) = v2;
m * der(v2) = -k * x2 - kc * (x2 - x1);
annotation(experiment(StopTime = 30.0));
end CoupledOscillators;
The beat period is set by the coupling strength — weaken kc and watch the
energy exchange slow down.
Lotka–Volterra Predator–Prey
A classic nonlinear system with closed orbits:
model LotkaVolterra
parameter Real alpha = 1.1 "Prey growth";
parameter Real beta = 0.4 "Predation";
parameter Real delta = 0.1 "Predator efficiency";
parameter Real gamma = 0.4 "Predator death";
Real prey(start = 10.0);
Real predator(start = 10.0);
equation
der(prey) = alpha * prey - beta * prey * predator;
der(predator) = delta * prey * predator - gamma * predator;
annotation(experiment(StopTime = 50.0));
end LotkaVolterra;
Browser Notes
The first ▶ Simulate on a page downloads the WASM compiler (cached
afterwards). Models honor their experiment annotation — StopTime,
Interval, Tolerance, and Solver. For bigger work, use the
Web Playground or the native tools.
Troubleshooting and FAQ
Compilation
“class not found” / unresolved names — The model references a package
Rumoca cannot see. Add the library with --source-root (CLI), top-level
source_roots (scenario), or rumoca.sourceRootPaths (VS Code). Check
MODELICAPATH if you rely on it. See
Using Modelica Libraries.
“Duplicate class ‘X’ … with non-identical definition” (EM001) — Two
files in the same source root define the same class name. This commonly
happens when an old copy of a model sits next to a new one in the same
directory; direct rumoca sim file.mo runs include the file’s directory as
a source root.
“structurally singular system: N matched out of M equations” — The
equation system cannot be matched one-to-one with its unknowns. The
diagnostic names the unmatched equations and unknowns; run
rumoca compile --inspect structure for the full matching. Common causes:
- Genuinely unbalanced models (forgotten equation, extra variable).
- High-index DAE formulations that current index reduction does not yet handle, such as a Cartesian pendulum with an explicit constraint — see Language Support Status. Reformulating with generalized coordinates usually fixes it.
Model is balanced but produces wrong dynamics — Dump the system the
solver actually integrates with rumoca compile --emit dae-mo and compare
it to your intent. Please file an issue with a minimal model if the lowering
looks wrong.
Simulation
“Step size is too small at time = …” — The implicit solver stalled,
most often near a dense cascade of state events (rapid relay switching).
Use the explicit solver: --solver rk-like or
annotation(experiment(Solver = "rk-like")). For genuinely stiff smooth
systems, try esdirk34 or trbdf2 instead.
NaN/Inf failures — rumoca sim automatically re-runs with NaN tracing
and names the offending variables. To investigate further, evaluate the
model at a chosen point: rumoca sim Model.mo --inspect eval --at "x=...@t". Typical causes: division by a variable that crosses zero,
sqrt/log of a negative value, or missing initial values.
Simulation stops early with a message — The model called
terminate(...); the message is recorded in the report. assert failures
likewise carry their message and source location.
My run used t_end = 1.0 even though the model has
experiment(StopTime=...) — Native direct CLI runs take the end time
from --t-end (default 1.0); scenario runs use [sim] t_end. The browser
examples and playground do honor the annotation.
Results
The HTML report did not open — It is written next to where you ran the
command (<MODEL>_results.html by default, -o to override). Open it in
any browser.
Too many variables in the report — Add [[plot.views]] sections to the
scenario to define focused plots.
VS Code
No diagnostics / completion — Check that the extension is active for
the file (it activates on .mo and rum.toml). If you enabled
rumoca.useSystemServer, ensure rumoca-lsp is on PATH; otherwise the
bundled server is used.
Library completion missing — Set rumoca.sourceRootPaths (workspace
scope). For the repository’s own examples, run
cargo xtask repo modelica-deps ensure first.
Live Examples in This Book
The ▶ Simulate button reports the WASM package is missing — Live
examples need the WASM package deployed next to the book. They work on
the published site; for
a local build, serve the repository root after cargo xtask wasm build.
The editor has no syntax highlighting — Monaco loads from a CDN; when offline, the examples fall back to plain text editors but still simulate.
Performance
Compilation feels slow on repeat runs — Check the cache:
rumoca cache status. Direct file runs are cached by content; use
rumoca sim bench to separate compile time from simulation throughput.
Browser runs are slower than native — Expected, especially for large package trees; use the native CLI for MSL-heavy work.
Reporting Bugs
File issues at https://github.com/CogniPilot/rumoca/issues with a minimal model. The Web Playground is a convenient way to confirm a reproduction without local setup.