What is Otwin? · The Otwin idea · The specification · Install · Quick start · How it works
Physics engine · Components · Multi-domain physics · Physics + data
Digital Twins · Estimation · Uncertainty · Validation · Examples · Roadmap · The Otwin ecosystem
What is Otwin?
Otwin is an open-source physics engine and modeling framework for engineering systems.
You describe a physical system as components, parameters and connections — the same way an engineer would draw it. Otwin compiles that description into a structured dynamical model and executes it in a high-performance Rust runtime. On top of that physics engine, Otwin provides the tools needed to connect the model to a real asset:
- state estimation;
- parameter calibration;
- hybrid physics + data models;
- forecasting;
- uncertainty quantification;
- out-of-sample validation;
- validity envelopes;
- Digital Twin operation.
---
config:
layout: fixed
theme: neo
fontFamily: '''Open Sans Variable'', sans-serif'
themeVariables:
fontFamily: '''Open Sans Variable'', sans-serif'
---
%%{init: {"flowchart": {"nodeSpacing": 12, "rankSpacing": 18, "padding": 6, "curve": "linear"}, "themeVariables": {"fontSize": "13px"}}}%%
flowchart LR
A["Engineering SYSTEM<br/>(components · connections · parameters)"]
B["Otwin MODEL<br/>physical structure"]
C["COMPILER<br/>IR · equations · checks"]
D["Physics ENGINE<br/>compiled dynamics"]
E["Measurements (DATA)<br/>real asset"]
F["DIGITAL TWIN<br/>estimate · predict · validate"]
A --> B --> C --> D
D --> F
E --> F
A simulation tells you what a model does. A Digital Twin tells you what a particular asset is doing — and what the model has earned the right to predict
The Otwin idea
Engineering systems are not purely mathematical and they are not purely data-driven. Some things are known:
- conservation laws;
- energy storage;
- component relationships;
- electrical, mechanical, hydraulic or thermal behaviour;
- physical constraints.
Other things are uncertain:
- friction;
- losses;
- degradation;
- unknown loads;
- unmodelled phenomena;
- sensor noise;
- changing parameters.
Otwin is designed around the idea that known physics should remain structure, while data should be used to identify and model what the physics does not know.
---
config:
layout: fixed
theme: neo
fontFamily: '''Open Sans Variable'', sans-serif'
themeVariables:
fontFamily: '''Open Sans Variable'', sans-serif'
---
%%{init: {"flowchart": {"nodeSpacing": 12, "rankSpacing": 18, "padding": 6, "curve": "linear"}, "themeVariables": {"fontSize": "13px"}}}%%
flowchart TB
A["Physical knowledge"]
B["Measured data"]
A --> C["Structured physics model"]
B --> D["Parameter estimation"]
B --> E["Residual / data model"]
C --> F["Otwin model"]
D --> F
E --> F
F --> G["Compiled dynamics"]
G --> H["Simulation"]
G --> I["State estimation"]
G --> J["Forecasting"]
G --> K["Validation"]
H --> L["Digital Twin"]
I --> L
J --> L
K --> L
This leads to a simple principle: Do not throw away known physics just because some of the physics is unknown. Use structure where it is known. Use data where it is needed. Keep the two separate enough that you can inspect, test and validate both.
From a Python library to an open modeling ecosystem
Otwin is Python-first, but the architecture is intentionally larger than a Python package. The long-term idea is to separate:
WHAT A PHYSICAL MODEL MEANS
│
▼
OTWIN SPECIFICATION
│
▼
CONFORMANCE TESTS
│
┌─────┼─────┐
│ │ │
Python Julia* MATLAB*
│ │ │
└─────┼─────┘
│
▼
CONFORMANT IMPLEMENTATIONS
A physics model should not be considered correct simply because one implementation produces plausible numbers. It should be possible to define what the model means, define physical properties that must hold, provide known reference answers, and test independent implementations against the same specification.
The Otwin specification
otwin-spec is part of the architecture, not an add-on. Otwin includes a normative specification and a language-agnostic conformance suite.
The specification defines what an Otwin model means. Implementations prove that they conform to it.
The conformance suite contains physical systems with known answers and tests structural properties such as:
- skew-symmetry of interconnection matrices;
- positive-semidefinite dissipation;
- passivity;
- energy conservation;
- energy non-increase where required;
- physical constitutive laws;
- thermodynamic constraints;
- manifest validity.
The suite is designed to catch deliberately broken implementations, not just confirm that a reference implementation passes its own tests.
---
config:
layout: fixed
theme: neo
fontFamily: '''Open Sans Variable'', sans-serif'
themeVariables:
fontFamily: '''Open Sans Variable'', sans-serif'
---
%%{init: {"flowchart": {"nodeSpacing": 12, "rankSpacing": 18, "padding": 6, "curve": "linear"}, "themeVariables": {"fontSize": "13px"}}}%%
flowchart LR
A["Otwin specification"]
B["Physical fixtures"]
C["Analytical / closed-form answers"]
D["Conformance suite"]
A --> D
B --> D
C --> D
D --> E["Python implementation"]
D --> F["Julia implementation"]
D --> G["MATLAB implementation"]
D --> H["Future implementations"]
E --> I{"Conformant?"}
F --> J{"Conformant?"}
G --> K{"Conformant?"}
H --> L{"Conformant?"}
This is an important architectural boundary: Otwin is not defined only by its Python implementation. The Python implementation is the reference implementation and the primary user interface today. The specification defines the contract that allows the ecosystem to grow beyond it.
See Otwin Specificiations for the normative specification and conformance suite.
Install
pip install "otwin[engine]"
-
otwinprovides the Python modeling framework. -
otwin[engine]adds the compiled Rust runtime, with binary wheels for Linux, macOS and Windows. Without the engine package, models can still run through the NumPy reference backend.
Quick start
A mass hanging from a spring and damper under gravity. You describe the physical system. You don't need to write Newton's equation yourself.
Two verbs describe any system, and one rule says which to use. >> joins components in a line, the way the drawing reads: mass >> spring >> ceiling. connect joins ports where a line is not enough: three things meeting at one point, or a second circuit attached to a port. Here the mass hangs from the spring, and the damper and the weight meet the mass at the same point.
import otwin
from otwin.components.mechanical import (
Mass,
Spring,
Damper,
ForceSource,
Fixed,
)
mass = Mass(1.0, name="mass")
spring = Spring(20.0, name="spring")
damper = Damper(0.3, name="damper")
weight = ForceSource(1.0 * 9.81, name="weight")
ceiling = Fixed(name="ceiling")
system = mass >> spring >> ceiling # the line
system.connect(mass.flange, damper.a, weight.flange) # the damper and the weight meet the mass
system.connect(damper.b, ceiling.port) # the damper's other end
model = otwin.compile(system)
run = model.simulate(
t_span=(0, 20),
dt=0.05,
)
q = run["spring.extension"]
print(f"Static equilibrium q* = m g / k = {weight.force} m")
print(f"Lowest point reached: q = {q.max():.3f} m")
print(f"Final position: q = {q[-1]:.3f} m")
print(
f"Largest energy violation: "
f"{run.energy_balance()['max_violation']:.1e} J"
)
Output:
Static equilibrium q* = m g / k = 0.491 m
Lowest point reached: q = 0.932 m
Final position: q = 0.477 m
Largest energy violation: 0.0e+00 J
Nobody wrote Newton's law.
The physical structure was described through components and connections, and the compiler derived the executable dynamics.
The compiled model also carries the energy structure of the system. These interfaces let you inspect what Otwin actually built.
model.summary()
model.structure()
model.ir()
From the data sheet to the model
Real work starts from devices, not from springs. A battery module, as its data sheet describes it: capacity, the open-circuit voltage curve, the internal resistance, two polarisation branches, the heat capacity. Then the cooling. Nothing else.
import otwin
from otwin.components.battery import Battery
from otwin.components.electrical import CurrentSource, Ground
from otwin.components.thermal import Ambient, Convection
cell = Battery(
capacity=100.0, # Ah
ocv=[(0.0, 2.8), (0.05, 3.15), (0.5, 3.3), (0.95, 3.4), (1.0, 3.55)], # open-circuit voltage (LFP cell)
resistance=1e-3, # ohm
rc_branches=[(0.5e-3, 20e3), (0.8e-3, 200e3)], # (ohm, F)
thermal=1200.0, # J/K
soc=0.9,
name="cell",
)
load = CurrentSource(None, name="load") # amperes, an input
cooling = Convection(0.5, name="cooling") # W/K
air = Ambient(298.15, name="air") # Kelvin
gnd = Ground(name="gnd")
system = gnd >> cell >> load >> gnd # the electrical loop, closed on ground
system.connect(cell.thermal, cooling.a) # the thermal circuit hangs off the cell
system.connect(cooling.b, air.port)
model = otwin.compile(system, dt=10.0)
state = model.initial_state()
for _ in range(360): # one hour at 50 A
state = model.step(state, {"load": 50.0})
out = model.outputs(state)
print(f"soc {out['cell.soc']:.2f} voltage {out['cell.voltage']:.3f} V "
f"cell {out['cell.temperature'] - 273.15:.1f} °C heat {out['cell.heat_flow']:.2f} W")
soc 0.40 voltage 3.152 V cell 33.8 °C heat 5.75 W
The model has four states: the charge, two polarisation charges and the heat in the cell. Every voltage, current, power and temperature inside the module is a named output. with_parameters ages the cells or clogs the cooling without rebuilding anything; examples/battery_that_runs_hot.py uses that to tell the two apart.
A pump line is a line and nothing else, so >> is all it takes; one-port components (a tank, the outfall) join at the ends:
from otwin.components.hydraulic import Atmosphere, Filter, Pipe, Pump, Tank
system = (
Tank(area=5000.0, level=3.0, name="tank")
>> Pipe(resistance=5e5, name="suction")
>> Pump(shutoff=4e5, max_flow=0.08, name="pump") # nameplate curve
>> Filter(resistance=2e6, name="filter")
>> Atmosphere(name="outfall")
)
model = otwin.compile(system, dt=1.0)
state = model.initial_state()
for _ in range(900):
state = model.step(state)
print(f"{model.outputs(state)['pump.flow'] * 3600:.0f} m³/h")
fouled = model.with_parameters({"filter.fouling": 1.0}) # twice the clean resistance
state = fouled.initial_state()
for _ in range(900):
state = fouled.step(state)
print(f"{fouled.outputs(state)['pump.flow'] * 3600:.0f} m³/h with a fouled filter")
235 m³/h
196 m³/h with a fouled filter
Nobody wrote a pump equation either. The pump is a pressure source at shut-off, a loss that follows the curve and the inertia of the water in it. The compiler reads the operating point off the network.
How it works
Otwin treats the physical system as a graph. Components define physical behaviour. Connections define how components interact. The compiler turns that graph into an executable dynamical model.
---
config:
layout: fixed
theme: neo
fontFamily: '''Open Sans Variable'', sans-serif'
themeVariables:
fontFamily: '''Open Sans Variable'', sans-serif'
---
%%{init: {"flowchart": {"nodeSpacing": 12, "rankSpacing": 18, "padding": 6, "curve": "linear"}, "themeVariables": {"fontSize": "13px"}}}%%
flowchart TD
A["Physical **system**<br/>components · ports · connections · parameters"]
B["otwin.System<br/>domain-checked component graph"]
C["otwin.compile()<br/>model compiler"]
D["Physical System IR<br/>nodes · branches · states · parameters · inputs"]
E["Port-Hamiltonian IR<br/>H · J · R · G · D"]
F["Rust dynamics engine<br/>implicit midpoint · RK4 · adaptive RK45 · batches"]
G["TwinModel<br/>simulate · estimate · forecast · validate · refuse"]
A --> B --> C --> D --> E --> F --> G
Every connection is a physical node.
- The across variable is shared at the node (voltage, velocity, pressure, temperature).
- The through variable balances at the node (current, force, fluid flow, heat flow)
The storage elements define how energy is stored. The dissipative elements define losses and the sources provide inputs. Finally, Two-ports connect different physical domains. The resulting dynamics are expressed internally in port-Hamiltonian form (PHS):
$$ \dot{x} = \big(J(x)-R(x)\big)\nabla H(x) + G(x)u $$
where:
- $H(x)$ is stored energy;
- $J(x)$ represents energy exchange;
- $R(x)$ represents dissipation;
- $G(x)$ represents external ports;
- $u$ represents external inputs.
The output is:
$$ y = G(x)^\top\nabla H(x) + D(x)u $$
Otwin builds $J$ as skew-symmetric, so the interconnection represents energy exchange rather than energy creation or destruction. Dissipation is represented explicitly through $R$. The important point is that this structure belongs to the model, not merely to the numerical solver. You can inspect the internal representation with:
model.ir()
Physical consistency before numerical simulation
A physics engine should not wait for a numerical solver to discover that the physical model is badly formed. Otwin checks physical topology during compilation. For example, directly connecting dependent storage elements or sources can create a physically ambiguous model. Instead of producing a low-level numerical failure later, the compiler reports the physical problem in terms of the components you created:
CompileError:
dependent storages or sources: load, motor.rotor form a loop
that fixes the same potential twice.
Two capacitors in parallel, two inertias on one shaft,
two masses rigidly joined, a voltage source across a capacitor
or a tank connected directly to a pressure source can create
this situation.
Merge the two stores into one, or introduce a resistor,
pipe, damper or stiff spring between them.
The principle is simple: If the physical topology is invalid or ambiguous, catch it before the numerical solver ever sees it.
The physics engine
The core Otwin workflow is:
---
config:
layout: fixed
theme: neo
fontFamily: '''Open Sans Variable'', sans-serif'
themeVariables:
fontFamily: '''Open Sans Variable'', sans-serif'
---
%%{init: {"flowchart": {"nodeSpacing": 12, "rankSpacing": 18, "padding": 6, "curve": "linear"}, "themeVariables": {"fontSize": "13px"}}}%%
flowchart LR
A["Components"] --> E["Otwin compiler"]
B["Connections"] --> E
C["Parameters"] --> E
D["Inputs"] --> E
E --> F["Physical System IR"]
F --> G["Port-Hamiltonian representation"]
G --> H["Compiled Rust dynamics"]
H --> I["TwinModel"]
I --> J["Simulation"]
I --> K["State estimation"]
I --> L["Forecasting"]
I --> M["Validation"]
The architecture deliberately separates the modeling layer from the execution layer.
- Python describes and operates the model.
- Rust executes the compiled dynamics.
The same compiled model can therefore become the foundation for simulation, estimation, forecasting and validation.
Components
Otwin provides physical component primitives across multiple engineering domains.
See COMPONENTS.md for the current component catalogue.
| Domain | Across | Through | Components |
|---|---|---|---|
| Electrical | voltage | current | Resistor, Capacitor, Inductor, VoltageSource, CurrentSource, Ground |
| Mechanical | velocity | force | Mass, Spring, Damper, ForceSource, VelocitySource, Fixed |
| Rotational | angular velocity | torque | Inertia, TorsionSpring, RotationalDamper, TorqueSource, SpeedSource, Housing |
| Hydraulic | pressure | flow | Tank, Orifice, Pipe, FluidInertance, FlowSource, PressureSource, Filter, Pump, Atmosphere |
| Thermal | temperature | heat flow | ThermalMass, ThermalResistance, Convection, HeatSource, Losses, Ambient |
| Two-ports | Transformer, Gyrator |
||
| Devices | Battery, Pump, DCMotor, catalogue reference systems |
||
| Fundamental | any | any | Storage, Dissipator, Source, Reference — the roles every component above plays, with the domain as an argument |
Dissipative elements can use nonlinear constitutive laws:
RotationalDamper(
law=lambda w: (c1 + c2*w**2) * w
)
Multi-domain physics
Real engineering systems rarely belong to a single physical domain. A motor is an electrical system coupled to a rotating mechanical system. A pump couples electrical, rotational and hydraulic behaviour. A heat pump couples electrical, mechanical, thermal and fluid systems. Otwin represents these relationships through the same component-and-connection model.
---
config:
layout: fixed
theme: neo
fontFamily: '''Open Sans Variable'', sans-serif'
themeVariables:
fontFamily: '''Open Sans Variable'', sans-serif'
---
%%{init: {"flowchart": {"nodeSpacing": 12, "rankSpacing": 18, "padding": 6, "curve": "linear"}, "themeVariables": {"fontSize": "13px"}}}%%
flowchart LR
A["Electrical domain<br/>voltage · current"]
B["DC Motor<br/>electromechanical coupling"]
C["Rotational domain<br/>torque · angular velocity"]
D["Mechanical load"]
A --> B --> C --> D
For example, a supply, a DC motor and a fan on its shaft:
import otwin
from otwin.components.electrical import (
VoltageSource,
Ground,
)
from otwin.components.composite import DCMotor
from otwin.components.rotational import (
RotationalDamper,
Housing,
)
supply = VoltageSource(
None,
name="supply",
)
motor = DCMotor(
resistance=1.0,
inductance=0.5,
torque_constant=0.5,
inertia=0.01,
friction=0.1,
name="motor",
)
fan = RotationalDamper(
law=lambda w: 0.002 * w * abs(w),
name="fan",
)
gnd = Ground()
housing = Housing()
drive = gnd >> supply >> motor >> gnd # the electrical loop
drive.connect(motor.shaft, fan.a) # the shaft: a second circuit, in another domain
drive.connect(fan.b, housing.port)
model = otwin.compile(drive)
run = model.simulate(
t_span=(0, 3),
dt=0.001,
inputs={"supply": 24.0},
)
print(
f"speed after 3 s: "
f"{run['motor.rotor.angular_velocity'][-1]:.2f} rad/s"
)
print(
f"current: "
f"{run['motor.armature.current'][-1]:.3f} A"
)
p_in = -run["supply.power"][-1]
p_out = (
run["motor.armature_resistance.power"]
+ run["motor.bearing.power"]
+ run["fan.power"]
)[-1]
print(
f"electrical power in {p_in:.2f} W = "
f"copper + bearing + fan {p_out:.2f} W at steady state"
)
Output:
speed after 3 s: 29.36 rad/s current: 9.320 A
electrical power in 223.68 W = copper + bearing + fan 223.68 W at steady state
The motor is a composition of physical primitives. The compiler sees the physical structure rather than relying on a special-case motor equation. Quantities such as:
motor.bearing.power
supply.power
fan.torque
are generated as model outputs alongside the states.
Control laws
Some engineering relationships are not constant inputs. A converter may operate at constant power, a valve may depend on pressure, a thermostat may depend on temperature, or a controller may depend on the current state. Otwin lets you express these relationships using the model's own quantities.
import otwin
from otwin.expr import maximum
from otwin.components.catalogue import water_tank
model = otwin.compile(
water_tank(level=2.0)
)
level = model.symbol("tank.level")
run = model.simulate(
t=range(0, 601),
inputs={
"inlet": maximum(
0.0,
5.0 * (1.5 - level),
)
},
)
print(
f"level {run['tank.level'][0]:.3f} m "
f"-> {run['tank.level'][-1]:.3f} m"
)
print(
f"inflow {run['inlet'][0]:.3f} "
f"-> {run['inlet'][-1]:.3f} m3/s"
)
Output:
level 2.000 m -> 1.436 m
inflow 0.000 -> 0.319 m3/s
A Python function f(t, x) is also accepted. It runs sample-and-hold at the grid rate, which is appropriate for a digital controller.
Physics + data
Not every physical mechanism is known exactly. That does not mean the entire system has to become a black box. Otwin supports a continuum between first-principles models and purely data-driven models.
| White box | The equations and parameters come from known physical relationships. | |
| Grey box | Physics defines the structure; data identifies unknown parameters or mechanisms. | |
| Black box | The model is determined primarily from data without an explicit physical structure. |
The compiled physical model is the white-box model foundation. Otwin lets data fill the gaps without throwing that structure away.
---
config:
layout: fixed
theme: neo
fontFamily: '''Open Sans Variable'', sans-serif'
themeVariables:
fontFamily: '''Open Sans Variable'', sans-serif'
---
%%{init: {"flowchart": {"nodeSpacing": 12, "rankSpacing": 18, "padding": 6, "curve": "linear"}, "themeVariables": {"fontSize": "13px"}}}%%
flowchart LR
A["Known physics"] --> D["Physical model"]
B["Unknown parameters"] --> E["Parameter estimation"]
C["Unknown phenomena"] --> F["Residual learning"]
D --> G["Hybrid Digital Twin"]
E --> G
F --> G
G --> H["Prediction + uncertainty"]
This means you can start from physical knowledge and progressively incorporate measurements without abandoning the model you understand.
Estimate the missing physics
Suppose an oscillator has quadratic drag that the original model does not include. The physical structure is still useful. Add a residual term and estimate its coefficient from measurements:
import numpy as np
import otwin
from otwin.components.catalogue import (
mass_spring_damper,
)
physics = otwin.compile(
mass_spring_damper(
m=1.0,
k=2.0,
c=0.3,
position=1.0,
)
)
v = physics.symbol("mass.velocity")
plant = physics.with_residual(
{
"mass.momentum":
-0.9 * v * abs(v)
}
)
t = np.linspace(0, 8, 161)
measured = (
plant.simulate(t=t)["spring.extension"]
+ np.random.default_rng(0).normal(
0,
1e-3,
t.size,
)
)
grey = physics.with_residual(
{
"mass.momentum":
-physics.parameter("drag")
* v
* abs(v)
},
parameters={
"drag": 0.1
},
)
fit = otwin.fit_parameters(
grey,
t,
{"spring.extension": measured},
["drag", "spring.stiffness"],
)
print(fit)
Output:
FitResult(
drag=0.900957,
spring.stiffness=2.00025;
cost=7.374e-05,
evaluations=21,
identified={
'drag': True,
'spring.stiffness': True
}
)
fit.identifiability also reports whether the fitted parameters are actually supported by the available data (collinearity, sufficient data span, stability under bootstrap resampling). A fitted number is not automatically an identified parameter.
Hybrid models
When the physics leaves something unexplained, do not automatically replace the physics with a black box. You can model the residual.
otwin.HybridModel(
physics,
residual,
)
The residual can be learned with a Gaussian process, neural network or another callable model. The physical model remains the structural prior.
---
config:
layout: fixed
theme: neo
fontFamily: '''Open Sans Variable'', sans-serif'
themeVariables:
fontFamily: '''Open Sans Variable'', sans-serif'
---
%%{init: {"flowchart": {"nodeSpacing": 12, "rankSpacing": 18, "padding": 6, "curve": "linear"}, "themeVariables": {"fontSize": "13px"}}}%%
flowchart LR
A["Known physics"] --> C["Physical model"]
B["Observed data"] --> D["Residual model"]
C --> E["Hybrid Digital Twin"]
D --> E
E --> F["Prediction"]
E --> G["Uncertainty"]
otwin.hybrid.residual_data provides the training target representing what the physical model leaves unexplained.
Custom dynamics
When a system is outside the component library, you can still use the rest of the Otwin stack.
otwin.CustomDynamics(
f,
n_states,
n_inputs,
)
The resulting model follows the same core interface:
simulate()
step()
rhs()
observe()
This provides a path for models that do not yet have a native component representation while keeping them compatible with estimation and forecasting workflows.
From model to Digital Twin
A simulation model describes a system. A Digital Twin represents a specific physical asset and keeps its model connected to observations of that asset.
---
config:
layout: fixed
theme: neo
fontFamily: '''Open Sans Variable'', sans-serif'
themeVariables:
fontFamily: '''Open Sans Variable'', sans-serif'
---
%%{init: {"flowchart": {"nodeSpacing": 12, "rankSpacing": 18, "padding": 6, "curve": "linear"}, "themeVariables": {"fontSize": "13px"}}}%%
flowchart TD
A["Physical asset"]
B["Measurements"]
C["Physics model"]
D["State estimation"]
E["Parameter calibration"]
F["Physics + data"]
G["Forecast"]
H["Uncertainty"]
I["Validation"]
J["Validated Digital Twin"]
K["Decision"]
L["Refusal"]
A --> B
B --> D
C --> D
D --> E
E --> F
F --> G
G --> H
H --> I
I --> J
J --> K
J --> L
The model can therefore move through a lifecycle:
physical knowledge
↓
physical model
↓
compiled dynamics
↓
measurements
↓
state estimation
↓
parameter calibration
↓
physics + data
↓
forecast
↓
uncertainty
↓
validation
↓
Digital Twin
The goal is not merely to produce a number, is to produce a number with evidence about why the number should be trusted.
State estimation
A compiled model is a TwinModel. Measurements can be mapped onto model quantities and used to estimate hidden states.
| Estimator | Use it when |
|---|---|
ExtendedKalmanFilter |
Nonlinear model with approximately Gaussian measurement noise |
MovingHorizonEstimator |
The state has physical bounds |
EnergyConsistentObserver |
State corrections must respect the energy balance |
For example, a bounded state should not become 1.05 simply because a noisy sensor suggested it. The moving-horizon estimator can enforce physical state bounds. The energy-consistent observer constrains corrections so they cannot introduce energy that the physical ports did not supply.
Uncertainty
An uncertainty interval should mean something measurable. If a model reports a 90% prediction interval, its coverage should be evaluated against out-of-sample observations. Otwin calibrates uncertainty from rolling-origin forecast errors rather than simply using in-sample residuals.
import numpy as np
from otwin.forecast import (
rolling_origin_residuals,
horizon_conformal,
)
rng = np.random.default_rng(0)
cycles = np.arange(300)
capacity = (
1.0
- 2.6e-4 * cycles
- 4.0e-3 * np.sqrt(cycles)
+ rng.normal(0, 1.5e-3, 300)
)
class FadeLaw:
"""Fits C = C0 - a*n - b*sqrt(n) to history."""
def forecast(self, history, horizon):
h = np.asarray(history, float).ravel()
n = np.arange(len(h))
coef, *_ = np.linalg.lstsq(
np.column_stack([
np.ones_like(n),
n,
np.sqrt(n),
]),
h,
rcond=None,
)
f = np.arange(
len(h),
len(h) + horizon,
)
return (
np.column_stack([
np.ones_like(f),
f,
np.sqrt(f),
])
@ coef
).reshape(-1, 1)
train = capacity[:240]
def refit_forecast(history, horizon):
return FadeLaw().forecast(
history,
horizon,
).ravel()
residuals, horizons = rolling_origin_residuals(
refit_forecast,
train,
step=5,
max_horizon=60,
)
band = horizon_conformal(
residuals,
horizons,
level=0.90,
max_horizon=60,
)
lower, upper = band.apply(
refit_forecast(train, 60)
)
truth = capacity[240:300]
print(
f"{residuals.size} residuals "
f"over horizons 1..{horizons.max()}"
)
print(
f"half-width "
f"{(upper[0]-lower[0])/2:.4f} at h=1, "
f"{(upper[-1]-lower[-1])/2:.4f} at h=60"
)
print(
f"measured coverage: "
f"{np.mean((truth >= lower) & (truth <= upper)):.0%} "
f"(target 90%)"
)
Output:
1590 residuals over horizons 1..60
half-width 0.0023 at h=1, 0.0030 at h=60
measured coverage: 90% (target 90%)
When the available calibration data is insufficient to support the requested confidence level, Otwin does not silently manufacture a narrow interval. It returns an interval that reflects the lack of evidence.
Validation
A model is not validated because it fits historical data. Its predictions must be evaluated out of sample. Otwin provides rolling-origin evaluation against meaningful baseline forecasts:
from otwin.forecast import evaluate
print(
evaluate(
FadeLaw(),
capacity.reshape(-1, 1),
protocol="rolling_origin",
n_folds=5,
horizon=30,
)
)
Example output:
EvalReport (rolling_origin, 5 folds)
──────────────────────────────────────────────────────────────
Skill Score (vs best baseline): 0.77 (77% better)
Baseline: persistence
Point Metrics:
RMSE 0.0017 (baseline: 0.0074)
MAE 0.0014 (baseline: 0.0064)
NRMSE 0.1141
MASE 0.8292
THEIL_U 0.2395
──────────────────────────────────────────────────────────────
The protocol is designed to avoid common forecasting mistakes:
- treating in-sample residuals as forecast errors;
- leaking information from test data;
- evaluating without a reference forecast;
- allowing future observations into the model.
A model should have to earn its predictive claim.
Identifiability
A fitted parameter is not necessarily a determined parameter. Otwin checks parameter identifiability through:
- collinearity — whether another parameter can reproduce the same sensitivity;
- span — whether the data covers enough of the relevant dynamics;
- stability — whether bootstrap resampling produces consistent estimates.
fit_parameters records these results.
This information can then become part of the model manifest and validation envelope.
Validity envelopes
A Digital Twin should know the boundary of its evidence. A model validated for one operating region is not automatically validated everywhere. A model validated for 60 forecast cycles is not automatically validated for 180. model.manifest() creates a TwinManifest describing the model and its evidence:
model.manifest()
The manifest records information such as:
- model structure;
- parameter values;
- estimated parameters;
- validation information;
- uncertainty calibration.
otwin.advise.Envelope can then turn that evidence into an answer or a refusal.
For example:
outside the validated envelope:
- horizon: beyond the validated forecast horizon
asked for 180
validated to 60
This is a refusal, not a failure.
The twin has not been shown to answer this question.
Returning a number anyway would hide that limitation.
The Digital Twin that knows when to say "no"
This is one of the central ideas in Otwin.
---
config:
layout: fixed
theme: neo
fontFamily: '''Open Sans Variable'', sans-serif'
themeVariables:
fontFamily: '''Open Sans Variable'', sans-serif'
---
%%{init: {"flowchart": {"nodeSpacing": 12, "rankSpacing": 18, "padding": 6, "curve": "linear"}, "themeVariables": {"fontSize": "13px"}}}%%
flowchart LR
A["Question"] --> B{"Inside validated envelope?"}
B -->|"Yes"| C["Answer"]
B -->|"No"| D["Refuse"]
C --> E["Prediction + evidence"]
D --> F["Explain which boundary was exceeded"]
A model should not become more certain simply because someone asks it a harder question. Outside the evidence established during validation, the correct behaviour may be to report the limitation.
Refusal is not a failure. It is evidence that the model is enforcing the limits of what has actually been demonstrated.**
What makes Otwin different?
Otwin brings several ideas together in one modeling stack.
- A physical model is executable. Components and connections are not just documentation. They compile into dynamics that can be simulated.
- Physics is structural. Energy storage, interconnection and dissipation are represented explicitly.
- The compiler understands the model. Physical topology can be checked before numerical integration.
- Multiple domains can be composed. Electrical, mechanical, rotational, hydraulic and thermal systems can be connected through common modeling concepts.
- Data does not have to replace physics. Unknown parameters and residual phenomena can be identified from measurements without throwing away known structure.
- Simulation and Digital Twin operation share the same model. The compiled model can support simulation, estimation, calibration, forecasting and validation.
- Predictions carry evidence. Validation, identifiability and uncertainty calibration become part of the model lifecycle.
- The specification can be independent of the implementation.
otwin-specprovides a normative specification and conformance suite that can be used to test implementations independently.
Examples
Two scripts start from a maintenance problem, build the system from its data sheet, and end with a number checked by hand. Neither contains an equation.
| Script | The problem | What decides it |
|---|---|---|
| The battery module that runs hot | A module reads a few degrees more than at commissioning. Aged cells, or a clogged air filter? | The voltage jump when the current reverses: only aged cells move it. |
| The pump that asks for more every month | The drive is turned up month after month to hold the flow through a fouling filter. | The month at which the extra pumping energy has paid for a cleaning. |
The notebooks are designed around engineering questions rather than isolated API demonstrations. Each is intended to show a complete idea and something that can be tested or deliberately broken.
| # | Notebook | Question | Data |
|---|---|---|---|
| 01 | A model that cannot invent energy | How do you know the equations obey the physical structure? | Simulation |
| 02 | When the process makes entropy | How do you represent a system that must obey the second law? | Simulation |
| 03 | Scoring a forecast so it cannot flatter you | How good is a forecast when evaluated out of sample? | NASA PCoE |
| 04 | A band whose 90% means 90% | How do you calibrate a prediction interval? | NASA PCoE |
| 05 | From a noisy sensor to a state you can trust | How do you estimate a physical state from imperfect measurements? | NASA PCoE |
| 06 | The twin that says no | What should a twin do outside its validation envelope? | Simulation |
| 07 | All of it, on eight years of field data | Does the protocol hold on real systems? | RWTH field data |
| 08 | Does the physics earn its place? | Does structured physics improve the prediction? | RWTH field data |
The complete battery workflow is also available as:
examples/bess_end_to_end.py
It demonstrates the complete chain from a modeled battery system to estimation, forecasting, uncertainty and refusal.
The Otwin ecosystem
Otwin is designed as an open-source ecosystem rather than a single package.
| Repository | Role |
|---|---|
| otwin | Python framework, model compiler and physics engine |
| otwin-spec | Normative specification and language-agnostic conformance suite |
| otwin-hybrid | Worked hybrid modeling case across Python, Julia and R |
| otwin-systems | Growing catalogue of physical systems with reference results |
The engine in this repository is split into Rust crates:
crates/
├── otwin-core
└── otwin-engine
otwin-core is a Rust crate containing the core runtime logic. otwin-engine provides the Python bindings through PyO3 and is published as the otwin-engine wheel.
See docs/developer for architecture and development documentation.
Architecture at a glance
---
config:
layout: fixed
theme: neo
fontFamily: '''Open Sans Variable'', sans-serif'
themeVariables:
fontFamily: '''Open Sans Variable'', sans-serif'
---
%%{init: {"flowchart": {"nodeSpacing": 12, "rankSpacing": 18, "padding": 6, "curve": "linear"}, "themeVariables": {"fontSize": "13px"}}}%%
flowchart TD
P["Python API"]
P --> S["System & components"]
P --> M["Model estimation"]
P --> F["Forecasting"]
P --> V["Validation"]
S --> C["Otwin compiler"]
C --> IR["Physical System IR"]
IR --> PHS["Port-Hamiltonian IR"]
PHS --> R["Rust core"]
R --> E["otwin-engine"]
E --> T["TwinModel"]
T --> SIM["Simulation"]
T --> EST["State estimation"]
T --> PRE["Prediction"]
T --> VAL["Validation"]
SIM --> DT["Digital Twin"]
EST --> DT
PRE --> DT
VAL --> DT
The separation is deliberate:
Python
│
│ describes and operates
▼
Physical model
│
▼
Compiler
│
▼
Intermediate representations
│
▼
Rust runtime
│
│ executes
▼
Compiled dynamics
The same architecture also makes it possible to separate the implementation from the specification:
OTWIN SPEC
│
┌───────────┼───────────┐
│ │ │
Reference Tests Manifest
semantics fixtures schema
│ │ │
└───────────┼───────────┘
│
CONFORMANCE
│
┌────────┼────────┐
│ │ │
Python Julia MATLAB
Design principles
| Principle | Description |
|---|---|
| Physics first | If a physical relationship is known, encode it explicitly. |
| Structure matters | Energy, dissipation, interconnection and conservation laws should be properties of the model whenever possible. |
| Compile the model | The physical description should become executable dynamics, not remain a diagram or collection of equations. |
| Check before solving | Physically invalid or ambiguous topologies should be detected before numerical simulation. |
| Data fills the gaps | Use measurements to identify parameters and residual physics instead of discarding known structure. |
| One model, multiple workflows | Simulation, estimation, forecasting and validation should operate on the same model representation. |
| Validation is part of the model | A prediction should carry evidence about how it was evaluated. |
| Uncertainty must be measured | A confidence level without measured coverage is not enough. |
| Specifications matter | A physical claim should be testable independently of one implementation. |
| Know when not to answer | A Digital Twin should expose the limits of its evidence rather than hide them. |
Contributing
Contributions are welcome.
A new physical component is one of the easiest places to start:
- define the component;
- define its ports;
- define its parameters;
- define its constitutive law;
- provide a closed-form or reference result where possible;
- add the corresponding tests;
- consider whether the behaviour belongs in the specification and conformance suite.
See CONTRIBUTING.md.
For specification work, see otwin-spec.
Roadmap
Where this is going, phase by phase, and what is done: ROADMAP.md.
Discussions and issues
Questions, ideas about how systems should be written, components that should exist, and things you have built go to Discussions. Start with Welcome to Otwin.
Bugs and agreed, concrete work go to Issues.
Citing
Each repository in the Otwin project includes a CITATION.cff.
If you use Otwin in research, engineering work or publications, please cite the relevant repository.
License
Otwin is released under the Apache License 2.0.
See LICENSE for the full license text.
Metadata
Release files for otwin 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| otwin-2.0.0.tar.gz | 1.4 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| otwin-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.6 MB
Release files / otwin-2.0.0.tar.gz
| Download URL | otwin-2.0.0.tar.gz |
|---|---|
| Size | 1.4 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
230d9c5ae48fd6d50e30d5276ccb627c3e49678f7530d26b347cbd10b548e7a9
|
|
BLAKE2b-256 checksum How to use checksums |
97ff5d1b0fea8bef25a1cbcbaf3875a222ba208b56a88e492c3614f993bd7200
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency logRelease files / otwin-2.0.0-py3-none-any.whl
| Download URL | otwin-2.0.0-py3-none-any.whl |
|---|---|
| Size | 266.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8d1fa21697458dfbd21d55b03ce5a521df79dbb1c2def6845bed139b49ef0a41
|
|
BLAKE2b-256 checksum How to use checksums |
8e652f0312abca7b0ee5f1fd10dd5f0a054d9d02fb8cad3801504d5b64244237
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency log