122 lines
4.4 KiB
Python
122 lines
4.4 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Tests for final accepted-port allocation and independent H4 audit."""
|
||
|
|
|
||
|
|
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 core.command_allocator import CommandAllocator # noqa: E402
|
||
|
|
from core.energy_audit import audit_haptic_energy # noqa: E402
|
||
|
|
from core.time_domain_popc import TimeDomainPOPC # noqa: E402
|
||
|
|
|
||
|
|
|
||
|
|
class AllocatorAuditTest(unittest.TestCase):
|
||
|
|
def test_reserved_headroom_avoids_total_command_clipping(self):
|
||
|
|
allocator = CommandAllocator(np.array([5.0, 4.0]))
|
||
|
|
prepared = allocator.prepare(
|
||
|
|
compensation=np.array([4.0, -3.5]),
|
||
|
|
haptic_raw=np.array([3.0, -2.0]),
|
||
|
|
)
|
||
|
|
np.testing.assert_allclose(prepared.haptic_candidate, [1.0, -0.5])
|
||
|
|
final = allocator.finalize(
|
||
|
|
prepared, 0.5 * prepared.haptic_candidate
|
||
|
|
)
|
||
|
|
self.assertFalse(final.downstream_modified)
|
||
|
|
np.testing.assert_allclose(
|
||
|
|
final.haptic_accepted, 0.5 * prepared.haptic_candidate
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_quantization_is_reported_and_accepted_increment_reconstructed(self):
|
||
|
|
allocator = CommandAllocator(
|
||
|
|
np.array([5.0]), quantization_step=np.array([0.2])
|
||
|
|
)
|
||
|
|
prepared = allocator.prepare(np.array([1.0]), np.array([0.34]))
|
||
|
|
final = allocator.finalize(prepared, np.array([0.34]))
|
||
|
|
self.assertTrue(final.quantization_active)
|
||
|
|
self.assertTrue(final.downstream_modified)
|
||
|
|
np.testing.assert_allclose(final.total_accepted, [1.4])
|
||
|
|
np.testing.assert_allclose(final.haptic_accepted, [0.4])
|
||
|
|
|
||
|
|
def test_independent_audit_catches_preclip_defect(self):
|
||
|
|
candidate = np.array([[2.0], [2.0]])
|
||
|
|
projected = np.array([[0.5], [0.0]])
|
||
|
|
qd = np.ones((2, 1))
|
||
|
|
clean = audit_haptic_energy(
|
||
|
|
tau_candidate=candidate,
|
||
|
|
tau_projected=projected,
|
||
|
|
tau_accepted=projected,
|
||
|
|
qd_master=qd,
|
||
|
|
dt=0.5,
|
||
|
|
energy_initial=1.25,
|
||
|
|
energy_min=1.0,
|
||
|
|
energy_max=5.0,
|
||
|
|
)
|
||
|
|
self.assertAlmostEqual(clean.max_floor_deficit, 0.0)
|
||
|
|
self.assertGreater(clean.delta_B, 0.0)
|
||
|
|
self.assertGreater(clean.projection_distortion, 0.0)
|
||
|
|
|
||
|
|
defective = audit_haptic_energy(
|
||
|
|
tau_candidate=candidate,
|
||
|
|
tau_projected=projected,
|
||
|
|
tau_accepted=np.array([[0.7], [0.0]]),
|
||
|
|
qd_master=qd,
|
||
|
|
dt=0.5,
|
||
|
|
energy_initial=1.25,
|
||
|
|
energy_min=1.0,
|
||
|
|
energy_max=5.0,
|
||
|
|
logged_preclip=np.array([1.0, 1.0]),
|
||
|
|
)
|
||
|
|
self.assertGreater(defective.max_floor_deficit, 0.0)
|
||
|
|
self.assertGreater(defective.downstream_modification_max, 0.0)
|
||
|
|
self.assertGreater(defective.preclip_log_max_error, 0.0)
|
||
|
|
|
||
|
|
def test_projection_distortion_is_zero_without_intervention(self):
|
||
|
|
torque = np.array([[1.0, -2.0], [0.5, 0.25]])
|
||
|
|
result = audit_haptic_energy(
|
||
|
|
tau_candidate=torque,
|
||
|
|
tau_projected=torque,
|
||
|
|
tau_accepted=torque,
|
||
|
|
qd_master=np.zeros_like(torque),
|
||
|
|
dt=0.01,
|
||
|
|
energy_initial=2.0,
|
||
|
|
energy_min=1.0,
|
||
|
|
energy_max=3.0,
|
||
|
|
)
|
||
|
|
self.assertAlmostEqual(result.projection_distortion, 0.0)
|
||
|
|
self.assertAlmostEqual(result.delta_B, 0.0)
|
||
|
|
|
||
|
|
|
||
|
|
class TimeDomainPOPCTest(unittest.TestCase):
|
||
|
|
def test_no_intervention_for_passive_candidate(self):
|
||
|
|
controller = TimeDomainPOPC(
|
||
|
|
initial_energy=0.0, minimum_energy=0.0, maximum_energy=5.0
|
||
|
|
)
|
||
|
|
applied, diagnostics = controller.apply(
|
||
|
|
np.array([-2.0]), np.array([1.0]), 0.1
|
||
|
|
)
|
||
|
|
np.testing.assert_allclose(applied, [-2.0])
|
||
|
|
self.assertFalse(diagnostics.intervention_active)
|
||
|
|
self.assertAlmostEqual(controller.energy, 0.2)
|
||
|
|
|
||
|
|
def test_active_candidate_receives_damping_injection(self):
|
||
|
|
controller = TimeDomainPOPC(
|
||
|
|
initial_energy=0.1, minimum_energy=0.0, maximum_energy=5.0
|
||
|
|
)
|
||
|
|
applied, diagnostics = controller.apply(
|
||
|
|
np.array([2.0]), np.array([1.0]), 0.1
|
||
|
|
)
|
||
|
|
np.testing.assert_allclose(applied, [1.0])
|
||
|
|
self.assertTrue(diagnostics.intervention_active)
|
||
|
|
self.assertAlmostEqual(diagnostics.damping_gain, 1.0)
|
||
|
|
self.assertAlmostEqual(controller.energy, 0.0)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|