|
6 | 6 | from typing import TYPE_CHECKING |
7 | 7 |
|
8 | 8 | import numpy as np |
| 9 | +from segy import SegyFile |
9 | 10 | from segy.arrays import HeaderArray |
| 11 | +from zarr import open_group as zarr_open_group |
10 | 12 |
|
11 | 13 | from mdio.core.config import MDIOSettings |
12 | 14 | from mdio.segy._raw_trace_wrapper import SegyFileRawTraceWrapper |
13 | 15 | from mdio.segy.file import SegyFileArguments |
14 | 16 | from mdio.segy.file import SegyFileWrapper |
15 | 17 |
|
16 | 18 | if TYPE_CHECKING: |
17 | | - from numpy.typing import NDArray |
18 | | - from segy import SegyFile |
19 | 19 | from zarr import Array as zarr_Array |
20 | 20 |
|
21 | 21 | from zarr.core.config import config as zarr_config |
@@ -69,32 +69,73 @@ def header_scan_worker( |
69 | 69 | return HeaderArray(trace_header) # wrap back so we can use aliases |
70 | 70 |
|
71 | 71 |
|
72 | | -def trace_worker( # noqa: PLR0913 |
73 | | - segy_file: SegyFile, |
74 | | - data_array: zarr_Array, |
75 | | - header_array: zarr_Array | None, |
76 | | - raw_header_array: zarr_Array | None, |
77 | | - region: dict[str, slice], |
78 | | - local_grid_map: NDArray, |
79 | | -) -> SummaryStatistics | None: |
| 72 | +# Per-worker process state populated once by `trace_worker_init`. Keeping the SEG-Y handle, |
| 73 | +# Zarr array handles, and the (compressed, in-memory) grid map here lets us pickle them a single |
| 74 | +# time per worker via the pool initializer instead of once per submitted block. The grid map is |
| 75 | +# retained as a compressed in-memory Zarr array and sliced lazily per region, so each worker only |
| 76 | +# materializes its own block rather than the full dense map. |
| 77 | +_worker_state: dict[str, object] = {} |
| 78 | + |
| 79 | + |
| 80 | +def trace_worker_init( |
| 81 | + segy_file_kwargs: SegyFileArguments, |
| 82 | + output_path: str, |
| 83 | + storage_options: dict[str, object] | None, |
| 84 | + use_consolidated: bool, |
| 85 | + data_variable_name: str, |
| 86 | + grid_map: zarr_Array, |
| 87 | +) -> None: |
| 88 | + """Initialize per-process state for trace ingestion workers. |
| 89 | +
|
| 90 | + Used as the `ProcessPoolExecutor` initializer so the SEG-Y file, Zarr output handles, and grid |
| 91 | + map are opened/transferred once per worker process rather than re-pickled for every block. |
| 92 | +
|
| 93 | + Args: |
| 94 | + segy_file_kwargs: Arguments to open the SegyFile instance. |
| 95 | + output_path: POSIX path to the output MDIO Zarr store. |
| 96 | + storage_options: fsspec storage options for the output store. |
| 97 | + use_consolidated: Whether to open the group with consolidated metadata (Zarr V2). |
| 98 | + data_variable_name: Name of the data variable in the dataset. |
| 99 | + grid_map: Compressed in-memory Zarr array mapping live traces to their positions. |
| 100 | + """ |
| 101 | + # Setting the zarr config to 1 thread to ensure we honor the `MDIO__IMPORT__CPU_COUNT` environment variable. |
| 102 | + # The Zarr 3 engine utilizes multiple threads. This can lead to resource contention and unpredictable memory usage. |
| 103 | + zarr_config.set({"threading.max_workers": 1}) |
| 104 | + |
| 105 | + zarr_group = zarr_open_group( |
| 106 | + output_path, |
| 107 | + mode="r+", |
| 108 | + storage_options=storage_options, |
| 109 | + use_consolidated=use_consolidated, |
| 110 | + ) |
| 111 | + |
| 112 | + _worker_state["segy_file"] = SegyFile(**segy_file_kwargs) |
| 113 | + _worker_state["data_array"] = zarr_group[data_variable_name] |
| 114 | + _worker_state["header_array"] = zarr_group.get("headers") |
| 115 | + _worker_state["raw_header_array"] = zarr_group.get("raw_headers") |
| 116 | + _worker_state["grid_map"] = grid_map |
| 117 | + |
| 118 | + |
| 119 | +def trace_worker(region: dict[str, slice]) -> SummaryStatistics | None: |
80 | 120 | """Writes a subset of traces from a region of the dataset of Zarr file. |
81 | 121 |
|
| 122 | + Reads its shared inputs (SEG-Y handle, Zarr arrays, grid map) from the per-process state set up |
| 123 | + by `trace_worker_init`, so only the lightweight `region` is pickled per block. |
| 124 | +
|
82 | 125 | Args: |
83 | | - segy_file: The opened SEG-Y file. |
84 | | - data_array: Zarr array for writing trace data. |
85 | | - header_array: Zarr array for writing trace headers (or None if not needed). |
86 | | - raw_header_array: Zarr array for writing raw headers (or None if not needed). |
87 | 126 | region: Region of the dataset to write to. |
88 | | - local_grid_map: Sliced numpy array mapping live traces to their positions. |
89 | 127 |
|
90 | 128 | Returns: |
91 | 129 | SummaryStatistics object containing statistics about the written traces. |
92 | 130 | """ |
93 | | - # Setting the zarr config to 1 thread to ensure we honor the `MDIO__IMPORT__CPU_COUNT` environment variable. |
94 | | - # The Zarr 3 engine utilizes multiple threads. This can lead to resource contention and unpredictable memory usage. |
95 | | - zarr_config.set({"threading.max_workers": 1}) |
| 131 | + segy_file: SegyFile = _worker_state["segy_file"] |
| 132 | + data_array: zarr_Array = _worker_state["data_array"] |
| 133 | + header_array: zarr_Array | None = _worker_state["header_array"] |
| 134 | + raw_header_array: zarr_Array | None = _worker_state["raw_header_array"] |
| 135 | + grid_map: zarr_Array = _worker_state["grid_map"] |
96 | 136 |
|
97 | 137 | region_slices = tuple(region.values()) |
| 138 | + local_grid_map = grid_map[region_slices[:-1]] # minus last (vertical) axis |
98 | 139 |
|
99 | 140 | # The dtype.max is the sentinel value for the grid map. |
100 | 141 | # Normally, this is uint32, but some grids need to be promoted to uint64. |
|
0 commit comments