Repository navigation
Description
Task Brief — DeadbeatControl in numerical-toolbox
Target repository: numerical-toolbox (this brief is written for an agent working inside that
repo; e-foc is the downstream consumer and is out of scope).
Goal: implement a generic discrete-time deadbeat (plant-inversion) controller so downstream
projects stop hand-rolling it.
Authoritative conventions: AGENTS.md at the toolbox root. This brief does not restate it —
follow it. The points below are only the ones where this algorithm has a specific choice to make.
1. Why
Deadbeat control is the only mainstream discrete controller family the toolbox lacks. It already
ships LQR, LQI, LQG, MPC, Luenberger, sliding-mode, ADRC and backstepping. The single existing
mention of "deadbeat" is a caveat in doc/controllers/LuenbergerObserver.md about deadbeat
observer pole placement — that is state estimation, not control, and is not an implementation.
Downstream, e-foc currently hand-rolls a scalar deadbeat current controller because no toolbox type
exists. Its two sibling algorithms both come from the toolbox (PidIncremental,
SlidingModeControl), so the local implementation is an inconsistency to be removed.
2. Mathematical foundation
Discrete LTI plant:
One-step law — set
For
when the reference lies in the reachable subspace. See §3 on how to handle this.
N-step law — reaching the reference in
amplification. For the SISO case this generalises to:
with
practice, because the one-step law amplifies measurement noise by
large whenever the input gain is small.
Reachability: the law requires the pair
unreachable plant makes the inversion singular. control_analysis::ControllabilityObservability
already provides IsControllable; reuse it, do not reimplement a rank test.
Settling: exactly
error
is the algorithm's defining weakness and must be stated in the doc.
3. Decisions to make (state your choice in the PR description)
These are genuine design choices, not oversights. Pick one and justify it briefly.
| # | Question | Options |
|---|---|---|
| 1 | Support general MIMO, or restrict to InputSize == 1? |
Restricting keeps the solve well-posed and covers the known consumers. Generalising requires a pseudo-inverse and a documented reachable-subspace contract. |
| 2 | Is |
Template = gains foldable at compile time, no runtime branch. Constructor = reconfigurable without recompiling. |
| 3 | Fail-closed behaviour on an unreachable or singular plant | Options: really_assert, a factory returning std::optional, or an inert zero-gain instance. The third is what e-foc needed downstream — an unconfigured controller must output exactly zero, never a fallback control law. |
| 4 | Does the type own output saturation? | The toolbox already has SaturationRateLimiter; composing is probably better than duplicating. |
Prefer the option that keeps ComputeControl free of branches and divisions — the intended callers
run it in a 20 kHz ISR.
4. Suggested API
Follow the shape of Lqr — precompute everything in the constructor, leave only the gain
application in the hot path.
- Inherit
controllers::StateFeedbackController<T, StateSize, InputSize>so the type composes with
the existingGainScheduledController/Feedforward2Dofwrappers. - Constructor overloads mirroring
Lqr: one takingA/B, one taking
math::LinearTimeInvariant, and one taking a precomputed gain. - Reference tracking needs a two-argument entry point; the base class only declares
ComputeControl(state). Decide whether the reference is a constructor/setter value or a second
ComputeControlargument —SlidingModeControlalready established the two-argument overload
precedent. - Expose the precomputed gains through a
[[nodiscard]]getter so tests can assert on them
directly rather than only through closed-loop behaviour.
Reuse, do not reimplement: math::LinearTimeInvariant, solvers::SolveSystem
(numerical/solvers/GaussianElimination.hpp), control_analysis::ControllabilityObservability.
5. Deliverables
| File | Notes |
|---|---|
numerical/controllers/implementations/DeadbeatControl.hpp |
Implementation |
numerical/controllers/implementations/DeadbeatControl.cpp |
Explicit instantiations, coverage build only |
numerical/controllers/implementations/test/TestDeadbeatControl.cpp |
Tests |
numerical/controllers/implementations/CMakeLists.txt |
Add to target_sources and numerical_add_coverage_sources |
numerical/controllers/implementations/test/CMakeLists.txt |
Add test source |
doc/controllers/DeadbeatControl.md |
Per doc/TEMPLATE.md |
doc/controllers/README.md |
Add the algorithm row |
Float-only per AGENTS.md — generic template<typename T> with
static_assert(std::is_floating_point_v<T>, ...), but instantiate and test float only. Do not
add Q15/Q31. Note that Lqr.cpp does instantiate them; that is legacy and is not the pattern to
copy here.
6. Test cases
One behaviour per test, TEST_F on float, EXPECT_NEAR with math::Tolerance<float>(). Tests
call std:: cmath directly, never math::.
- First-order plant reaches the reference in exactly one step (simulate the plant forward and assert
the state, not just the gain). - Two-step variant reaches the reference in exactly two steps.
- Two-step input gain is strictly smaller than one-step — this is the noise-amplification claim, and
it is the reason the variant exists. - Zero error at the reference still commands the input needed to hold it against plant decay
(a deadbeat controller is not a pure error regulator; this catches a sign/structure error that
the other tests pass through). - Computed gain matches the closed-form
$1/B_d$ and$A_d/B_d$ for a known scalar plant. - Whatever fail-closed behaviour decision feat: add clarke and park transforms #3 selects, asserted directly.
- If MIMO is supported: a reachable 2-state / 1-input plant converges in 2 steps.
No redundant tests. Coverage ≥ 90 %.
7. Documentation
doc/controllers/DeadbeatControl.md per doc/TEMPLATE.md — math, theory, complexity, pitfalls.
No code, no class names, no usage examples.
Must cover:
- Both the one-step and N-step laws, with the noise-amplification tradeoff quantified.
- The reachability precondition and what happens when it fails.
- Sensitivity to model error: a relative error in
$B_d$ maps to a proportional residual after
settling. Deadbeat is the least robust controller in the library and the doc must say so plainly,
alongside the pointer that sliding-mode is the robust alternative for the same plant. - Behaviour under input saturation: settling extends beyond
$N$ samples but remains time-optimal for
the available authority.
8. Acceptance criteria
cmake --preset host && cmake --build --preset hostclean, no new warningsctest --preset hostgreen- Coverage ≥ 90 % on the new files
.clang-formatclean- Decisions from §3 stated in the PR description
9. Downstream context (informational — do not implement here)
e-foc will adopt this in DeadbeatCurrentController, which controls the d- and q-axis currents of a
PMSM. Its plant is a scalar RL circuit per axis, discretized as
20 kHz in an ISR with a budget of roughly 400 cycles for the entire FOC chain, and it needs the
one-step/two-step variant choice exposed at runtime.
Be aware of the likely outcome: after precomputing gains, the scalar case reduces to two multiply-
accumulates, so e-foc may well keep its specialised version regardless. This type should earn its
place as a general library algorithm — do not contort the API to fit that one consumer.
Activity