Skip to content

Commit b904a63

Browse files
authored
Create test_thermal.py
1 parent 01ce3e8 commit b904a63

1 file changed

Lines changed: 255 additions & 0 deletions

File tree

tests/test_thermal.py

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
"""
2+
IX-HapticSight — Tests for normalized thermal interface models.
3+
4+
These tests verify that the backend-agnostic thermal layer can:
5+
- normalize thermal samples safely
6+
- compute hottest and range summaries
7+
- expose freshness/health usability checks
8+
- derive compact thermal assessments for caution and stop thresholds
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.signal_health import ( # noqa: E402
18+
FreshnessPolicy,
19+
SignalHealth,
20+
SignalQuality,
21+
SignalSourceMode,
22+
)
23+
from ohip_interfaces.thermal import ( # noqa: E402
24+
ThermalFrame,
25+
assess_thermal,
26+
make_thermal_sample,
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=41,
37+
source_name="surface_thermal",
38+
frame="forearm_link",
39+
)
40+
41+
42+
def test_make_thermal_sample_normalizes_values():
43+
sample = make_thermal_sample(
44+
zone_id="z1",
45+
temperature_c=36.5,
46+
location_xyz=[0.01, 0.02, 0.03],
47+
confidence=0.95,
48+
)
49+
50+
assert sample.zone_id == "z1"
51+
assert sample.temperature_c == 36.5
52+
assert sample.location_xyz is not None
53+
assert sample.location_xyz.x == 0.01
54+
assert sample.location_xyz.y == 0.02
55+
assert sample.location_xyz.z == 0.03
56+
assert sample.confidence == 0.95
57+
58+
59+
def test_thermal_frame_summaries_work_for_multiple_samples():
60+
sample_a = make_thermal_sample(
61+
zone_id="a",
62+
temperature_c=34.0,
63+
location_xyz=[0.0, 0.0, 0.0],
64+
confidence=1.0,
65+
)
66+
sample_b = make_thermal_sample(
67+
zone_id="b",
68+
temperature_c=41.2,
69+
location_xyz=[0.1, 0.0, 0.0],
70+
confidence=0.9,
71+
)
72+
sample_c = make_thermal_sample(
73+
zone_id="c",
74+
temperature_c=37.8,
75+
location_xyz=[0.2, 0.0, 0.0],
76+
confidence=0.8,
77+
)
78+
79+
frame = ThermalFrame(
80+
sensor_name="surface_array",
81+
frame="forearm_link",
82+
quality=make_quality(),
83+
samples=(sample_a, sample_b, sample_c),
84+
)
85+
86+
assert frame.sample_count() == 3
87+
assert frame.has_samples() is True
88+
assert frame.min_temperature_c() == 34.0
89+
assert frame.max_temperature_c() == 41.2
90+
91+
hottest = frame.hottest_sample()
92+
assert hottest is not None
93+
assert hottest.zone_id == "b"
94+
assert hottest.temperature_c == 41.2
95+
96+
97+
def test_thermal_frame_handles_empty_samples():
98+
frame = ThermalFrame(
99+
sensor_name="surface_array",
100+
frame="forearm_link",
101+
quality=make_quality(),
102+
samples=(),
103+
)
104+
105+
assert frame.sample_count() == 0
106+
assert frame.has_samples() is False
107+
assert frame.min_temperature_c() is None
108+
assert frame.max_temperature_c() is None
109+
assert frame.hottest_sample() is None
110+
111+
112+
def test_thermal_frame_respects_freshness_and_usability():
113+
policy = FreshnessPolicy(max_age_ms=250, required=True)
114+
115+
frame = ThermalFrame(
116+
sensor_name="surface_array",
117+
frame="forearm_link",
118+
quality=make_quality(sample_t=50.0, received_t=50.002),
119+
samples=(),
120+
)
121+
122+
assert frame.is_fresh(policy, now_utc_s=50.20) is True
123+
assert frame.is_usable(policy, now_utc_s=50.20) is True
124+
assert frame.is_fresh(policy, now_utc_s=50.30) is False
125+
assert frame.is_usable(policy, now_utc_s=50.30) is False
126+
127+
128+
def test_assess_thermal_detects_caution_and_stop_thresholds():
129+
sample_a = make_thermal_sample(
130+
zone_id="a",
131+
temperature_c=37.9,
132+
confidence=1.0,
133+
)
134+
sample_b = make_thermal_sample(
135+
zone_id="b",
136+
temperature_c=46.2,
137+
confidence=1.0,
138+
)
139+
140+
frame = ThermalFrame(
141+
sensor_name="surface_array",
142+
frame="forearm_link",
143+
quality=make_quality(),
144+
samples=(sample_a, sample_b),
145+
)
146+
147+
assessment = assess_thermal(
148+
frame,
149+
caution_temperature_c=38.0,
150+
stop_temperature_c=45.0,
151+
)
152+
153+
assert assessment.heat_detected is True
154+
assert assessment.over_limit is True
155+
assert assessment.sample_count == 2
156+
assert assessment.hottest_temperature_c == 46.2
157+
assert assessment.caution_temperature_c == 38.0
158+
assert assessment.stop_temperature_c == 45.0
159+
160+
161+
def test_assess_thermal_can_report_below_threshold_or_empty():
162+
below_frame = ThermalFrame(
163+
sensor_name="surface_array",
164+
frame="forearm_link",
165+
quality=make_quality(),
166+
samples=(
167+
make_thermal_sample(zone_id="a", temperature_c=35.0),
168+
make_thermal_sample(zone_id="b", temperature_c=37.5),
169+
),
170+
)
171+
172+
below_assessment = assess_thermal(
173+
below_frame,
174+
caution_temperature_c=38.0,
175+
stop_temperature_c=45.0,
176+
)
177+
178+
assert below_assessment.heat_detected is False
179+
assert below_assessment.over_limit is False
180+
assert below_assessment.hottest_temperature_c == 37.5
181+
182+
empty_frame = ThermalFrame(
183+
sensor_name="surface_array",
184+
frame="forearm_link",
185+
quality=make_quality(),
186+
samples=(),
187+
)
188+
189+
empty_assessment = assess_thermal(
190+
empty_frame,
191+
caution_temperature_c=38.0,
192+
stop_temperature_c=45.0,
193+
)
194+
195+
assert empty_assessment.heat_detected is False
196+
assert empty_assessment.over_limit is False
197+
assert empty_assessment.sample_count == 0
198+
assert empty_assessment.hottest_temperature_c is None
199+
200+
201+
def test_make_thermal_sample_rejects_invalid_inputs():
202+
try:
203+
make_thermal_sample(
204+
zone_id="bad-location",
205+
temperature_c=36.0,
206+
location_xyz=[0.0, 1.0],
207+
)
208+
bad_location = False
209+
except ValueError:
210+
bad_location = True
211+
212+
try:
213+
make_thermal_sample(
214+
zone_id="bad-confidence-low",
215+
temperature_c=36.0,
216+
confidence=-0.1,
217+
)
218+
bad_conf_low = False
219+
except ValueError:
220+
bad_conf_low = True
221+
222+
try:
223+
make_thermal_sample(
224+
zone_id="bad-confidence-high",
225+
temperature_c=36.0,
226+
confidence=1.1,
227+
)
228+
bad_conf_high = False
229+
except ValueError:
230+
bad_conf_high = True
231+
232+
assert bad_location is True
233+
assert bad_conf_low is True
234+
assert bad_conf_high is True
235+
236+
237+
def test_assess_thermal_rejects_invalid_threshold_order():
238+
frame = ThermalFrame(
239+
sensor_name="surface_array",
240+
frame="forearm_link",
241+
quality=make_quality(),
242+
samples=(),
243+
)
244+
245+
try:
246+
assess_thermal(
247+
frame,
248+
caution_temperature_c=45.0,
249+
stop_temperature_c=38.0,
250+
)
251+
raised = False
252+
except ValueError:
253+
raised = True
254+
255+
assert raised is True

0 commit comments

Comments
 (0)