#!/usr/bin/env python3 """Regression tests for the rigid-body closed-loop simulation.""" from dataclasses import replace from pathlib import Path import sys import unittest import numpy as np CODE_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(CODE_ROOT)) from simulate_closed_loop import ( # noqa: E402 DEFAULT_CONFIG_PATH, SCENARIOS, SimulationConfig, Wall, build_mapper, load_models, load_simulation_config, make_wall, master_endpoint_configurations, simulate_scenario, ) from core.energy_audit import audit_haptic_energy # noqa: E402 class ClosedLoopSimulationTest(unittest.TestCase): @classmethod def setUpClass(cls) -> None: cls.models = load_models(add_simulated_tcp=True) def test_yaml_configuration_is_complete(self) -> None: config = load_simulation_config(DEFAULT_CONFIG_PATH) self.assertEqual(config.slave_contact_frame, "R_EE_SIM") self.assertEqual(config.dt, 0.002) self.assertEqual(config.mapping_hz, 50.0) self.assertLess(config.energy_min, config.energy_max) def test_wall_is_unilateral_dissipative_and_force_limited(self) -> None: wall = Wall( point=np.zeros(3), normal=np.array([1.0, 0.0, 0.0]), stiffness=100.0, damping=10.0, force_limit=5.0, ) free_wrench, free_penetration = wall.wrench( np.array([-0.1, 0.0, 0.0]), np.array([1.0, 0.0, 0.0]), ) np.testing.assert_allclose(free_wrench, 0.0) self.assertEqual(free_penetration, 0.0) contact_wrench, penetration = wall.wrench( np.array([0.1, 0.0, 0.0]), np.array([2.0, 0.0, 0.0]), ) self.assertAlmostEqual(penetration, 0.1) np.testing.assert_allclose(contact_wrench[:3], [-5.0, 0.0, 0.0]) self.assertLessEqual(float(contact_wrench[:3] @ np.array([2.0, 0.0, 0.0])), 0.0) def test_generalized_mapping_preserves_virtual_work(self) -> None: mapper = build_mapper(self.models) q_master, _ = master_endpoint_configurations() _, differential, debug = mapper.retarget_with_differential(q_master) self.assertTrue(debug["differential_valid"]) qd_master = np.array([0.2, -0.1, 0.15, -0.3, 0.05, 0.08, -0.04]) tau_slave = np.array([-1.0, 0.4, 0.8, -0.5, 0.2, -0.3, 0.1]) tau_master = differential.T @ tau_slave self.assertAlmostEqual( float(tau_master @ qd_master), float(tau_slave @ (differential @ qd_master)), places=12, ) def test_short_rigid_body_run_is_finite_and_respects_energy_floor(self) -> None: config = replace( SimulationConfig(), duration=0.8, feedback_delay_s=0.04, contact_probe_fraction=0.0, ) mapper = build_mapper(self.models) wall, _, q_slave_start = make_wall(config, self.models, mapper) result = simulate_scenario( SCENARIOS[0], config, self.models, wall, q_slave_start, ) self.assertTrue(result.metrics["completed"]) self.assertTrue(result.metrics["finite_state"]) self.assertEqual(result.metrics["master_joint_limit_events"], 0) self.assertEqual(result.metrics["slave_joint_limit_events"], 0) self.assertEqual(result.metrics["differential_fallback_count"], 0) self.assertGreater(result.metrics["wall_contact_duration_s"], 0.0) self.assertGreaterEqual( result.metrics["tank_energy_min_J"], config.energy_min - 1e-12, ) def test_matched_source_map_and_independent_energy_audit(self) -> None: config = replace( SimulationConfig(), duration=0.6, feedback_delay_s=0.04, contact_probe_fraction=0.0, ) mapper = build_mapper(self.models) wall, _, q_slave_start = make_wall(config, self.models, mapper) matched = next( scenario for scenario in SCENARIOS if scenario.key == "matched_wrench_energy" ) result = simulate_scenario( matched, config, self.models, wall, q_slave_start ) active = result.logs["return_packet_active"] > 0.5 self.assertTrue(np.any(active)) self.assertTrue(np.all(result.logs["source_map_id"][active] >= 0)) self.assertTrue(np.all(result.logs["map_id"][active] >= 0)) audit = audit_haptic_energy( tau_candidate=result.logs["tau_master_candidate"], tau_projected=result.logs["tau_master_applied"], tau_accepted=result.logs["tau_master_accepted"], qd_master=result.logs["qd_master"], dt=config.dt, energy_initial=config.energy_initial, energy_min=config.energy_min, energy_max=config.energy_max, logged_preclip=result.logs["energy_preclip"], ) self.assertLessEqual(audit.max_floor_deficit, 1e-12) self.assertLessEqual(audit.preclip_log_max_error, 1e-12) def test_time_domain_popc_is_a_distinct_executable_baseline(self) -> None: config = replace( SimulationConfig(), duration=0.6, feedback_delay_s=0.02, contact_probe_fraction=0.0, ) mapper = build_mapper(self.models) wall, _, q_slave_start = make_wall(config, self.models, mapper) popc = next( scenario for scenario in SCENARIOS if scenario.key == "proposed_popc" ) result = simulate_scenario( popc, config, self.models, wall, q_slave_start ) self.assertTrue(result.metrics["completed"]) self.assertIsNone(result.metrics["rho_min"]) self.assertTrue(np.all(np.isfinite(result.logs["popc_damping_gain"]))) if __name__ == "__main__": unittest.main()