Every digital twin demo looks great, because a demo compares the simulation to itself. Run the model, plot the trace, note how smooth it is. Nothing in that loop can tell you the model is wrong.
Twin Bench exists because we got tired of that. It runs the simulation and the physical board against the same input, in the same wall-clock second, and reports where they diverge.
Divergence is the product
The useful output of a twin is not the predicted trajectory. It is the residual — the gap between what the model said and what the hardware did. Track that gap over time and it tells you things a clean simulation never will:
- Where your model's assumptions stop holding, expressed as an operating region rather than a hunch
- Which physical effects you left out that actually matter, ranked by how much error they contribute
- When the hardware itself has drifted — a twin that suddenly disagrees with a board it used to match is a maintenance alarm
That last one surprised us. We built the rig to validate models and it turned into a hardware diagnostic.
Getting the timing honest
The hard part is not the model. It is running both sides on the same clock without one waiting on the other.
Our first version stepped the simulation, then read the board, then compared. It produced beautiful agreement and was completely meaningless — the simulation was being handed the board's state at every step, so of course it tracked. The twin was not predicting anything. It was following.
If your twin never gets a chance to be wrong, it will never look wrong.
The rebuild runs the simulation open-loop from a single initial condition and lets it diverge. The board runs freely. A separate collector timestamps both streams against a shared monotonic clock, and comparison happens offline. The simulation gets no feedback from reality at all — that is the whole point.
Where the error actually came from
On our first honest run the residual was far larger than the model's documented tolerance. The obvious suspects were the plant model, the integration step, the sensor calibration.
It was none of them. It was timestamp skew. The board's ADC sampled on its own oscillator, our collector timestamped on arrival, and the transport jitter between them was on the same order as the dynamics we were measuring. We were comparing signals that were correct but misaligned in time, and calling the misalignment model error.
What the rig looks like now
bench = TwinBench(
plant=Simulation("motor_v4"),
device=Board("/dev/ttyACM0", rate=1000),
clock=SharedMonotonic(sync="square_wave"),
)
trace = bench.run(Input.step(amplitude=0.4), seconds=30)
print(trace.residual.rms(), trace.residual.worst_window())
Thirty seconds of data, one number that says how wrong the model is, and a window pointing at where it went wrong. That is the entire interface, and it is enough.
Next
Right now a human reads the residual and decides what it means. We want the rig to propose which model term is most likely responsible — fit the residual against a library of candidate physical effects and rank them. That is an inverse problem with a lot of ways to fool yourself, which makes it the right kind of problem for the lab.