"""KindredSolver — IKCSolver implementation bridging KCSolve to the expression-based Newton-Raphson solver.""" from __future__ import annotations import kcsolve from .bfgs import bfgs_solve from .constraints import ( AngleConstraint, BallConstraint, CamConstraint, CoincidentConstraint, ConcentricConstraint, ConstraintBase, CylindricalConstraint, DistanceCylSphConstraint, DistancePointPointConstraint, FixedConstraint, GearConstraint, LineInPlaneConstraint, ParallelConstraint, PerpendicularConstraint, PlanarConstraint, PointInPlaneConstraint, PointOnLineConstraint, RackPinionConstraint, RevoluteConstraint, ScrewConstraint, SliderConstraint, SlotConstraint, TangentConstraint, UniversalConstraint, ) from .decompose import decompose, solve_decomposed from .dof import count_dof from .entities import RigidBody from .newton import newton_solve from .params import ParamTable from .prepass import single_equation_pass, substitution_pass # Assemblies with fewer free bodies than this use the monolithic path. _DECOMPOSE_THRESHOLD = 8 # All BaseJointKind values this solver can handle _SUPPORTED = { # Phase 1 kcsolve.BaseJointKind.Coincident, kcsolve.BaseJointKind.DistancePointPoint, kcsolve.BaseJointKind.Fixed, # Phase 2: point constraints kcsolve.BaseJointKind.PointOnLine, kcsolve.BaseJointKind.PointInPlane, # Phase 2: orientation kcsolve.BaseJointKind.Parallel, kcsolve.BaseJointKind.Perpendicular, kcsolve.BaseJointKind.Angle, # Phase 2: axis/surface kcsolve.BaseJointKind.Concentric, kcsolve.BaseJointKind.Tangent, kcsolve.BaseJointKind.Planar, kcsolve.BaseJointKind.LineInPlane, # Phase 2: kinematic joints kcsolve.BaseJointKind.Ball, kcsolve.BaseJointKind.Revolute, kcsolve.BaseJointKind.Cylindrical, kcsolve.BaseJointKind.Slider, kcsolve.BaseJointKind.Screw, kcsolve.BaseJointKind.Universal, # Phase 2: mechanical kcsolve.BaseJointKind.Gear, kcsolve.BaseJointKind.RackPinion, } class KindredSolver(kcsolve.IKCSolver): """Expression-based Newton-Raphson constraint solver.""" def name(self): return "Kindred (Newton-Raphson)" def supported_joints(self): return list(_SUPPORTED) def solve(self, ctx): params = ParamTable() bodies = {} # part_id -> RigidBody # 1. Build entities from parts for part in ctx.parts: pos = tuple(part.placement.position) quat = tuple(part.placement.quaternion) # (w, x, y, z) body = RigidBody( part.id, params, position=pos, quaternion=quat, grounded=part.grounded, ) bodies[part.id] = body # 2. Build constraint residuals (track index mapping for decomposition) all_residuals = [] constraint_objs = [] constraint_indices = [] # parallel to constraint_objs: index in ctx.constraints for idx, c in enumerate(ctx.constraints): if not c.activated: continue body_i = bodies.get(c.part_i) body_j = bodies.get(c.part_j) if body_i is None or body_j is None: continue marker_i_pos = tuple(c.marker_i.position) marker_j_pos = tuple(c.marker_j.position) obj = _build_constraint( c.type, body_i, marker_i_pos, body_j, marker_j_pos, c.marker_i, c.marker_j, c.params, ) if obj is None: continue constraint_objs.append(obj) constraint_indices.append(idx) all_residuals.extend(obj.residuals()) # 3. Add quaternion normalization residuals for non-grounded bodies quat_groups = [] for body in bodies.values(): if not body.grounded: all_residuals.append(body.quat_norm_residual()) quat_groups.append(body.quat_param_names()) # 4. Pre-passes on full system all_residuals = substitution_pass(all_residuals, params) all_residuals = single_equation_pass(all_residuals, params) # 5. Solve (decomposed for large assemblies, monolithic for small) n_free_bodies = sum(1 for b in bodies.values() if not b.grounded) if n_free_bodies >= _DECOMPOSE_THRESHOLD: grounded_ids = {pid for pid, b in bodies.items() if b.grounded} clusters = decompose(ctx.constraints, grounded_ids) if len(clusters) > 1: converged = solve_decomposed( clusters, bodies, constraint_objs, constraint_indices, params, ) else: converged = _monolithic_solve( all_residuals, params, quat_groups, ) else: converged = _monolithic_solve(all_residuals, params, quat_groups) # 6. DOF dof = count_dof(all_residuals, params) # 7. Build result result = kcsolve.SolveResult() result.status = ( kcsolve.SolveStatus.Success if converged else kcsolve.SolveStatus.Failed ) result.dof = dof env = params.get_env() placements = [] for body in bodies.values(): if body.grounded: continue pr = kcsolve.SolveResult.PartResult() pr.id = body.part_id pr.placement = kcsolve.Transform() pr.placement.position = list(body.extract_position(env)) pr.placement.quaternion = list(body.extract_quaternion(env)) placements.append(pr) result.placements = placements return result def is_deterministic(self): return True def _monolithic_solve(all_residuals, params, quat_groups): """Newton-Raphson solve with BFGS fallback on the full system.""" converged = newton_solve( all_residuals, params, quat_groups=quat_groups, max_iter=100, tol=1e-10, ) if not converged: converged = bfgs_solve( all_residuals, params, quat_groups=quat_groups, max_iter=200, tol=1e-10, ) return converged def _build_constraint( kind, body_i, marker_i_pos, body_j, marker_j_pos, marker_i, marker_j, c_params, ) -> ConstraintBase | None: """Create the appropriate constraint object from a BaseJointKind.""" marker_i_quat = tuple(marker_i.quaternion) marker_j_quat = tuple(marker_j.quaternion) # -- Phase 1 constraints -------------------------------------------------- if kind == kcsolve.BaseJointKind.Coincident: return CoincidentConstraint(body_i, marker_i_pos, body_j, marker_j_pos) if kind == kcsolve.BaseJointKind.DistancePointPoint: distance = c_params[0] if c_params else 0.0 return DistancePointPointConstraint( body_i, marker_i_pos, body_j, marker_j_pos, distance, ) if kind == kcsolve.BaseJointKind.Fixed: return FixedConstraint( body_i, marker_i_pos, marker_i_quat, body_j, marker_j_pos, marker_j_quat, ) # -- Phase 2: point constraints ------------------------------------------- if kind == kcsolve.BaseJointKind.PointOnLine: return PointOnLineConstraint( body_i, marker_i_pos, marker_i_quat, body_j, marker_j_pos, marker_j_quat, ) if kind == kcsolve.BaseJointKind.PointInPlane: offset = c_params[0] if c_params else 0.0 return PointInPlaneConstraint( body_i, marker_i_pos, marker_i_quat, body_j, marker_j_pos, marker_j_quat, offset=offset, ) # -- Phase 2: orientation constraints ------------------------------------- if kind == kcsolve.BaseJointKind.Parallel: return ParallelConstraint(body_i, marker_i_quat, body_j, marker_j_quat) if kind == kcsolve.BaseJointKind.Perpendicular: return PerpendicularConstraint(body_i, marker_i_quat, body_j, marker_j_quat) if kind == kcsolve.BaseJointKind.Angle: angle = c_params[0] if c_params else 0.0 return AngleConstraint(body_i, marker_i_quat, body_j, marker_j_quat, angle) # -- Phase 2: axis/surface constraints ------------------------------------ if kind == kcsolve.BaseJointKind.Concentric: distance = c_params[0] if c_params else 0.0 return ConcentricConstraint( body_i, marker_i_pos, marker_i_quat, body_j, marker_j_pos, marker_j_quat, distance=distance, ) if kind == kcsolve.BaseJointKind.Tangent: return TangentConstraint( body_i, marker_i_pos, marker_i_quat, body_j, marker_j_pos, marker_j_quat, ) if kind == kcsolve.BaseJointKind.Planar: offset = c_params[0] if c_params else 0.0 return PlanarConstraint( body_i, marker_i_pos, marker_i_quat, body_j, marker_j_pos, marker_j_quat, offset=offset, ) if kind == kcsolve.BaseJointKind.LineInPlane: offset = c_params[0] if c_params else 0.0 return LineInPlaneConstraint( body_i, marker_i_pos, marker_i_quat, body_j, marker_j_pos, marker_j_quat, offset=offset, ) # -- Phase 2: kinematic joints -------------------------------------------- if kind == kcsolve.BaseJointKind.Ball: return BallConstraint(body_i, marker_i_pos, body_j, marker_j_pos) if kind == kcsolve.BaseJointKind.Revolute: return RevoluteConstraint( body_i, marker_i_pos, marker_i_quat, body_j, marker_j_pos, marker_j_quat, ) if kind == kcsolve.BaseJointKind.Cylindrical: return CylindricalConstraint( body_i, marker_i_pos, marker_i_quat, body_j, marker_j_pos, marker_j_quat, ) if kind == kcsolve.BaseJointKind.Slider: return SliderConstraint( body_i, marker_i_pos, marker_i_quat, body_j, marker_j_pos, marker_j_quat, ) if kind == kcsolve.BaseJointKind.Screw: pitch = c_params[0] if c_params else 1.0 return ScrewConstraint( body_i, marker_i_pos, marker_i_quat, body_j, marker_j_pos, marker_j_quat, pitch=pitch, ) if kind == kcsolve.BaseJointKind.Universal: return UniversalConstraint( body_i, marker_i_pos, marker_i_quat, body_j, marker_j_pos, marker_j_quat, ) # -- Phase 2: mechanical constraints -------------------------------------- if kind == kcsolve.BaseJointKind.Gear: radius_i = c_params[0] if len(c_params) > 0 else 1.0 radius_j = c_params[1] if len(c_params) > 1 else 1.0 return GearConstraint( body_i, marker_i_quat, body_j, marker_j_quat, radius_i, radius_j, ) if kind == kcsolve.BaseJointKind.RackPinion: pitch_radius = c_params[0] if c_params else 1.0 return RackPinionConstraint( body_i, marker_i_pos, marker_i_quat, body_j, marker_j_pos, marker_j_quat, pitch_radius=pitch_radius, ) # -- Stubs (accepted but produce no residuals) ---------------------------- if kind == kcsolve.BaseJointKind.Cam: return CamConstraint() if kind == kcsolve.BaseJointKind.Slot: return SlotConstraint() if kind == kcsolve.BaseJointKind.DistanceCylSph: return DistanceCylSphConstraint() return None