Skip to content

Commit eb1cb6a

Browse files
authored
Create test_tactile.py
1 parent 84a3f59 commit eb1cb6a

1 file changed

Lines changed: 303 additions & 0 deletions

File tree

tests/test_tactile.py

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
"""
2+
IX-HapticSight — Tests for normalized tactile interface models.
3+
4+
These tests verify that the backend-agnostic tactile layer can:
5+
- normalize tactile patches safely
6+
- compute patch, area, pressure, and shear summaries
7+
- expose freshness/health usability checks
8+
- derive compact tactile contact 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.signal_health import ( # noqa: E402
19+
FreshnessPolicy,
20+
SignalHealth,
21+
SignalQuality,
22+
SignalSourceMode,
23+
)
24+
from ohip_interfaces.tactile import ( # noqa: E402
25+
TactileFrame,
26+
assess_tactile_contact,
27+
make_tactile_patch,
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=21,
38+
source_name="forearm_tactile",
39+
frame="forearm_link",
40+
)
41+
42+
43+
def test_make_tactile_patch_normalizes_values():
44+
patch = make_tactile_patch(
45+
patch_id="p1",
46+
location_xyz=[0.01, 0.02, 0.03],
47+
normal_xyz=[0.0, 0.0, 1.0],
48+
area_mm2=15.5,
49+
pressure_kpa=2.2,
50+
shear_xy_kpa=[0.5, 0.25],
51+
)
52+
53+
assert patch.patch_id == "p1"
54+
assert patch.location_xyz.x == 0.01
55+
assert patch.location_xyz.y == 0.02
56+
assert patch.location_xyz.z == 0.03
57+
assert patch.normal_xyz.z == 1.0
58+
assert patch.area_mm2 == 15.5
59+
assert patch.pressure_kpa == 2.2
60+
assert patch.shear_xy_kpa == (0.5, 0.25)
61+
62+
63+
def test_tactile_patch_shear_magnitude_is_computed_correctly():
64+
patch = make_tactile_patch(
65+
patch_id="p2",
66+
location_xyz=[0.0, 0.0, 0.0],
67+
normal_xyz=[0.0, 1.0, 0.0],
68+
area_mm2=10.0,
69+
pressure_kpa=1.0,
70+
shear_xy_kpa=[3.0, 4.0],
71+
)
72+
73+
assert math.isclose(patch.shear_magnitude_kpa(), 5.0, rel_tol=0.0, abs_tol=1e-12)
74+
75+
76+
def test_tactile_frame_summaries_work_for_multiple_patches():
77+
patch_a = make_tactile_patch(
78+
patch_id="a",
79+
location_xyz=[0.01, 0.01, 0.00],
80+
normal_xyz=[0.0, 0.0, 1.0],
81+
area_mm2=12.0,
82+
pressure_kpa=2.0,
83+
shear_xy_kpa=[0.3, 0.4],
84+
)
85+
patch_b = make_tactile_patch(
86+
patch_id="b",
87+
location_xyz=[0.02, 0.02, 0.00],
88+
normal_xyz=[0.0, 0.0, 1.0],
89+
area_mm2=18.0,
90+
pressure_kpa=4.5,
91+
shear_xy_kpa=[0.0, 1.2],
92+
)
93+
94+
frame = TactileFrame(
95+
surface_name="forearm_pad",
96+
frame="forearm_link",
97+
quality=make_quality(),
98+
patches=(patch_a, patch_b),
99+
)
100+
101+
assert frame.patch_count() == 2
102+
assert frame.has_contact() is True
103+
assert frame.total_area_mm2() == 30.0
104+
assert frame.max_pressure_kpa() == 4.5
105+
assert frame.max_shear_kpa() == 1.2
106+
107+
108+
def test_tactile_frame_respects_freshness_and_usability():
109+
policy = FreshnessPolicy(max_age_ms=250, required=True)
110+
111+
frame = TactileFrame(
112+
surface_name="palm_pad",
113+
frame="palm_link",
114+
quality=make_quality(sample_t=50.0, received_t=50.002),
115+
patches=(),
116+
)
117+
118+
assert frame.is_fresh(policy, now_utc_s=50.20) is True
119+
assert frame.is_usable(policy, now_utc_s=50.20) is True
120+
assert frame.is_fresh(policy, now_utc_s=50.30) is False
121+
assert frame.is_usable(policy, now_utc_s=50.30) is False
122+
123+
124+
def test_assess_tactile_contact_detects_contact_and_excessive_values():
125+
patch_a = make_tactile_patch(
126+
patch_id="a",
127+
location_xyz=[0.0, 0.0, 0.0],
128+
normal_xyz=[0.0, 0.0, 1.0],
129+
area_mm2=20.0,
130+
pressure_kpa=6.0,
131+
shear_xy_kpa=[1.0, 1.0],
132+
)
133+
patch_b = make_tactile_patch(
134+
patch_id="b",
135+
location_xyz=[0.0, 0.0, 0.0],
136+
normal_xyz=[0.0, 0.0, 1.0],
137+
area_mm2=10.0,
138+
pressure_kpa=12.0,
139+
shear_xy_kpa=[4.0, 4.0],
140+
)
141+
142+
frame = TactileFrame(
143+
surface_name="forearm_pad",
144+
frame="forearm_link",
145+
quality=make_quality(),
146+
patches=(patch_a, patch_b),
147+
)
148+
149+
assessment = assess_tactile_contact(
150+
frame,
151+
pressure_threshold_kpa=0.5,
152+
excessive_pressure_threshold_kpa=10.0,
153+
excessive_shear_threshold_kpa=5.0,
154+
)
155+
156+
assert assessment.contact_detected is True
157+
assert assessment.multi_patch_contact is True
158+
assert assessment.patch_count == 2
159+
assert assessment.total_area_mm2 == 30.0
160+
assert assessment.max_pressure_kpa == 12.0
161+
assert math.isclose(assessment.max_shear_kpa, math.sqrt(32.0), rel_tol=0.0, abs_tol=1e-12)
162+
assert assessment.excessive_pressure is True
163+
assert assessment.excessive_shear is True
164+
165+
166+
def test_assess_tactile_contact_can_report_no_contact():
167+
frame = TactileFrame(
168+
surface_name="palm_pad",
169+
frame="palm_link",
170+
quality=make_quality(),
171+
patches=(),
172+
)
173+
174+
assessment = assess_tactile_contact(
175+
frame,
176+
pressure_threshold_kpa=0.5,
177+
excessive_pressure_threshold_kpa=10.0,
178+
excessive_shear_threshold_kpa=5.0,
179+
)
180+
181+
assert assessment.contact_detected is False
182+
assert assessment.multi_patch_contact is False
183+
assert assessment.patch_count == 0
184+
assert assessment.total_area_mm2 == 0.0
185+
assert assessment.max_pressure_kpa == 0.0
186+
assert assessment.max_shear_kpa == 0.0
187+
assert assessment.excessive_pressure is False
188+
assert assessment.excessive_shear is False
189+
190+
191+
def test_make_tactile_patch_rejects_invalid_inputs():
192+
try:
193+
make_tactile_patch(
194+
patch_id="bad-loc",
195+
location_xyz=[0.0, 0.0],
196+
normal_xyz=[0.0, 0.0, 1.0],
197+
area_mm2=1.0,
198+
pressure_kpa=1.0,
199+
)
200+
bad_loc = False
201+
except ValueError:
202+
bad_loc = True
203+
204+
try:
205+
make_tactile_patch(
206+
patch_id="bad-normal",
207+
location_xyz=[0.0, 0.0, 0.0],
208+
normal_xyz=[0.0, 1.0],
209+
area_mm2=1.0,
210+
pressure_kpa=1.0,
211+
)
212+
bad_normal = False
213+
except ValueError:
214+
bad_normal = True
215+
216+
try:
217+
make_tactile_patch(
218+
patch_id="bad-shear",
219+
location_xyz=[0.0, 0.0, 0.0],
220+
normal_xyz=[0.0, 0.0, 1.0],
221+
area_mm2=1.0,
222+
pressure_kpa=1.0,
223+
shear_xy_kpa=[1.0],
224+
)
225+
bad_shear = False
226+
except ValueError:
227+
bad_shear = True
228+
229+
try:
230+
make_tactile_patch(
231+
patch_id="bad-area",
232+
location_xyz=[0.0, 0.0, 0.0],
233+
normal_xyz=[0.0, 0.0, 1.0],
234+
area_mm2=-1.0,
235+
pressure_kpa=1.0,
236+
)
237+
bad_area = False
238+
except ValueError:
239+
bad_area = True
240+
241+
try:
242+
make_tactile_patch(
243+
patch_id="bad-pressure",
244+
location_xyz=[0.0, 0.0, 0.0],
245+
normal_xyz=[0.0, 0.0, 1.0],
246+
area_mm2=1.0,
247+
pressure_kpa=-0.1,
248+
)
249+
bad_pressure = False
250+
except ValueError:
251+
bad_pressure = True
252+
253+
assert bad_loc is True
254+
assert bad_normal is True
255+
assert bad_shear is True
256+
assert bad_area is True
257+
assert bad_pressure is True
258+
259+
260+
def test_assess_tactile_contact_rejects_invalid_thresholds():
261+
frame = TactileFrame(
262+
surface_name="pad",
263+
frame="pad_link",
264+
quality=make_quality(),
265+
patches=(),
266+
)
267+
268+
try:
269+
assess_tactile_contact(
270+
frame,
271+
pressure_threshold_kpa=-0.1,
272+
excessive_pressure_threshold_kpa=10.0,
273+
excessive_shear_threshold_kpa=5.0,
274+
)
275+
bad_pressure_threshold = False
276+
except ValueError:
277+
bad_pressure_threshold = True
278+
279+
try:
280+
assess_tactile_contact(
281+
frame,
282+
pressure_threshold_kpa=1.0,
283+
excessive_pressure_threshold_kpa=0.5,
284+
excessive_shear_threshold_kpa=5.0,
285+
)
286+
bad_excessive_pressure = False
287+
except ValueError:
288+
bad_excessive_pressure = True
289+
290+
try:
291+
assess_tactile_contact(
292+
frame,
293+
pressure_threshold_kpa=0.5,
294+
excessive_pressure_threshold_kpa=10.0,
295+
excessive_shear_threshold_kpa=-1.0,
296+
)
297+
bad_shear_threshold = False
298+
except ValueError:
299+
bad_shear_threshold = True
300+
301+
assert bad_pressure_threshold is True
302+
assert bad_excessive_pressure is True
303+
assert bad_shear_threshold is True

0 commit comments

Comments
 (0)