Back to report index

Step 3 segmentationtrajectory 60bcec1: AI3D-382 Pin zero-fill semantics in the schema parity test and document the fallback cast

Miroslav Simko <ms@iolabs.ch> 2026-09-01T23:35:31+02:00

Commit #102 · 5 snippets

 .../segment_mapper.py                              |  7 ++++++
 tests/test_number_of_returns.py                    | 28 +++++++++++++++++++++-
 2 files changed, 34 insertions(+), 1 deletion(-)
Importance #1: src/iolabs_point_cloud_segmentation_trajectory/segment_mapper.py @@ -92,8 +92,15 @@
92 LAS-backed field means calling this with a new `key`/`dtype`, not writing a92 LAS-backed field means calling this with a new `key`/`dtype`, not writing a
93 new helper. For `number_of_returns` the zero fill reads downstream as93 new helper. For `number_of_returns` the zero fill reads downstream as
94 "unknown" (see :data:`NUMBER_OF_RETURNS_DTYPE`).94 "unknown" (see :data:`NUMBER_OF_RETURNS_DTYPE`).
9595
96 The cast is a plain `astype`, which wraps out-of-range values silently.
97 That cannot happen for `number_of_returns` (a 34 bit LAS field), but a
98 future field whose LAS source is wider than *dtype* needs a range check at
99 the call site `iolabs.common.segment_points_io` raises on out-of-range
100 values at save time, and this helper must not smuggle wrapped values past
101 that contract.
102
96 Args:103 Args:
97 record: A laspy point record (whole-file `LasData` or a chunk).104 record: A laspy point record (whole-file `LasData` or a chunk).
98 key: Name of the LAS field to read off *record*.105 key: Name of the LAS field to read off *record*.
99 dtype: Storage dtype the values are cast to, and of the zero fill.106 dtype: Storage dtype the values are cast to, and of the zero fill.
Importance #2: tests/test_number_of_returns.py @@ -194,13 +194,15 @@
194 with pytest.raises(TypeError):194 with pytest.raises(TypeError):
195 sm.DEFAULT_FIELD_DTYPES["points"] = np.dtype(np.float32) # type: ignore[index]195 sm.DEFAULT_FIELD_DTYPES["points"] = np.dtype(np.float32) # type: ignore[index]
196196
197197
198def test_npz_schema_matches_common_segment_points_io() -> None:198def test_npz_schema_matches_common_segment_points_io(tmp_path: Path) -> None:
199 """The duplicated run3 NPZ schema must not drift from the SSOT in common.199 """The duplicated run3 NPZ schema must not drift from the SSOT in common.
200200
201 Skips against an `iolabs-common` that predates `segment_points_io`, and201 Skips against an `iolabs-common` that predates `segment_points_io`, and
202 activates by itself once the floor is raised to a release that has it.202 activates by itself once the floor is raised to a release that has it.
203 Pins semantics as well as names: legacy NPZs must zero-fill (0 = unknown,
204 never 1), and stored counts must survive a load round-trip untouched.
203 """205 """
204 segment_points_io = pytest.importorskip(206 segment_points_io = pytest.importorskip(
205 "iolabs.common.segment_points_io",207 "iolabs.common.segment_points_io",
206 reason="installed iolabs-common predates the segment_points_io SSOT",208 reason="installed iolabs-common predates the segment_points_io SSOT",
Importance #3: tests/test_number_of_returns.py @@ -211,4 +213,28 @@
211 assert (213 assert (
212 np.dtype(sm.NUMBER_OF_RETURNS_DTYPE)214 np.dtype(sm.NUMBER_OF_RETURNS_DTYPE)
213 == np.dtype(segment_points_io.NUMBER_OF_RETURNS_DTYPE)215 == np.dtype(segment_points_io.NUMBER_OF_RETURNS_DTYPE)
214 )216 )
217
218 legacy_fields = {
219 "points": np.zeros((3, 3), dtype=np.float64),
220 "red": np.ones(3, dtype=np.uint16),
221 "green": np.ones(3, dtype=np.uint16),
222 "blue": np.ones(3, dtype=np.uint16),
223 "intensity": np.ones(3, dtype=np.uint16),
224 "scan_angle": np.zeros(3, dtype=np.int16),
225 }
226 legacy_path = tmp_path / "legacy.npz"
227 np.savez(legacy_path, **legacy_fields)
228 filled = segment_points_io.load_points_npz(legacy_path)[sm.NUMBER_OF_RETURNS_KEY]
229 assert filled.dtype == np.dtype(sm.NUMBER_OF_RETURNS_DTYPE)
230 np.testing.assert_array_equal(
231 filled, np.zeros(3, dtype=sm.NUMBER_OF_RETURNS_DTYPE)
232 )
233
234 counts = np.array([1, 2, 7], dtype=sm.NUMBER_OF_RETURNS_DTYPE)
235 modern_path = tmp_path / "modern.npz"
236 np.savez(modern_path, **legacy_fields, **{sm.NUMBER_OF_RETURNS_KEY: counts})
237 round_tripped = segment_points_io.load_points_npz(modern_path)[
238 sm.NUMBER_OF_RETURNS_KEY
239 ]
240 np.testing.assert_array_equal(round_tripped, counts)
Importance #4: tests/test_number_of_returns.py @@ -194,13 +194,15 @@
194 with pytest.raises(TypeError):194 with pytest.raises(TypeError):
195 sm.DEFAULT_FIELD_DTYPES["points"] = np.dtype(np.float32) # type: ignore[index]195 sm.DEFAULT_FIELD_DTYPES["points"] = np.dtype(np.float32) # type: ignore[index]
196196
197197
198def test_npz_schema_matches_common_segment_points_io() -> None:198def test_npz_schema_matches_common_segment_points_io(tmp_path: Path) -> None:
199 """The duplicated run3 NPZ schema must not drift from the SSOT in common.199 """The duplicated run3 NPZ schema must not drift from the SSOT in common.
200200
201 Skips against an `iolabs-common` that predates `segment_points_io`, and201 Skips against an `iolabs-common` that predates `segment_points_io`, and
202 activates by itself once the floor is raised to a release that has it.202 activates by itself once the floor is raised to a release that has it.
203 Pins semantics as well as names: legacy NPZs must zero-fill (0 = unknown,
204 never 1), and stored counts must survive a load round-trip untouched.
203 """205 """
204 segment_points_io = pytest.importorskip(206 segment_points_io = pytest.importorskip(
205 "iolabs.common.segment_points_io",207 "iolabs.common.segment_points_io",
206 reason="installed iolabs-common predates the segment_points_io SSOT",208 reason="installed iolabs-common predates the segment_points_io SSOT",
Importance #5: tests/test_number_of_returns.py @@ -211,4 +213,28 @@
211 assert (213 assert (
212 np.dtype(sm.NUMBER_OF_RETURNS_DTYPE)214 np.dtype(sm.NUMBER_OF_RETURNS_DTYPE)
213 == np.dtype(segment_points_io.NUMBER_OF_RETURNS_DTYPE)215 == np.dtype(segment_points_io.NUMBER_OF_RETURNS_DTYPE)
214 )216 )
217
218 legacy_fields = {
219 "points": np.zeros((3, 3), dtype=np.float64),
220 "red": np.ones(3, dtype=np.uint16),
221 "green": np.ones(3, dtype=np.uint16),
222 "blue": np.ones(3, dtype=np.uint16),
223 "intensity": np.ones(3, dtype=np.uint16),
224 "scan_angle": np.zeros(3, dtype=np.int16),
225 }
226 legacy_path = tmp_path / "legacy.npz"
227 np.savez(legacy_path, **legacy_fields)
228 filled = segment_points_io.load_points_npz(legacy_path)[sm.NUMBER_OF_RETURNS_KEY]
229 assert filled.dtype == np.dtype(sm.NUMBER_OF_RETURNS_DTYPE)
230 np.testing.assert_array_equal(
231 filled, np.zeros(3, dtype=sm.NUMBER_OF_RETURNS_DTYPE)
232 )
233
234 counts = np.array([1, 2, 7], dtype=sm.NUMBER_OF_RETURNS_DTYPE)
235 modern_path = tmp_path / "modern.npz"
236 np.savez(modern_path, **legacy_fields, **{sm.NUMBER_OF_RETURNS_KEY: counts})
237 round_tripped = segment_points_io.load_points_npz(modern_path)[
238 sm.NUMBER_OF_RETURNS_KEY
239 ]
240 np.testing.assert_array_equal(round_tripped, counts)