Summary
Feedforward2Dof is the natural home for a two-degree-of-freedom structure, but its feedforward interface is memoryless by construction:
template<typename T>
class Feedforward
{
public:
virtual ~Feedforward() = default;
virtual T Evaluate(T reference) const = 0; // const
};
Evaluate being const, with no Reset() on the interface, rules out any feedforward that carries state. The canonical 2-DOF design — a reference pre-filter F(z) shaping tracking independently of the feedback that sets disturbance stiffness — needs exactly that.
Feedforward2Dof::Reset() reinforces the assumption:
template<typename T>
void Feedforward2Dof<T>::Reset()
{
feedback.Reset();
}
The feedforward is never reset, because it is presumed to have nothing to reset.
Why it matters
The two structures are not interchangeable:
- What the class implements:
u = clamp(F(r) + C(r - y)), with F a static map. Good for inverse-plant or gravity/friction feedforward.
- What a 2-DOF controller usually means:
u = C(F(z) r - y), with F(z) dynamic. This is what decouples command tracking from load stiffness, and it is the form described in the standard references (Astrom & Hagglund, Advanced PID Control).
A caller wanting the second form has three options today, all bad: mutable state behind a const method, a const_cast, or bypassing Feedforward2Dof altogether.
Suggested direction
- Drop
const from Evaluate.
- Add
virtual void Reset() to Feedforward (defaulted to a no-op so existing static implementations are unaffected), and call it from Feedforward2Dof::Reset().
Both are source-compatible for memoryless feedforwards, which appear to be the only ones in the tree today.
If the intent is that Feedforward2Dof only ever models the static-feedforward structure, then the naming and documentation should say so, since "2-DOF" reads as the pre-filter form to most control engineers.
Context
Hit while implementing a two-degree-of-freedom speed controller. Ended up composing filters::passive::ExponentialMovingAverage as F(z) with an incremental PI directly, rather than through Feedforward2Dof, because the pre-filter needs to advance its state on every Compute.
Summary
Feedforward2Dofis the natural home for a two-degree-of-freedom structure, but its feedforward interface is memoryless by construction:Evaluatebeingconst, with noReset()on the interface, rules out any feedforward that carries state. The canonical 2-DOF design — a reference pre-filterF(z)shaping tracking independently of the feedback that sets disturbance stiffness — needs exactly that.Feedforward2Dof::Reset()reinforces the assumption:The feedforward is never reset, because it is presumed to have nothing to reset.
Why it matters
The two structures are not interchangeable:
u = clamp(F(r) + C(r - y)), withFa static map. Good for inverse-plant or gravity/friction feedforward.u = C(F(z) r - y), withF(z)dynamic. This is what decouples command tracking from load stiffness, and it is the form described in the standard references (Astrom & Hagglund, Advanced PID Control).A caller wanting the second form has three options today, all bad:
mutablestate behind aconstmethod, aconst_cast, or bypassingFeedforward2Dofaltogether.Suggested direction
constfromEvaluate.virtual void Reset()toFeedforward(defaulted to a no-op so existing static implementations are unaffected), and call it fromFeedforward2Dof::Reset().Both are source-compatible for memoryless feedforwards, which appear to be the only ones in the tree today.
If the intent is that
Feedforward2Dofonly ever models the static-feedforward structure, then the naming and documentation should say so, since "2-DOF" reads as the pre-filter form to most control engineers.Context
Hit while implementing a two-degree-of-freedom speed controller. Ended up composing
filters::passive::ExponentialMovingAverageasF(z)with an incremental PI directly, rather than throughFeedforward2Dof, because the pre-filter needs to advance its state on everyCompute.