Skip to content

Commit 8a35beb

Browse files
committed
Begin isolating checksum logic
1 parent 05fd32a commit 8a35beb

7 files changed

Lines changed: 228 additions & 51 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@ classifiers = [
2020
dependencies = [
2121
"click>=8.3.0",
2222
"click-params>=0.5.0",
23-
"crc32c_dist_rs",
2423
"dask>=2025.9.1",
2524
"fsspec>=2025.9.0",
26-
"google-crc32c>=1.5.0",
2725
"pint>=0.25.0",
2826
"psutil>=7.1.0",
2927
"pydantic>=2.12.0",
@@ -36,6 +34,7 @@ dependencies = [
3634
]
3735

3836
[project.optional-dependencies]
37+
checksum = ["google-crc32c>=1.5.0", "crc32c_dist_rs"]
3938
cloud = ["s3fs>=2025.9.0", "gcsfs>=2025.9.0", "adlfs>=2025.8.0"]
4039
distributed = ["distributed>=2025.9.1", "bokeh>=3.8.0"]
4140
lossy = ["zfpy>=1.0.1"]

src/mdio/converters/segy.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -529,8 +529,9 @@ def segy_to_mdio( # noqa PLR0913
529529
}
530530
segy_file_info = get_segy_file_info(segy_file_kwargs)
531531

532-
# Only calculate CRC32C when raw headers are enabled
533-
calculate_checksum = os.getenv("MDIO__IMPORT__DO_CRC32C") in ("1", "true", "yes", "on")
532+
# Check if checksum calculation should be performed
533+
# This checks both the env var and library availability
534+
calculate_checksum = should_calculate_checksum()
534535

535536
segy_dimensions, segy_headers, trace_data_crc32c = _scan_for_headers(
536537
segy_file_kwargs,

src/mdio/segy/_workers.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,16 @@ def header_scan_worker(
7676
if not calculate_checksum:
7777
return HeaderArray(trace_header) # wrap back so we can use aliases
7878

79+
# Verify checksum libraries are available (should have been checked earlier)
80+
if not is_checksum_available():
81+
logger.warning("Checksum calculation requested but libraries not available. Skipping checksum.")
82+
# Return headers without checksum info - this should not happen in practice
83+
# as the caller should check availability first
84+
return HeaderArray(trace_header)
85+
7986
# Calculate checksum from the raw bytes ALREADY IN MEMORY (NO ADDITIONAL I/O!)
8087
raw_bytes = traces.trace_buffer_array.tobytes()
81-
crc = google_crc32c.Checksum(raw_bytes)
82-
partial_crc32c = int.from_bytes(crc.digest(), byteorder="big")
88+
partial_crc32c = calculate_bytes_crc32c(raw_bytes)
8389

8490
# Calculate byte offset and length
8591
trace_header_size = segy_file.spec.trace.header.itemsize

src/mdio/segy/checksum.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
"""Checksum utilities for SEG-Y ingestion.
2+
3+
This module provides optional CRC32C checksum functionality for SEG-Y ingestion.
4+
All checksum-related imports and logic are isolated here to maintain loose coupling
5+
with the core functionality.
6+
7+
Checksum calculation is controlled by the MDIO__IMPORT__DO_CRC32C environment variable
8+
and requires optional dependencies (google-crc32c, crc32c_dist_rs).
9+
10+
IMPORTANT: This module uses distributed CRC32C calculation to avoid reading entire files
11+
into memory. Never add functions that read entire files - this has severe performance
12+
and cost implications, especially for cloud storage.
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import logging
18+
import os
19+
from typing import TYPE_CHECKING
20+
from typing import Any
21+
22+
if TYPE_CHECKING:
23+
from segy.arrays import HeaderArray
24+
25+
logger = logging.getLogger(__name__)
26+
27+
# Try to import CRC32C libraries, but gracefully handle if they're not installed
28+
_CHECKSUM_AVAILABLE = False
29+
_IMPORT_ERROR_MSG = ""
30+
31+
try:
32+
import google_crc32c
33+
from crc32c_dist_rs import DistributedCRC32C
34+
35+
_CHECKSUM_AVAILABLE = True
36+
except ImportError as e:
37+
_IMPORT_ERROR_MSG = (
38+
f"CRC32C checksum libraries not available: {e}. "
39+
"Install with: pip install multidimio[checksum] or pip install google-crc32c crc32c_dist_rs"
40+
)
41+
# Define placeholder types for type checking
42+
google_crc32c = None # type: ignore[assignment]
43+
DistributedCRC32C = None # type: ignore[assignment,misc]
44+
45+
46+
def is_checksum_available() -> bool:
47+
"""Check if checksum libraries are available.
48+
49+
Returns:
50+
True if google-crc32c and crc32c_dist_rs are installed, False otherwise.
51+
"""
52+
return _CHECKSUM_AVAILABLE
53+
54+
55+
def should_calculate_checksum() -> bool:
56+
"""Determine if checksum calculation should be performed.
57+
58+
Checks both the environment variable and library availability.
59+
60+
Returns:
61+
True if MDIO__IMPORT__DO_CRC32C is enabled and libraries are available.
62+
"""
63+
env_enabled = os.getenv("MDIO__IMPORT__DO_CRC32C", "false").lower() in ("1", "true", "yes", "on")
64+
65+
if env_enabled and not _CHECKSUM_AVAILABLE:
66+
logger.warning(
67+
"MDIO__IMPORT__DO_CRC32C is enabled but checksum libraries are not available. "
68+
"Checksum calculation will be skipped. %s",
69+
_IMPORT_ERROR_MSG,
70+
)
71+
return False
72+
73+
return env_enabled
74+
75+
76+
def require_checksum_libraries() -> None:
77+
"""Raise an error if checksum libraries are not available.
78+
79+
Raises:
80+
ImportError: If checksum libraries are not installed.
81+
"""
82+
if not _CHECKSUM_AVAILABLE:
83+
raise ImportError(_IMPORT_ERROR_MSG)
84+
85+
86+
def calculate_bytes_crc32c(data: bytes) -> int:
87+
"""Calculate CRC32C checksum for a byte array.
88+
89+
Args:
90+
data: Byte array to checksum.
91+
92+
Returns:
93+
CRC32C checksum as integer.
94+
95+
Raises:
96+
ImportError: If checksum libraries are not available.
97+
"""
98+
require_checksum_libraries()
99+
100+
crc = google_crc32c.Checksum(data)
101+
return int.from_bytes(crc.digest(), byteorder="big")
102+
103+
104+
def create_distributed_crc32c(initial_bytes: bytes, total_length: int) -> Any:
105+
"""Create a distributed CRC32C combiner instance.
106+
107+
Args:
108+
initial_bytes: Initial bytes (e.g., file header).
109+
total_length: Total expected file length in bytes.
110+
111+
Returns:
112+
DistributedCRC32C instance.
113+
114+
Raises:
115+
ImportError: If checksum libraries are not available.
116+
"""
117+
require_checksum_libraries()
118+
119+
return DistributedCRC32C(initial_bytes, total_length)
120+
121+
122+
def finalize_distributed_checksum(
123+
results: list[tuple[HeaderArray, tuple[int, int, int]]], combiner: Any
124+
) -> tuple[HeaderArray, int]:
125+
"""Finalize a distributed CRC32C checksum from scan results.
126+
127+
Args:
128+
results: List of (HeaderArray, (byte_offset, partial_crc, byte_length)) tuples.
129+
combiner: DistributedCRC32C instance.
130+
131+
Returns:
132+
Tuple of (concatenated HeaderArray, final CRC32C checksum).
133+
134+
Raises:
135+
ValueError: If checksum finalization fails.
136+
ImportError: If checksum libraries are not available.
137+
"""
138+
require_checksum_libraries()
139+
140+
import numpy as np
141+
142+
headers: list[HeaderArray] = []
143+
for result in results:
144+
headers.append(result[0])
145+
byte_offset, partial_crc, byte_length = result[1]
146+
combiner.add_fragment(byte_offset, byte_length, partial_crc)
147+
148+
combined_crc = combiner.try_finalize()
149+
if combined_crc is None:
150+
msg = "Failed to finalize CRC32C - file may not be fully covered"
151+
raise ValueError(msg)
152+
153+
# Merge headers and return with checksum
154+
return np.concatenate(headers), combined_crc
155+
156+
157+
__all__ = [
158+
"is_checksum_available",
159+
"should_calculate_checksum",
160+
"require_checksum_libraries",
161+
"calculate_bytes_crc32c",
162+
"create_distributed_crc32c",
163+
"finalize_distributed_checksum",
164+
]
165+

src/mdio/segy/parsers.py

Lines changed: 31 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@
88
from itertools import repeat
99
from math import ceil
1010
from typing import TYPE_CHECKING
11+
from typing import Any
1112

1213
import numpy as np
13-
from crc32c_dist_rs import DistributedCRC32C
1414
from psutil import cpu_count
1515
from segy import SegyFile
1616
from tqdm.auto import tqdm
1717
from upath import UPath
1818

1919
from mdio.segy._workers import header_scan_worker
20+
from mdio.segy.checksum import create_distributed_crc32c
21+
from mdio.segy.checksum import finalize_distributed_checksum
22+
from mdio.segy.checksum import is_checksum_available
2023

2124
if TYPE_CHECKING:
2225
from segy.arrays import HeaderArray
@@ -26,24 +29,6 @@
2629
default_cpus = cpu_count(logical=True)
2730

2831

29-
def _finalize_checksum(
30-
results: list[tuple[HeaderArray, tuple[int, int, int]]], combiner: DistributedCRC32C
31-
) -> tuple[HeaderArray, int]:
32-
"""Finalize the checksum from the results of the header scan."""
33-
headers: list[HeaderArray] = []
34-
for result in results:
35-
headers.append(result[0])
36-
byte_offset, partial_crc, byte_length = result[1]
37-
combiner.add_fragment(byte_offset, byte_length, partial_crc)
38-
combined_crc = combiner.try_finalize()
39-
if combined_crc is None:
40-
msg = "Failed to finalize CRC32C - file may not be fully covered"
41-
raise ValueError(msg)
42-
43-
# Merge headers and return with checksum
44-
return np.concatenate(headers), combined_crc
45-
46-
4732
def parse_headers( # noqa: PLR0913
4833
segy_file_kwargs: SegyFileArguments,
4934
num_traces: int,
@@ -66,25 +51,32 @@ def parse_headers( # noqa: PLR0913
6651
HeaderArray if calculate_checksum is False.
6752
Tuple of (HeaderArray, combined_crc32c) if calculate_checksum is True.
6853
"""
69-
# Use UPath for cloud/filesystem agnostic reading
70-
path = UPath(segy_file_kwargs["url"])
71-
raw_bytes = path.fs.read_block(
72-
fn=str(path),
73-
offset=0,
74-
length=3600,
75-
)
76-
77-
# Calculate trace size to determine total data length
78-
# We need to open the file briefly to get the spec
79-
sf = SegyFile(**segy_file_kwargs)
80-
trace_header_size = sf.spec.trace.header.itemsize
81-
sample_size = 4 # This will always be a 4-byte float
82-
num_samples = len(sf.sample_labels)
83-
trace_size = trace_header_size + (num_samples * sample_size)
84-
85-
# Total length is header (3600) + trace data
86-
total_len = 3600 + num_traces * trace_size
87-
combiner = DistributedCRC32C(raw_bytes, total_len)
54+
# Initialize combiner only if checksum calculation is needed and available
55+
combiner: Any = None
56+
if calculate_checksum:
57+
if not is_checksum_available():
58+
# Checksum was requested but libraries not available - disable it
59+
calculate_checksum = False
60+
else:
61+
# Use UPath for cloud/filesystem agnostic reading
62+
path = UPath(segy_file_kwargs["url"])
63+
raw_bytes = path.fs.read_block(
64+
fn=str(path),
65+
offset=0,
66+
length=3600,
67+
)
68+
69+
# Calculate trace size to determine total data length
70+
# We need to open the file briefly to get the spec
71+
sf = SegyFile(**segy_file_kwargs)
72+
trace_header_size = sf.spec.trace.header.itemsize
73+
sample_size = 4 # This will always be a 4-byte float
74+
num_samples = len(sf.sample_labels)
75+
trace_size = trace_header_size + (num_samples * sample_size)
76+
77+
# Total length is header (3600) + trace data
78+
total_len = 3600 + num_traces * trace_size
79+
combiner = create_distributed_crc32c(raw_bytes, total_len)
8880

8981
trace_count = num_traces
9082
n_blocks = int(ceil(trace_count / block_size))
@@ -132,4 +124,4 @@ def parse_headers( # noqa: PLR0913
132124
# Merge headers and return
133125
return np.concatenate(results)
134126

135-
return _finalize_checksum(results, combiner)
127+
return finalize_distributed_checksum(results, combiner)

tests/integration/test_crc32c_checksum.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from pathlib import Path
88

9-
import google_crc32c
109
import pytest
1110
import zarr
1211
from tests.integration.test_segy_import_export_masked import COCA_3D_CONF
@@ -22,21 +21,34 @@
2221
from mdio import segy_to_mdio
2322
from mdio.builder.template_registry import TemplateRegistry
2423
from mdio.segy._workers import info_worker
24+
from mdio.segy.checksum import is_checksum_available
2525
from mdio.segy.parsers import parse_headers
2626

27+
# Skip all tests in this module if checksum libraries are not available
28+
pytestmark = pytest.mark.skipif(
29+
not is_checksum_available(), reason="CRC32C checksum libraries (google-crc32c, crc32c_dist_rs) not installed"
30+
)
31+
2732

2833
def get_expected_crc32c(segy_path: Path) -> int:
2934
"""Calculate the expected CRC32C checksum using Google crc32c library.
3035
3136
This calculates CRC32C over the entire file (headers + trace data),
3237
matching how MDIO calculates the checksum.
3338
39+
NOTE: This is TEST-ONLY code. Never use this in production as it reads
40+
the entire file into memory which has significant performance and cost
41+
penalties, especially for cloud storage.
42+
3443
Args:
3544
segy_path: Path to the SEG-Y file
3645
3746
Returns:
3847
CRC32C checksum as integer
3948
"""
49+
# Import here to keep it test-only
50+
import google_crc32c
51+
4052
crc = google_crc32c.Checksum()
4153

4254
with segy_path.open("rb") as f:

0 commit comments

Comments
 (0)