Skip to content

fix(blocks): FDyn_X.output() crashed/asserted on unset xdd before deriv() ran - #647

Merged
petercorke merged 1 commit into
mainfrom
fix/fdyn-x-stale-qdd-cache
Aug 26, 2026
Merged

fix(blocks): FDyn_X.output() crashed/asserted on unset xdd before deriv() ran#647
petercorke merged 1 commit into
mainfrom
fix/fdyn-x-stale-qdd-cache

Conversation

@petercorke

Copy link
Copy Markdown
Owner

Summary

FDyn_X.output() returned xdd=None until self._qdd got set as a side effect of deriv() running at least once. That's fine when deriv() always runs first, but bdsim calls a continuous block's output() wherever the compute graph needs its state-derived ports (q, qd, x, xd here) -- including during compile()'s dry-run connectivity check and sim.run()'s t=0 IC-sample pass, both before any deriv() call. Any watch=[...] or wiring that reads xdd at that point hit bdsim's outport_value() assertion: AssertionError: block fdyn_x.0 output value 4 not set.

Found via RVC3-python's opspace.py example (task-space operational-space control, bd.FDYN_X(...) with watch=[..., robot_x.xdd]).

Confirmed this isn't just an initialization-ordering bug fixable by caching more cleverly: robot_x.w (this block's own input) depends on robot_x.xd (this block's own output) via the force-control feedback loop in opspace.py (robot_x.w = fsum + pprod, fprod[1] = ... * robot_x.xd). So w is only resolvable after this same output() call has already returned xd -- output() genuinely can't compute xdd itself in one atomic call, regardless of timestamp-based staleness tracking. Filed the general case as petercorke/bdsim#81 (stateful blocks whose output() needs a resolved input, unlike e.g. INTEG which only needs its input inside deriv()).

Fix

Cache a zero-acceleration placeholder for xdd from __init__ instead of None, overwritten by every real deriv() call. output() never touches inports, matching every other continuous block's state-only assumption. Only the very first watched/logged xdd sample reads 0 instead of the true value; every step from deriv()'s first real call onward is exact.

Test plan

  • opspace.py (the example that surfaced this) now runs to completion end-to-end under a real bdsim simulation -- previously crashed with the assertion above
  • RVC3-python's tests/test_notebooks.py::test_notebook[chap9.ipynb] (which runs this model via %run -i) now passes clean

…iv() ran

FDyn_X.output() returned xdd=None until self._qdd got set as a side
effect of deriv() running at least once. That's fine when deriv()
always runs first, but bdsim calls a continuous block's output()
wherever the compute graph needs its state-derived ports (q, qd, x, xd
here) -- including during compile()'s dry-run connectivity check and
sim.run()'s t=0 IC-sample pass, both before any deriv() call. Any
watch=[...] or wiring that reads xdd at that point hit bdsim's
outport_value() assertion: "output value 4 not set".

Confirmed via RVC3-python's opspace.py example: robot_x.w depends on
robot_x.xd through the force-control feedback loop, so xdd's only
input (w) is only resolvable *after* this same output() call has
already returned xd -- output() genuinely cannot compute xdd itself
here, in one atomic call, regardless of caching. Filed the general
case as petercorke/bdsim#81 (stateful blocks whose output() needs a
resolved input, unlike e.g. INTEG which only needs it in deriv()).

Fix: cache a zero-acceleration placeholder for xdd from __init__
instead of None, overwritten by every real deriv() call. output()
never touches inports, matching every other continuous block's
state-only assumption. Only the very first watched/logged xdd sample
reads 0 instead of the true value; every step from deriv()'s first
real call onward is exact.

Test plan:
- opspace.py (RVC3-python's task-space operational-space-control
  example, the model that surfaced this) now runs to completion
  end-to-end under a real bdsim simulation
@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 0.00%. Comparing base (5b5a0fe) to head (493d0fe).

Files with missing lines Patch % Lines
src/roboticstoolbox/blocks/arm.py 0.00% 4 Missing ⚠️
Additional details and impacted files
@@          Coverage Diff          @@
##            main    #647   +/-   ##
=====================================
  Coverage   0.00%   0.00%           
=====================================
  Files        143     143           
  Lines      14035   14033    -2     
=====================================
+ Misses     14035   14033    -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@petercorke
petercorke merged commit 8caa363 into main Aug 26, 2026
23 checks passed
@petercorke
petercorke deleted the fix/fdyn-x-stale-qdd-cache branch August 26, 2026 10:59
@github-actions github-actions Bot mentioned this pull request Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant