Skip to content

A pendulum from a confounded log

Pendulum-v1 raised 0.30 rad from hanging and held there, planned by prescribe from a log whose torque was confounded. While logging, an operator cancelled half of the wind torque it measured, u = -k w + e, so the logged torque moves with a disturbance that enters omega' exactly where the torque does: the confounding sits on the control channel itself.

d(theta)/dt = omega
d(omega)/dt = 15 sin(theta) + 3 (u + w)

Gravity is first principles and goes to prescribe as known=, so what the log has to identify is the actuator, the 3 on the torque. Fitted without the wind, that channel is b (k(k-1) s_w^2 + s_e^2) / (k^2 s_w^2 + s_e^2) for white wind, which is negative for an operator who cancels half of it: the log teaches that torque pushes backwards.

uv run python scripts/pendulum_demo.py

What it printed when this site was built:

== Pendulum-v1, logged while an operator cancelled half the measured wind; true torque channel 3.000; computed in float32 ==

task: from hanging, raise the pendulum 0.30 rad and hold it for 40 steps of 0.05 s, |torque| <= 2.0, |omega| <= 1.0 rad/s held as the barrier

| reading | identification | adjusted for | torque channel | trusted steps | final angle error | rms angle error | peak omega |
|---|---|---|---|---|---|---|---|
| adjusted | identified | wind | +3.001 | 1 | +0.006 | 0.141 | 0.464 |
| asserted | asserted | nothing | -1.090 | 0 | +0.607 | 0.667 | 0.849 |
| latent | not_identified | - | - | 0 | no schedule | - | - |
| none | - | - | - | - | +0.300 | 0.300 | 0.000 |

Adjusted for the wind, the torque channel is +3.001 against 3.000, and its schedule, run on the true pendulum, ends 0.006 rad from the target.
Asserting that nothing needs adjusting returns -1.090 -- the white-wind formula predicts -1.052 -- so the plan pushes the wrong way and ends +0.607 rad from the target, 2.0x as far as never acting.
The certificate trusts the adjusted schedule for its first 1 steps and the asserted one for 0: a channel with no standard error bounds no trajectory, whatever the barrier says. Declared latent, the wind leaves nothing to adjust for, and there is no schedule to trust.

The rows are the same planner reading the same log through different causal claims, every schedule run on the true pendulum:

  • adjusted — the graph derives the adjustment set {wind};
  • asserted — an empty set asserted, which is what fitting the log as it stands amounts to;
  • latent — the same graph with the wind declared unobserved: nothing identifies the channel, and prescribe returns no schedule at all;
  • none — never acting, the reference the angle errors are read against.

Scope, and it bounds what the numbers mean:

  • The schedule is open loop on a frictionless plant, and it lands because the fitted model is right. Deployed, it would be re-planned from each measured state (chc.RecedingHorizon).
  • |omega| <= 1 is held in the solve as the barrier, at the library's class-K gain of 1 per second, so what binds is the approach to the speed limit, not the limit.
  • The certificate's trusted prefix is short. Its tube compounds at the norm of the hanging pendulum's slope, 15 per second, a local rate, since gravity bends the field away from where the plan starts. It is fed the channel's standard error at the states the log visited, and the plan's one-step error on the log, which it holds with probability 0.95 at each step. The run on the true plant shows the rest of the schedule holding; the certificate does not claim it.

Code: scripts/pendulum_demo.py, through prescribe.