Miroslav Simko <ms@iolabs.ch> 2026-09-01T23:35:31+02:00
Commit #19 · 5 snippets
.../segment_mapper.py | 7 ++++++ tests/test_number_of_returns.py | 28 +++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-)
Docstring warning: plain astype wraps silently, so a wider future field needs a range check at the call site.
| 92 | LAS-backed field means calling this with a new `key`/`dtype`, not writing a | 92 | 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 as | 93 | 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`). |
| 95 | 95 | ||
| 96 | The cast is a plain `astype`, which wraps out-of-range values silently. | ||
| 97 | That cannot happen for `number_of_returns` (a 3–4 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. |
| 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) |
| 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] |
| 196 | 196 | ||
| 197 | 197 | ||
| 198 | def test_npz_schema_matches_common_segment_points_io() -> None: | 198 | def 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. |
| 200 | 200 | ||
| 201 | Skips against an `iolabs-common` that predates `segment_points_io`, and | 201 | 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", |
| 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] |
| 196 | 196 | ||
| 197 | 197 | ||
| 198 | def test_npz_schema_matches_common_segment_points_io() -> None: | 198 | def 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. |
| 200 | 200 | ||
| 201 | Skips against an `iolabs-common` that predates `segment_points_io`, and | 201 | 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", |
| 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) |
Test/doc only. Parity test now pins zero-fill semantics and a round-trip; fallback helper docstring warns that its plain
astypewould wrap a wider future field silently, so range checks belong at the call site.