Files
solver/tests/test_entities.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

78 lines
2.8 KiB
Python

"""Tests for RigidBody entities."""
import math
import pytest
from kindred_solver.entities import RigidBody
from kindred_solver.params import ParamTable
class TestRigidBody:
def test_identity_world_point(self):
"""Body at origin with identity rotation: local == world."""
pt = ParamTable()
body = RigidBody("p1", pt, (0, 0, 0), (1, 0, 0, 0))
wx, wy, wz = body.world_point(1.0, 2.0, 3.0)
env = pt.get_env()
assert abs(wx.eval(env) - 1.0) < 1e-10
assert abs(wy.eval(env) - 2.0) < 1e-10
assert abs(wz.eval(env) - 3.0) < 1e-10
def test_translated(self):
"""Body translated by (10, 20, 30) with identity rotation."""
pt = ParamTable()
body = RigidBody("p1", pt, (10, 20, 30), (1, 0, 0, 0))
wx, wy, wz = body.world_point(1.0, 2.0, 3.0)
env = pt.get_env()
assert abs(wx.eval(env) - 11.0) < 1e-10
assert abs(wy.eval(env) - 22.0) < 1e-10
assert abs(wz.eval(env) - 33.0) < 1e-10
def test_rotated_90_z(self):
"""90-degree rotation about Z: (1,0,0) -> (0,1,0)."""
pt = ParamTable()
# 90-deg about Z: q = (cos(45), 0, 0, sin(45))
c = math.cos(math.pi / 4)
s = math.sin(math.pi / 4)
body = RigidBody("p1", pt, (0, 0, 0), (c, 0, 0, s))
wx, wy, wz = body.world_point(1.0, 0.0, 0.0)
env = pt.get_env()
assert abs(wx.eval(env) - 0.0) < 1e-10
assert abs(wy.eval(env) - 1.0) < 1e-10
assert abs(wz.eval(env) - 0.0) < 1e-10
def test_quat_norm_residual(self):
"""Normalization residual is zero for unit quaternion."""
pt = ParamTable()
body = RigidBody("p1", pt, (0, 0, 0), (1, 0, 0, 0))
r = body.quat_norm_residual()
env = pt.get_env()
assert abs(r.eval(env)) < 1e-10
def test_grounded(self):
"""Grounded body has all params fixed."""
pt = ParamTable()
body = RigidBody("p1", pt, (1, 2, 3), (1, 0, 0, 0), grounded=True)
assert pt.n_free() == 0
assert body.grounded
def test_extract(self):
"""Extract position and quaternion from env."""
pt = ParamTable()
body = RigidBody("p1", pt, (1, 2, 3), (0.5, 0.5, 0.5, 0.5))
env = pt.get_env()
pos = body.extract_position(env)
quat = body.extract_quaternion(env)
assert pos == (1.0, 2.0, 3.0)
assert quat == (0.5, 0.5, 0.5, 0.5)
def test_differentiation(self):
"""World point expressions are differentiable w.r.t. body params."""
pt = ParamTable()
body = RigidBody("p1", pt, (0, 0, 0), (1, 0, 0, 0))
wx, wy, wz = body.world_point(1.0, 0.0, 0.0)
# d(wx)/d(tx) should be 1
dtx = wx.diff("p1/tx").simplify()
env = pt.get_env()
assert abs(dtx.eval(env) - 1.0) < 1e-10