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

Python API

Rumoca ships a first-class, typed Python API. Create a Session once, compile models through it, and everything — metadata, simulation, code generation, symbolic export — hangs off the returned Model. Returned objects are real typed classes (full autocomplete), never JSON you have to json.loads.

pip install rumoca            # core
pip install rumoca[data]      # + numpy/pandas (result.to_numpy / to_dataframe)
pip install rumoca[plot]      # + matplotlib (result.plot)
pip install rumoca[casadi]    # + casadi   (model.to_casadi)
pip install rumoca[all]       # everything, incl. the %%modelica magic

Optional dependencies are genuinely optional: import rumoca never needs them, and a method that does (e.g. result.plot()) raises a message naming the extra to install.

Quick start

import rumoca as rm

session = rm.Session(roots=["libs/CMM"])
m = session.load("Quadrotor.mo", model="Quadrotor")
m                                  # repr shows a summary
m.parameters["mass"].value         # typed, autocompletes
m.parameters["mass"].kind          # "tunable" or "structural"
m.states["body.v[1]"]              # subscripted names resolve

r = m.simulate(t=(0, 10), dt=0.01)
r.plot("body.v[1]")                # needs rumoca[plot]
df = r.to_dataframe()              # needs rumoca[data]

Compile from a string with session.loads(source, model=...). Keep and reuse the Session; it owns source roots and compiler caches.

Or load everything from a rumoca-scenario.toml in one call — its model, source roots, and solver settings (paths resolved relative to the file):

session, model, config = rm.Session.from_scenario("rumoca-scenario.toml")
r = model.simulate(t=(0, 10), config=config)   # config carries solver/dt

For build systems and scenario-driven runs, use the session scenario entry point:

session = rm.Session()
run = session.run_scenario("rumoca-scenario.toml")
run.status          # "completed"
run.output_paths    # generated reports, CSVs, codegen files, debug logs
run.result          # Result for batch simulation scenarios, otherwise None
run.codegen         # CodegenResult for codegen scenarios, otherwise None

Runtime knobs are scenario overrides, not a separate execution surface:

session = rm.Session(roots=["libs/CMM"])
run = session.run_scenario(
    "rumoca-scenario.toml",
    overrides={"t_end": 2.0, "mode": "lockstep", "output": "results.csv"},
)

Inspecting a model

AccessorReturns
m.states, m.algebraics, m.inputs, m.outputsVarView of VariableInfo
m.parametersParamView of ParameterInfo (.value, .kind)
m.structure()StructuralInfo (counts, balance, BLT blocks, algebraic loops)
m.to_dict("dae") / m.to_json("dae")the IR, only when you ask

Views behave like sequences: len(m.states), m.states[0], m.states["x"], for v in m.states, m.states.names. An unknown name raises a KeyError that suggests the closest match.

Parameter sweeps

Tunable parameters change without recompiling, so a sweep compiles once:

for drag in [0.0, 0.05, 0.1]:
    r = m.with_params(rotor_drag=drag).simulate(t=(0, 10))
    ...

m.simulate(params={...}, start={...}) applies overrides for a single run. Overriding a structural parameter (one that affects sizing/instantiation) or a parameter another parameter’s value depends on raises StructuralParamError — those need a recompile, not a runtime override, so a sweep is never silently wrong.

A structural parameter can be changed by re-instantiating the model — array dimensions and conditional components re-evaluate:

m6 = m.with_params(n_rotors=6, recompile=True)   # re-instantiate with n_rotors=6
m6.simulate(t=(0, 10))

Live symbolic export

Turn a model into a live object in the symbolic framework of your choice — no file dance:

cm = m.to_casadi()                 # CasadiModel (form="dae")
cm.ode                             # explicit RHS (CasADi's native DAE form)
cm.dae                             # {x, z, p, t, ode, alg} — feed ca.integrator
S  = cm.jacobian("ode", "p")       # exact AD parameter sensitivity

sm = m.to_sympy()                  # SymPy model; sm.solve_explicit()
jm = m.to_jax()                    # JAX ode_fn + diffrax simulate

Pass form="solve" for the scalarized, causalized explicit form (the same source the C/FMI/Rust backends use) — you get a SolveExport exposing the explicit right-hand side xdot = rhs(x, u, p) plus state_names/ parameter_names:

se = m.to_casadi(form="solve")     # SolveExport; se.rhs is a ca.Function
se = m.to_jax(form="solve")        # SolveExport; se.rhs is jit/grad/vmap-able

These render the same tested codegen targets used by m.codegen(target), so the live object and the generated files never drift. For writing files instead, use m.codegen("casadi-mx").save_all("out/").

For build systems that need a single callable operation, use:

session = rm.Session(roots=["libs/Modelica"])
written = session.codegen_file(
    "model.mo",
    "MyModel",
    "galec-production",
    "generated/galec",
)

CMake Integration

CMake needs an executable build step, but downstream projects should not invent a second Rumoca CLI. Use the Python interpreter as the executable and call the stable API:

find_package(Python3 REQUIRED COMPONENTS Interpreter)

add_custom_command(
  OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/rumoca-codegen.stamp"
  COMMAND "${Python3_EXECUTABLE}" -c
          "import pathlib, rumoca as rm; \
session = rm.Session(roots=[r'${CMAKE_CURRENT_SOURCE_DIR}/libs/Modelica']); \
session.codegen_file(r'${CMAKE_CURRENT_SOURCE_DIR}/model.mo', 'MyModel', \
'galec-production', r'${CMAKE_CURRENT_BINARY_DIR}/generated/galec'); \
pathlib.Path(r'${CMAKE_CURRENT_BINARY_DIR}/rumoca-codegen.stamp').write_text('ok\\n')"
  DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/model.mo"
  VERBATIM
)

Scenario-driven build or test steps use the same pattern:

add_custom_target(run_rumoca_scenario
  COMMAND "${Python3_EXECUTABLE}" -c
          "import rumoca as rm; rm.Session().run_scenario(r'${CMAKE_CURRENT_SOURCE_DIR}/rumoca-scenario.toml')"
  VERBATIM
)

Reusable CMake modules should be thin wrappers around Session.run_scenario and Session.codegen_file; they should not parse scenario TOML or probe private binding names. Create one session per build step and reuse it for all Rumoca work in that step so compiler caches stay warm.

Jupyter: the %%modelica magic

With rumoca[notebook], write Modelica in a cell and get back a typed object:

%%modelica -m Decay --name m
model Decay Real x(start=1); equation der(x) = -x; end Decay;

%%modelica sim ... returns a Result; %%modelica export casadi ... returns a CasadiModel. --name VAR also binds the result into the notebook namespace.

Diagnostics

rm.validate(path) / rm.validate_source(src) return a list of Diagnostic objects and never raise on model errors. Session.load / Session.loads / Model.simulate raise the typed hierarchy — RumocaError and its subclasses ParseError, CompileError, SimulationError, StructuralParamError — each carrying a message that teaches.