Files
solver/kindred_solver/solver.py
forbes-0023 98051ba0c9 feat: add Phase 1 constraint solver addon, move prior content to GNN/
- Move existing OndselSolver, GNN ML layer, and tooling into GNN/
  directory for integration in later phases
- Add Create addon scaffold: package.xml, Init.py
- Add expression DAG with eval, symbolic diff, simplification
- Add parameter table with fixed/free variable tracking
- Add quaternion rotation as polynomial Expr trees
- Add RigidBody entity (7 DOF: position + unit quaternion)
- Add constraint classes: Coincident, DistancePointPoint, Fixed
- Add Newton-Raphson solver with symbolic Jacobian + numpy lstsq
- Add pre-solve passes: substitution + single-equation
- Add DOF counting via Jacobian SVD rank
- Add KindredSolver IKCSolver bridge for kcsolve integration
- Add 82 unit tests covering all modules

Registers as 'kindred' solver via kcsolve.register_solver() when
loaded by Create's addon_loader.
2026-02-20 20:35:47 -06:00

170 lines
4.7 KiB
Python

"""KindredSolver — IKCSolver implementation bridging KCSolve to the
expression-based Newton-Raphson solver."""
from __future__ import annotations
import kcsolve
from .constraints import (
CoincidentConstraint,
ConstraintBase,
DistancePointPointConstraint,
FixedConstraint,
)
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
# Map BaseJointKind enum values to handler names
_SUPPORTED = {
kcsolve.BaseJointKind.Coincident,
kcsolve.BaseJointKind.DistancePointPoint,
kcsolve.BaseJointKind.Fixed,
}
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
all_residuals = []
constraint_objs = []
for c in 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)
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
all_residuals = substitution_pass(all_residuals, params)
all_residuals = single_equation_pass(all_residuals, params)
# 5. Newton-Raphson
converged = newton_solve(
all_residuals,
params,
quat_groups=quat_groups,
max_iter=100,
tol=1e-10,
)
# 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 _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."""
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:
marker_i_quat = tuple(marker_i.quaternion)
marker_j_quat = tuple(marker_j.quaternion)
return FixedConstraint(
body_i,
marker_i_pos,
marker_i_quat,
body_j,
marker_j_pos,
marker_j_quat,
)
return None