Skip to content

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

Open
baggepinnen wants to merge 1 commit into
mainfrom
mpc/nonuniform-grid-5ms
Open

The multirate MPC at 5 ms over a non-uniform shooting grid#26
baggepinnen wants to merge 1 commit into
mainfrom
mpc/nonuniform-grid-5ms

Conversation

@baggepinnen

Copy link
Copy Markdown
Collaborator

FurutaMPCMultirate now solves at 5 ms over 50 shooting intervals that cover 0.8 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.8 s costs 50 decision variables instead of the 160 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 the 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:

Ts / Np / horizon grid catch median / 90 % [s] solve median / 99 % [ms] status != 0
8 ms / 75 / 0.60 s (before) uniform 1.04 / 1.64 1.23 / 2.78 0 of 62 500
5 ms / 50 / 0.80 s (now) linear, 5 -> 27 ms 0.36 / 1.36 0.82 / 1.64 2 of 100 000
5 ms / 50 / 0.80 s, integrator_stages = 4 linear 0.48 / 1.25 1.36 / 2.40 5 of 100 000
5 ms / 50 / 0.25 s uniform 0.35 / 0.55 0.83 / 1.09 0 of 100 000

and 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 / 50 / 0.80 s, linear 0.97 / 1.47 / 1.92 0.82 / 1.95 295 of 100 000
5 ms / 50 / 0.25 s, uniform 0.75 / 1.07 / 1.28 0.83 / 1.46 175 of 100 000

The computational budget is comfortable: 0.82 ms of a 5 ms period at the median and 1.64 ms at the
99th percentile, 17 % utilization against the 15 % the 8 ms controller had, with the same 0.2 % of
solves running over the period as before (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.

One result worth deciding on: the 0.8 s horizon is not what the improvement comes from. A
uniform 0.25 s grid at the same Ts and Np catches sooner from both start sets (0.75 s against
0.97 s at the median from random starts, 1.07 against 1.47 at the 90th percentile, 1.28 against
1.92 in the worst rollout), returns a nonzero acados status half as often, and costs the same. The
same ordering holds on the perturbed plant, where a long horizon should help most: 0.86 / 1.25 /
1.55 s against 1.14 / 2.07 / 3.68 s. 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
both dilutes that with stage weights of up to 5.4 and pushes the terminal LQR cost 0.8 s out. The
0.8 s horizon asked for is what this branch sets; horizon = Np * Ts is the whole change to the
other, and NOTES.md records the comparison.

Two smaller findings, both in NOTES.md:

  • The integration error over the longest interval does not matter in closed loop. One step of
    the 2-stage scheme over 27 ms is off by up to 4.1 rad/s against a fine reference (0.034 rad/s
    over 5 ms). Buying that back with integrator_stages = 4 costs 65 % more solve time and swings
    up no better, so the default stays 2.
  • qp_cond_N = 5 survives the smaller horizon. Re-swept at Np = 50: 3 to 10 are within a few
    percent, 1, 2 and the uncondensed horizon are worse -- the same shape as at Np = 60.

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.

🤖 Generated with Claude Code

https://claude.ai/code/session_013damsLv5TqkpHHcMi8RdKT

`FurutaMPCMultirate` solved at 8 ms over 75 uniform shooting intervals,
because the period and the horizon were one number: 0.6 s 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 50 intervals
covering 0.8 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.82 ms at the median and
from 2.78 to 1.64 ms at the 99th percentile of a period that is itself
3 ms shorter, and the catch from 1.04 to 0.36 s from rest and from 1.14
to 0.97 s from the random operating space.

The horizon is not what pays for that: a uniform 0.25 s grid at the same
`Ts` and `Np` catches sooner still (0.75 s from random starts, 1.07 s at
the 90th percentile against 1.47) and halves the nonzero acados
statuses, on the identified and on the perturbed plant alike.
`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 with stage weights of up to 5.4 while
pushing the terminal LQR cost 0.8 s out. NOTES.md records the
comparison, so that `horizon = Np * Ts` is one word away.

Two smaller measurements, also in NOTES.md: one step of the 2-stage
integrator over the longest interval is off by up to 4.1 rad/s against a
fine reference (0.034 over the shortest), and buying that back with
`integrator_stages = 4` costs 65 % more solve time and swings up no
better; and `qp_cond_N = 5` survives the re-sweep at `Np = 50`, where 3
to 10 are again within a few percent and 1, 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 263e119 to bb0a49d Compare September 11, 2026 10:51
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