exoskeleton/code/test/test_h1_calibration_v3.py

317 lines
12 KiB
Python

#!/usr/bin/env python3
"""H1 calibration-v3 branch-crossing and differential evidence contracts."""
from __future__ import annotations
from copy import deepcopy
from pathlib import Path
import sys
import unittest
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 analysis.metrics import MetricError, derive_trial_metrics # noqa: E402
from core.model_contract import ( # noqa: E402
MASTER_JOINT_NAMES,
finite_joint_limits,
load_models,
)
from core.retargeting_baselines import ( # noqa: E402
build_canonical_sew_target_baselines,
)
from experiments.executors import ( # noqa: E402
H1ValidityReason,
_master_trajectory,
execute_h1_retargeting,
)
from experiments.plan import build_trial_plan, load_document # noqa: E402
CONFIG_PATH = (
CODE_ROOT / "config" / "experiments" / "h1_calibration_v3.json"
)
METRIC_CONFIG_PATH = (
CODE_ROOT
/ "config"
/ "experiments"
/ "metrics_h1_calibration_v3.json"
)
class H1CalibrationV3Test(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.models = load_models(add_simulated_tcp=True)
cls.lower, cls.upper = finite_joint_limits(
cls.models.master, MASTER_JOINT_NAMES
)
cls.plan = build_trial_plan(load_document(CONFIG_PATH))
cls.metric_configuration = load_document(METRIC_CONFIG_PATH)
_, cls.sew_method = build_canonical_sew_target_baselines(cls.models)
cls.sew_payloads = {}
for path_type in ("linear", "cosine_roundtrip"):
cls.sew_payloads[path_type] = execute_h1_retargeting(
cls._trial("sew", path_type)
)
cls.na_payload = execute_h1_retargeting(
cls._trial("bounded_dls_ik", "linear")
)
@classmethod
def _trial(cls, method_id: str, path_type: str):
return next(
trial
for trial in cls.plan["trials"]
if trial["method"]["method_id"] == method_id
and trial["trajectory"]["path_type"] == path_type
)
@classmethod
def _trajectory(cls, trial):
return _master_trajectory(
trial, lower=cls.lower, upper=cls.upper
)
def test_explicit_linear_and_roundtrip_paths_are_strictly_paired(self) -> None:
self.assertEqual(self.plan["pair_count"], 2)
self.assertEqual(self.plan["trial_count"], 8)
by_pair = {}
for trial in self.plan["trials"]:
trajectory = self._trajectory(trial)
by_pair.setdefault(trial["pair_id"], []).append(trajectory)
self.assertLess(
np.max(np.linalg.norm(np.diff(trajectory, axis=0), axis=1)),
0.05,
)
for trajectories in by_pair.values():
self.assertEqual(len(trajectories), 4)
for candidate in trajectories[1:]:
np.testing.assert_array_equal(candidate, trajectories[0])
linear_trial = self._trial("sew", "linear")
linear = self._trajectory(linear_trial)
np.testing.assert_array_equal(
linear[0], np.asarray(linear_trial["trajectory"]["start"])
)
np.testing.assert_array_equal(
linear[-1], np.asarray(linear_trial["trajectory"]["end"])
)
roundtrip_trial = self._trial("sew", "cosine_roundtrip")
roundtrip = self._trajectory(roundtrip_trial)
np.testing.assert_array_equal(
roundtrip[0], np.asarray(roundtrip_trial["trajectory"]["start"])
)
np.testing.assert_allclose(
roundtrip[len(roundtrip) // 2],
np.asarray(roundtrip_trial["trajectory"]["end"]),
atol=1e-15,
rtol=0.0,
)
np.testing.assert_array_equal(roundtrip[-1], roundtrip[0])
def test_explicit_path_contract_rejects_partial_or_unknown_paths(self) -> None:
trial = deepcopy(self._trial("sew", "linear"))
del trial["trajectory"]["end"]
with self.assertRaisesRegex(ValueError, "both start and end"):
self._trajectory(trial)
trial = deepcopy(self._trial("sew", "linear"))
trial["trajectory"]["path_type"] = "triangle"
with self.assertRaisesRegex(ValueError, "path_type"):
self._trajectory(trial)
def test_target_debug_has_phi_reference_and_reach_margins(self) -> None:
linear = self.sew_payloads["linear"].samples
crossing = np.flatnonzero(
np.abs(np.diff(linear["map_sew_phi_rad"])) > np.pi
)
self.assertEqual(crossing.size, 1)
debug = self.sew_method.mapper._target_from_master(
linear["q_master"][int(crossing[0]) + 1]
)
for field in (
"phi_rad",
"reference_axis_norm",
"reach_lower_margin_m",
"reach_upper_margin_m",
"master_arm_normal_norm",
):
self.assertIn(field, debug)
self.assertTrue(np.isfinite(debug[field]))
self.assertGreater(debug["reference_axis_norm"], 0.0)
self.assertGreater(debug["reach_lower_margin_m"], 0.0)
self.assertGreater(debug["reach_upper_margin_m"], 0.0)
for payload in self.sew_payloads.values():
samples = payload.samples
self.assertTrue(np.all(samples["map_reach_clip_code"] == 0))
self.assertTrue(
np.all(samples["map_reach_lower_margin_m"] > 0.0)
)
self.assertTrue(
np.all(samples["map_reach_upper_margin_m"] > 0.0)
)
self.assertTrue(np.all(samples["map_reference_axis_norm"] > 0.0))
def test_actual_sew_differential_and_branch_metrics_are_reconstructable(
self,
) -> None:
expected_crossings = {"linear": 1, "cosine_roundtrip": 2}
for path_type, payload in self.sew_payloads.items():
with self.subTest(path_type=path_type):
samples = payload.samples
self.assertEqual(
samples["map_differential_A"].shape, (81, 7, 7)
)
self.assertTrue(
np.all(samples["map_differential_applicable"] == 1)
)
self.assertTrue(np.all(samples["map_branch_smooth"] == 1))
self.assertTrue(
np.all(samples["map_differential_valid"] == 1)
)
self.assertTrue(
np.all(np.isfinite(samples["map_differential_A"]))
)
self.assertTrue(
np.all(samples["map_differential_runtime_s"] >= 0.0)
)
self.assertTrue(
np.all(
np.isfinite(
samples[
"map_differential_max_"
"one_sided_consistency"
]
)
)
)
metrics = derive_trial_metrics(
samples, self.metric_configuration
)
self.assertEqual(metrics["h1_F_r"], 0)
self.assertEqual(metrics["h1_D_r"], 0)
self.assertEqual(metrics["h1_C_r"], 0)
self.assertEqual(
metrics["h1_phi_raw_wrap_crossing_count"],
expected_crossings[path_type],
)
self.assertTrue(metrics["h1_phi_wrap_metric_valid"])
self.assertLess(
metrics[
"h1_phi_wrap_crossing_max_slave_joint_step_rad"
],
0.25,
)
self.assertEqual(
metrics["h1_differential_valid_fraction"], 1.0
)
for name in (
"h1_pose_runtime_p50_ms",
"h1_pose_runtime_p95_ms",
"h1_pose_runtime_p99_ms",
"h1_pose_runtime_max_ms",
"h1_pose_runtime_warm_p95_ms",
"h1_differential_runtime_p50_ms",
"h1_differential_runtime_p95_ms",
"h1_differential_runtime_p99_ms",
"h1_differential_runtime_max_ms",
"h1_feedback_ready_runtime_p95_ms",
):
self.assertIn(name, metrics)
self.assertIsNotNone(metrics[name])
self.assertGreaterEqual(metrics[name], 0.0)
def test_non_sew_actual_differential_is_explicit_na_not_failure(self) -> None:
payload = self.na_payload
samples = payload.samples
self.assertTrue(
np.all(samples["map_differential_applicable"] == 0)
)
self.assertTrue(np.all(np.isnan(samples["map_differential_A"])))
np.testing.assert_array_equal(
samples["map_differential_valid"],
samples["map_branch_smooth"],
)
self.assertIsNotNone(payload.metadata["differential_n_a_reason"])
metrics = derive_trial_metrics(samples, self.metric_configuration)
self.assertEqual(metrics["h1_F_r"], 0)
self.assertEqual(metrics["h1_D_r"], 0)
self.assertEqual(metrics["h1_differential_applicable_fraction"], 0.0)
self.assertIsNone(metrics["h1_differential_valid_fraction"])
self.assertIsNone(metrics["h1_differential_runtime_p95_ms"])
def test_v3_metrics_reject_missing_or_nonbinary_evidence(self) -> None:
original = self.sew_payloads["linear"].samples
missing = dict(original)
missing.pop("map_differential_applicable")
with self.assertRaisesRegex(
MetricError, "requires v3 evidence fields"
):
derive_trial_metrics(missing, self.metric_configuration)
nonbinary = {
name: value.copy() for name, value in original.items()
}
nonbinary["map_pose_success"] = np.asarray(
nonbinary["map_pose_success"], dtype=float
)
nonbinary["map_pose_success"][0] = np.nan
with self.assertRaisesRegex(MetricError, "finite 0/1 flags"):
derive_trial_metrics(nonbinary, self.metric_configuration)
def test_empty_or_missing_wrap_evidence_cannot_look_perfect(self) -> None:
original = self.sew_payloads["linear"].samples
no_crossing = {
name: value.copy() for name, value in original.items()
}
no_crossing["map_sew_phi_rad"] = np.zeros_like(
no_crossing["map_sew_phi_rad"]
)
no_crossing_metrics = derive_trial_metrics(
no_crossing, self.metric_configuration
)
self.assertFalse(
no_crossing_metrics["h1_phi_wrap_metric_valid"]
)
self.assertIsNone(
no_crossing_metrics[
"h1_phi_wrap_crossing_max_slave_joint_step_rad"
]
)
no_accepted = {
name: value.copy() for name, value in original.items()
}
no_accepted["map_accepted"] = np.zeros_like(
no_accepted["map_accepted"]
)
empty_metrics = derive_trial_metrics(
no_accepted, self.metric_configuration
)
self.assertFalse(empty_metrics["h1_composite_metric_valid"])
self.assertIsNone(empty_metrics["h1_F_r"])
self.assertIsNone(empty_metrics["h1_D_r"])
self.assertIsNone(empty_metrics["h1_C_r"])
def test_metric_reason_labels_match_executor_enum(self) -> None:
self.assertEqual(
self.metric_configuration["h1"]["validity_reason_labels"],
[member.value for member in H1ValidityReason],
)
if __name__ == "__main__":
unittest.main()