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.
Summary
bdsimassumes a continuous block'soutput()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 whoseoutput()does need a resolved input to compute one of its ports -- and the failure mode is a confusing assertion deep inoutport_value(), not something that points back at the real cause.Concrete case
roboticstoolbox'sFDyn_Xblock (src/roboticstoolbox/blocks/arm.py) computes 5 outputs:q, qd, x, xdfrom state alone, butxddadditionally needs the block's own inputw(the applied wrench) --xdd = Ja @ qdd + Ja_dot @ qd, whereqddcomes fromrobot.accel(q, qd, Q)andQis derived fromw.In RVC3-python's
opspace.pyexample model,wis wired from a force-control loop that itself depends on this same block'sxdoutput (robot_x.w = fsum + pprod,fprod[1] = ... * robot_x.xd). So:q, qd, x, xdare 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).wis only resolvable afterfdyn_x.output()has already returnedxd.xddneedsw, butoutput()is one atomic call that can't defer just one port untilwexists.Result: whenever
bd.evaluate()calls this block'soutput()-- including duringcompile()'s dry-run connectivity check, and again duringsim.run()'s t=0 IC-sample pass --inports[0]isNone/unresolved. Ifoutput()tries to use it, that's an outright crash (TypeError). If it instead returnsNonefor 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 viaoutport_value()(e.g. awatch=[...]recording call) hits:...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
INTEGnever hit this because theiroutput()genuinely only needs state -- their live input only matters insidederiv(), which the integrator calls with a properly-resolved input by construction.FDyn_Xis unusual in needing its input insideoutput()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 forxdd(from__init__, overwritten by every realderiv()call) instead of ever touchinginportsinsideoutput()-- see roboticstoolbox-python PR (branchfix/fdyn-x-stale-qdd-cache). This sidesteps the ordering problem entirely for this block, at the cost of the very first watched/loggedxddsample reading0instead of the true value beforederiv()has run once.What's worth revisiting here
bdsimsupport 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?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"?FDyn_X)?No urgency -- flagging for a deliberate future look, not blocking anything currently.