Files
create/docs/src/solver/constraints.md
forbes acc255972d
Some checks failed
Build and Test / build (pull_request) Failing after 7m52s
feat(assembly): fixed reference planes + solver docs
Assembly Origin Planes:
- AssemblyObject::setupObject() relabels origin planes to
  Top (XY), Front (XZ), Right (YZ) on assembly creation
- CommandCreateAssembly.py makes origin planes visible by default
- AssemblyUtils.cpp getObjFromRef() resolves LocalCoordinateSystem
  to child datum elements for joint references to origin planes
- TestAssemblyOriginPlanes.py: 9 integration tests covering
  structure, labels, grounding, reference resolution, solver,
  and save/load round-trip

Solver Documentation:
- docs/src/solver/: 7 new pages covering architecture overview,
  expression DAG, constraints, solving algorithms, diagnostics,
  assembly integration, and writing custom solvers
- docs/src/SUMMARY.md: added Kindred Solver section
2026-02-21 09:09:16 -06:00

5.4 KiB

Constraints

Each constraint type maps to a class that produces residual expressions. The residuals equal zero when the constraint is satisfied. The number of residuals equals the number of degrees of freedom removed.

Source: mods/solver/kindred_solver/constraints.py, mods/solver/kindred_solver/geometry.py

Constraint vocabulary

Point constraints

Type DOF removed Residuals
Coincident 3 p_i - p_j (world-frame marker origins coincide)
PointOnLine 2 Two components of (p_i - p_j) x z_j (point lies on line through p_j along z_j)
PointInPlane 1 (p_i - p_j) . z_j - offset (signed distance to plane)

Orientation constraints

Type DOF removed Residuals
Parallel 2 Two components of z_i x z_j (cross product of Z-axes is zero)
Perpendicular 1 z_i . z_j (dot product of Z-axes is zero)
Angle 1 z_i . z_j - cos(angle)

Axis/surface constraints

Type DOF removed Residuals
Concentric 4 Parallel Z-axes (2) + point-on-line (2)
Tangent 1 (p_i - p_j) . z_j (signed distance along normal)
Planar 3 Parallel normals (2) + point-in-plane (1)
LineInPlane 2 Point-in-plane (1) + z_i . n_j (line direction perpendicular to normal) (1)

Kinematic joints

Type DOF removed DOF remaining Residuals
Fixed 6 0 Coincident origins (3) + quaternion error imaginary parts (3)
Ball 3 3 Coincident origins (same as Coincident)
Revolute 5 1 (rotation about Z) Coincident origins (3) + parallel Z-axes (2)
Cylindrical 4 2 (rotation + slide) Parallel Z-axes (2) + point-on-line (2)
Slider 5 1 (slide along Z) Parallel Z-axes (2) + point-on-line (2) + twist lock: x_i . y_j (1)
Screw 5 1 (helical) Cylindrical (4) + pitch coupling: axial - pitch * qz_rel / pi (1)
Universal 4 2 (rotation about each Z) Coincident origins (3) + perpendicular Z-axes (1)

Mechanical elements

Type DOF removed Residuals
Gear 1 r_i * qz_i + r_j * qz_j (coupled rotation via quaternion Z-components)
RackPinion 1 translation - 2 * pitch_radius * qz_i (rotation-translation coupling)
Cam 0 Stub (no residuals)
Slot 0 Stub (no residuals)

Distance constraints

Type DOF removed Residuals
DistancePointPoint 1 |p_i - p_j|^2 - d^2 (squared form avoids sqrt in Jacobian)
DistanceCylSph 0 Stub (geometry classification dependent)

Marker convention

Every constraint references two parts (body_i, body_j) with local coordinate frames called markers. Each marker has a position (attachment point on the part) and a quaternion (orientation).

The marker Z-axis defines the constraint direction:

  • Revolute: Z-axis = hinge axis
  • Planar: Z-axis = face normal
  • PointOnLine: Z-axis = line direction
  • Slider: Z-axis = slide direction

The solver computes world-frame marker axes by composing the body quaternion with the marker quaternion: q_world = q_body * q_marker, then rotating unit vectors through the result.

Fixed constraint orientation

The Fixed constraint locks all 6 DOF using a quaternion error formulation:

  1. Compute total orientation: q_i = q_body_i * q_marker_i, q_j = q_body_j * q_marker_j
  2. Compute relative quaternion: q_err = conj(q_i) * q_j
  3. When orientations match, q_err is the identity quaternion (1, 0, 0, 0)
  4. Residuals are the three imaginary components of q_err (should be zero)

The quaternion normalization constraint on each body provides the fourth equation needed to fully determine the quaternion.

Rotation proxies for mechanical constraints

Gear, RackPinion, and Screw constraints need to measure rotation angles. Rather than extracting Euler angles (which would introduce transcendentals), they use the Z-component of a relative quaternion as a proxy:

q_local = conj(q_marker) * q_body * q_marker
angle ~ 2 * qz_local  (for small angles)

This is exact at the solution and has correct gradient direction, which is sufficient for Newton-Raphson convergence from a nearby initial guess.

Geometry helpers

The geometry.py module provides Expr-level vector operations used by constraint classes:

  • marker_z_axis(body, marker_quat) -- world-frame Z-axis via quat_rotate(q_body * q_marker, [0,0,1])
  • marker_x_axis(body, marker_quat) -- world-frame X-axis (used by Slider twist lock)
  • marker_y_axis(body, marker_quat) -- world-frame Y-axis (used by Slider twist lock)
  • dot3(a, b) -- dot product of Expr triples
  • cross3(a, b) -- cross product of Expr triples
  • point_plane_distance(point, origin, normal) -- signed distance
  • point_line_perp_components(point, origin, dir) -- two perpendicular distance components

Writing a new constraint

To add a constraint type:

  1. Subclass ConstraintBase in constraints.py
  2. Implement residuals() returning a list of Expr nodes
  3. Add a case in solver.py:_build_constraint() to instantiate it from BaseJointKind
  4. Add the BaseJointKind value to _SUPPORTED in solver.py
  5. Add the residual count to the tables in decompose.py