| 65 | SEGMENT_NPZ_FIELD_NAMES: tuple[str, ...] = (POINTS_FIELD_NAME, *ANCILLARY_FIELD_NAMES) | 67 | SEGMENT_NPZ_FIELD_NAMES: tuple[str, ...] = (POINTS_FIELD_NAME, *ANCILLARY_FIELD_NAMES) |
| 66 | | 68 | |
| 67 | #: Dtypes used when a field is missing from the observed `field_dtypes` mapping. | 69 | #: Dtypes used when a field is missing from the observed `field_dtypes` mapping. |
| 68 | #: Fields absent here are looked up strictly (a missing entry is a bug). | 70 | #: Fields absent here are looked up strictly (a missing entry is a bug). |
| 69 | DEFAULT_FIELD_DTYPES: dict[str, np.dtype] = { | 71 | #: Frozen so a consumer cannot mutate the producer's schema process-wide, the |
| | 72 | #: same way `iolabs.common.segment_points_io` freezes its field registry. |
| | 73 | DEFAULT_FIELD_DTYPES: Mapping[str, np.dtype] = types.MappingProxyType({ |
| 70 | POINTS_FIELD_NAME: np.dtype(np.float64), | 74 | POINTS_FIELD_NAME: np.dtype(np.float64), |
| 71 | NUMBER_OF_RETURNS_KEY: np.dtype(NUMBER_OF_RETURNS_DTYPE), | 75 | NUMBER_OF_RETURNS_KEY: np.dtype(NUMBER_OF_RETURNS_DTYPE), |
| 72 | } | 76 | }) |
| 73 | | 77 | |
| 74 | | 78 | |
| 75 | def _record_number_of_returns( | 79 | def _record_field_or_zeros( |
| 76 | record: Any, | 80 | record: Any, |
| 77 | *, | 81 | *, |
| | 82 | key: str, |
| | 83 | dtype: np.dtype | type[np.generic], |
| 78 | point_count: int, | 84 | point_count: int, |
| 79 | source: str, | 85 | source: str, |
| 80 | logger: logging.Logger | None = None, | 86 | logger: logging.Logger | None = None, |
| 81 | ) -> np.ndarray: | 87 | ) -> np.ndarray: |
| 82 | """Return per-point LAS return counts as uint8, zero-filled when absent. | 88 | """Return a per-point LAS field as *dtype*, zero-filled when absent. |
| 83 | | 89 | |
| 84 | Mirrors the scan-angle field fallback: a LAS record that does not expose | 90 | Generic counterpart of the scan-angle fallback: a LAS record that does not |
| 85 | `number_of_returns` degrades to zeros rather than raising, and 0 is read | 91 | expose *key* degrades to zeros rather than raising. Adding another optional |
| 86 | downstream as "unknown" (see :data:`NUMBER_OF_RETURNS_DTYPE`). | 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 |
| | 94 | "unknown" (see :data:`NUMBER_OF_RETURNS_DTYPE`). |
| 87 | | 95 | |
| 88 | Args: | 96 | Args: |
| 89 | record: A laspy point record (whole-file `LasData` or a chunk). | 97 | record: A laspy point record (whole-file `LasData` or a chunk). |
| | 98 | key: Name of the LAS field to read off *record*. |
| | 99 | dtype: Storage dtype the values are cast to, and of the zero fill. |
| 90 | point_count: Number of points in *record*, used to size the fallback. | 100 | point_count: Number of points in *record*, used to size the fallback. |
| 91 | source: Label used in the fallback log message (typically a file name). | 101 | source: Label used in the fallback log message (typically a file name). |
| 92 | logger: Optional logger for the fallback notice. | 102 | logger: Optional logger for the fallback notice. |
| 93 | | 103 | |
| 94 | Returns: | 104 | Returns: |
| 95 | A uint8 array of shape `(point_count,)`. | 105 | An array of shape `(point_count,)` with dtype *dtype*. |
| 96 | """ | 106 | """ |
| 97 | values = getattr(record, NUMBER_OF_RETURNS_KEY, None) | 107 | values = getattr(record, key, None) |
| 98 | if values is None: | 108 | if values is None: |
| 99 | if logger is not None: | 109 | if logger is not None: |
| 100 | logger.debug( | 110 | logger.debug( |
| 101 | "No `%s` field for %s; filling %d zeros (unknown).", | 111 | "No `%s` field for %s; filling %d zeros (unknown).", |
| 102 | NUMBER_OF_RETURNS_KEY, | 112 | key, |
| 103 | source, | 113 | source, |
| 104 | point_count, | 114 | point_count, |
| 105 | ) | 115 | ) |
| 106 | return np.zeros(point_count, dtype=NUMBER_OF_RETURNS_DTYPE) | 116 | return np.zeros(point_count, dtype=dtype) |
| 107 | return np.asarray(values).astype(NUMBER_OF_RETURNS_DTYPE, copy=False) | 117 | return np.asarray(values).astype(dtype, copy=False) |
| 108 | | 118 | |
| 109 | | 119 | |
| 110 | def _load_geoshift_from_json(geoshift_path: Path) -> np.ndarray: | 120 | def _load_geoshift_from_json(geoshift_path: Path) -> np.ndarray: |
| 111 | if not geoshift_path.exists(): | 121 | if not geoshift_path.exists(): |
Makes the LAS fallback helper generic over key/dtype (
_record_field_or_zeros) and freezesDEFAULT_FIELD_DTYPESas aMappingProxyTypeso the schema can't be mutated process-wide.