Miroslav Simko <ms@iolabs.ch> 2026-09-01T10:50:28+02:00
Commit #96 · 5 snippets
AGENTS.md | 2 +- CLAUDE.md | 2 +- knowledge.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-)
| 33 | 4. For each consecutive plane pair, intersect every spline (`fit_spline.Spline3D.plane_intersections`) and assign LAS files to a segment when an intersection is within `max_distance_to_plane`. Splines that miss every plane are placed via `_point_segment` (sign-of-distance check between consecutive planes). | 33 | 4. For each consecutive plane pair, intersect every spline (`fit_spline.Spline3D.plane_intersections`) and assign LAS files to a segment when an intersection is within `max_distance_to_plane`. Splines that miss every plane are placed via `_point_segment` (sign-of-distance check between consecutive planes). |
| 34 | 5. Write `segment_trajectories.json` (segment_idx → list of LAS paths). | 34 | 5. Write `segment_trajectories.json` (segment_idx → list of LAS paths). |
| 35 | 6. If `save_points_between_planes`, call `divide_las_file_by_planes` for each LAS — single-threaded on CUDA, otherwise a `ThreadPoolExecutor(max_workers=max_parallel_las_files)`. Per-segment `.npz` files land under `<segments_base_dir>/segment_NNN/` (3-digit zero-padded index, matching the `point{i:03d}`/`normal{i:03d}` keys in `run3_planes.npz`) and `save_version_json` drops a `run3_versions.json` next to them. | 35 | 6. If `save_points_between_planes`, call `divide_las_file_by_planes` for each LAS — single-threaded on CUDA, otherwise a `ThreadPoolExecutor(max_workers=max_parallel_las_files)`. Per-segment `.npz` files land under `<segments_base_dir>/segment_NNN/` (3-digit zero-padded index, matching the `point{i:03d}`/`normal{i:03d}` keys in `run3_planes.npz`) and `save_version_json` drops a `run3_versions.json` next to them. |
| 36 | 36 | ||
| 37 | - **`divide_las_file_by_planes`** streams the LAS via `laspy.open(...).chunk_iterator(las_points_per_chunk)`, never loading the whole file. Per chunk: optional `|scan_angle| < angle_limit` filter, then a sign-count mask against all selected planes assigns each point to a segment bucket. Scan-angle field is auto-detected (`scan_angle_rank` legacy vs `scan_angle` newer); RGB and intensity are required and the code raises if missing. Output points are written **geoshift-relative**. | 37 | - **`divide_las_file_by_planes`** streams the LAS via `laspy.open(...).chunk_iterator(las_points_per_chunk)`, never loading the whole file. Per chunk: optional `|scan_angle| < angle_limit` filter, then a sign-count mask against all selected planes assigns each point to a segment bucket. Scan-angle field is auto-detected (`scan_angle_rank` legacy vs `scan_angle` newer); RGB and intensity are required and the code raises if missing. Each per-segment `.npz` also carries `number_of_returns` (uint8, AI3D-382); LAS files without that field degrade to zeros, which the run3 NPZ contract reads as "unknown" (0 is not a legal LAS return count). Output points are written **geoshift-relative**. |
| 38 | 38 | ||
| 39 | - **`_config.py`** — strict whitelist validation. `ALLOWED_SEGMENT_MAPPER_CONFIG_KEYS` and `ALLOWED_SEGMENT_MAPPER_FILE_NAMING_KEYS` are enforced; unknown keys raise `SegmentMapperConfigError`. `normalize_segment_mapper_config` fills defaults; `build_segment_mapper_config(overrides=..., config_path=...)` does deep-merge over `segment_mapper.default.json`. **When adding a new config key you must update both the whitelist set and `normalize_segment_mapper_config`'s `setdefault` block, and add a default in `segment_mapper.default.json`.** | 39 | - **`_config.py`** — strict whitelist validation. `ALLOWED_SEGMENT_MAPPER_CONFIG_KEYS` and `ALLOWED_SEGMENT_MAPPER_FILE_NAMING_KEYS` are enforced; unknown keys raise `SegmentMapperConfigError`. `normalize_segment_mapper_config` fills defaults; `build_segment_mapper_config(overrides=..., config_path=...)` does deep-merge over `segment_mapper.default.json`. **When adding a new config key you must update both the whitelist set and `normalize_segment_mapper_config`'s `setdefault` block, and add a default in `segment_mapper.default.json`.** |
| 40 | 40 | ||
| 41 | - **`segment_mapper.default.json`** — bundled defaults. It is force-included into the wheel via `[tool.hatch.build.targets.wheel.force-include]` in `pyproject.toml`; if you rename or move it, update that mapping or the installed package will be missing the file at runtime. | 41 | - **`segment_mapper.default.json`** — bundled defaults. It is force-included into the wheel via `[tool.hatch.build.targets.wheel.force-include]` in `pyproject.toml`; if you rename or move it, update that mapping or the installed package will be missing the file at runtime. |
| 32 | 4. For each consecutive plane pair, intersect every spline (`fit_spline.Spline3D.plane_intersections`) and assign LAS files to a segment when an intersection is within `max_distance_to_plane`. Splines that miss every plane are placed via `_point_segment` (sign-of-distance check between consecutive planes). | 32 | 4. For each consecutive plane pair, intersect every spline (`fit_spline.Spline3D.plane_intersections`) and assign LAS files to a segment when an intersection is within `max_distance_to_plane`. Splines that miss every plane are placed via `_point_segment` (sign-of-distance check between consecutive planes). |
| 33 | 5. Write `segment_trajectories.json` (segment_idx → list of LAS paths). | 33 | 5. Write `segment_trajectories.json` (segment_idx → list of LAS paths). |
| 34 | 6. If `save_points_between_planes`, call `divide_las_file_by_planes` for each LAS via a `ThreadPoolExecutor(max_workers=max_parallel_las_files)` (capped at `os.cpu_count()`). Per-segment `.npz` files land under `<segments_base_dir>/segment_NNN/` (3-digit zero-padded index, matching the `point{i:03d}`/`normal{i:03d}` keys in `run3_planes.npz`) and `save_version_json` drops a `run3_versions.json` next to them. | 34 | 6. If `save_points_between_planes`, call `divide_las_file_by_planes` for each LAS via a `ThreadPoolExecutor(max_workers=max_parallel_las_files)` (capped at `os.cpu_count()`). Per-segment `.npz` files land under `<segments_base_dir>/segment_NNN/` (3-digit zero-padded index, matching the `point{i:03d}`/`normal{i:03d}` keys in `run3_planes.npz`) and `save_version_json` drops a `run3_versions.json` next to them. |
| 35 | 35 | ||
| 36 | - **`divide_las_file_by_planes`** streams the LAS via `laspy.open(...).chunk_iterator(las_points_per_chunk)`, never loading the whole file. Per chunk: optional `|scan_angle| < angle_limit` filter, then a sign-count mask against all selected planes assigns each point to a segment bucket. Scan-angle field is auto-detected (`scan_angle_rank` legacy vs `scan_angle` newer); RGB and intensity are required and the code raises if missing. Output points are written **geoshift-relative**. | 36 | - **`divide_las_file_by_planes`** streams the LAS via `laspy.open(...).chunk_iterator(las_points_per_chunk)`, never loading the whole file. Per chunk: optional `|scan_angle| < angle_limit` filter, then a sign-count mask against all selected planes assigns each point to a segment bucket. Scan-angle field is auto-detected (`scan_angle_rank` legacy vs `scan_angle` newer); RGB and intensity are required and the code raises if missing. Each per-segment `.npz` also carries `number_of_returns` (uint8, AI3D-382); LAS files without that field degrade to zeros, which the run3 NPZ contract reads as "unknown" (0 is not a legal LAS return count). Output points are written **geoshift-relative**. |
| 37 | 37 | ||
| 38 | - **`_config.py`** — strict whitelist validation. `ALLOWED_SEGMENT_MAPPER_CONFIG_KEYS` and `ALLOWED_SEGMENT_MAPPER_FILE_NAMING_KEYS` are enforced; unknown keys raise `SegmentMapperConfigError`. `normalize_segment_mapper_config` fills defaults; `build_segment_mapper_config(overrides=..., config_path=...)` does deep-merge over `segment_mapper.default.json`. **When adding a new config key you must update both the whitelist set and `normalize_segment_mapper_config`'s `setdefault` block, and add a default in `segment_mapper.default.json`.** | 38 | - **`_config.py`** — strict whitelist validation. `ALLOWED_SEGMENT_MAPPER_CONFIG_KEYS` and `ALLOWED_SEGMENT_MAPPER_FILE_NAMING_KEYS` are enforced; unknown keys raise `SegmentMapperConfigError`. `normalize_segment_mapper_config` fills defaults; `build_segment_mapper_config(overrides=..., config_path=...)` does deep-merge over `segment_mapper.default.json`. **When adding a new config key you must update both the whitelist set and `normalize_segment_mapper_config`'s `setdefault` block, and add a default in `segment_mapper.default.json`.** |
| 39 | 39 | ||
| 40 | - **`segment_mapper.default.json`** — bundled defaults. It is force-included into the wheel via `[tool.hatch.build.targets.wheel.force-include]` in `pyproject.toml`; if you rename or move it, update that mapping or the installed package will be missing the file at runtime. | 40 | - **`segment_mapper.default.json`** — bundled defaults. It is force-included into the wheel via `[tool.hatch.build.targets.wheel.force-include]` in `pyproject.toml`; if you rename or move it, update that mapping or the installed package will be missing the file at runtime. |
| 34 | 5. For each consecutive plane pair, intersect every spline (`Spline3D.plane_intersections`) and assign LAS files to a segment when an intersection is within `max_distance_to_plane`. Splines missing every plane are placed via `_point_segment` (sign-of-distance check between consecutive planes). | 34 | 5. For each consecutive plane pair, intersect every spline (`Spline3D.plane_intersections`) and assign LAS files to a segment when an intersection is within `max_distance_to_plane`. Splines missing every plane are placed via `_point_segment` (sign-of-distance check between consecutive planes). |
| 35 | 6. Write `segment_trajectories.json` (`segment_idx -> list[LAS path]`). | 35 | 6. Write `segment_trajectories.json` (`segment_idx -> list[LAS path]`). |
| 36 | 7. If `save_points_between_planes`: call `divide_las_file_by_planes` per LAS. **Single-threaded on CUDA**; otherwise `max_workers = max(1, min(max_parallel_las_files, os.cpu_count()))`, and a serial path is taken when `max_workers == 1` or only one LAS file is queued — else a `ThreadPoolExecutor` runs the splits in parallel. Per-segment `.npz` files land under `<segments_base_dir>/segment_NNN/` (3-digit zero-padded, matching the `point{i:03d}`/`normal{i:03d}` keys in `run3_planes.npz`). `save_version_json` then drops `run3_versions.json` in each segment dir. | 36 | 7. If `save_points_between_planes`: call `divide_las_file_by_planes` per LAS. **Single-threaded on CUDA**; otherwise `max_workers = max(1, min(max_parallel_las_files, os.cpu_count()))`, and a serial path is taken when `max_workers == 1` or only one LAS file is queued — else a `ThreadPoolExecutor` runs the splits in parallel. Per-segment `.npz` files land under `<segments_base_dir>/segment_NNN/` (3-digit zero-padded, matching the `point{i:03d}`/`normal{i:03d}` keys in `run3_planes.npz`). `save_version_json` then drops `run3_versions.json` in each segment dir. |
| 37 | 37 | ||
| 38 | - **`divide_las_file_by_planes`** streams the LAS via `laspy.open(...).chunk_iterator(las_points_per_chunk)` — never loads the whole file. Per chunk: optional `|scan_angle| < angle_limit` filter, then a sign-count mask against all selected planes assigns each point to a segment bucket. Scan-angle field is auto-detected (`scan_angle_rank` legacy vs `scan_angle` newer). RGB and intensity are **required** — missing fields raise. Output points are written **geoshift-relative**. | 38 | - **`divide_las_file_by_planes`** streams the LAS via `laspy.open(...).chunk_iterator(las_points_per_chunk)` — never loads the whole file. Per chunk: optional `|scan_angle| < angle_limit` filter, then a sign-count mask against all selected planes assigns each point to a segment bucket. Scan-angle field is auto-detected (`scan_angle_rank` legacy vs `scan_angle` newer). RGB and intensity are **required** — missing fields raise. Each per-segment `.npz` also carries `number_of_returns` (uint8, AI3D-382); LAS files without that field degrade to zeros, which the run3 NPZ contract reads as "unknown" (0 is not a legal LAS return count). Output points are written **geoshift-relative**. |
| 39 | 39 | ||
| 40 | - **`_config.py`** — strict whitelist validation. `ALLOWED_SEGMENT_MAPPER_CONFIG_KEYS` and `ALLOWED_SEGMENT_MAPPER_FILE_NAMING_KEYS` are enforced; unknown keys raise `SegmentMapperConfigError`. `normalize_segment_mapper_config` fills defaults. `build_segment_mapper_config(overrides=..., config_path=...)` deep-merges overrides over `segment_mapper.default.json`. | 40 | - **`_config.py`** — strict whitelist validation. `ALLOWED_SEGMENT_MAPPER_CONFIG_KEYS` and `ALLOWED_SEGMENT_MAPPER_FILE_NAMING_KEYS` are enforced; unknown keys raise `SegmentMapperConfigError`. `normalize_segment_mapper_config` fills defaults. `build_segment_mapper_config(overrides=..., config_path=...)` deep-merges overrides over `segment_mapper.default.json`. |
| 41 | 41 | ||
| 42 | - **`segment_mapper.default.json`** — bundled defaults. Force-included into the wheel via `[tool.hatch.build.targets.wheel.force-include]` in `pyproject.toml`. If you rename/move it, update that mapping or the installed package will be missing it at runtime. | 42 | - **`segment_mapper.default.json`** — bundled defaults. Force-included into the wheel via `[tool.hatch.build.targets.wheel.force-include]` in `pyproject.toml`. If you rename/move it, update that mapping or the installed package will be missing it at runtime. |
| 32 | 4. For each consecutive plane pair, intersect every spline (`fit_spline.Spline3D.plane_intersections`) and assign LAS files to a segment when an intersection is within `max_distance_to_plane`. Splines that miss every plane are placed via `_point_segment` (sign-of-distance check between consecutive planes). | 32 | 4. For each consecutive plane pair, intersect every spline (`fit_spline.Spline3D.plane_intersections`) and assign LAS files to a segment when an intersection is within `max_distance_to_plane`. Splines that miss every plane are placed via `_point_segment` (sign-of-distance check between consecutive planes). |
| 33 | 5. Write `segment_trajectories.json` (segment_idx → list of LAS paths). | 33 | 5. Write `segment_trajectories.json` (segment_idx → list of LAS paths). |
| 34 | 6. If `save_points_between_planes`, call `divide_las_file_by_planes` for each LAS via a `ThreadPoolExecutor(max_workers=max_parallel_las_files)` (capped at `os.cpu_count()`). Per-segment `.npz` files land under `<segments_base_dir>/segment_NNN/` (3-digit zero-padded index, matching the `point{i:03d}`/`normal{i:03d}` keys in `run3_planes.npz`) and `save_version_json` drops a `run3_versions.json` next to them. | 34 | 6. If `save_points_between_planes`, call `divide_las_file_by_planes` for each LAS via a `ThreadPoolExecutor(max_workers=max_parallel_las_files)` (capped at `os.cpu_count()`). Per-segment `.npz` files land under `<segments_base_dir>/segment_NNN/` (3-digit zero-padded index, matching the `point{i:03d}`/`normal{i:03d}` keys in `run3_planes.npz`) and `save_version_json` drops a `run3_versions.json` next to them. |
| 35 | 35 | ||
| 36 | - **`divide_las_file_by_planes`** streams the LAS via `laspy.open(...).chunk_iterator(las_points_per_chunk)`, never loading the whole file. Per chunk: optional `|scan_angle| < angle_limit` filter, then a sign-count mask against all selected planes assigns each point to a segment bucket. Scan-angle field is auto-detected (`scan_angle_rank` legacy vs `scan_angle` newer); RGB and intensity are required and the code raises if missing. Output points are written **geoshift-relative**. | 36 | - **`divide_las_file_by_planes`** streams the LAS via `laspy.open(...).chunk_iterator(las_points_per_chunk)`, never loading the whole file. Per chunk: optional `|scan_angle| < angle_limit` filter, then a sign-count mask against all selected planes assigns each point to a segment bucket. Scan-angle field is auto-detected (`scan_angle_rank` legacy vs `scan_angle` newer); RGB and intensity are required and the code raises if missing. Each per-segment `.npz` also carries `number_of_returns` (uint8, AI3D-382); LAS files without that field degrade to zeros, which the run3 NPZ contract reads as "unknown" (0 is not a legal LAS return count). Output points are written **geoshift-relative**. |
| 37 | 37 | ||
| 38 | - **`_config.py`** — strict whitelist validation. `ALLOWED_SEGMENT_MAPPER_CONFIG_KEYS` and `ALLOWED_SEGMENT_MAPPER_FILE_NAMING_KEYS` are enforced; unknown keys raise `SegmentMapperConfigError`. `normalize_segment_mapper_config` fills defaults; `build_segment_mapper_config(overrides=..., config_path=...)` does deep-merge over `segment_mapper.default.json`. **When adding a new config key you must update both the whitelist set and `normalize_segment_mapper_config`'s `setdefault` block, and add a default in `segment_mapper.default.json`.** | 38 | - **`_config.py`** — strict whitelist validation. `ALLOWED_SEGMENT_MAPPER_CONFIG_KEYS` and `ALLOWED_SEGMENT_MAPPER_FILE_NAMING_KEYS` are enforced; unknown keys raise `SegmentMapperConfigError`. `normalize_segment_mapper_config` fills defaults; `build_segment_mapper_config(overrides=..., config_path=...)` does deep-merge over `segment_mapper.default.json`. **When adding a new config key you must update both the whitelist set and `normalize_segment_mapper_config`'s `setdefault` block, and add a default in `segment_mapper.default.json`.** |
| 39 | 39 | ||
| 40 | - **`segment_mapper.default.json`** — bundled defaults. It is force-included into the wheel via `[tool.hatch.build.targets.wheel.force-include]` in `pyproject.toml`; if you rename or move it, update that mapping or the installed package will be missing the file at runtime. | 40 | - **`segment_mapper.default.json`** — bundled defaults. It is force-included into the wheel via `[tool.hatch.build.targets.wheel.force-include]` in `pyproject.toml`; if you rename or move it, update that mapping or the installed package will be missing the file at runtime. |
| 34 | 5. For each consecutive plane pair, intersect every spline (`Spline3D.plane_intersections`) and assign LAS files to a segment when an intersection is within `max_distance_to_plane`. Splines missing every plane are placed via `_point_segment` (sign-of-distance check between consecutive planes). | 34 | 5. For each consecutive plane pair, intersect every spline (`Spline3D.plane_intersections`) and assign LAS files to a segment when an intersection is within `max_distance_to_plane`. Splines missing every plane are placed via `_point_segment` (sign-of-distance check between consecutive planes). |
| 35 | 6. Write `segment_trajectories.json` (`segment_idx -> list[LAS path]`). | 35 | 6. Write `segment_trajectories.json` (`segment_idx -> list[LAS path]`). |
| 36 | 7. If `save_points_between_planes`: call `divide_las_file_by_planes` per LAS. **Single-threaded on CUDA**; otherwise `max_workers = max(1, min(max_parallel_las_files, os.cpu_count()))`, and a serial path is taken when `max_workers == 1` or only one LAS file is queued — else a `ThreadPoolExecutor` runs the splits in parallel. Per-segment `.npz` files land under `<segments_base_dir>/segment_NNN/` (3-digit zero-padded, matching the `point{i:03d}`/`normal{i:03d}` keys in `run3_planes.npz`). `save_version_json` then drops `run3_versions.json` in each segment dir. | 36 | 7. If `save_points_between_planes`: call `divide_las_file_by_planes` per LAS. **Single-threaded on CUDA**; otherwise `max_workers = max(1, min(max_parallel_las_files, os.cpu_count()))`, and a serial path is taken when `max_workers == 1` or only one LAS file is queued — else a `ThreadPoolExecutor` runs the splits in parallel. Per-segment `.npz` files land under `<segments_base_dir>/segment_NNN/` (3-digit zero-padded, matching the `point{i:03d}`/`normal{i:03d}` keys in `run3_planes.npz`). `save_version_json` then drops `run3_versions.json` in each segment dir. |
| 37 | 37 | ||
| 38 | - **`divide_las_file_by_planes`** streams the LAS via `laspy.open(...).chunk_iterator(las_points_per_chunk)` — never loads the whole file. Per chunk: optional `|scan_angle| < angle_limit` filter, then a sign-count mask against all selected planes assigns each point to a segment bucket. Scan-angle field is auto-detected (`scan_angle_rank` legacy vs `scan_angle` newer). RGB and intensity are **required** — missing fields raise. Output points are written **geoshift-relative**. | 38 | - **`divide_las_file_by_planes`** streams the LAS via `laspy.open(...).chunk_iterator(las_points_per_chunk)` — never loads the whole file. Per chunk: optional `|scan_angle| < angle_limit` filter, then a sign-count mask against all selected planes assigns each point to a segment bucket. Scan-angle field is auto-detected (`scan_angle_rank` legacy vs `scan_angle` newer). RGB and intensity are **required** — missing fields raise. Each per-segment `.npz` also carries `number_of_returns` (uint8, AI3D-382); LAS files without that field degrade to zeros, which the run3 NPZ contract reads as "unknown" (0 is not a legal LAS return count). Output points are written **geoshift-relative**. |
| 39 | 39 | ||
| 40 | - **`_config.py`** — strict whitelist validation. `ALLOWED_SEGMENT_MAPPER_CONFIG_KEYS` and `ALLOWED_SEGMENT_MAPPER_FILE_NAMING_KEYS` are enforced; unknown keys raise `SegmentMapperConfigError`. `normalize_segment_mapper_config` fills defaults. `build_segment_mapper_config(overrides=..., config_path=...)` deep-merges overrides over `segment_mapper.default.json`. | 40 | - **`_config.py`** — strict whitelist validation. `ALLOWED_SEGMENT_MAPPER_CONFIG_KEYS` and `ALLOWED_SEGMENT_MAPPER_FILE_NAMING_KEYS` are enforced; unknown keys raise `SegmentMapperConfigError`. `normalize_segment_mapper_config` fills defaults. `build_segment_mapper_config(overrides=..., config_path=...)` deep-merges overrides over `segment_mapper.default.json`. |
| 41 | 41 | ||
| 42 | - **`segment_mapper.default.json`** — bundled defaults. Force-included into the wheel via `[tool.hatch.build.targets.wheel.force-include]` in `pyproject.toml`. If you rename/move it, update that mapping or the installed package will be missing it at runtime. | 42 | - **`segment_mapper.default.json`** — bundled defaults. Force-included into the wheel via `[tool.hatch.build.targets.wheel.force-include]` in `pyproject.toml`. If you rename/move it, update that mapping or the installed package will be missing it at runtime. |