docs/research: tempo fallback PID plant, sweep and chosen gains
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a40be6201b
commit
aecfab1d7c
1 changed files with 111 additions and 0 deletions
111
docs/research/2026-11-01-tempo-pid-sim.md
Normal file
111
docs/research/2026-11-01-tempo-pid-sim.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# Tempo fallback PID: plant, sweep, chosen gains (2026-11-01)
|
||||
|
||||
Rulings: `tempo-default-is-usage-fitter`, `tempo-fallback-pid-tuned-by-sim`,
|
||||
`minds-control-burn-democratically`, `tempo-silence-is-abstention`. The
|
||||
controller is `tempo::control` in `crates/mind-body/src/tempo.rs`; `fallback_beat`
|
||||
is it with the committed `GAINS`. The plant is `crates/mind-body/src/tempo/sim.rs`
|
||||
(test-only). Nothing is wired into the heartbeat yet.
|
||||
|
||||
## Control law
|
||||
|
||||
Actuator: the turn rate `u` in beats per hour (usage per hour is roughly linear
|
||||
in `u`; the beat is not). `target(now) = target_permille x elapsed / 7 d`,
|
||||
`e = target(now) - used` (permille, positive means behind).
|
||||
`u = 3600/900 + kp e + ki I - kd d(used)/dt`, `beat = clamp(round(3600/u), 60, 3600)`
|
||||
seconds, `u <= 0` gives 3600. Derivative on the measurement and only between
|
||||
distinct observations; conditional integration (no integrating when the output is
|
||||
at a limit and `e` pushes further into it); while a vote decides, `I` tracks the
|
||||
voted rate so release is bumpless; a new `resets_at` clears `I` and the derivative
|
||||
memory; no reading, an expired window or a reading older than its window holds the
|
||||
last beat and integrates nothing (time spent holding is not integrated later).
|
||||
|
||||
## Plant
|
||||
|
||||
One seven-day window, 60 s steps, `target = 900` permille. Each beat that elapses
|
||||
is one turn: cost lognormal with mean `c` and sigma(ln) 0.5; every 25th turn is a
|
||||
Self run at 5 x. Per-turn cost range used: `c` in 0.6 (cheap) .. 1.3 (base) ..
|
||||
3.0 (dear) permille; the 900 s bias beat reaches 90% at about `c` = 1.34. Usage
|
||||
saturates at 1000. Sensor: `Stream` refreshes the reading when a turn ends,
|
||||
`Poll` every 15 minutes; readings are rounded to 1 permille; a gap shows no
|
||||
reading. Three seeds per scenario and sensor.
|
||||
|
||||
Scenarios: quiet (c 1.3); cheap-turns (0.6); dear-turns (3.0); burst (+200 permille
|
||||
over hours 48-52); cost-x3 (turn cost x3 from hour 84); vote-300s-24h (a standing
|
||||
vote of 300 s from hour 48 to 72, then release); rollover (two windows, used
|
||||
resets to 0, the sensor shows the old window until its next refresh); gap-6h (no
|
||||
reading from hour 72 to 78); start-ahead (500 permille used at the start);
|
||||
unreachable (c 0.05: even at the 60 s minimum the target cannot be reached, the
|
||||
controller must sit at 60 s over the last day).
|
||||
|
||||
A vote of 60 s for 24 h, as first sketched, costs about 1900 permille at c 1.3,
|
||||
more than the week; it is unreachable by construction, so the vote scenario uses
|
||||
300 s.
|
||||
|
||||
Metrics: used at each window's reset; worst |error| over the last day; "jumps":
|
||||
minutes where the beat moved by more than 5%, per hour, outside votes; "band
|
||||
crossings": the error going from beyond +15 to beyond -15 permille (or back) in
|
||||
the last three days. No oscillation is sustained when crossings stay at or under
|
||||
2 (one overshoot).
|
||||
|
||||
Recorded trace: not used. Each Mind's `persona-trace.cc` on Yggdrasil holds one
|
||||
`plan seven_day` line (the newest), not a series, so there is nothing to replay.
|
||||
|
||||
## Sweep
|
||||
|
||||
`kp` in {0.01, 0.02, 0.03, 0.05, 0.1}, `ki` in {0.002, 0.005, 0.01, 0.02, 0.04},
|
||||
`kd` in {0, 0.002, 0.005, 0.01, 0.02, 0.05}: 150 candidates x 10 scenarios x 2
|
||||
sensors x 3 seeds. Rerun:
|
||||
|
||||
cargo test -p mind-body --lib tempo::sim::sweep -- --ignored --nocapture
|
||||
|
||||
(on Yggdrasil through `tools/stopgap/ygg-verify.sh`). Candidates are ranked by
|
||||
`final error + 0.5 x last-day error + 20 x jumps/h + 15 x crossings`. Best fifteen:
|
||||
|
||||
| kp ki kd | worst final error | worst error, last day | worst jumps/h | worst band crossings | holds MIN when unreachable |
|
||||
|---|---|---|---|---|---|
|
||||
| 0.1 0.04 0 | 30.9 | 54.9 | 1.43 | 0 | true |
|
||||
| 0.1 0.04 0.002 | 30.9 | 54.4 | 1.49 | 0 | true |
|
||||
| 0.1 0.02 0 | 30.9 | 56.8 | 1.44 | 0 | true |
|
||||
| 0.1 0.02 0.002 | 30.9 | 56.8 | 1.54 | 0 | true |
|
||||
| 0.1 0.01 0 | 30.9 | 65.7 | 1.45 | 0 | true |
|
||||
| 0.1 0.04 0.005 | 30.9 | 54.9 | 1.82 | 0 | true |
|
||||
| 0.1 0.01 0.002 | 30.9 | 65.7 | 1.55 | 0 | true |
|
||||
| 0.1 0.02 0.005 | 30.9 | 56.8 | 1.89 | 0 | true |
|
||||
| 0.02 0.01 0 | 30.0 | 65.7 | 0.37 | 2 | true |
|
||||
| 0.1 0.01 0.005 | 30.9 | 65.7 | 1.89 | 0 | true |
|
||||
| 0.1 0.005 0 | 30.9 | 85.9 | 1.48 | 0 | true |
|
||||
| 0.01 0.02 0 | 30.9 | 54.4 | 0.07 | 3 | true |
|
||||
| 0.01 0.04 0 | 29.6 | 56.7 | 0.10 | 3 | true |
|
||||
| 0.1 0.005 0.002 | 30.9 | 85.9 | 1.55 | 0 | true |
|
||||
| 0.02 0.01 0.002 | 30.0 | 65.7 | 0.67 | 2 | true |
|
||||
|
||||
Chosen `GAINS = { kp: 0.02, ki: 0.01, kd: 0.0 }`, per scenario (worst over sensors
|
||||
and seeds):
|
||||
|
||||
```
|
||||
quiet final_err 5.4 worst_err_last_day 14.3 jumps/h 0.03 last_beat 1028
|
||||
cheap-turns final_err 9.3 worst_err_last_day 19.5 jumps/h 0.00 last_beat 392
|
||||
dear-turns final_err 29.6 worst_err_last_day 33.8 jumps/h 0.37 last_beat 2041
|
||||
burst final_err 14.1 worst_err_last_day 17.2 jumps/h 0.07 last_beat 876
|
||||
cost-x3 final_err 7.8 worst_err_last_day 32.5 jumps/h 0.21 last_beat 3465
|
||||
vote-300s-24h final_err 30.0 worst_err_last_day 65.7 jumps/h 0.02 last_beat 1442
|
||||
rollover final_err 7.3 worst_err_last_day 10.1 jumps/h 0.04 last_beat 875
|
||||
gap-6h final_err 8.7 worst_err_last_day 13.1 jumps/h 0.04 last_beat 1014
|
||||
start-ahead final_err 27.6 worst_err_last_day 29.8 jumps/h 0.02 last_beat 1479
|
||||
unreachable final_err 0.0 worst_err_last_day 0.0 jumps/h 0.00 last_beat 60
|
||||
```
|
||||
|
||||
The chosen row is ninth by that rank; the rank weights accuracy over chatter, and the choice trades two crossings for a quarter of the jumps. `kp` 0.1 removes the overshoot after the vote (0 crossings) but moves the
|
||||
beat on nearly every turn (1.4 jumps/h) because each turn's usage lump is read as
|
||||
error; 0.02 with `ki` 0.01 is nearly as accurate with a quarter of the chatter and
|
||||
one overshoot. `kd` helps nowhere: with 1-permille readings and about four turns an
|
||||
hour, the measured slope is mostly quantisation, and every `kd` > 0 only adds jumps
|
||||
(0.37 to 0.67 at `kd` 0.002). The committed controller is therefore a PI in
|
||||
practice; the D term stays in the law (tested with explicit gains) so a plant with
|
||||
finer readings can use it.
|
||||
|
||||
Bounds the acceptance test holds `GAINS` to: reachable scenarios end within 35
|
||||
permille of the target at every reset (30 was the first sketch; the dear-turns and
|
||||
vote scenarios sit at 29.6-30.0 and a Self run in the dear scenario is itself 15
|
||||
permille); worst last-day error at most 75; at most 2 band crossings; at most 0.5
|
||||
jumps/h; the unreachable case sits at 60 s.
|
||||
Loading…
Reference in a new issue