Skip to content

Commit 8e765f6

Browse files
authored
Create test_proximity.py
1 parent af6a223 commit 8e765f6

1 file changed

Lines changed: 314 additions & 0 deletions

File tree

tests/test_proximity.py

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
"""
2+
IX-HapticSight — Tests for normalized proximity interface models.
3+
4+
These tests verify that the backend-agnostic proximity layer can:
5+
- normalize proximity returns safely
6+
- compute nearest and range summaries
7+
- expose freshness/health usability checks
8+
- derive compact proximity assessments for near-contact and corridor checks
9+
"""
10+
11+
import os
12+
import sys
13+
14+
# Make project packages importable without packaging/install
15+
sys.path.insert(0, os.path.abspath("src"))
16+
17+
from ohip_interfaces.proximity import ( # noqa: E402
18+
ProximityFrame,
19+
assess_proximity,
20+
make_proximity_return,
21+
)
22+
from ohip_interfaces.signal_health import ( # noqa: E402
23+
FreshnessPolicy,
24+
SignalHealth,
25+
SignalQuality,
26+
SignalSourceMode,
27+
)
28+
29+
30+
def make_quality(*, sample_t: float = 100.0, received_t: float = 100.01) -> SignalQuality:
31+
return SignalQuality(
32+
source_mode=SignalSourceMode.LIVE,
33+
health=SignalHealth.NOMINAL,
34+
sample_timestamp_utc_s=sample_t,
35+
received_timestamp_utc_s=received_t,
36+
sequence_id=31,
37+
source_name="forearm_proximity",
38+
frame="forearm_link",
39+
)
40+
41+
42+
def test_make_proximity_return_normalizes_values():
43+
prox = make_proximity_return(
44+
zone_id="z1",
45+
distance_mm=85.0,
46+
direction_xyz=[0.0, 0.0, 1.0],
47+
point_xyz=[0.01, 0.02, 0.03],
48+
confidence=0.9,
49+
)
50+
51+
assert prox.zone_id == "z1"
52+
assert prox.distance_mm == 85.0
53+
assert prox.direction_xyz.z == 1.0
54+
assert prox.point_xyz is not None
55+
assert prox.point_xyz.x == 0.01
56+
assert prox.point_xyz.y == 0.02
57+
assert prox.point_xyz.z == 0.03
58+
assert prox.confidence == 0.9
59+
60+
61+
def test_proximity_frame_summaries_work_for_multiple_returns():
62+
ret_a = make_proximity_return(
63+
zone_id="a",
64+
distance_mm=120.0,
65+
direction_xyz=[1.0, 0.0, 0.0],
66+
confidence=0.8,
67+
)
68+
ret_b = make_proximity_return(
69+
zone_id="b",
70+
distance_mm=55.0,
71+
direction_xyz=[0.0, 1.0, 0.0],
72+
confidence=1.0,
73+
)
74+
ret_c = make_proximity_return(
75+
zone_id="c",
76+
distance_mm=200.0,
77+
direction_xyz=[0.0, 0.0, 1.0],
78+
confidence=0.7,
79+
)
80+
81+
frame = ProximityFrame(
82+
sensor_name="forearm_ring",
83+
frame="forearm_link",
84+
quality=make_quality(),
85+
returns=(ret_a, ret_b, ret_c),
86+
)
87+
88+
assert frame.return_count() == 3
89+
assert frame.has_returns() is True
90+
assert frame.min_distance_mm() == 55.0
91+
assert frame.max_distance_mm() == 200.0
92+
nearest = frame.nearest_return()
93+
assert nearest is not None
94+
assert nearest.zone_id == "b"
95+
assert nearest.distance_mm == 55.0
96+
97+
98+
def test_proximity_frame_handles_empty_returns():
99+
frame = ProximityFrame(
100+
sensor_name="forearm_ring",
101+
frame="forearm_link",
102+
quality=make_quality(),
103+
returns=(),
104+
)
105+
106+
assert frame.return_count() == 0
107+
assert frame.has_returns() is False
108+
assert frame.min_distance_mm() is None
109+
assert frame.max_distance_mm() is None
110+
assert frame.nearest_return() is None
111+
112+
113+
def test_proximity_frame_respects_freshness_and_usability():
114+
policy = FreshnessPolicy(max_age_ms=250, required=True)
115+
116+
frame = ProximityFrame(
117+
sensor_name="palm_ring",
118+
frame="palm_link",
119+
quality=make_quality(sample_t=50.0, received_t=50.002),
120+
returns=(),
121+
)
122+
123+
assert frame.is_fresh(policy, now_utc_s=50.20) is True
124+
assert frame.is_usable(policy, now_utc_s=50.20) is True
125+
assert frame.is_fresh(policy, now_utc_s=50.30) is False
126+
assert frame.is_usable(policy, now_utc_s=50.30) is False
127+
128+
129+
def test_assess_proximity_detects_near_contact_and_stop_zone():
130+
ret_a = make_proximity_return(
131+
zone_id="a",
132+
distance_mm=35.0,
133+
direction_xyz=[1.0, 0.0, 0.0],
134+
confidence=1.0,
135+
)
136+
ret_b = make_proximity_return(
137+
zone_id="b",
138+
distance_mm=90.0,
139+
direction_xyz=[0.0, 1.0, 0.0],
140+
confidence=1.0,
141+
)
142+
143+
frame = ProximityFrame(
144+
sensor_name="wrist_ring",
145+
frame="tool0",
146+
quality=make_quality(),
147+
returns=(ret_a, ret_b),
148+
)
149+
150+
assessment = assess_proximity(
151+
frame,
152+
caution_distance_mm=120.0,
153+
stop_distance_mm=40.0,
154+
)
155+
156+
assert assessment.object_detected is True
157+
assert assessment.near_contact is True
158+
assert assessment.corridor_clear is False
159+
assert assessment.return_count == 2
160+
assert assessment.nearest_distance_mm == 35.0
161+
assert assessment.caution_distance_mm == 120.0
162+
assert assessment.stop_distance_mm == 40.0
163+
164+
165+
def test_assess_proximity_can_report_caution_without_stop_violation():
166+
ret = make_proximity_return(
167+
zone_id="only",
168+
distance_mm=75.0,
169+
direction_xyz=[0.0, 0.0, 1.0],
170+
confidence=0.95,
171+
)
172+
173+
frame = ProximityFrame(
174+
sensor_name="wrist_ring",
175+
frame="tool0",
176+
quality=make_quality(),
177+
returns=(ret,),
178+
)
179+
180+
assessment = assess_proximity(
181+
frame,
182+
caution_distance_mm=120.0,
183+
stop_distance_mm=40.0,
184+
)
185+
186+
assert assessment.object_detected is True
187+
assert assessment.near_contact is True
188+
assert assessment.corridor_clear is True
189+
assert assessment.nearest_distance_mm == 75.0
190+
191+
192+
def test_assess_proximity_can_report_clear_corridor_when_no_returns():
193+
frame = ProximityFrame(
194+
sensor_name="wrist_ring",
195+
frame="tool0",
196+
quality=make_quality(),
197+
returns=(),
198+
)
199+
200+
assessment = assess_proximity(
201+
frame,
202+
caution_distance_mm=120.0,
203+
stop_distance_mm=40.0,
204+
)
205+
206+
assert assessment.object_detected is False
207+
assert assessment.near_contact is False
208+
assert assessment.corridor_clear is True
209+
assert assessment.return_count == 0
210+
assert assessment.nearest_distance_mm is None
211+
212+
213+
def test_make_proximity_return_rejects_invalid_inputs():
214+
try:
215+
make_proximity_return(
216+
zone_id="bad-dir",
217+
distance_mm=50.0,
218+
direction_xyz=[1.0, 0.0],
219+
)
220+
bad_dir = False
221+
except ValueError:
222+
bad_dir = True
223+
224+
try:
225+
make_proximity_return(
226+
zone_id="bad-point",
227+
distance_mm=50.0,
228+
direction_xyz=[1.0, 0.0, 0.0],
229+
point_xyz=[0.0, 1.0],
230+
)
231+
bad_point = False
232+
except ValueError:
233+
bad_point = True
234+
235+
try:
236+
make_proximity_return(
237+
zone_id="bad-distance",
238+
distance_mm=-1.0,
239+
direction_xyz=[1.0, 0.0, 0.0],
240+
)
241+
bad_distance = False
242+
except ValueError:
243+
bad_distance = True
244+
245+
try:
246+
make_proximity_return(
247+
zone_id="bad-confidence-low",
248+
distance_mm=10.0,
249+
direction_xyz=[1.0, 0.0, 0.0],
250+
confidence=-0.1,
251+
)
252+
bad_conf_low = False
253+
except ValueError:
254+
bad_conf_low = True
255+
256+
try:
257+
make_proximity_return(
258+
zone_id="bad-confidence-high",
259+
distance_mm=10.0,
260+
direction_xyz=[1.0, 0.0, 0.0],
261+
confidence=1.1,
262+
)
263+
bad_conf_high = False
264+
except ValueError:
265+
bad_conf_high = True
266+
267+
assert bad_dir is True
268+
assert bad_point is True
269+
assert bad_distance is True
270+
assert bad_conf_low is True
271+
assert bad_conf_high is True
272+
273+
274+
def test_assess_proximity_rejects_invalid_thresholds():
275+
frame = ProximityFrame(
276+
sensor_name="ring",
277+
frame="tool0",
278+
quality=make_quality(),
279+
returns=(),
280+
)
281+
282+
try:
283+
assess_proximity(
284+
frame,
285+
caution_distance_mm=-1.0,
286+
stop_distance_mm=40.0,
287+
)
288+
bad_caution = False
289+
except ValueError:
290+
bad_caution = True
291+
292+
try:
293+
assess_proximity(
294+
frame,
295+
caution_distance_mm=120.0,
296+
stop_distance_mm=-1.0,
297+
)
298+
bad_stop = False
299+
except ValueError:
300+
bad_stop = True
301+
302+
try:
303+
assess_proximity(
304+
frame,
305+
caution_distance_mm=40.0,
306+
stop_distance_mm=120.0,
307+
)
308+
bad_order = False
309+
except ValueError:
310+
bad_order = True
311+
312+
assert bad_caution is True
313+
assert bad_stop is True
314+
assert bad_order is True

0 commit comments

Comments
 (0)