Skip to content

Commit 5e94c42

Browse files
committed
Isolate parse_headers logic
1 parent 509a5c8 commit 5e94c42

4 files changed

Lines changed: 123 additions & 118 deletions

File tree

src/mdio/segy/checksum.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,21 @@
2121
import numpy as np
2222
from segy import SegyFile
2323
from mdio.segy._raw_trace_wrapper import SegyFileRawTraceWrapper
24+
from math import ceil
25+
from concurrent.futures import ProcessPoolExecutor
26+
from itertools import repeat
27+
from tqdm.auto import tqdm
28+
from upath import UPath
29+
from psutil import cpu_count
30+
import multiprocessing as mp
31+
2432

2533
from segy.arrays import HeaderArray
2634
if TYPE_CHECKING:
2735
from mdio.segy._workers import SegyFileArguments
2836

37+
default_cpus = cpu_count(logical=True)
38+
2939
logger = logging.getLogger(__name__)
3040

3141
# Try to import CRC32C libraries, but gracefully handle if they're not installed
@@ -112,6 +122,98 @@ def header_scan_worker(
112122

113123
return HeaderArray(trace_header), checksum_info
114124

125+
def parse_headers( # noqa: PLR0913
126+
segy_file_kwargs: SegyFileArguments,
127+
num_traces: int,
128+
subset: list[str] | None = None,
129+
block_size: int = 10000,
130+
progress_bar: bool = True,
131+
calculate_checksum: bool = True,
132+
) -> HeaderArray | tuple[HeaderArray, int]:
133+
"""Read and parse given `byte_locations` from SEG-Y file.
134+
135+
Args:
136+
segy_file_kwargs: SEG-Y file arguments.
137+
num_traces: Total number of traces in the SEG-Y file.
138+
subset: List of header names to filter and keep.
139+
block_size: Number of traces to read for each block.
140+
progress_bar: Enable or disable progress bar. Default is True.
141+
calculate_checksum: If True, also calculate CRC32C checksum for all trace data.
142+
143+
Returns:
144+
HeaderArray if calculate_checksum is False.
145+
Tuple of (HeaderArray, combined_crc32c) if calculate_checksum is True.
146+
"""
147+
# Dynamically import the appropriate header_scan_worker based on checksum requirement
148+
# if should_calculate_checksum() and is_checksum_available():
149+
# from mdio.segy.checksum import header_scan_worker
150+
# else:
151+
# from mdio.segy._workers import header_scan_worker
152+
153+
# Type hint for the dynamically imported function
154+
# header_scan_worker: Any
155+
# Initialize combiner only if checksum calculation is needed and available
156+
# combiner: Any = None
157+
# Use UPath for cloud/filesystem agnostic reading
158+
path = UPath(segy_file_kwargs["url"])
159+
raw_bytes = path.fs.read_block(
160+
fn=str(path),
161+
offset=0,
162+
length=3600,
163+
)
164+
165+
# Calculate trace size to determine total data length
166+
# We need to open the file briefly to get the spec
167+
sf = SegyFile(**segy_file_kwargs)
168+
trace_header_size = sf.spec.trace.header.itemsize
169+
sample_size = 4 # This will always be a 4-byte float
170+
num_samples = len(sf.sample_labels)
171+
trace_size = trace_header_size + (num_samples * sample_size)
172+
173+
# Total length is header (3600) + trace data
174+
total_len = 3600 + num_traces * trace_size
175+
combiner = create_distributed_crc32c(raw_bytes, total_len)
176+
177+
trace_count = num_traces
178+
n_blocks = int(ceil(trace_count / block_size))
179+
180+
trace_ranges = []
181+
for idx in range(n_blocks):
182+
start, stop = idx * block_size, (idx + 1) * block_size
183+
stop = min(stop, trace_count)
184+
185+
trace_ranges.append((start, stop))
186+
187+
num_cpus = int(os.getenv("MDIO__IMPORT__CPU_COUNT", default_cpus))
188+
num_workers = min(n_blocks, num_cpus)
189+
190+
tqdm_kw = {"unit": "block", "dynamic_ncols": True}
191+
# For Unix async writes with s3fs/fsspec & multiprocessing, use 'spawn' instead of default
192+
# 'fork' to avoid deadlocks on cloud stores. Slower but necessary. Default on Windows.
193+
context = mp.get_context("spawn")
194+
with ProcessPoolExecutor(num_workers, mp_context=context) as executor:
195+
lazy_work = executor.map(
196+
header_scan_worker,
197+
repeat(segy_file_kwargs),
198+
trace_ranges,
199+
repeat(subset),
200+
)
201+
202+
if progress_bar is True:
203+
desc = (
204+
"Scanning SEG-Y & calculating checksum"
205+
)
206+
lazy_work = tqdm(
207+
iterable=lazy_work,
208+
total=n_blocks,
209+
desc=desc,
210+
**tqdm_kw,
211+
)
212+
213+
# This executes the lazy work.
214+
results = list(lazy_work)
215+
216+
return finalize_distributed_checksum(results, combiner)
115217

116218
def is_checksum_available() -> bool:
117219
"""Check if checksum libraries are available.

src/mdio/segy/parsers.py

Lines changed: 12 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,22 @@
2222
from mdio.segy.checksum import is_checksum_available
2323
from mdio.segy.checksum import should_calculate_checksum
2424

25+
from mdio.segy._workers import header_scan_worker
26+
2527
if TYPE_CHECKING:
2628
from segy.arrays import HeaderArray
2729

2830
from mdio.segy.file import SegyFileArguments
2931

3032
default_cpus = cpu_count(logical=True)
3133

32-
33-
def parse_headers( # noqa: PLR0913
34+
def parse_headers(
3435
segy_file_kwargs: SegyFileArguments,
3536
num_traces: int,
3637
subset: list[str] | None = None,
3738
block_size: int = 10000,
3839
progress_bar: bool = True,
39-
calculate_checksum: bool = True,
40-
) -> HeaderArray | tuple[HeaderArray, int]:
40+
) -> HeaderArray:
4141
"""Read and parse given `byte_locations` from SEG-Y file.
4242
4343
Args:
@@ -46,47 +46,11 @@ def parse_headers( # noqa: PLR0913
4646
subset: List of header names to filter and keep.
4747
block_size: Number of traces to read for each block.
4848
progress_bar: Enable or disable progress bar. Default is True.
49-
calculate_checksum: If True, also calculate CRC32C checksum for all trace data.
5049
5150
Returns:
52-
HeaderArray if calculate_checksum is False.
53-
Tuple of (HeaderArray, combined_crc32c) if calculate_checksum is True.
51+
HeaderArray. Keys are the index names, values are numpy arrays of parsed headers for the
52+
current block. Array is of type byte_type except IBM32 which is mapped to FLOAT32.
5453
"""
55-
# Dynamically import the appropriate header_scan_worker based on checksum requirement
56-
if should_calculate_checksum() and is_checksum_available():
57-
from mdio.segy.checksum import header_scan_worker
58-
else:
59-
from mdio.segy._workers import header_scan_worker
60-
61-
# Type hint for the dynamically imported function
62-
header_scan_worker: Any
63-
# Initialize combiner only if checksum calculation is needed and available
64-
combiner: Any = None
65-
if calculate_checksum:
66-
if not is_checksum_available():
67-
# Checksum was requested but libraries not available - disable it
68-
calculate_checksum = False
69-
else:
70-
# Use UPath for cloud/filesystem agnostic reading
71-
path = UPath(segy_file_kwargs["url"])
72-
raw_bytes = path.fs.read_block(
73-
fn=str(path),
74-
offset=0,
75-
length=3600,
76-
)
77-
78-
# Calculate trace size to determine total data length
79-
# We need to open the file briefly to get the spec
80-
sf = SegyFile(**segy_file_kwargs)
81-
trace_header_size = sf.spec.trace.header.itemsize
82-
sample_size = 4 # This will always be a 4-byte float
83-
num_samples = len(sf.sample_labels)
84-
trace_size = trace_header_size + (num_samples * sample_size)
85-
86-
# Total length is header (3600) + trace data
87-
total_len = 3600 + num_traces * trace_size
88-
combiner = create_distributed_crc32c(raw_bytes, total_len)
89-
9054
trace_count = num_traces
9155
n_blocks = int(ceil(trace_count / block_size))
9256

@@ -105,85 +69,18 @@ def parse_headers( # noqa: PLR0913
10569
# 'fork' to avoid deadlocks on cloud stores. Slower but necessary. Default on Windows.
10670
context = mp.get_context("spawn")
10771
with ProcessPoolExecutor(num_workers, mp_context=context) as executor:
108-
lazy_work = executor.map(
109-
header_scan_worker,
110-
repeat(segy_file_kwargs),
111-
trace_ranges,
112-
repeat(subset),
113-
)
72+
lazy_work = executor.map(header_scan_worker, repeat(segy_file_kwargs), trace_ranges, repeat(subset))
11473

11574
if progress_bar is True:
116-
desc = (
117-
"Scanning SEG-Y & calculating checksum"
118-
if calculate_checksum
119-
else "Scanning SEG-Y for geometry attributes"
120-
)
12175
lazy_work = tqdm(
12276
iterable=lazy_work,
12377
total=n_blocks,
124-
desc=desc,
78+
desc="Scanning SEG-Y for geometry attributes",
12579
**tqdm_kw,
12680
)
12781

12882
# This executes the lazy work.
129-
results = list(lazy_work)
130-
131-
if not calculate_checksum:
132-
# Merge headers and return
133-
return np.concatenate(results)
134-
135-
return finalize_distributed_checksum(results, combiner)
136-
137-
# def _parse_headers(
138-
# segy_file_kwargs: SegyFileArguments,
139-
# num_traces: int,
140-
# subset: list[str] | None = None,
141-
# block_size: int = 10000,
142-
# progress_bar: bool = True,
143-
# ) -> HeaderArray:
144-
# """Read and parse given `byte_locations` from SEG-Y file.
145-
146-
# Args:
147-
# segy_file_kwargs: SEG-Y file arguments.
148-
# num_traces: Total number of traces in the SEG-Y file.
149-
# subset: List of header names to filter and keep.
150-
# block_size: Number of traces to read for each block.
151-
# progress_bar: Enable or disable progress bar. Default is True.
152-
153-
# Returns:
154-
# HeaderArray. Keys are the index names, values are numpy arrays of parsed headers for the
155-
# current block. Array is of type byte_type except IBM32 which is mapped to FLOAT32.
156-
# """
157-
# trace_count = num_traces
158-
# n_blocks = int(ceil(trace_count / block_size))
159-
160-
# trace_ranges = []
161-
# for idx in range(n_blocks):
162-
# start, stop = idx * block_size, (idx + 1) * block_size
163-
# stop = min(stop, trace_count)
164-
165-
# trace_ranges.append((start, stop))
166-
167-
# num_cpus = int(os.getenv("MDIO__IMPORT__CPU_COUNT", default_cpus))
168-
# num_workers = min(n_blocks, num_cpus)
169-
170-
# tqdm_kw = {"unit": "block", "dynamic_ncols": True}
171-
# # For Unix async writes with s3fs/fsspec & multiprocessing, use 'spawn' instead of default
172-
# # 'fork' to avoid deadlocks on cloud stores. Slower but necessary. Default on Windows.
173-
# context = mp.get_context("spawn")
174-
# with ProcessPoolExecutor(num_workers, mp_context=context) as executor:
175-
# lazy_work = executor.map(header_scan_worker, repeat(segy_file_kwargs), trace_ranges, repeat(subset))
176-
177-
# if progress_bar is True:
178-
# lazy_work = tqdm(
179-
# iterable=lazy_work,
180-
# total=n_blocks,
181-
# desc="Scanning SEG-Y for geometry attributes",
182-
# **tqdm_kw,
183-
# )
184-
185-
# # This executes the lazy work.
186-
# headers: list[HeaderArray] = list(lazy_work)
187-
188-
# # Merge blocks before return
189-
# return np.concatenate(headers)
83+
headers: list[HeaderArray] = list(lazy_work)
84+
85+
# Merge blocks before return
86+
return np.concatenate(headers)

src/mdio/segy/utilities.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
from mdio.segy.geometry import GridOverrider
1515
from mdio.segy.parsers import parse_headers
1616

17+
from mdio.segy.checksum import should_calculate_checksum
18+
from mdio.segy.checksum import is_checksum_available
19+
1720
if TYPE_CHECKING:
1821
from numpy.typing import DTypeLike
1922
from segy.arrays import HeaderArray
@@ -58,6 +61,11 @@ def get_grid_plan( # noqa: C901, PLR0913
5861
Returns:
5962
All index dimensions and chunksize, optionally with header values and/or checksum.
6063
"""
64+
if should_calculate_checksum() and is_checksum_available():
65+
from mdio.segy.checksum import parse_headers
66+
else:
67+
from mdio.segy.parsers import parse_headers
68+
6169
if grid_overrides is None:
6270
grid_overrides = {}
6371

@@ -68,7 +76,6 @@ def get_grid_plan( # noqa: C901, PLR0913
6876
segy_file_kwargs=segy_file_kwargs,
6977
num_traces=segy_file_info.num_traces,
7078
subset=horizontal_coordinates,
71-
calculate_checksum=calculate_checksum,
7279
)
7380

7481
# Unpack result based on whether checksum was calculated

tests/integration/test_crc32c_checksum.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from mdio.builder.template_registry import TemplateRegistry
2323
from mdio.segy._workers import info_worker
2424
from mdio.segy.checksum import is_checksum_available
25-
from mdio.segy.parsers import parse_headers
25+
from mdio.segy.checksum import parse_headers
2626

2727
# Skip all tests in this module if checksum libraries are not available
2828
pytestmark = pytest.mark.skipif(
@@ -170,7 +170,6 @@ def test_corrupted_data_detects_changes(self, tmp_path: Path, monkeypatch: pytes
170170
_, mdio_crc = parse_headers(
171171
segy_file_kwargs=segy_file_kwargs,
172172
num_traces=segy_file_info.num_traces,
173-
calculate_checksum=True,
174173
)
175174

176175
assert mdio_crc == corrupted_crc, (

0 commit comments

Comments
 (0)