Skip to content

Control synthesis (LQR)

Draft

This page is scaffolded. The outline below marks what it should cover.

LQR(world, …) synthesizes a state-feedback regulator about an operating point — the third sibling transform. It regulates a controllable subset and freezes the rest.

To cover

  • The operating pointx_ref / u_ref (trim), and why a free rigid body is underactuated so full-state LQR isn't stabilizable.
  • regulate= — selecting the controllable subspace (e.g. ["drone.position", "drone.velocity"]) and freezing the remainder at the operating point.
  • Q / R cost weights — distinct from the EKF's process/measurement noise; how they shape the gain.
  • The runtime surfacelqr.control(state_dict) → {input: u}.
  • Under-actuation through attitude — how a single-thruster craft regulates position via attitude (the quadcopter demo).

Moving the operating point

A gain is solved about a point. Two ways to move it, and the difference is not cosmetic:

retarget(x_ref) moves the reference and keeps the gain. That is exact wherever the dynamics are invariant along the move — a translation of a hover setpoint under uniform gravity — and cheap enough to do every tick.

It is wrong for a heading change. The tangent error uses world-frame position and velocity, so K permanently encodes the actuator→world-force map at the attitude it was solved at. Retarget the reference heading by Δψ and every translational feedback comes out rotated by Δψ: at 90° the feedback is perpendicular to the error and the craft orbits its target; at 180° it is positive feedback and the craft accelerates away.

resolve_at is the general answer. It re-evaluates A, B at the new reference and re-runs the Riccati solve, returning an LQRSolution — gain, feed-forward and reference as plain arrays:

sol = lqr.resolve_at(x_ref={"sub": {"position": p, "orientation": q}})
ctrl.reprogram(sol)          # gain, trim and reference move together

The symbolic linearization is already compiled, so this is a matrix evaluation plus a small dense DARE — µs plus ms on a ~12-dim tangent, not a rebuild.

The law's K and u_ff are Ports, not baked constants, defaulting to the built solve. So a regulator lowered to any backend can be reprogrammed in place, and nothing changes for a caller that ignores them:

// wasm: the same triple, straight off a retarget endpoint as JSON
reg.reprogram(await (await fetch("/api/retarget", )).json());
// C++: control(x) flies the built point; the gain is an argument
auto u = lqr.control(x, ref, K, u_ff);

Two limits worth knowing. Only slots this LQR regulates can move — the frozen complement is baked into A/B as a constant, and resolve_at raises rather than silently ignore a move there. And the trim u_ref is attitude-dependent in general; solving for it is a root-solve and stays yours (pass it as u_ref=).

Source material