single_equation_pass analytically solves variables and bakes their values
as Const() nodes into downstream residual expressions. During drag, the
cached residuals use these stale constants even though part positions have
changed, causing constraints like Planar distance=0 to silently stop
being enforced.
Skip single_equation_pass in the pre_drag() path. Only substitution_pass
(which replaces genuinely grounded parameters) is safe to cache across
drag steps. Newton-Raphson converges in 1-2 iterations from a nearby
initial guess anyway, so the prepass optimization is unnecessary for drag.
Add regression tests covering the bug scenario and the fix.
The parallel-normal constraints (ParallelConstraint, PlanarConstraint,
ConcentricConstraint, RevoluteConstraint, CylindricalConstraint,
SliderConstraint, ScrewConstraint) and point-on-line constraints
previously used only the x and y components of the cross product,
dropping the z component.
This created a singularity when both normal vectors lay in the XY
plane: a yaw rotation produced a cross product entirely along Z,
which was discarded, making the constraint blind to the rotation.
Fix: return all 3 cross-product components. The Jacobian has a
rank deficiency at the solution (3 residuals, rank 2), but the
Newton solver handles this correctly via its pseudoinverse.
Similarly, point_line_perp_components now returns all 3 components
of the displacement cross product to avoid singularity when the
line direction aligns with a coordinate axis.
Add a code generation pipeline that compiles Expr DAGs into flat Python
functions, eliminating recursive tree-walk dispatch in the Newton-Raphson
inner loop.
Key changes:
- Add to_code() method to all 11 Expr node types (expr.py)
- New codegen.py module with CSE (common subexpression elimination),
sparsity detection, and compile()/exec() compilation pipeline
- Add ParamTable.env_ref() to avoid dict copies per iteration (params.py)
- Newton and BFGS solvers accept pre-built jac_exprs and compiled_eval
to avoid redundant diff/simplify and enable compiled evaluation
- count_dof() and diagnostics accept pre-built jac_exprs
- solver.py builds symbolic Jacobian once, compiles once, passes to all
consumers (_monolithic_solve, count_dof, diagnostics)
- Automatic fallback: if codegen fails, tree-walk eval is used
Expected performance impact:
- ~10-20x faster Jacobian evaluation (no recursive dispatch)
- ~2-5x additional from CSE on quaternion-heavy systems
- ~3x fewer entries evaluated via sparsity detection
- Eliminates redundant diff().simplify() in DOF/diagnostics
Add a Python decomposition layer using NetworkX that partitions the
constraint graph into biconnected components (rigid clusters), orders
them via a block-cut tree, and solves each cluster independently.
Articulation-point bodies propagate as boundary conditions between
clusters.
New module kindred_solver/decompose.py:
- DOF table mapping BaseJointKind to residual counts
- Constraint graph construction (nx.MultiGraph)
- Biconnected component detection + articulation points
- Block-cut tree solve ordering (root-first from grounded cluster)
- Cluster-by-cluster solver with boundary body fix/unfix cycling
- Pebble game integration for per-cluster rigidity classification
Changes to existing modules:
- params.py: add unfix() for boundary body cycling
- solver.py: extract _monolithic_solve(), add decomposition branch
for assemblies with >= 8 free bodies
Performance: for k clusters of ~n/k params each, total cost drops
from O(n^3) to O(n^3/k^2).
220 tests passing (up from 207).
MateLabel and MateAssemblyLabels dataclasses with label_mate_assembly()
that back-attributes joint-level independence to originating mates.
Detects redundant and degenerate mates with pattern membership tracking.
Closes#15
convert_mates_to_joints() bridges mate-level constraints to the existing
joint-based analysis pipeline. analyze_mate_assembly() orchestrates the
full pipeline with bidirectional mate-joint traceability.
Closes#13
Add 4 new topology generators to SyntheticAssemblyGenerator:
- generate_tree_assembly: random spanning tree with configurable branching
- generate_loop_assembly: closed ring producing overconstrained data
- generate_star_assembly: hub-and-spoke topology
- generate_mixed_assembly: tree + loops with configurable edge density
Each accepts joint_types as JointType | list[JointType] for per-joint
type sampling.
Add complexity tiers (simple/medium/complex) with predefined body count
ranges via COMPLEXITY_RANGES dict and ComplexityTier type alias.
Update generate_training_batch with 7-way generator selection,
complexity_tier parameter, and generator_type field in output dicts.
Extract private helpers (_random_position, _random_axis,
_select_joint_type, _create_joint) to reduce duplication.
44 generator tests, 130 total — all passing.
Closes#7