Skip to content

Fix combinatorial blowup in ConstraintGraphFactory._recurse() from missing negative-memoization - #177

Open
thanhndv212 wants to merge 1 commit into
humanoid-path-planner:develfrom
thanhndv212:fix/constraint-graph-factory-memoization
Open

Fix combinatorial blowup in ConstraintGraphFactory._recurse() from missing negative-memoization#177
thanhndv212 wants to merge 1 commit into
humanoid-path-planner:develfrom
thanhndv212:fix/constraint-graph-factory-memoization

Conversation

@thanhndv212

@thanhndv212 thanhndv212 commented Aug 1, 2026

Copy link
Copy Markdown

Summary

GraphFactoryAbstract._recurse() memoizes visited grasp combinations
via self.states/_existState(), but self.states is only populated
for combinations accepted by graspIsAllowed. Rejected combinations
were never marked visited, so their entire descendant subtree was
redundantly re-explored from every distinct parent path that reached
them.

Downstream, this caused a 20+ minute hang building the constraint
graph for a phase with 8 simultaneously-held grippers / 7 objects — a
case with a restrictive filter that should have been fast per this
module's own docstring (O(N) for such filters), but wasn't, because
rejections were the majority case and none of them were cached.

Fix

Add a _visitedGrasps set, separate from self.states (real created
State objects, used elsewhere), populated for every nGrasps tuple
regardless of whether graspIsAllowed accepted or rejected it.

Testing

Standalone, HPP-independent reproduction (stubs the two compiled deps
_recurse doesn't actually touch — pyhpp.constraints.{Implicit, LockedJoint}, numpy; a trivial concrete subclass recording into
plain sets instead of touching real graph objects):

  • Diffed the resulting {grasps} state-set and {(from,to,ig)}
    transition-set between unpatched and patched _recurse for N=2..8
    grippers/handles, including an adversarial non-monotonic filter
    (rejects most intermediate combinations, accepts one specific deep
    target) — states/transitions match exactly in every case; only the
    redundant recursion is eliminated.
  • _recurse call counts drop sharply for the patched version (n=6:
    137,266 → 4,051 calls) and stay fast at n=8 (394,353 calls, matching
    the closed-form combinatorial floor Σ C(nG,k)·P(nH,k) exactly).
  • Full downstream 13-phase sequential manipulation test previously
    hung indefinitely on its final, most complex phase; now completes.

…issing negative-memoization

GraphFactoryAbstract._recurse()'s isNewState check was
`not self._existState(nGrasps)`, but self.states (backing
_existState) is only populated when graspIsAllowed(nGrasps) is True.
So a *rejected* grasp combination was never marked visited, and got
its entire descendant subtree redundantly re-explored from every
distinct parent path that reached it.

The subtree rooted at a given nGrasps is a pure function of its
content alone (ngrippers/nhandles are the input lists with a fixed
set removed, independent of removal order; depth is likewise
path-independent), so re-reaching the same nGrasps via a different
gripper-assignment order is always pure wasted recomputation, never
new discovery. Add a separate _visitedGrasps set, populated for every
nGrasps regardless of accept/reject, independent of self.states
(which has a different meaning: actually-created State objects that
other code depends on).

For 8 grippers x 7 handles (a downstream project's worst-case scene),
this caused a 20+ minute hang building the constraint graph for a
single phase.

Also corrects the class docstring, which described recursion stopping
on a rejected grasp set — it doesn't and shouldn't (needed to support
non-monotonic filter rules); the docstring just didn't match the code.
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