Skip to content

Feedforward2Dof cannot host a dynamic feedforward because Evaluate() is const #277

Description

@gabrielsantosphilips

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

  1. Drop const from Evaluate.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions