- 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.
170 lines
4.7 KiB
Python
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
|