Skip to content

The multirate MPC at 5 ms over a non-uniform shooting grid - #26

Merged
baggepinnen merged 1 commit into
mainfrom
mpc/nonuniform-grid-5ms
Sep 21, 2026
Merged

baggepinnen merged 1 commit into
mainfrom
mpc/nonuniform-grid-5ms

Conversation

@baggepinnen

@baggepinnen baggepinnen commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

FurutaMPCMultirate now solves at 5 ms over 60 shooting intervals that still cover 0.6 s, with the
encoders read and the state estimated at 1 ms as before. The interval count and the horizon are no
longer the same number: JuliaComputing/MPCComponents.jl#44 gave ACADOSMPC a time_steps
parameter, and MPCComponents.linear_time_steps(Ts, Np, horizon) lays out Np intervals that grow
from Ts to span the horizon, so 0.6 s costs 60 decision variables instead of the 120 a uniform
5 ms grid would need. The first interval stays at Ts, the one whose control is applied and held
for a clock period, and each stage's cost is weighted by its interval's length over Ts.

FurutaMPCMultirate therefore gains a horizon parameter beside Ts and Np (with
horizon = Np * Ts recovering the uniform grid) and an integrator_stages parameter, since a
stretched grid makes the integrator's order a real choice rather than a constant. The single-rate
FurutaMPC is untouched.

Measurements

test/mpc_rollouts.jl gained a multirate mode: it ticks FurutaMPCMultirateHardware against the
simulated pendulum with the QUBE's encoder quantization, reports the solving ticks against the MPC
period and the ticks in between against the sensing period, and takes its whole configuration from
key=value arguments so that two of them can be measured from the same seeds. 50 rollouts of 10 s,
identified plant, every configuration swinging up and balancing in all 50.

From rest near hanging, as the rig starts:

Ts / Np / horizon grid catch median / 90 % / max [s] solve median / 99 % [ms] status != 0
8 ms / 75 / 0.60 s (before) uniform 1.04 / 1.64 / 2.19 1.23 / 2.78 0 of 62 500
5 ms / 60 / 0.60 s (now) linear, 5 -> 15 ms 0.35 / 1.19 / 4.16 0.98 / 1.77 0 of 100 000
5 ms / 60 / 0.60 s, integrator_stages = 4 linear 0.42 / 1.03 / 7.67 1.62 / 2.77 0 of 100 000
5 ms / 60 / 0.30 s uniform 0.34 / 0.55 / 1.10 0.98 / 1.23 0 of 100 000

From the random operating space:

Ts / Np / horizon catch median / 90 % / max [s] solve median / 99 % [ms] status != 0
8 ms / 75 / 0.60 s, uniform 1.14 / 1.88 / 3.51 1.24 / 2.92 98 of 62 500
5 ms / 60 / 0.60 s, linear 0.99 / 1.74 / 5.99 0.98 / 2.21 226 of 100 000
5 ms / 60 / 0.30 s, uniform 0.80 / 1.26 / 1.56 0.98 / 1.94 176 of 100 000

The computational budget is comfortable: 0.98 ms of a 5 ms period at the median and 1.77 ms at the
99th percentile, 20 % utilization against the 15 % the 8 ms controller had, with 0.3 % of solves
running over the period (the allocation tail NOTES.md already documents). The solve does not fit a
1 ms sensing slot, as it did not before; that remains jitter on the sensing clock, and the rig's
dt and exec log columns are what measure it.

How far the grid is stretched is limited by the integrator. acados takes one step of the same
scheme per interval, so the maximum one-step error over the states a swing-up traverses, against a
finely integrated reference, is:

interval 5 ms (the first) 8 ms (the previous default, everywhere) 15 ms (the last) 27 ms
2-stage ERK 0.034 0.128 0.600 4.08
4-stage ERK 0.0010 0.0044 0.0326 0.332

in rad/s. At 0.6 s over 60 intervals the last one is 15 ms and its error is a factor of five above
what the previous default accepted on every interval; at 0.8 s over 50 it would be 27 ms and
4 rad/s, which predicts nothing. Recovering the accuracy with integrator_stages = 4 costs 66 %
more solve time and does not swing up better, so the default stays 2.

The horizon is not the source of the improvement. A uniform grid at the same Ts and Np --
half the span, 0.3 s -- still catches sooner at every quantile (0.80 against 0.99 s at the median
from random starts, 1.26 against 1.74 at the 90th percentile, 1.56 against 5.99 in the worst
rollout) and returns fewer nonzero acados statuses, at the same solve time. energy_weight = 1e5
on the pendulum's energy makes the stage cost a shaping term that plans the pump without needing to
see the catch, and stretching the grid dilutes that while pushing the terminal LQR cost twice as
far out. The horizon is kept at 0.6 s because that is what the 8 ms controller had and what the
weights were tuned against; horizon = Np * Ts is the whole change to the other, and NOTES.md
records the comparison.

qp_cond_N = 5 survives the re-sweep on this grid: 3 to 10 are within a few percent, and 2 and the
uncondensed horizon are worse, the same shape as at Np = 60 on the single-rate controller.

Also here

  • Manifest.toml and test/Manifest.toml take MPCComponents from the main that carries #44
    (the tree hash only; the environment is otherwise untouched), and Project.toml's [sources]
    note names the second pull request this package needs from that branch.
  • The clock documentation asked for a power-of-two ratio between the two periods. What the code
    needs is an integer ratio whose tick instants coincide exactly in floating point, which 5 x 1 ms
    does (m * 0.005 == 5m * 0.001 for every m); the prose in the three components and in
    src/mpc.jl says that now.
  • test/mpc_tests.jl pins the new defaults: divisors (1, 5), the MPC solving on ticks 1, 6, 11,
    16 with one motor write each.

Not covered: no rig run. The device sustaining 1 kHz reads under a 5 ms solve is a measurement on
the hardware, and the log's dt and exec columns are where it shows. Separately,
#27 tracks the run_ode! / mpc_gui route, which the multirate
model cannot use because of the upstream clock-inference bug JuliaComputing/StateSelection.jl#159.

🤖 Generated with Claude Code

https://claude.ai/code/session_013damsLv5TqkpHHcMi8RdKT

@baggepinnen
baggepinnen force-pushed the mpc/nonuniform-grid-5ms branch from 263e119 to bb0a49d Compare September 11, 2026 10:51
`FurutaMPCMultirate` solved at 8 ms over 75 uniform shooting intervals,
because the period and the horizon were one number: keeping the 0.6 s
horizon at 5 ms would have cost 120 decision variables.
JuliaComputing/MPCComponents.jl#44 separates them. `ACADOSMPC`'s
`time_steps` gives each interval its own length and
`linear_time_steps(Ts, Np, horizon)` grows them from `Ts` to span the
horizon, so the controller now solves at 5 ms over 60 intervals that still
cover 0.6 s, with the encoders read and the state estimated at 1 ms as
before. The first interval stays at `Ts`, the one whose control is applied
and held for a clock period.

`FurutaMPCMultirate` gains a `horizon` parameter beside `Ts` and `Np`
(`horizon = Np * Ts` recovers the uniform grid) and an
`integrator_stages` parameter, since a stretched grid makes the
integrator's order a real choice. The single-rate `FurutaMPC` is
unchanged.

Measured with `test/mpc_rollouts.jl`, which gains a multirate mode: it
ticks `FurutaMPCMultirateHardware` against the simulated pendulum with
the QUBE's encoder quantization, separates the solving ticks from the
ticks between them, and takes its whole configuration from `key=value`
arguments so that two of them are measured from the same seeds. 50
rollouts of 10 s balanced in all 50 at every configuration; against the
previous default the solve falls from 1.23 to 0.98 ms at the median and
from 2.78 to 1.77 ms at the 99th percentile of a period that is itself
3 ms shorter, the catch from rest from 1.04 to 0.35 s and from the random
operating space from 1.14 to 0.99 s, and the arm goes past the end stops
in 7 of 50 rest starts instead of 27.

How far the grid is stretched is limited by the integrator, which takes
one step per interval: over the last interval of this grid, 15 ms, the
2-stage scheme is off by up to 0.60 rad/s against a fine reference, where
it is off by 0.034 over the first interval and by 0.13 over the 8 ms
intervals the previous default integrated everywhere. At `horizon = 0.8`
and `Np = 50` the last interval is 27 ms and the error is 4 rad/s, which
predicts nothing. `integrator_stages = 4` removes the error but costs
66 % more solve time and does not swing up better, so the default stays 2.

The horizon itself is not the source of the improvement: a uniform grid
at the same `Ts` and `Np`, half the span, still catches sooner (0.80
against 0.99 s at the median from random starts, 1.26 against 1.74 at the
90th percentile) and returns fewer nonzero acados statuses.
`energy_weight = 1e5` on the pendulum's energy makes the stage cost a
shaping term that plans the pump without seeing the catch, and stretching
the grid dilutes it while pushing the terminal LQR cost further out. The
horizon is kept at what the 8 ms controller had and what the weights were
tuned against; NOTES.md records the comparison, so that
`horizon = Np * Ts` is one word away. `qp_cond_N = 5` survives the
re-sweep on this grid, where 3 to 10 are again within a few percent and 2
and the uncondensed horizon are again worse.

The clock documentation asked for a power-of-two ratio between the two
periods. What the code needs is an integer ratio whose tick instants
coincide exactly in floating point, which 5 x 1 ms does; the three
components and `src/mpc.jl` say that now.

Both manifests take MPCComponents from the `main` that carries #44, and
`Project.toml`'s `[sources]` note names it as the second pull request
this package needs from that branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013damsLv5TqkpHHcMi8RdKT
@baggepinnen
baggepinnen force-pushed the mpc/nonuniform-grid-5ms branch from bb0a49d to d13c92a Compare September 21, 2026 08:45
@baggepinnen
baggepinnen merged commit 9b15a43 into main Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant