Cyber-Physical

A digital twin nobody tests against hardware is a drawing

Simulations agree with themselves. The interesting question is where the model and the board disagree, and how fast you find out.

Code Research Lab Research Team 6 min read

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.

The lesson Before you trust a residual, prove your two clocks agree. We now inject a known square wave through both paths at the start of every session and solve for the offset. It takes eleven seconds and it has saved us weeks.

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.

Code Research Lab Research Team All posts
Keep reading
Agentic AI

Why long-horizon agents fail at hour three

Nothing dramatic happens. The agent keeps producing plausible output, the tests keep passing, and the work quietly stops being about the task.

28 July 2026 · 7 min