Skip to content

Commit c17b367

Browse files
committed
Lidar Parser updates and unit tests
2 parents 2bdc73a + 52f39eb commit c17b367

File tree

4 files changed

+231
-7
lines changed

4 files changed

+231
-7
lines changed

modules/lidar_oscillation.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""
2+
Representing a LiDAR Oscillation
3+
"""
4+
5+
from . import lidar_detection
6+
7+
8+
class LidarOscillation:
9+
"""
10+
Class to represent a collection of LiDAR readings that make up an oscillation.
11+
"""
12+
13+
__create_key = object()
14+
15+
@classmethod
16+
def create(
17+
cls, readings: list[lidar_detection.LidarDetection]
18+
) -> "tuple[bool, LidarOscillation | None]":
19+
"""
20+
Create a new LidarOscillation object from a list of LidarReading objects.
21+
"""
22+
if not readings:
23+
return False, None
24+
return True, LidarOscillation(cls.__create_key, readings)
25+
26+
def __init__(
27+
self, class_private_create_key: object, readings: list[lidar_detection.LidarDetection]
28+
) -> None:
29+
"""
30+
Private constructor, use create() method to instantiate.
31+
"""
32+
assert class_private_create_key is LidarOscillation.__create_key, "Use the create() method"
33+
34+
self.readings = readings
35+
angles = [reading.angle for reading in readings]
36+
self.min_angle = min(angles)
37+
self.max_angle = max(angles)
38+
39+
def __str__(self) -> str:
40+
"""
41+
Return a string representation of the LiDAR oscillation data.
42+
"""
43+
reading_strs = []
44+
for reading in self.readings:
45+
reading_strs.append(str(reading))
46+
formatted_readings = ", ".join(reading_strs)
47+
return f"LidarOscillation: {len(self.readings)} readings, Min angle: {self.min_angle}, Max angle: {self.max_angle}, Readings: {formatted_readings}."

modules/lidar_parser/lidar_parser.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,10 @@ class LidarParser:
2323
Class to handle parsing of LiDAR data stream and detecting complete oscillations.
2424
"""
2525

26-
def __init__(self, class_private_create_key: object) -> None:
26+
def __init__(self) -> None:
2727
"""
2828
Private constructor for LidarParser. Use create() method.
2929
"""
30-
assert class_private_create_key is LidarParser.__create_key, "Use the create() method"
3130

3231
self.lidar_readings = []
3332
self.current_oscillation = None
@@ -41,24 +40,26 @@ def run(
4140
"""
4241
Process a single LidarDetection and return the oscillation if complete.
4342
"""
44-
4543
self.lidar_readings.append(lidar_detection)
46-
4744
current_angle = lidar_detection.angle
45+
4846
if self.last_angle is None:
4947
self.last_angle = current_angle
5048
return False, None
5149

50+
# Detect oscillation on angle change with correct direction reset
5251
if current_angle > self.last_angle and self.direction == Direction.DOWN:
5352
result, oscillation = lidar_oscillation.LidarOscillation.create(self.lidar_readings)
5453
self.direction = Direction.UP
5554
self.lidar_readings = []
55+
self.last_angle = current_angle
5656
return result, oscillation
5757

5858
elif current_angle < self.last_angle and self.direction == Direction.UP:
5959
result, oscillation = lidar_oscillation.LidarOscillation.create(self.lidar_readings)
6060
self.direction = Direction.DOWN
6161
self.lidar_readings = []
62+
self.last_angle = current_angle
6263
return result, oscillation
6364

6465
elif self.direction is Direction.NONE:

modules/lidar_parser/lidar_parser_worker.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def lidar_oscillation_worker(
1919
"""
2020

2121
parser = lidar_parser.LidarParser()
22-
if not result:
22+
if not parser:
2323
print("Failed to initialise LidarParser.")
2424
return
2525

@@ -28,8 +28,7 @@ def lidar_oscillation_worker(
2828

2929
lidar_reading: lidar_detection.LidarDetection = detection_in_queue.queue.get()
3030
if lidar_reading is None:
31-
continue
32-
31+
break
3332
result, oscillation = parser.run(lidar_reading)
3433
if not result:
3534
continue

tests/unit/test_lidar_parser.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"""
2+
Unit tests for lidar_parser module.
3+
"""
4+
5+
import pytest
6+
from modules import lidar_detection
7+
from modules import lidar_oscillation
8+
from modules.lidar_parser import lidar_parser
9+
10+
ANGLE_UP = 15.0
11+
ANGLE_DOWN = -15.0
12+
13+
14+
@pytest.fixture()
15+
def lidar_parser_instance() -> lidar_parser.LidarParser: # type: ignore
16+
parser = lidar_parser.LidarParser()
17+
yield parser
18+
19+
20+
@pytest.fixture()
21+
def lidar_detection_up() -> lidar_detection.LidarDetection: # type: ignore
22+
result, detection = lidar_detection.LidarDetection.create(5.0, ANGLE_UP)
23+
assert result
24+
assert detection is not None
25+
yield detection
26+
27+
28+
@pytest.fixture()
29+
def higher_angle_detection_up(lidar_detection_up: lidar_detection.LidarDetection) -> lidar_detection.LidarDetection: # type: ignore
30+
result, higher_detection = lidar_detection.LidarDetection.create(
31+
5.0, lidar_detection_up.angle + 5.0
32+
)
33+
assert result
34+
assert higher_detection is not None
35+
yield higher_detection
36+
37+
38+
@pytest.fixture()
39+
def lidar_detection_down() -> lidar_detection.LidarDetection: # type: ignore
40+
result, detection = lidar_detection.LidarDetection.create(5.0, ANGLE_DOWN)
41+
assert result
42+
assert detection is not None
43+
yield detection
44+
45+
46+
@pytest.fixture()
47+
def lower_angle_detection_down(lidar_detection_up: lidar_detection.LidarDetection) -> lidar_detection.LidarDetection: # type: ignore
48+
result, lower_detection = lidar_detection.LidarDetection.create(
49+
5.0, lidar_detection_up.angle - 5.0
50+
)
51+
assert result
52+
assert lower_detection is not None
53+
yield lower_detection
54+
55+
56+
class TestLidarParser:
57+
"""
58+
Tests for the LidarParser run() method.
59+
"""
60+
61+
def test_initial_run_no_oscillation(
62+
self,
63+
lidar_parser_instance: lidar_parser.LidarParser,
64+
lidar_detection_up: lidar_detection.LidarDetection,
65+
) -> None:
66+
result, oscillation = lidar_parser_instance.run(lidar_detection_up)
67+
assert not result
68+
assert oscillation is None
69+
70+
def test_oscillation_detected_up_to_down(
71+
self,
72+
lidar_parser_instance: lidar_parser.LidarParser,
73+
lidar_detection_up: lidar_detection.LidarDetection,
74+
higher_angle_detection_up: lidar_detection.LidarDetection,
75+
lidar_detection_down: lidar_detection.LidarDetection,
76+
) -> None:
77+
78+
lidar_parser_instance.run(lidar_detection_up)
79+
lidar_parser_instance.run(higher_angle_detection_up)
80+
result, oscillation = lidar_parser_instance.run(lidar_detection_down)
81+
assert result
82+
assert oscillation is not None
83+
84+
def test_no_oscillation_on_same_direction(
85+
self,
86+
lidar_parser_instance: lidar_parser.LidarParser,
87+
lidar_detection_up: lidar_detection.LidarDetection,
88+
) -> None:
89+
90+
for _ in range(5):
91+
result, oscillation = lidar_parser_instance.run(lidar_detection_up)
92+
assert not result
93+
assert oscillation is None
94+
95+
def test_direction_initialization_on_first_run(
96+
self,
97+
lidar_parser_instance: lidar_parser.LidarParser,
98+
lidar_detection_up: lidar_detection.LidarDetection,
99+
lower_angle_detection_down: lidar_detection.LidarDetection,
100+
) -> None:
101+
102+
lidar_parser_instance.run(lidar_detection_up)
103+
lidar_parser_instance.run(lower_angle_detection_down)
104+
assert lidar_parser_instance.direction == lidar_parser.Direction.DOWN
105+
106+
def test_oscillation_reset_after_detection(
107+
self,
108+
lidar_parser_instance: lidar_parser.LidarParser,
109+
lidar_detection_up: lidar_detection.LidarDetection,
110+
higher_angle_detection_up: lidar_detection.LidarDetection,
111+
lidar_detection_down: lidar_detection.LidarDetection,
112+
) -> None:
113+
114+
lidar_parser_instance.run(lidar_detection_up)
115+
lidar_parser_instance.run(higher_angle_detection_up)
116+
result, oscillation = lidar_parser_instance.run(lidar_detection_down)
117+
assert result
118+
assert oscillation is not None
119+
assert len(lidar_parser_instance.lidar_readings) == 0
120+
121+
def test_alternating_up_down_oscillations(
122+
self,
123+
lidar_parser_instance: lidar_parser.LidarParser,
124+
lidar_detection_up: lidar_detection.LidarDetection,
125+
higher_angle_detection_up: lidar_detection.LidarDetection,
126+
lidar_detection_down: lidar_detection.LidarDetection,
127+
) -> None:
128+
"""
129+
Test the parser with alternating up and down angles to simulate multiple oscillations.
130+
"""
131+
oscillation_count = 0
132+
133+
# Detect the first oscillation with three readings: two up, one down
134+
lidar_parser_instance.run(lidar_detection_up)
135+
lidar_parser_instance.run(higher_angle_detection_up)
136+
result, oscillation = lidar_parser_instance.run(lidar_detection_down)
137+
138+
if result:
139+
oscillation_count += 1
140+
assert oscillation is not None
141+
142+
# Subsequent oscillations should alternate and be detected on every change of angle
143+
for i in range(4):
144+
if i % 2 == 0: # Every even index provides an upward reading
145+
result, oscillation = lidar_parser_instance.run(higher_angle_detection_up)
146+
else: # Every odd index provides a downward reading
147+
result, oscillation = lidar_parser_instance.run(lidar_detection_down)
148+
149+
if result:
150+
oscillation_count += 1
151+
assert oscillation is not None
152+
153+
# Verify that five oscillations were detected in total
154+
assert oscillation_count == 5
155+
156+
def test_no_oscillation_with_single_reading(
157+
self,
158+
lidar_parser_instance: lidar_parser.LidarParser,
159+
lidar_detection_up: lidar_detection.LidarDetection,
160+
) -> None:
161+
result, oscillation = lidar_parser_instance.run(lidar_detection_up)
162+
assert not result
163+
assert oscillation is None
164+
165+
def test_oscillation_on_direction_change_after_none(
166+
self,
167+
lidar_parser_instance: lidar_parser.LidarParser,
168+
lidar_detection_up: lidar_detection.LidarDetection,
169+
higher_angle_detection_up: lidar_detection.LidarDetection,
170+
lidar_detection_down: lidar_detection.LidarDetection,
171+
) -> None:
172+
lidar_parser_instance.run(lidar_detection_up)
173+
lidar_parser_instance.run(higher_angle_detection_up)
174+
assert lidar_parser_instance.direction == lidar_parser.Direction.UP
175+
result, oscillation = lidar_parser_instance.run(lidar_detection_down)
176+
assert result
177+
assert oscillation is not None

0 commit comments

Comments
 (0)