Skip to content

tech-debt: stateful blocks whose output() needs a resolved input (FDyn_X.xdd case) #81

Description

@petercorke

Summary

bdsim assumes a continuous block's output() is a pure function of state (x), never needing its live input (inports) resolved. That's true for the common case (e.g. INTEG), but breaks for a stateful block whose output() does need a resolved input to compute one of its ports -- and the failure mode is a confusing assertion deep in outport_value(), not something that points back at the real cause.

Concrete case

roboticstoolbox's FDyn_X block (src/roboticstoolbox/blocks/arm.py) computes 5 outputs: q, qd, x, xd from state alone, but xdd additionally needs the block's own input w (the applied wrench) -- xdd = Ja @ qdd + Ja_dot @ qd, where qdd comes from robot.accel(q, qd, Q) and Q is derived from w.

In RVC3-python's opspace.py example model, w is wired from a force-control loop that itself depends on this same block's xd output (robot_x.w = fsum + pprod, fprod[1] = ... * robot_x.xd). So:

  • q, qd, x, xd are resolvable from state alone -- no problem, and this is exactly what lets the feedback loop close at all (state-space blocks are supposed to be able to break algebraic loops like this).
  • w is only resolvable after fdyn_x.output() has already returned xd.
  • xdd needs w, but output() is one atomic call that can't defer just one port until w exists.

Result: whenever bd.evaluate() calls this block's output() -- including during compile()'s dry-run connectivity check, and again during sim.run()'s t=0 IC-sample pass -- inports[0] is None/unresolved. If output() tries to use it, that's an outright crash (TypeError). If it instead returns None for that one port (matching bdsim's own "not yet available" convention), the value gets cached via _publish_output_values, and the next thing that reads that port via outport_value() (e.g. a watch=[...] recording call) hits:

AssertionError: block fdyn_x.0 output value 4 not set

...even though nothing is actually wrong with the model -- it's a legitimate one-step-behind dependency, not a broken diagram.

Why this doesn't affect INTEG etc.

Confirmed with Peter Corke: ordinary stateful blocks like INTEG never hit this because their output() genuinely only needs state -- their live input only matters inside deriv(), which the integrator calls with a properly-resolved input by construction. FDyn_X is unusual in needing its input inside output() too, to compute a derived quantity (xdd) that isn't part of the integrated state itself.

Workaround applied (not a bdsim fix)

FDyn_X.output() now returns a cached zero-acceleration placeholder for xdd (from __init__, overwritten by every real deriv() call) instead of ever touching inports inside output() -- see roboticstoolbox-python PR (branch fix/fdyn-x-stale-qdd-cache). This sidesteps the ordering problem entirely for this block, at the cost of the very first watched/logged xdd sample reading 0 instead of the true value before deriv() has run once.

What's worth revisiting here

  • Should bdsim support a block declaring "this derived output needs an extra evaluation pass once its own input is resolvable," rather than only offering the binary output()-is-state-only / needs-live-input choice?
  • Should outport_value()'s assertion have a clearer message pointing at why a value is unset (dry-run vs. genuine wiring bug), rather than a bare "not set"?
  • Is there a general pattern here for other stateful blocks that derive a port from both state and input (not just FDyn_X)?

No urgency -- flagging for a deliberate future look, not blocking anything currently.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    tech-debtKnown technical debt, tracked for a deliberate future revisit

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions