From d5bfe68d14be8f6b95aa8953dbf5c3f894309808 Mon Sep 17 00:00:00 2001 From: Martin Dlouhy Date: Tue, 28 Jul 2026 22:03:40 +0200 Subject: [PATCH 01/20] Starting lidarroad --- lidarroad/README.md | 2 ++ lidarroad/lidarroad.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 lidarroad/README.md create mode 100644 lidarroad/lidarroad.py diff --git a/lidarroad/README.md b/lidarroad/README.md new file mode 100644 index 0000000..081a008 --- /dev/null +++ b/lidarroad/README.md @@ -0,0 +1,2 @@ +Extract road boundary from WanJee down pointing lidar (10 degrees) + diff --git a/lidarroad/lidarroad.py b/lidarroad/lidarroad.py new file mode 100644 index 0000000..d9df7eb --- /dev/null +++ b/lidarroad/lidarroad.py @@ -0,0 +1,38 @@ +import argparse +from datetime import timedelta + +import numpy as np + +from osgar.logger import LogReaderEx + + +def analyze_scan(scan): + assert len(scan)==1800, len(scan) + diff = np.diff(scan) #[450:-450]) + draw_scan(diff) + + +def draw_scan(scan): + import matplotlib.pyplot as plt + + plt.plot(scan) + plt.show() + + +def main(): + parser = argparse.ArgumentParser(description='Analyze smoothness of the road/scan10') + parser.add_argument('logfile', help='logfile path') + args = parser.parse_args() + + with LogReaderEx(args.logfile, ['vanjee.scan10']) as log: + for timestamp, name, data in log: + if timestamp < timedelta(seconds=100): + continue + print(timestamp, len(data)) + analyze_scan(data) + break + + + +if __name__ == '__main__': + main() From 0a145a5933aacef32b5be6deebbc809eab342109 Mon Sep 17 00:00:00 2001 From: Martin Dlouhy Date: Thu, 30 Jul 2026 15:50:40 +0200 Subject: [PATCH 02/20] add jump option --- lidarroad/lidarroad.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lidarroad/lidarroad.py b/lidarroad/lidarroad.py index d9df7eb..a654ef8 100644 --- a/lidarroad/lidarroad.py +++ b/lidarroad/lidarroad.py @@ -22,11 +22,12 @@ def draw_scan(scan): def main(): parser = argparse.ArgumentParser(description='Analyze smoothness of the road/scan10') parser.add_argument('logfile', help='logfile path') + parser.add_argument('--jump', '-j', help='jump in seconds', type=float) args = parser.parse_args() with LogReaderEx(args.logfile, ['vanjee.scan10']) as log: for timestamp, name, data in log: - if timestamp < timedelta(seconds=100): + if args.jump is not None and timestamp < timedelta(seconds=args.jump): continue print(timestamp, len(data)) analyze_scan(data) From 4f1b194c091a53c65c0ac660d05c24300927fb8c Mon Sep 17 00:00:00 2001 From: Martin Dlouhy Date: Thu, 30 Jul 2026 18:00:52 +0200 Subject: [PATCH 03/20] brute force max match --- lidarroad/lidarroad.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lidarroad/lidarroad.py b/lidarroad/lidarroad.py index a654ef8..264a133 100644 --- a/lidarroad/lidarroad.py +++ b/lidarroad/lidarroad.py @@ -6,9 +6,23 @@ from osgar.logger import LogReaderEx -def analyze_scan(scan): +def get_best_match(mask, window_size): + best_i = 0 + best_sum = None + for i in range(0, len(mask) - window_size): + value = sum(mask[i:i+window_size]) + if best_sum is None or value > best_sum: + best_sum = value + best_i = i + return best_i, best_i + window_size + + +def analyze_scan(scan, tolerance=10, window_size = 500): assert len(scan)==1800, len(scan) diff = np.diff(scan) #[450:-450]) + mask = np.abs(diff) < tolerance + from_i, to_i = get_best_match(mask, window_size) + print(from_i, to_i) draw_scan(diff) From 85cf27b0f1352114a04de5e38e66fc618cfa39e4 Mon Sep 17 00:00:00 2001 From: Elena Ai Date: Thu, 30 Jul 2026 18:16:53 +0200 Subject: [PATCH 04/20] add test_analyze_scan (and drawing tolerance) --- lidarroad/lidarroad.py | 7 ++++-- lidarroad/test_lidarroad.py | 50 +++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 lidarroad/test_lidarroad.py diff --git a/lidarroad/lidarroad.py b/lidarroad/lidarroad.py index 264a133..11e442b 100644 --- a/lidarroad/lidarroad.py +++ b/lidarroad/lidarroad.py @@ -23,13 +23,16 @@ def analyze_scan(scan, tolerance=10, window_size = 500): mask = np.abs(diff) < tolerance from_i, to_i = get_best_match(mask, window_size) print(from_i, to_i) - draw_scan(diff) + draw_scan(diff, tolerance) -def draw_scan(scan): +def draw_scan(scan, tolerance=None): import matplotlib.pyplot as plt plt.plot(scan) + if tolerance is not None: + plt.axhline(y=tolerance, color='r', linestyle='--') + plt.axhline(y=-tolerance, color='r', linestyle='--') plt.show() diff --git a/lidarroad/test_lidarroad.py b/lidarroad/test_lidarroad.py new file mode 100644 index 0000000..55fe7f9 --- /dev/null +++ b/lidarroad/test_lidarroad.py @@ -0,0 +1,50 @@ +import unittest +from unittest.mock import patch +import numpy as np + +from lidarroad import get_best_match, analyze_scan, draw_scan + +class TestLidarRoad(unittest.TestCase): + def test_get_best_match(self): + mask = [0, 0, 1, 1, 1, 0, 0] + from_i, to_i = get_best_match(mask, 3) + self.assertEqual(from_i, 2) + self.assertEqual(to_i, 5) + + @patch('matplotlib.pyplot.show') + @patch('matplotlib.pyplot.axhline') + @patch('matplotlib.pyplot.plot') + def test_draw_scan_with_tolerance(self, mock_plot, mock_axhline, mock_show): + scan = [1, 2, 3] + draw_scan(scan, tolerance=15) + mock_plot.assert_called_once_with(scan) + # Check that plt.axhline was called with tolerance and -tolerance + self.assertEqual(mock_axhline.call_count, 2) + mock_axhline.assert_any_call(y=15, color='r', linestyle='--') + mock_axhline.assert_any_call(y=-15, color='r', linestyle='--') + mock_show.assert_called_once() + + @patch('matplotlib.pyplot.show') + @patch('matplotlib.pyplot.axhline') + @patch('matplotlib.pyplot.plot') + def test_draw_scan_without_tolerance(self, mock_plot, mock_axhline, mock_show): + scan = [1, 2, 3] + draw_scan(scan, tolerance=None) + mock_plot.assert_called_once_with(scan) + mock_axhline.assert_not_called() + mock_show.assert_called_once() + + @patch('lidarroad.draw_scan') + def test_analyze_scan(self, mock_draw_scan): + # scan of length 1800 + scan = [10] * 1800 + analyze_scan(scan, tolerance=10, window_size=500) + # Expected diff of [10] * 1800 is an array of 1799 zeros + mock_draw_scan.assert_called_once() + called_args, called_kwargs = mock_draw_scan.call_args + np.testing.assert_array_equal(called_args[0], np.zeros(1799)) + self.assertEqual(called_args[1], 10) + + +if __name__ == '__main__': + unittest.main() From 2757612560f2baba4a8d51cfad053570a2c42e80 Mon Sep 17 00:00:00 2001 From: Elena Ai Date: Thu, 30 Jul 2026 18:21:58 +0200 Subject: [PATCH 05/20] draw vertical limits --- lidarroad/lidarroad.py | 8 ++++++-- lidarroad/test_lidarroad.py | 21 ++++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lidarroad/lidarroad.py b/lidarroad/lidarroad.py index 11e442b..71d8ed2 100644 --- a/lidarroad/lidarroad.py +++ b/lidarroad/lidarroad.py @@ -23,16 +23,20 @@ def analyze_scan(scan, tolerance=10, window_size = 500): mask = np.abs(diff) < tolerance from_i, to_i = get_best_match(mask, window_size) print(from_i, to_i) - draw_scan(diff, tolerance) + draw_scan(diff, tolerance, interval=(from_i, to_i)) -def draw_scan(scan, tolerance=None): +def draw_scan(scan, tolerance=None, interval=None): import matplotlib.pyplot as plt plt.plot(scan) if tolerance is not None: plt.axhline(y=tolerance, color='r', linestyle='--') plt.axhline(y=-tolerance, color='r', linestyle='--') + if interval is not None: + from_i, to_i = interval + plt.axvline(x=from_i, color='g', linestyle='--') + plt.axvline(x=to_i, color='g', linestyle='--') plt.show() diff --git a/lidarroad/test_lidarroad.py b/lidarroad/test_lidarroad.py index 55fe7f9..2ba043d 100644 --- a/lidarroad/test_lidarroad.py +++ b/lidarroad/test_lidarroad.py @@ -12,26 +12,36 @@ def test_get_best_match(self): self.assertEqual(to_i, 5) @patch('matplotlib.pyplot.show') + @patch('matplotlib.pyplot.axvline') @patch('matplotlib.pyplot.axhline') @patch('matplotlib.pyplot.plot') - def test_draw_scan_with_tolerance(self, mock_plot, mock_axhline, mock_show): + def test_draw_scan_with_tolerance_and_interval(self, mock_plot, mock_axhline, mock_axvline, mock_show): scan = [1, 2, 3] - draw_scan(scan, tolerance=15) + draw_scan(scan, tolerance=15, interval=(10, 20)) mock_plot.assert_called_once_with(scan) - # Check that plt.axhline was called with tolerance and -tolerance + + # Check horizontal lines self.assertEqual(mock_axhline.call_count, 2) mock_axhline.assert_any_call(y=15, color='r', linestyle='--') mock_axhline.assert_any_call(y=-15, color='r', linestyle='--') + + # Check vertical lines + self.assertEqual(mock_axvline.call_count, 2) + mock_axvline.assert_any_call(x=10, color='g', linestyle='--') + mock_axvline.assert_any_call(x=20, color='g', linestyle='--') + mock_show.assert_called_once() @patch('matplotlib.pyplot.show') + @patch('matplotlib.pyplot.axvline') @patch('matplotlib.pyplot.axhline') @patch('matplotlib.pyplot.plot') - def test_draw_scan_without_tolerance(self, mock_plot, mock_axhline, mock_show): + def test_draw_scan_without_tolerance_and_interval(self, mock_plot, mock_axhline, mock_axvline, mock_show): scan = [1, 2, 3] - draw_scan(scan, tolerance=None) + draw_scan(scan, tolerance=None, interval=None) mock_plot.assert_called_once_with(scan) mock_axhline.assert_not_called() + mock_axvline.assert_not_called() mock_show.assert_called_once() @patch('lidarroad.draw_scan') @@ -44,6 +54,7 @@ def test_analyze_scan(self, mock_draw_scan): called_args, called_kwargs = mock_draw_scan.call_args np.testing.assert_array_equal(called_args[0], np.zeros(1799)) self.assertEqual(called_args[1], 10) + self.assertEqual(called_kwargs.get('interval'), (0, 500)) if __name__ == '__main__': From 975e57813b0d443f7516af8cf9611df88af1d35a Mon Sep 17 00:00:00 2001 From: Martin Dlouhy Date: Thu, 30 Jul 2026 18:29:23 +0200 Subject: [PATCH 06/20] add width parameter --- lidarroad/lidarroad.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lidarroad/lidarroad.py b/lidarroad/lidarroad.py index 71d8ed2..f2f7cc9 100644 --- a/lidarroad/lidarroad.py +++ b/lidarroad/lidarroad.py @@ -17,7 +17,7 @@ def get_best_match(mask, window_size): return best_i, best_i + window_size -def analyze_scan(scan, tolerance=10, window_size = 500): +def analyze_scan(scan, tolerance=10, window_size = 300): assert len(scan)==1800, len(scan) diff = np.diff(scan) #[450:-450]) mask = np.abs(diff) < tolerance @@ -44,14 +44,17 @@ def main(): parser = argparse.ArgumentParser(description='Analyze smoothness of the road/scan10') parser.add_argument('logfile', help='logfile path') parser.add_argument('--jump', '-j', help='jump in seconds', type=float) + parser.add_argument('--width', '-w', help='width in meters', type=float, default=3.0) args = parser.parse_args() + window_size = int(args.width * 100) # simplified conversion to scan indexes - TODO proper calibration + with LogReaderEx(args.logfile, ['vanjee.scan10']) as log: for timestamp, name, data in log: if args.jump is not None and timestamp < timedelta(seconds=args.jump): continue print(timestamp, len(data)) - analyze_scan(data) + analyze_scan(data, window_size=window_size) break From bc4425ec8a9b060c95839f45b3b6460368a81007 Mon Sep 17 00:00:00 2001 From: Elena Ai Date: Thu, 30 Jul 2026 18:34:55 +0200 Subject: [PATCH 07/20] refactoring --- lidarroad/lidarroad.py | 8 +++++--- lidarroad/test_lidarroad.py | 13 +++++-------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/lidarroad/lidarroad.py b/lidarroad/lidarroad.py index f2f7cc9..c0dd665 100644 --- a/lidarroad/lidarroad.py +++ b/lidarroad/lidarroad.py @@ -22,8 +22,7 @@ def analyze_scan(scan, tolerance=10, window_size = 300): diff = np.diff(scan) #[450:-450]) mask = np.abs(diff) < tolerance from_i, to_i = get_best_match(mask, window_size) - print(from_i, to_i) - draw_scan(diff, tolerance, interval=(from_i, to_i)) + return diff, from_i, to_i def draw_scan(scan, tolerance=None, interval=None): @@ -48,13 +47,16 @@ def main(): args = parser.parse_args() window_size = int(args.width * 100) # simplified conversion to scan indexes - TODO proper calibration + tolerance = 10 with LogReaderEx(args.logfile, ['vanjee.scan10']) as log: for timestamp, name, data in log: if args.jump is not None and timestamp < timedelta(seconds=args.jump): continue print(timestamp, len(data)) - analyze_scan(data, window_size=window_size) + diff, from_i, to_i = analyze_scan(data, tolerance=tolerance, window_size=window_size) + print(from_i, to_i) + draw_scan(diff, tolerance=tolerance, interval=(from_i, to_i)) break diff --git a/lidarroad/test_lidarroad.py b/lidarroad/test_lidarroad.py index 2ba043d..996f56d 100644 --- a/lidarroad/test_lidarroad.py +++ b/lidarroad/test_lidarroad.py @@ -44,17 +44,14 @@ def test_draw_scan_without_tolerance_and_interval(self, mock_plot, mock_axhline, mock_axvline.assert_not_called() mock_show.assert_called_once() - @patch('lidarroad.draw_scan') - def test_analyze_scan(self, mock_draw_scan): + def test_analyze_scan(self): # scan of length 1800 scan = [10] * 1800 - analyze_scan(scan, tolerance=10, window_size=500) + diff, from_i, to_i = analyze_scan(scan, tolerance=10, window_size=500) # Expected diff of [10] * 1800 is an array of 1799 zeros - mock_draw_scan.assert_called_once() - called_args, called_kwargs = mock_draw_scan.call_args - np.testing.assert_array_equal(called_args[0], np.zeros(1799)) - self.assertEqual(called_args[1], 10) - self.assertEqual(called_kwargs.get('interval'), (0, 500)) + np.testing.assert_array_equal(diff, np.zeros(1799)) + self.assertEqual(from_i, 0) + self.assertEqual(to_i, 500) if __name__ == '__main__': From 9aeb52e6e0b2478cf6f9b10c6410e237e52c0930 Mon Sep 17 00:00:00 2001 From: Elena Ai Date: Thu, 30 Jul 2026 18:37:08 +0200 Subject: [PATCH 08/20] add --tolerance parameter --- lidarroad/lidarroad.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lidarroad/lidarroad.py b/lidarroad/lidarroad.py index c0dd665..e52611c 100644 --- a/lidarroad/lidarroad.py +++ b/lidarroad/lidarroad.py @@ -44,10 +44,11 @@ def main(): parser.add_argument('logfile', help='logfile path') parser.add_argument('--jump', '-j', help='jump in seconds', type=float) parser.add_argument('--width', '-w', help='width in meters', type=float, default=3.0) + parser.add_argument('--tolerance', '-t', help='tolerance in millimeters', type=int, default=10) args = parser.parse_args() window_size = int(args.width * 100) # simplified conversion to scan indexes - TODO proper calibration - tolerance = 10 + tolerance = args.tolerance with LogReaderEx(args.logfile, ['vanjee.scan10']) as log: for timestamp, name, data in log: From 2708cf189ddaad3f664a930e6051301b8b16a6b9 Mon Sep 17 00:00:00 2001 From: Elena Ai Date: Thu, 30 Jul 2026 18:47:14 +0200 Subject: [PATCH 09/20] optimization of get_best_match() --- lidarroad/lidarroad.py | 15 ++++++++++++++- lidarroad/test_lidarroad.py | 22 +++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lidarroad/lidarroad.py b/lidarroad/lidarroad.py index e52611c..1ffe78d 100644 --- a/lidarroad/lidarroad.py +++ b/lidarroad/lidarroad.py @@ -6,7 +6,7 @@ from osgar.logger import LogReaderEx -def get_best_match(mask, window_size): +def slow_get_best_match(mask, window_size): best_i = 0 best_sum = None for i in range(0, len(mask) - window_size): @@ -17,11 +17,24 @@ def get_best_match(mask, window_size): return best_i, best_i + window_size +def get_best_match(mask, window_size): + if len(mask) <= window_size: + return 0, window_size + cum = np.cumsum(np.asarray(mask)) + window_sums = np.empty(len(mask) - window_size, dtype=cum.dtype) + window_sums[0] = cum[window_size - 1] + window_sums[1:] = cum[window_size:-1] - cum[:-window_size-1] + best_i = int(np.argmax(window_sums)) + return best_i, best_i + window_size + + def analyze_scan(scan, tolerance=10, window_size = 300): assert len(scan)==1800, len(scan) diff = np.diff(scan) #[450:-450]) mask = np.abs(diff) < tolerance from_i, to_i = get_best_match(mask, window_size) + from_i_slow, to_i_slow = slow_get_best_match(mask, window_size) + assert (from_i, to_i) == (from_i_slow, to_i_slow), f"Optimization mismatch: {(from_i, to_i)} != {(from_i_slow, to_i_slow)}" return diff, from_i, to_i diff --git a/lidarroad/test_lidarroad.py b/lidarroad/test_lidarroad.py index 996f56d..912a5bc 100644 --- a/lidarroad/test_lidarroad.py +++ b/lidarroad/test_lidarroad.py @@ -2,15 +2,35 @@ from unittest.mock import patch import numpy as np -from lidarroad import get_best_match, analyze_scan, draw_scan +from lidarroad import get_best_match, slow_get_best_match, analyze_scan, draw_scan class TestLidarRoad(unittest.TestCase): def test_get_best_match(self): + # Basic validation case mask = [0, 0, 1, 1, 1, 0, 0] from_i, to_i = get_best_match(mask, 3) self.assertEqual(from_i, 2) self.assertEqual(to_i, 5) + # Extended comparative validation between fast and slow implementations + np.random.seed(42) # For deterministic reproducibility + masks = [ + [0, 0, 1, 1, 1, 0, 0], + [1, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0], + [1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1], + np.random.randint(0, 2, 1000).tolist() + ] + for m in masks: + for window_size in [1, 3, 5, 50, 100]: + if len(m) > window_size: + from_fast, to_fast = get_best_match(m, window_size) + from_slow, to_slow = slow_get_best_match(m, window_size) + self.assertEqual( + (from_fast, to_fast), (from_slow, to_slow), + f"Mismatch for window_size {window_size} on mask {m if len(m) < 20 else 'random'}" + ) + @patch('matplotlib.pyplot.show') @patch('matplotlib.pyplot.axvline') @patch('matplotlib.pyplot.axhline') From 1f4df37d9c895f5807d26bf84ced93750b294bdd Mon Sep 17 00:00:00 2001 From: Elena Ai Date: Thu, 30 Jul 2026 18:56:18 +0200 Subject: [PATCH 10/20] batch processing --- lidarroad/lidarroad.py | 56 +++++++++++++++++++++++++++++++------ lidarroad/test_lidarroad.py | 24 +++++++++++++++- 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/lidarroad/lidarroad.py b/lidarroad/lidarroad.py index 1ffe78d..682a21b 100644 --- a/lidarroad/lidarroad.py +++ b/lidarroad/lidarroad.py @@ -52,26 +52,66 @@ def draw_scan(scan, tolerance=None, interval=None): plt.show() +def draw_batch(timestamps, from_indices, to_indices): + import matplotlib.pyplot as plt + + plt.plot(timestamps, from_indices, 'g.-', label='from_i') + plt.plot(timestamps, to_indices, 'b.-', label='to_i') + plt.xlabel('Time (s)') + plt.ylabel('Scan Index') + plt.legend() + plt.title('Best Matching Interval Boundaries Over Time') + plt.show() + + def main(): parser = argparse.ArgumentParser(description='Analyze smoothness of the road/scan10') parser.add_argument('logfile', help='logfile path') parser.add_argument('--jump', '-j', help='jump in seconds', type=float) parser.add_argument('--width', '-w', help='width in meters', type=float, default=3.0) parser.add_argument('--tolerance', '-t', help='tolerance in millimeters', type=int, default=10) + parser.add_argument('--batch', nargs=2, type=float, metavar=('START', 'END'), help='run in batch mode for time range in seconds') args = parser.parse_args() window_size = int(args.width * 100) # simplified conversion to scan indexes - TODO proper calibration tolerance = args.tolerance with LogReaderEx(args.logfile, ['vanjee.scan10']) as log: - for timestamp, name, data in log: - if args.jump is not None and timestamp < timedelta(seconds=args.jump): - continue - print(timestamp, len(data)) - diff, from_i, to_i = analyze_scan(data, tolerance=tolerance, window_size=window_size) - print(from_i, to_i) - draw_scan(diff, tolerance=tolerance, interval=(from_i, to_i)) - break + if args.batch is not None: + start_sec, end_sec = args.batch + start_time = timedelta(seconds=start_sec) + end_time = timedelta(seconds=end_sec) + + times = [] + from_indices = [] + to_indices = [] + + for timestamp, name, data in log: + if timestamp < start_time: + continue + if timestamp > end_time: + break + + diff, from_i, to_i = analyze_scan(data, tolerance=tolerance, window_size=window_size) + print(timestamp, len(data), from_i, to_i) + + times.append(timestamp.total_seconds()) + from_indices.append(from_i) + to_indices.append(to_i) + + if times: + draw_batch(times, from_indices, to_indices) + else: + print("No scans found in the specified time range.") + else: + for timestamp, name, data in log: + if args.jump is not None and timestamp < timedelta(seconds=args.jump): + continue + print(timestamp, len(data)) + diff, from_i, to_i = analyze_scan(data, tolerance=tolerance, window_size=window_size) + print(from_i, to_i) + draw_scan(diff, tolerance=tolerance, interval=(from_i, to_i)) + break diff --git a/lidarroad/test_lidarroad.py b/lidarroad/test_lidarroad.py index 912a5bc..d048844 100644 --- a/lidarroad/test_lidarroad.py +++ b/lidarroad/test_lidarroad.py @@ -2,7 +2,7 @@ from unittest.mock import patch import numpy as np -from lidarroad import get_best_match, slow_get_best_match, analyze_scan, draw_scan +from lidarroad import get_best_match, slow_get_best_match, analyze_scan, draw_scan, draw_batch class TestLidarRoad(unittest.TestCase): def test_get_best_match(self): @@ -64,6 +64,28 @@ def test_draw_scan_without_tolerance_and_interval(self, mock_plot, mock_axhline, mock_axvline.assert_not_called() mock_show.assert_called_once() + @patch('matplotlib.pyplot.show') + @patch('matplotlib.pyplot.legend') + @patch('matplotlib.pyplot.ylabel') + @patch('matplotlib.pyplot.xlabel') + @patch('matplotlib.pyplot.title') + @patch('matplotlib.pyplot.plot') + def test_draw_batch(self, mock_plot, mock_title, mock_xlabel, mock_ylabel, mock_legend, mock_show): + times = [1.0, 2.0, 3.0] + from_i = [100, 110, 120] + to_i = [400, 410, 420] + draw_batch(times, from_i, to_i) + + self.assertEqual(mock_plot.call_count, 2) + mock_plot.assert_any_call(times, from_i, 'g.-', label='from_i') + mock_plot.assert_any_call(times, to_i, 'b.-', label='to_i') + + mock_xlabel.assert_called_once_with('Time (s)') + mock_ylabel.assert_called_once_with('Scan Index') + mock_title.assert_called_once_with('Best Matching Interval Boundaries Over Time') + mock_legend.assert_called_once() + mock_show.assert_called_once() + def test_analyze_scan(self): # scan of length 1800 scan = [10] * 1800 From e637ad4b56ab2b89af1da55c11b99350d07a5081 Mon Sep 17 00:00:00 2001 From: Elena Ai Date: Thu, 30 Jul 2026 19:01:56 +0200 Subject: [PATCH 11/20] refactoring1 --- lidarroad/lidarroad.py | 60 ++++++++++++++++++++----------------- lidarroad/test_lidarroad.py | 22 +++++++++++++- 2 files changed, 54 insertions(+), 28 deletions(-) diff --git a/lidarroad/lidarroad.py b/lidarroad/lidarroad.py index 682a21b..84c6835 100644 --- a/lidarroad/lidarroad.py +++ b/lidarroad/lidarroad.py @@ -64,6 +64,31 @@ def draw_batch(timestamps, from_indices, to_indices): plt.show() +def batch_processing(logfile, start_sec, end_sec, tolerance, window_size): + start_time = timedelta(seconds=start_sec) + end_time = timedelta(seconds=end_sec) + + times = [] + from_indices = [] + to_indices = [] + + with LogReaderEx(logfile, ['vanjee.scan10']) as log: + for timestamp, name, data in log: + if timestamp < start_time: + continue + if timestamp > end_time: + break + + diff, from_i, to_i = analyze_scan(data, tolerance=tolerance, window_size=window_size) + print(timestamp, len(data), from_i, to_i) + + times.append(timestamp.total_seconds()) + from_indices.append(from_i) + to_indices.append(to_i) + + return times, from_indices, to_indices + + def main(): parser = argparse.ArgumentParser(description='Analyze smoothness of the road/scan10') parser.add_argument('logfile', help='logfile path') @@ -76,34 +101,15 @@ def main(): window_size = int(args.width * 100) # simplified conversion to scan indexes - TODO proper calibration tolerance = args.tolerance - with LogReaderEx(args.logfile, ['vanjee.scan10']) as log: - if args.batch is not None: - start_sec, end_sec = args.batch - start_time = timedelta(seconds=start_sec) - end_time = timedelta(seconds=end_sec) - - times = [] - from_indices = [] - to_indices = [] - - for timestamp, name, data in log: - if timestamp < start_time: - continue - if timestamp > end_time: - break - - diff, from_i, to_i = analyze_scan(data, tolerance=tolerance, window_size=window_size) - print(timestamp, len(data), from_i, to_i) - - times.append(timestamp.total_seconds()) - from_indices.append(from_i) - to_indices.append(to_i) - - if times: - draw_batch(times, from_indices, to_indices) - else: - print("No scans found in the specified time range.") + if args.batch is not None: + start_sec, end_sec = args.batch + times, from_indices, to_indices = batch_processing(args.logfile, start_sec, end_sec, tolerance, window_size) + if times: + draw_batch(times, from_indices, to_indices) else: + print("No scans found in the specified time range.") + else: + with LogReaderEx(args.logfile, ['vanjee.scan10']) as log: for timestamp, name, data in log: if args.jump is not None and timestamp < timedelta(seconds=args.jump): continue diff --git a/lidarroad/test_lidarroad.py b/lidarroad/test_lidarroad.py index d048844..b338c78 100644 --- a/lidarroad/test_lidarroad.py +++ b/lidarroad/test_lidarroad.py @@ -1,8 +1,9 @@ import unittest from unittest.mock import patch import numpy as np +from datetime import timedelta -from lidarroad import get_best_match, slow_get_best_match, analyze_scan, draw_scan, draw_batch +from lidarroad import get_best_match, slow_get_best_match, analyze_scan, draw_scan, draw_batch, batch_processing class TestLidarRoad(unittest.TestCase): def test_get_best_match(self): @@ -86,6 +87,25 @@ def test_draw_batch(self, mock_plot, mock_title, mock_xlabel, mock_ylabel, mock_ mock_legend.assert_called_once() mock_show.assert_called_once() + @patch('lidarroad.LogReaderEx') + def test_batch_processing(self, mock_log_reader): + mock_scan = [10] * 1800 + mock_log_reader.return_value.__enter__.return_value = [ + (timedelta(seconds=1.0), 'vanjee.scan10', mock_scan), + (timedelta(seconds=2.0), 'vanjee.scan10', mock_scan), + (timedelta(seconds=3.0), 'vanjee.scan10', mock_scan), + (timedelta(seconds=4.0), 'vanjee.scan10', mock_scan) + ] + + times, from_indices, to_indices = batch_processing( + 'mock_log_path.log', start_sec=1.5, end_sec=3.5, tolerance=10, window_size=500 + ) + + self.assertEqual(times, [2.0, 3.0]) + self.assertEqual(from_indices, [0, 0]) + self.assertEqual(to_indices, [500, 500]) + mock_log_reader.assert_called_once_with('mock_log_path.log', ['vanjee.scan10']) + def test_analyze_scan(self): # scan of length 1800 scan = [10] * 1800 From 11f71325232db92882dd00639be29099c44242f1 Mon Sep 17 00:00:00 2001 From: Elena Ai Date: Thu, 30 Jul 2026 19:05:12 +0200 Subject: [PATCH 12/20] pass log step1 --- lidarroad/lidarroad.py | 26 +++++++++++++------------- lidarroad/test_lidarroad.py | 8 +++----- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/lidarroad/lidarroad.py b/lidarroad/lidarroad.py index 84c6835..69b4985 100644 --- a/lidarroad/lidarroad.py +++ b/lidarroad/lidarroad.py @@ -64,7 +64,7 @@ def draw_batch(timestamps, from_indices, to_indices): plt.show() -def batch_processing(logfile, start_sec, end_sec, tolerance, window_size): +def batch_processing(log, start_sec, end_sec, tolerance, window_size): start_time = timedelta(seconds=start_sec) end_time = timedelta(seconds=end_sec) @@ -72,19 +72,18 @@ def batch_processing(logfile, start_sec, end_sec, tolerance, window_size): from_indices = [] to_indices = [] - with LogReaderEx(logfile, ['vanjee.scan10']) as log: - for timestamp, name, data in log: - if timestamp < start_time: - continue - if timestamp > end_time: - break + for timestamp, name, data in log: + if timestamp < start_time: + continue + if timestamp > end_time: + break - diff, from_i, to_i = analyze_scan(data, tolerance=tolerance, window_size=window_size) - print(timestamp, len(data), from_i, to_i) + diff, from_i, to_i = analyze_scan(data, tolerance=tolerance, window_size=window_size) + print(timestamp, len(data), from_i, to_i) - times.append(timestamp.total_seconds()) - from_indices.append(from_i) - to_indices.append(to_i) + times.append(timestamp.total_seconds()) + from_indices.append(from_i) + to_indices.append(to_i) return times, from_indices, to_indices @@ -103,7 +102,8 @@ def main(): if args.batch is not None: start_sec, end_sec = args.batch - times, from_indices, to_indices = batch_processing(args.logfile, start_sec, end_sec, tolerance, window_size) + with LogReaderEx(args.logfile, ['vanjee.scan10']) as log: + times, from_indices, to_indices = batch_processing(log, start_sec, end_sec, tolerance, window_size) if times: draw_batch(times, from_indices, to_indices) else: diff --git a/lidarroad/test_lidarroad.py b/lidarroad/test_lidarroad.py index b338c78..f08ad3d 100644 --- a/lidarroad/test_lidarroad.py +++ b/lidarroad/test_lidarroad.py @@ -87,10 +87,9 @@ def test_draw_batch(self, mock_plot, mock_title, mock_xlabel, mock_ylabel, mock_ mock_legend.assert_called_once() mock_show.assert_called_once() - @patch('lidarroad.LogReaderEx') - def test_batch_processing(self, mock_log_reader): + def test_batch_processing(self): mock_scan = [10] * 1800 - mock_log_reader.return_value.__enter__.return_value = [ + log = [ (timedelta(seconds=1.0), 'vanjee.scan10', mock_scan), (timedelta(seconds=2.0), 'vanjee.scan10', mock_scan), (timedelta(seconds=3.0), 'vanjee.scan10', mock_scan), @@ -98,13 +97,12 @@ def test_batch_processing(self, mock_log_reader): ] times, from_indices, to_indices = batch_processing( - 'mock_log_path.log', start_sec=1.5, end_sec=3.5, tolerance=10, window_size=500 + log, start_sec=1.5, end_sec=3.5, tolerance=10, window_size=500 ) self.assertEqual(times, [2.0, 3.0]) self.assertEqual(from_indices, [0, 0]) self.assertEqual(to_indices, [500, 500]) - mock_log_reader.assert_called_once_with('mock_log_path.log', ['vanjee.scan10']) def test_analyze_scan(self): # scan of length 1800 From 389547616c716c7237ea787820c5a791f4191454 Mon Sep 17 00:00:00 2001 From: Elena Ai Date: Thu, 30 Jul 2026 19:07:01 +0200 Subject: [PATCH 13/20] step2 - reuse log --- lidarroad/lidarroad.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lidarroad/lidarroad.py b/lidarroad/lidarroad.py index 69b4985..7579f84 100644 --- a/lidarroad/lidarroad.py +++ b/lidarroad/lidarroad.py @@ -100,16 +100,15 @@ def main(): window_size = int(args.width * 100) # simplified conversion to scan indexes - TODO proper calibration tolerance = args.tolerance - if args.batch is not None: - start_sec, end_sec = args.batch - with LogReaderEx(args.logfile, ['vanjee.scan10']) as log: + with LogReaderEx(args.logfile, ['vanjee.scan10']) as log: + if args.batch is not None: + start_sec, end_sec = args.batch times, from_indices, to_indices = batch_processing(log, start_sec, end_sec, tolerance, window_size) - if times: - draw_batch(times, from_indices, to_indices) + if times: + draw_batch(times, from_indices, to_indices) + else: + print("No scans found in the specified time range.") else: - print("No scans found in the specified time range.") - else: - with LogReaderEx(args.logfile, ['vanjee.scan10']) as log: for timestamp, name, data in log: if args.jump is not None and timestamp < timedelta(seconds=args.jump): continue From 81e67be0f1efc71bc61d78d1f605cb620ae47f81 Mon Sep 17 00:00:00 2001 From: Elena Ai Date: Thu, 30 Jul 2026 19:16:12 +0200 Subject: [PATCH 14/20] provide --fast option to skip verification --- lidarroad/lidarroad.py | 17 ++++++++++------- lidarroad/test_lidarroad.py | 32 ++++++++++++++++++++++++++------ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/lidarroad/lidarroad.py b/lidarroad/lidarroad.py index 7579f84..cabf52e 100644 --- a/lidarroad/lidarroad.py +++ b/lidarroad/lidarroad.py @@ -28,13 +28,14 @@ def get_best_match(mask, window_size): return best_i, best_i + window_size -def analyze_scan(scan, tolerance=10, window_size = 300): +def analyze_scan(scan, tolerance=10, window_size = 300, fast=False): assert len(scan)==1800, len(scan) diff = np.diff(scan) #[450:-450]) mask = np.abs(diff) < tolerance from_i, to_i = get_best_match(mask, window_size) - from_i_slow, to_i_slow = slow_get_best_match(mask, window_size) - assert (from_i, to_i) == (from_i_slow, to_i_slow), f"Optimization mismatch: {(from_i, to_i)} != {(from_i_slow, to_i_slow)}" + if not fast: + from_i_slow, to_i_slow = slow_get_best_match(mask, window_size) + assert (from_i, to_i) == (from_i_slow, to_i_slow), f"Optimization mismatch: {(from_i, to_i)} != {(from_i_slow, to_i_slow)}" return diff, from_i, to_i @@ -64,7 +65,7 @@ def draw_batch(timestamps, from_indices, to_indices): plt.show() -def batch_processing(log, start_sec, end_sec, tolerance, window_size): +def batch_processing(log, start_sec, end_sec, tolerance, window_size, fast=False): start_time = timedelta(seconds=start_sec) end_time = timedelta(seconds=end_sec) @@ -78,7 +79,7 @@ def batch_processing(log, start_sec, end_sec, tolerance, window_size): if timestamp > end_time: break - diff, from_i, to_i = analyze_scan(data, tolerance=tolerance, window_size=window_size) + diff, from_i, to_i = analyze_scan(data, tolerance=tolerance, window_size=window_size, fast=fast) print(timestamp, len(data), from_i, to_i) times.append(timestamp.total_seconds()) @@ -95,15 +96,17 @@ def main(): parser.add_argument('--width', '-w', help='width in meters', type=float, default=3.0) parser.add_argument('--tolerance', '-t', help='tolerance in millimeters', type=int, default=10) parser.add_argument('--batch', nargs=2, type=float, metavar=('START', 'END'), help='run in batch mode for time range in seconds') + parser.add_argument('--fast', action='store_true', help='skip slow match calculation and assertion') args = parser.parse_args() window_size = int(args.width * 100) # simplified conversion to scan indexes - TODO proper calibration tolerance = args.tolerance + fast = args.fast with LogReaderEx(args.logfile, ['vanjee.scan10']) as log: if args.batch is not None: start_sec, end_sec = args.batch - times, from_indices, to_indices = batch_processing(log, start_sec, end_sec, tolerance, window_size) + times, from_indices, to_indices = batch_processing(log, start_sec, end_sec, tolerance, window_size, fast=fast) if times: draw_batch(times, from_indices, to_indices) else: @@ -113,7 +116,7 @@ def main(): if args.jump is not None and timestamp < timedelta(seconds=args.jump): continue print(timestamp, len(data)) - diff, from_i, to_i = analyze_scan(data, tolerance=tolerance, window_size=window_size) + diff, from_i, to_i = analyze_scan(data, tolerance=tolerance, window_size=window_size, fast=fast) print(from_i, to_i) draw_scan(diff, tolerance=tolerance, interval=(from_i, to_i)) break diff --git a/lidarroad/test_lidarroad.py b/lidarroad/test_lidarroad.py index f08ad3d..47dcedb 100644 --- a/lidarroad/test_lidarroad.py +++ b/lidarroad/test_lidarroad.py @@ -96,22 +96,42 @@ def test_batch_processing(self): (timedelta(seconds=4.0), 'vanjee.scan10', mock_scan) ] + # Test standard batch processing times, from_indices, to_indices = batch_processing( - log, start_sec=1.5, end_sec=3.5, tolerance=10, window_size=500 + log, start_sec=1.5, end_sec=3.5, tolerance=10, window_size=500, fast=False ) - self.assertEqual(times, [2.0, 3.0]) self.assertEqual(from_indices, [0, 0]) self.assertEqual(to_indices, [500, 500]) - def test_analyze_scan(self): - # scan of length 1800 + # Test fast batch processing + times_f, from_indices_f, to_indices_f = batch_processing( + log, start_sec=1.5, end_sec=3.5, tolerance=10, window_size=500, fast=True + ) + self.assertEqual(times_f, [2.0, 3.0]) + self.assertEqual(from_indices_f, [0, 0]) + self.assertEqual(to_indices_f, [500, 500]) + + @patch('lidarroad.slow_get_best_match') + def test_analyze_scan(self, mock_slow): + mock_slow.return_value = (0, 500) scan = [10] * 1800 - diff, from_i, to_i = analyze_scan(scan, tolerance=10, window_size=500) - # Expected diff of [10] * 1800 is an array of 1799 zeros + + # Standard mode (runs and asserts slow version matches) + diff, from_i, to_i = analyze_scan(scan, tolerance=10, window_size=500, fast=False) np.testing.assert_array_equal(diff, np.zeros(1799)) self.assertEqual(from_i, 0) self.assertEqual(to_i, 500) + mock_slow.assert_called_once() + + mock_slow.reset_mock() + + # Fast mode (skips slow match calculation and assertion) + diff_fast, from_i_fast, to_i_fast = analyze_scan(scan, tolerance=10, window_size=500, fast=True) + np.testing.assert_array_equal(diff_fast, np.zeros(1799)) + self.assertEqual(from_i_fast, 0) + self.assertEqual(to_i_fast, 500) + mock_slow.assert_not_called() if __name__ == '__main__': From b4520d736168d51fed70913c0d4c6ef894c5eb0e Mon Sep 17 00:00:00 2001 From: Elena Ai Date: Thu, 30 Jul 2026 19:25:29 +0200 Subject: [PATCH 15/20] ruff and README.md --- lidarroad/README.md | 29 ++++++++++++++++++++++++++++- lidarroad/lidarroad.py | 14 ++++++++++---- lidarroad/test_lidarroad.py | 20 +++++++++++--------- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/lidarroad/README.md b/lidarroad/README.md index 081a008..34cf29e 100644 --- a/lidarroad/README.md +++ b/lidarroad/README.md @@ -1,2 +1,29 @@ -Extract road boundary from WanJee down pointing lidar (10 degrees) +# LidarRoad +Extract road boundary from WanJee down pointing lidar (10 degrees). + +## Usage + +You can run the script in single scan analysis mode or in batch processing mode. + +### Single Scan Analysis + +Analyzes and visualizes the smoothness of a road from a single scan: +```bash +uv run python lidarroad.py [--jump ] [--width ] [--tolerance ] [--fast] +``` + +- ``: Path to the OSGAR logfile. +- `--jump`, `-j`: Jump forward to the specified time in seconds before starting analysis. +- `--width`, `-w`: Window size/width in meters (default is `3.0`). +- `--tolerance`, `-t`: Smoothness difference tolerance in millimeters between neighboring scan points (default is `10`). +- `--fast`: Bypass the legacy comparative calculation and dual assertions for maximum execution speed. + +### Batch Processing Mode + +Analyzes scans over a specified time interval and draws a unified plot of the best matching boundaries over time: +```bash +uv run python lidarroad.py --batch [--width ] [--tolerance ] [--fast] +``` + +- `--batch`: Runs in batch mode, taking exact `` and `` limits. diff --git a/lidarroad/lidarroad.py b/lidarroad/lidarroad.py index cabf52e..0f3d2e9 100644 --- a/lidarroad/lidarroad.py +++ b/lidarroad/lidarroad.py @@ -2,7 +2,6 @@ from datetime import timedelta import numpy as np - from osgar.logger import LogReaderEx @@ -35,7 +34,9 @@ def analyze_scan(scan, tolerance=10, window_size = 300, fast=False): from_i, to_i = get_best_match(mask, window_size) if not fast: from_i_slow, to_i_slow = slow_get_best_match(mask, window_size) - assert (from_i, to_i) == (from_i_slow, to_i_slow), f"Optimization mismatch: {(from_i, to_i)} != {(from_i_slow, to_i_slow)}" + assert (from_i, to_i) == (from_i_slow, to_i_slow), ( + f"Optimization mismatch: {(from_i, to_i)} != {(from_i_slow, to_i_slow)}" + ) return diff, from_i, to_i @@ -95,7 +96,10 @@ def main(): parser.add_argument('--jump', '-j', help='jump in seconds', type=float) parser.add_argument('--width', '-w', help='width in meters', type=float, default=3.0) parser.add_argument('--tolerance', '-t', help='tolerance in millimeters', type=int, default=10) - parser.add_argument('--batch', nargs=2, type=float, metavar=('START', 'END'), help='run in batch mode for time range in seconds') + parser.add_argument( + '--batch', nargs=2, type=float, metavar=('START', 'END'), + help='run in batch mode for time range in seconds' + ) parser.add_argument('--fast', action='store_true', help='skip slow match calculation and assertion') args = parser.parse_args() @@ -106,7 +110,9 @@ def main(): with LogReaderEx(args.logfile, ['vanjee.scan10']) as log: if args.batch is not None: start_sec, end_sec = args.batch - times, from_indices, to_indices = batch_processing(log, start_sec, end_sec, tolerance, window_size, fast=fast) + times, from_indices, to_indices = batch_processing( + log, start_sec, end_sec, tolerance, window_size, fast=fast + ) if times: draw_batch(times, from_indices, to_indices) else: diff --git a/lidarroad/test_lidarroad.py b/lidarroad/test_lidarroad.py index 47dcedb..3529e8d 100644 --- a/lidarroad/test_lidarroad.py +++ b/lidarroad/test_lidarroad.py @@ -1,9 +1,11 @@ import unittest +from datetime import timedelta from unittest.mock import patch + import numpy as np -from datetime import timedelta -from lidarroad import get_best_match, slow_get_best_match, analyze_scan, draw_scan, draw_batch, batch_processing +from lidarroad import analyze_scan, batch_processing, draw_batch, draw_scan, get_best_match, slow_get_best_match + class TestLidarRoad(unittest.TestCase): def test_get_best_match(self): @@ -40,17 +42,17 @@ def test_draw_scan_with_tolerance_and_interval(self, mock_plot, mock_axhline, mo scan = [1, 2, 3] draw_scan(scan, tolerance=15, interval=(10, 20)) mock_plot.assert_called_once_with(scan) - + # Check horizontal lines self.assertEqual(mock_axhline.call_count, 2) mock_axhline.assert_any_call(y=15, color='r', linestyle='--') mock_axhline.assert_any_call(y=-15, color='r', linestyle='--') - + # Check vertical lines self.assertEqual(mock_axvline.call_count, 2) mock_axvline.assert_any_call(x=10, color='g', linestyle='--') mock_axvline.assert_any_call(x=20, color='g', linestyle='--') - + mock_show.assert_called_once() @patch('matplotlib.pyplot.show') @@ -76,11 +78,11 @@ def test_draw_batch(self, mock_plot, mock_title, mock_xlabel, mock_ylabel, mock_ from_i = [100, 110, 120] to_i = [400, 410, 420] draw_batch(times, from_i, to_i) - + self.assertEqual(mock_plot.call_count, 2) mock_plot.assert_any_call(times, from_i, 'g.-', label='from_i') mock_plot.assert_any_call(times, to_i, 'b.-', label='to_i') - + mock_xlabel.assert_called_once_with('Time (s)') mock_ylabel.assert_called_once_with('Scan Index') mock_title.assert_called_once_with('Best Matching Interval Boundaries Over Time') @@ -95,7 +97,7 @@ def test_batch_processing(self): (timedelta(seconds=3.0), 'vanjee.scan10', mock_scan), (timedelta(seconds=4.0), 'vanjee.scan10', mock_scan) ] - + # Test standard batch processing times, from_indices, to_indices = batch_processing( log, start_sec=1.5, end_sec=3.5, tolerance=10, window_size=500, fast=False @@ -116,7 +118,7 @@ def test_batch_processing(self): def test_analyze_scan(self, mock_slow): mock_slow.return_value = (0, 500) scan = [10] * 1800 - + # Standard mode (runs and asserts slow version matches) diff, from_i, to_i = analyze_scan(scan, tolerance=10, window_size=500, fast=False) np.testing.assert_array_equal(diff, np.zeros(1799)) From ad91835b4a1a1c7af1f9e677a4fe10f7c6959440 Mon Sep 17 00:00:00 2001 From: Martin Dlouhy Date: Fri, 31 Jul 2026 06:43:20 +0200 Subject: [PATCH 16/20] use absolute imports --- lidarroad/__init__.py | 0 lidarroad/test_lidarroad.py | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 lidarroad/__init__.py diff --git a/lidarroad/__init__.py b/lidarroad/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lidarroad/test_lidarroad.py b/lidarroad/test_lidarroad.py index 3529e8d..a449fd2 100644 --- a/lidarroad/test_lidarroad.py +++ b/lidarroad/test_lidarroad.py @@ -4,7 +4,8 @@ import numpy as np -from lidarroad import analyze_scan, batch_processing, draw_batch, draw_scan, get_best_match, slow_get_best_match +from lidarroad.lidarroad import (analyze_scan, batch_processing, draw_batch, draw_scan, + get_best_match, slow_get_best_match) class TestLidarRoad(unittest.TestCase): @@ -114,7 +115,7 @@ def test_batch_processing(self): self.assertEqual(from_indices_f, [0, 0]) self.assertEqual(to_indices_f, [500, 500]) - @patch('lidarroad.slow_get_best_match') + @patch('lidarroad.lidarroad.slow_get_best_match') def test_analyze_scan(self, mock_slow): mock_slow.return_value = (0, 500) scan = [10] * 1800 From 7e52e480dca8fcf9b9bb8903379f34431c64a84d Mon Sep 17 00:00:00 2001 From: Elena Ai Date: Fri, 31 Jul 2026 06:58:04 +0200 Subject: [PATCH 17/20] Elena's undo :( --- lidarroad/__init__.py | 18 ++++++++++++++++++ lidarroad/test_lidarroad.py | 5 ++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lidarroad/__init__.py b/lidarroad/__init__.py index e69de29..9417845 100644 --- a/lidarroad/__init__.py +++ b/lidarroad/__init__.py @@ -0,0 +1,18 @@ +from .lidarroad import ( + analyze_scan as analyze_scan, +) +from .lidarroad import ( + batch_processing as batch_processing, +) +from .lidarroad import ( + draw_batch as draw_batch, +) +from .lidarroad import ( + draw_scan as draw_scan, +) +from .lidarroad import ( + get_best_match as get_best_match, +) +from .lidarroad import ( + slow_get_best_match as slow_get_best_match, +) diff --git a/lidarroad/test_lidarroad.py b/lidarroad/test_lidarroad.py index a449fd2..3529e8d 100644 --- a/lidarroad/test_lidarroad.py +++ b/lidarroad/test_lidarroad.py @@ -4,8 +4,7 @@ import numpy as np -from lidarroad.lidarroad import (analyze_scan, batch_processing, draw_batch, draw_scan, - get_best_match, slow_get_best_match) +from lidarroad import analyze_scan, batch_processing, draw_batch, draw_scan, get_best_match, slow_get_best_match class TestLidarRoad(unittest.TestCase): @@ -115,7 +114,7 @@ def test_batch_processing(self): self.assertEqual(from_indices_f, [0, 0]) self.assertEqual(to_indices_f, [500, 500]) - @patch('lidarroad.lidarroad.slow_get_best_match') + @patch('lidarroad.slow_get_best_match') def test_analyze_scan(self, mock_slow): mock_slow.return_value = (0, 500) scan = [10] * 1800 From b00da9247cb13354e5e2a891266579ce08859da8 Mon Sep 17 00:00:00 2001 From: Elena Ai Date: Fri, 31 Jul 2026 09:21:31 +0200 Subject: [PATCH 18/20] FIXUP1 1. Critical Off-by-One Bug in Sliding Window Logic Both slow_get_best_match and the vectorized get_best_match implementations contain an identical off-by-one error. They skip the very last possible window of the array. * In slow_get_best_match: The loop for i in range(0, len(mask) - window_size): stops one element early. It should be range(0, len(mask) - window_size + 1). * In get_best_match: The window_sums array allocation np.empty(len(mask) - window_size) is one element too small. Additionally, the slice cum[window_size:-1] drops the last element. The correct vectorized form should be: 1 window_sums = np.empty(len(mask) - window_size + 1, dtype=cum.dtype) 2 window_sums[0] = cum[window_size - 1] 3 window_sums[1:] = cum[window_size:] - cum[:-window_size] * Test Masking the Bug: The test_get_best_match passes because both the slow and fast implementations share this exact same bug, so their outputs match. If you add a test case where the optimal window is exactly at the end of the array (e.g., mask = [0, 0, 1, 1, 1] with window_size = 3), both algorithms will incorrectly return index 1 instead of 2. --- lidarroad/lidarroad.py | 6 +++--- lidarroad/test_lidarroad.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lidarroad/lidarroad.py b/lidarroad/lidarroad.py index 0f3d2e9..6a8cbbc 100644 --- a/lidarroad/lidarroad.py +++ b/lidarroad/lidarroad.py @@ -8,7 +8,7 @@ def slow_get_best_match(mask, window_size): best_i = 0 best_sum = None - for i in range(0, len(mask) - window_size): + for i in range(0, len(mask) - window_size + 1): value = sum(mask[i:i+window_size]) if best_sum is None or value > best_sum: best_sum = value @@ -20,9 +20,9 @@ def get_best_match(mask, window_size): if len(mask) <= window_size: return 0, window_size cum = np.cumsum(np.asarray(mask)) - window_sums = np.empty(len(mask) - window_size, dtype=cum.dtype) + window_sums = np.empty(len(mask) - window_size + 1, dtype=cum.dtype) window_sums[0] = cum[window_size - 1] - window_sums[1:] = cum[window_size:-1] - cum[:-window_size-1] + window_sums[1:] = cum[window_size:] - cum[:-window_size] best_i = int(np.argmax(window_sums)) return best_i, best_i + window_size diff --git a/lidarroad/test_lidarroad.py b/lidarroad/test_lidarroad.py index 3529e8d..f9f3878 100644 --- a/lidarroad/test_lidarroad.py +++ b/lidarroad/test_lidarroad.py @@ -15,6 +15,16 @@ def test_get_best_match(self): self.assertEqual(from_i, 2) self.assertEqual(to_i, 5) + # Bug reproduction: optimal window is at the very end of the array + mask_end = [0, 0, 1, 1, 1] + from_i_end, to_i_end = get_best_match(mask_end, 3) + self.assertEqual(from_i_end, 2) + self.assertEqual(to_i_end, 5) + + from_i_slow, to_i_slow = slow_get_best_match(mask_end, 3) + self.assertEqual(from_i_slow, 2) + self.assertEqual(to_i_slow, 5) + # Extended comparative validation between fast and slow implementations np.random.seed(42) # For deterministic reproducibility masks = [ From 20d65023833c5f1a438f377a3e5ffc6fa739135a Mon Sep 17 00:00:00 2001 From: Elena Ai Date: Fri, 31 Jul 2026 09:24:43 +0200 Subject: [PATCH 19/20] FIXUP2 2. Redundant Aliasing in __init__.py The lidarroad/__init__.py file uses explicit aliases like analyze_scan as analyze_scan. While this syntax is occasionally used to explicitly re-export symbols for strict type checkers (like pyright), this codebase doesn't use type hints. This makes the imports overly verbose. A clean from .lidarroad import analyze_scan, batch_processing, ... would be more idiomatic. --- lidarroad/__init__.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/lidarroad/__init__.py b/lidarroad/__init__.py index 9417845..34e2c1b 100644 --- a/lidarroad/__init__.py +++ b/lidarroad/__init__.py @@ -1,18 +1,8 @@ from .lidarroad import ( - analyze_scan as analyze_scan, -) -from .lidarroad import ( - batch_processing as batch_processing, -) -from .lidarroad import ( - draw_batch as draw_batch, -) -from .lidarroad import ( - draw_scan as draw_scan, -) -from .lidarroad import ( - get_best_match as get_best_match, -) -from .lidarroad import ( - slow_get_best_match as slow_get_best_match, + analyze_scan, + batch_processing, + draw_batch, + draw_scan, + get_best_match, + slow_get_best_match, ) From ac4408a48e107516921d97de3cdda99a6066219b Mon Sep 17 00:00:00 2001 From: Elena Ai Date: Fri, 31 Jul 2026 09:34:21 +0200 Subject: [PATCH 20/20] FIXUP3 3. Missing Test Coverage for Window Boundary Edge Case In test_lidarroad.py, the test_get_best_match test loops over multiple arrays and window sizes but intentionally skips testing when the sizes are equal: if len(m) > window_size:. While the implementations do currently handle len(m) == window_size without crashing (they correctly return 0, window_size), it is best practice to remove that if constraint in the test to ensure this edge case remains explicitly covered and doesn't regress in the future. --- lidarroad/test_lidarroad.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lidarroad/test_lidarroad.py b/lidarroad/test_lidarroad.py index f9f3878..cee72a5 100644 --- a/lidarroad/test_lidarroad.py +++ b/lidarroad/test_lidarroad.py @@ -36,13 +36,12 @@ def test_get_best_match(self): ] for m in masks: for window_size in [1, 3, 5, 50, 100]: - if len(m) > window_size: - from_fast, to_fast = get_best_match(m, window_size) - from_slow, to_slow = slow_get_best_match(m, window_size) - self.assertEqual( - (from_fast, to_fast), (from_slow, to_slow), - f"Mismatch for window_size {window_size} on mask {m if len(m) < 20 else 'random'}" - ) + from_fast, to_fast = get_best_match(m, window_size) + from_slow, to_slow = slow_get_best_match(m, window_size) + self.assertEqual( + (from_fast, to_fast), (from_slow, to_slow), + f"Mismatch for window_size {window_size} on mask {m if len(m) < 20 else 'random'}" + ) @patch('matplotlib.pyplot.show') @patch('matplotlib.pyplot.axvline')