Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

CapabilityWhere to read more
Compile and simulate Modelica modelsQuick Start, Running Simulations
Repeatable scenario files (rum.toml) for simulation and codegenScenario Files
Interactive, human-in-the-loop simulation with browser 3D viewersInteractive Simulation
Code generation to SymPy, JAX, CasADi, C, Rust, FMI, and moreTargets and Templates
IDE support: diagnostics, completion, hover, run buttonsVS Code Extension
Formatter and linter for Modelica sourceFormatter and Linter
Full compiler in WebAssemblyWeb Playground
Structural analysis and debugging of modelsInspecting and Debugging Models

The Normal Workflow

  1. Write or open a Modelica model (.mo file).
  2. Configure any external Modelica package roots, such as the Modelica Standard Library (MSL).
  3. Run a direct command (rumoca sim model.mo) or a colocated rum.toml scenario (rumoca sim -c rum.toml).
  4. 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.

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

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 named Coffee. 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. start gives its initial value. Because der(T) appears in an equation, T is 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

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:

DeclarationMeaning
constant Real c = 2.0Fixed forever, usable in types and array sizes
parameter Real k = 1.0Fixed during a run, settable between runs
discrete Real uChanges only at events, holds its value between them
Real xContinuous-time variable
input Real f / output Real yCausal 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 by connect(...).

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:

  1. parses and resolves the model and everything it references,
  2. type-checks and instantiates components with their modifications,
  3. flattens the class hierarchy and connect sets into one equation system,
  4. sorts and analyzes the system structurally (matching equations to unknowns, finding simultaneous blocks, tearing algebraic loops),
  5. 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 of v immediately before the event.
  • reinit(v, ...) — restarts the state v from 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

  • when bodies relate discrete values; use reinit to 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) declares N states at once; each applies the modification to every element.
  • for-equations are unrolled at compile time. The loop range must be known structurally, which is why N is a parameter Integer. After flattening, the compiler sees 5 * N + 4 plain 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⁴)) — the T⁴ 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 — 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/tau that 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 (drop Interval and 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, connect with potential/flow semantics.
  • Continuous dynamics — DAE compilation with structural analysis: matching, sorting (BLT), algebraic-loop tearing, and dummy-derivative index reduction for many higher-index systems.
  • Eventswhen/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.
  • Initializationstart attributes and initial equation handling.
  • Experiment annotationsStopTime, StartTime, Tolerance, Interval, Solver are 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-like or annotation(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 experiment stop time — pass --t-end or 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
OptionMeaning
-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:

SubcommandPurpose
rumoca sim check -c rum.tomlValidate a scenario file without running it
rumoca sim initPrint a fully commented rum.toml starter template
rumoca sim benchBenchmark 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>-json dumps an intermediate representation. Stages are ast, flat, dae, solve (solve has no Modelica form, so only solve-json exists).
  • --target <id|dir|file.jinja> runs code generation: a built-in target id (see rumoca targets), a directory containing target.toml, or a raw Jinja template.
  • --phase <ast|flat|dae|solve> picks which IR a raw .jinja template receives (default dae). 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-root flags.

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 CodeSettings 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.sourceRootPaths at workspace scope. Use this for MSL, CMM, and other shared packages.
  • Scenario Source Root Paths edits the active rum.toml scenario. 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.

CommandPurpose
Create Rumoca ScenarioGenerate a rum.toml next to a model
Run Current Rumoca ScenarioCompile and simulate the active scenario
Open Scenario SettingsEdit the active scenario’s settings
Open Rumoca SettingsExtension settings menu
Open Rumoca User GuideThis book
Open Rumoca Internals GuideThe contributor book
Toggle / Expand All / Collapse All Annotation ExpansionControl 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

ProfileBehavior
dymolaPreserves MSL/Dymola-compatible local whitespace
canonicalStricter spacing and indentation defaults
rumoca fmt --profile canonical

Individual rules can be toggled regardless of profile:

FlagEffect
--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:

ModeBehavior
as_fast_as_possibleDrain available inputs and run without sleeping
realtimeZero-order-hold inputs, sleep to wall-clock dt
lockstepWait 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.toml
  • examples/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

SolverKindUse for
autoDefault; picks a method from the model’s structure
bdfImplicit multistep (diffsol)Stiff systems, smooth DAEs
esdirk34Implicit SDIRK tableau (diffsol)Stiff DAEs, one-step alternative to BDF
trbdf2Implicit SDIRK tableau (diffsol)Stiff DAEs
rk-likeExplicit Runge–Kutta-styleNon-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:

  1. CLI/scenario settings (--dt, [sim] dt).
  2. The model’s experiment annotation: Tolerance and Interval are used by scenario and browser runs.
  3. 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 model input variables.
  • [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:

ModeBehaviorUse
as_fast_as_possibleNo sleeping; drain inputsBatch-like exploration
realtimeSleep to wall-clock dt, zero-order-hold inputsHuman-in-the-loop browser control
lockstepStep only when an external packet arrivesExternal 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:

StageWhat you see
ast-mo / ast-jsonThe parsed, resolved syntax tree
flat-mo / flat-jsonThe flattened model: hierarchy and connects expanded
dae-mo / dae-jsonThe DAE system: equations partitioned, ready for analysis
solve-jsonThe 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:

TargetIRModeOutput
sympydaesymbolicSymPy model classes
jaxdaesymbolicJAX functions
casadi-sx / casadi-mxdaesymbolicCasADi expressions
julia-mtkdaesymbolicModelingToolkit.jl
symforcedaesymbolicSymForce, with native AD support
onnxdaesymbolicONNX graph
rust-solve / c-solve / embedded-csolvecompiledSelf-contained simulation kernels
cuda-c / cuda-nvrtc-solve-jitsolvecompiled/JITGPU kernels
cranelift-solve-jit / mlirsolveJIT/compiledIn-process execution backends
fmi2 / fmi3solvepackagedFMU export
modelica / flat-modelica / dae-modelica / base-modelicaast/flat/daesource-transformModelica 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 target
  • examples/codegen/rum.sympy_decay_sympy.toml — built-in SymPy target
  • examples/codegen/rum.sympy_decay_standalone_web.toml — custom web target
  • examples/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.

ExampleWhat it shows
examples/simulation/rum.ball.tomlSmall batch simulation with plots
examples/models/Shared Modelica models used by simulation and codegen scenarios
examples/codegen/rum.ball_jax.tomlBuilt-in JAX target
examples/codegen/rum.sympy_decay_sympy.tomlBuilt-in SymPy target
examples/codegen/rum.sympy_decay_standalone_web.tomlCustom standalone web target bundle
examples/codegen/rum.sympy_decay_custom_casadi.tomlDirect raw Jinja template
examples/interactive/quadrotor/rum.acro.tomlInteractive quadrotor SIL with 3D viewer
examples/interactive/rover/rum.tomlInteractive 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 failuresrumoca 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.