"""Numerical tests for the dimensionally scaled H2 solvers.""" from __future__ import annotations import sys import unittest from pathlib import Path import numpy as np CODE_ROOT = Path(__file__).resolve().parents[1] if str(CODE_ROOT) not in sys.path: sys.path.insert(0, str(CODE_ROOT)) from core.wrench_solver import ( # noqa: E402 ScaledDLSSolver, UndampedSVDSolver, WrenchSolveStatus, ) class WrenchSolverTest(unittest.TestCase): def setUp(self) -> None: rng = np.random.default_rng(240319) self.jacobian = rng.normal(size=(6, 7)) self.wrench = np.array([8.0, -3.0, 5.0, 0.7, -0.4, 0.2]) self.length = 0.37 self.residual = self.jacobian.T @ self.wrench def test_scaling_preserves_virtual_work(self) -> None: solver = UndampedSVDSolver(self.length) scaled_jacobian = solver.scaled_jacobian(self.jacobian) scaled_wrench = np.concatenate( (self.length * self.wrench[:3], self.wrench[3:]) ) np.testing.assert_allclose( scaled_jacobian.T @ scaled_wrench, self.jacobian.T @ self.wrench, rtol=1e-14, atol=1e-14, ) def test_undamped_full_rank_recovers_known_wrench(self) -> None: result = UndampedSVDSolver( self.length, relative_rank_tolerance=1e-10 ).solve(self.jacobian, self.residual) self.assertIs(result.status, WrenchSolveStatus.FULL_RANK) self.assertEqual(result.rank, 6) np.testing.assert_allclose( result.wrench, self.wrench, rtol=2e-13, atol=2e-13 ) np.testing.assert_allclose( result.reconstructed_residual, self.residual, rtol=2e-13, atol=2e-13, ) def test_dls_matches_scaled_svd_filter_formula(self) -> None: damping = 0.08 solver = ScaledDLSSolver(self.length, damping) result = solver.solve(self.jacobian, self.residual) scaled_jacobian = solver.scaled_jacobian(self.jacobian) u, singular_values, vt = np.linalg.svd( scaled_jacobian, full_matrices=False ) expected_scaled = u @ ( singular_values / (singular_values**2 + damping**2) * (vt @ self.residual) ) expected_wrench = np.diag( [1.0 / self.length] * 3 + [1.0] * 3 ) @ expected_scaled np.testing.assert_allclose(result.wrench, expected_wrench, atol=1e-14) np.testing.assert_allclose( result.singular_values, singular_values, atol=0.0 ) self.assertAlmostEqual(result.damping, damping) def test_rank_deficiency_is_retained_and_reported(self) -> None: jacobian = self.jacobian.copy() jacobian[-1, :] = jacobian[-2, :] residual = jacobian.T @ self.wrench for solver in ( UndampedSVDSolver(self.length), ScaledDLSSolver(self.length, 0.05), ): with self.subTest(method=solver.name): result = solver.solve(jacobian, residual) self.assertIs( result.status, WrenchSolveStatus.RANK_DEFICIENT ) self.assertEqual(result.rank, 5) self.assertTrue(np.isinf(result.condition_number)) self.assertTrue(np.all(np.isfinite(result.wrench))) def test_invalid_units_and_shapes_are_rejected(self) -> None: with self.assertRaises(ValueError): ScaledDLSSolver(0.0, 0.1) with self.assertRaises(ValueError): ScaledDLSSolver(self.length, 0.0) with self.assertRaises(ValueError): UndampedSVDSolver(self.length, relative_rank_tolerance=1.0) solver = UndampedSVDSolver(self.length) with self.assertRaisesRegex(ValueError, "jacobian"): solver.solve(np.eye(7), np.ones(7)) with self.assertRaisesRegex(ValueError, "joint_residual"): solver.solve(self.jacobian, np.ones(6)) if __name__ == "__main__": unittest.main()