Repository navigation
fix(solver): give each dyad its own joint type, stop returning NaN - #42
Merged
Merged
Conversation
linkage_to_solver_data() typed every dyad as JOINT_REVOLUTE and read distance1/distance2 off it. Only RRRDyad has those attributes. A FixedDyad has distance/angle, so both getattr calls fell through to their 0.0 default and the solver intersected two zero-radius circles, which never meet -- the joint, and everything solved downstream of it, came out NaN. Nothing raised. Callers got a full-shaped array of NaN where positions should be, while step() computed the same mechanism correctly. A FixedDyad is how a traced point is rigidly attached to a coupler link, so this covered most mechanisms anyone would want to simulate fast, including everything pylinkage.synthesis emits. JOINT_FIXED and JOINT_PRISMATIC were already defined in solver/types.py and handled in solver/simulation.py, and update_solver_constraints() even had a correct FixedDyad branch. The primary builder just never emitted those types, so all of it was unreachable. The conversion now dispatches on the actual dyad class through a single table shared with update_solver_constraints(), so the two cannot drift. Dyads the solver cannot represent -- PPDyad, which needs four parents against MAX_PARENTS of three, and the cam followers -- raise NotImplementedError naming step() as the alternative, rather than being mistyped into a wrong answer. step() and step_fast() now agree to atol=0 on RRRDyad, FixedDyad and RRPDyad mechanisms, tested per type. The gap that let this through was that the only agreement test covered the Mechanism path, and the bridge test for FixedDyad asserted an array shape and nothing else. Closes #40.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #40.
The bug
linkage_to_solver_data()typed every dyad asJOINT_REVOLUTEand readdistance1/distance2off it:is_dyad()matches seven classes; onlyRRRDyadhas those attributes. AFixedDyadhasdistanceandangle, so bothgetattrcalls fell through to0.0and the solver intersected two zero-radius circles. They never meet, so the joint — and everything solved downstream of it — came outNaN.Nothing raised. Callers got a full-shaped array of
NaNwhere positions should be, whilestep()computed the same mechanism correctly.Before / after
step_fast()P[nan, nan][2.3512, 0.2476]2(JOINT_REVOLUTE)3(JOINT_FIXED)[0.0, 0.0][1.4, -0.37]step()gives(2.3512, 0.2476)— the two paths now agree.The fix
JOINT_FIXEDandJOINT_PRISMATICwere already defined insolver/types.pyand handled insolver/simulation.py, andupdate_solver_constraints()even had a correctFixedDyadbranch. The primary builder simply never emitted those types, so all of it was unreachable.The conversion now dispatches on the actual dyad class through one table:
RRRDyad,BinaryDyadJOINT_REVOLUTEanchor1,anchor2distance1,distance2FixedDyadJOINT_FIXEDanchor1,anchor2distance,angleRRPDyadJOINT_PRISMATICrevolute_anchor,line_anchor1,line_anchor2distanceupdate_solver_constraints()reads the same table, so the two cannot drift apart again.PPDyadneeds four parents againstMAX_PARENTSof three, and the cam followers have no solver representation at all. Rather than being mistyped into a wrong answer, they now raise:RRPDyadwas broken the same way and works now too — that one was never reported.Verification
step()againststep_fast(), twelve iterations,atol=0:Both paths call the same
solve_*functions, so anything but an exact match would mean the conversion handed the solver different numbers.14 new tests: 8 in
tests/simulation/test_step_fast_agreement.py(agreement and no-NaN per dyad type, plus the rejection and its message), 6 extending the bridge tests. Full suite 2477 passed, 5 skipped with all extras. Lint andmypy --strictclean on 134 files.How it went unnoticed: the only
step()/step_fast()agreement test covered theMechanismpath, and the bridge test namedtest_with_fixed_dyadasserted an array shape and nothing else.Effect on #29
With the solver fixed, routing
_verify_coupler_paththroughstep_fast()now works and returns real solutions:step()step_fast()2.9x is essentially the Amdahl ceiling for a stage that was 67% of runtime.
One caveat for whoever picks up #29, found while measuring: on the benchmark's points the two produce different sets of ten solutions.
step()raisesUnbuildableErroron a candidate that cannot assemble and the caller treats that as rejection, whereasstep_fast()returnsNaNrows instead. A straight swap changes which candidates are accepted, so the rejection path needs handling deliberately. That is out of scope here.Known gap left open
_mechanism_to_solver_data()— the separateMechanismpath — still treatsPrismaticJointasJOINT_STATIC, with a comment saying prismatic kinematics "are not yet wired". That is a documented limitation rather than a silent mistype, and it is untouched here.