Skip to content

Commit d3e3523

Browse files
authored
Add tests for force/torque interface models
This file contains tests for the normalized force and torque interface models, verifying functionality such as vector normalization, magnitude computation, freshness checks, and contact force assessments.
1 parent 8c96065 commit d3e3523

1 file changed

Lines changed: 182 additions & 0 deletions

File tree

tests/test_force_torque.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""
2+
IX-HapticSight — Tests for normalized force/torque interface models.
3+
4+
These tests verify that the backend-agnostic force/torque layer can:
5+
- normalize 3-axis force and torque payloads
6+
- compute magnitudes correctly
7+
- expose freshness/health usability checks
8+
- derive compact contact-force assessments
9+
"""
10+
11+
import math
12+
import os
13+
import sys
14+
15+
# Make project packages importable without packaging/install
16+
sys.path.insert(0, os.path.abspath("src"))
17+
18+
from ohip_interfaces.force_torque import ( # noqa: E402
19+
ForceTorqueSample,
20+
assess_contact_force,
21+
make_force_torque_sample,
22+
)
23+
from ohip_interfaces.signal_health import ( # noqa: E402
24+
FreshnessPolicy,
25+
SignalHealth,
26+
SignalQuality,
27+
SignalSourceMode,
28+
)
29+
30+
31+
def make_quality(*, sample_t: float = 100.0, received_t: float = 100.01) -> SignalQuality:
32+
return SignalQuality(
33+
source_mode=SignalSourceMode.LIVE,
34+
health=SignalHealth.NOMINAL,
35+
sample_timestamp_utc_s=sample_t,
36+
received_timestamp_utc_s=received_t,
37+
sequence_id=7,
38+
source_name="wrist_ft",
39+
frame="tool0",
40+
)
41+
42+
43+
def test_make_force_torque_sample_normalizes_vectors():
44+
sample = make_force_torque_sample(
45+
frame="tool0",
46+
force_xyz=[0.3, 0.4, 0.0],
47+
torque_xyz=[0.0, 0.0, 0.2],
48+
quality=make_quality(),
49+
)
50+
51+
assert isinstance(sample, ForceTorqueSample)
52+
assert sample.frame == "tool0"
53+
assert sample.force.x == 0.3
54+
assert sample.force.y == 0.4
55+
assert sample.force.z == 0.0
56+
assert sample.torque.z == 0.2
57+
58+
59+
def test_force_and_torque_magnitudes_are_computed_correctly():
60+
sample = make_force_torque_sample(
61+
frame="tool0",
62+
force_xyz=[0.3, 0.4, 0.0],
63+
torque_xyz=[0.0, 0.0, 0.5],
64+
quality=make_quality(),
65+
)
66+
67+
assert math.isclose(sample.force_magnitude_N(), 0.5, rel_tol=0.0, abs_tol=1e-12)
68+
assert math.isclose(sample.torque_magnitude_Nm(), 0.5, rel_tol=0.0, abs_tol=1e-12)
69+
70+
71+
def test_force_torque_sample_respects_freshness_and_usability():
72+
policy = FreshnessPolicy(max_age_ms=250, required=True)
73+
74+
fresh_sample = make_force_torque_sample(
75+
frame="tool0",
76+
force_xyz=[0.1, 0.1, 0.1],
77+
torque_xyz=[0.0, 0.0, 0.0],
78+
quality=make_quality(sample_t=50.0, received_t=50.002),
79+
)
80+
assert fresh_sample.is_fresh(policy, now_utc_s=50.20) is True
81+
assert fresh_sample.is_usable(policy, now_utc_s=50.20) is True
82+
83+
stale_sample = make_force_torque_sample(
84+
frame="tool0",
85+
force_xyz=[0.1, 0.1, 0.1],
86+
torque_xyz=[0.0, 0.0, 0.0],
87+
quality=make_quality(sample_t=50.0, received_t=50.002),
88+
)
89+
assert stale_sample.is_fresh(policy, now_utc_s=50.30) is False
90+
assert stale_sample.is_usable(policy, now_utc_s=50.30) is False
91+
92+
93+
def test_assess_contact_force_detects_contact_and_excessive_force():
94+
sample = make_force_torque_sample(
95+
frame="tool0",
96+
force_xyz=[0.0, 0.0, 1.25],
97+
torque_xyz=[0.0, 0.1, 0.0],
98+
quality=make_quality(),
99+
)
100+
101+
assessment = assess_contact_force(
102+
sample,
103+
contact_threshold_N=0.25,
104+
excessive_threshold_N=1.0,
105+
)
106+
107+
assert assessment.contact_detected is True
108+
assert assessment.excessive_force is True
109+
assert math.isclose(assessment.force_magnitude_N, 1.25, rel_tol=0.0, abs_tol=1e-12)
110+
assert math.isclose(assessment.torque_magnitude_Nm, 0.1, rel_tol=0.0, abs_tol=1e-12)
111+
assert assessment.threshold_contact_N == 0.25
112+
assert assessment.threshold_excessive_N == 1.0
113+
114+
115+
def test_assess_contact_force_can_report_no_contact():
116+
sample = make_force_torque_sample(
117+
frame="tool0",
118+
force_xyz=[0.05, 0.05, 0.05],
119+
torque_xyz=[0.0, 0.0, 0.0],
120+
quality=make_quality(),
121+
)
122+
123+
assessment = assess_contact_force(
124+
sample,
125+
contact_threshold_N=0.25,
126+
excessive_threshold_N=1.0,
127+
)
128+
129+
assert assessment.contact_detected is False
130+
assert assessment.excessive_force is False
131+
assert assessment.force_magnitude_N < 0.25
132+
133+
134+
def test_make_force_torque_sample_rejects_bad_vector_lengths():
135+
try:
136+
make_force_torque_sample(
137+
frame="tool0",
138+
force_xyz=[1.0, 2.0],
139+
torque_xyz=[0.0, 0.0, 0.0],
140+
quality=make_quality(),
141+
)
142+
raised_force = False
143+
except ValueError:
144+
raised_force = True
145+
146+
try:
147+
make_force_torque_sample(
148+
frame="tool0",
149+
force_xyz=[0.0, 0.0, 0.0],
150+
torque_xyz=[1.0, 2.0],
151+
quality=make_quality(),
152+
)
153+
raised_torque = False
154+
except ValueError:
155+
raised_torque = True
156+
157+
assert raised_force is True
158+
assert raised_torque is True
159+
160+
161+
def test_assess_contact_force_rejects_invalid_thresholds():
162+
sample = make_force_torque_sample(
163+
frame="tool0",
164+
force_xyz=[0.0, 0.0, 0.5],
165+
torque_xyz=[0.0, 0.0, 0.0],
166+
quality=make_quality(),
167+
)
168+
169+
try:
170+
assess_contact_force(sample, contact_threshold_N=-0.1, excessive_threshold_N=1.0)
171+
raised_negative = False
172+
except ValueError:
173+
raised_negative = True
174+
175+
try:
176+
assess_contact_force(sample, contact_threshold_N=1.0, excessive_threshold_N=0.5)
177+
raised_order = False
178+
except ValueError:
179+
raised_order = True
180+
181+
assert raised_negative is True
182+
assert raised_order is True

0 commit comments

Comments
 (0)