Skip to content

Commit e8d0ae5

Browse files
Refactor SEG-Y ingestion pipeline into general modular components (TGSAI#823)
* Refactor ingestion pipeline and dataset templates * Introduce a new `dataset_factory` module for schema-driven dataset creation. * Implement `run_segy_ingestion` orchestrator to streamline SEG-Y ingestion. * Enhance dataset templates with dimension specification methods to ensure consistency in data types. * Update `__init__.py` files to include new ingestion components and improve module organization. * Remove deprecated `schema_resolver.py` to clean up the codebase. * Add unit tests to validate new functionalities and ensure robustness. * chore: stop tracking refactor_plan_bak.md Co-authored-by: Cursor <cursoragent@cursor.com> * Cleanup docstrings * Remove deep copy requirement * Make method name more intentional and clear * Deslop and cleanup comments * Make base `ingestion` module more generic and move segy specific vocabulary and requirements up the hierarchy * Fix public interface to be `segy_to_mdio` * Update raw headers warning with phase-out plan * Cleanup issues * Further cleanup * Pre-commit * Deslop * Deslop * Deslop * Prefer instance name instead of hardcoded string * Update author * Remove unnecessary try/catch * Isolate segy ingestion leakage into base ingestion pipeline * Remove specific version removal callout until roadmap can be made --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c3f32a9 commit e8d0ae5

48 files changed

Lines changed: 2571 additions & 1605 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/tutorials/custom_template.ipynb

Lines changed: 310 additions & 299 deletions
Large diffs are not rendered by default.

src/mdio/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from mdio.api.io import to_mdio
99
from mdio.converters import mdio_to_segy
1010
from mdio.converters import segy_to_mdio
11+
from mdio.ingestion import ResolvedSchema
1112
from mdio.optimize.access_pattern import OptimizedAccessPatternConfig
1213
from mdio.optimize.access_pattern import optimize_access_patterns
1314
from mdio.segy.geometry import GridOverrides
@@ -27,4 +28,5 @@
2728
"segy_to_mdio",
2829
"OptimizedAccessPatternConfig",
2930
"optimize_access_patterns",
31+
"ResolvedSchema",
3032
]

src/mdio/builder/dataset_builder.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
from datetime import datetime
55
from enum import Enum
66
from enum import auto
7+
from importlib import metadata
78
from typing import Any
89

9-
from mdio import __version__
10+
try:
11+
__version__ = metadata.version("multidimio")
12+
except metadata.PackageNotFoundError:
13+
__version__ = "unknown"
1014
from mdio.builder.formatting_html import dataset_builder_repr_html
1115
from mdio.builder.schemas.compressors import ZFP
1216
from mdio.builder.schemas.compressors import Blosc

src/mdio/builder/templates/base.py

Lines changed: 69 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
import copy
5+
import logging
66
from abc import ABC
77
from abc import abstractmethod
88
from typing import TYPE_CHECKING
@@ -19,11 +19,14 @@
1919
from mdio.builder.schemas.v1.variable import CoordinateMetadata
2020
from mdio.builder.schemas.v1.variable import VariableMetadata
2121
from mdio.builder.templates.types import CoordinateSpec
22+
from mdio.builder.templates.types import DimCoordinateTypes
2223

2324
if TYPE_CHECKING:
2425
from mdio.builder.schemas.v1.dataset import Dataset
2526
from mdio.builder.templates.types import SeismicDataDomain
2627

28+
logger = logging.getLogger(__name__)
29+
2730

2831
class AbstractDatasetTemplate(ABC):
2932
"""Abstract base class that defines the template method for Dataset building factory.
@@ -46,12 +49,6 @@ def __init__(self, data_domain: SeismicDataDomain) -> None:
4649
self._var_chunk_shape: tuple[int, ...] = ()
4750
self.synthesize_missing_dims: tuple[str, ...] = ()
4851

49-
# TEMPORARY (removed with declare_coordinate_specs): set when grid overrides mutate this
50-
# template in-place (dims collapsed into 'trace', extra coordinates added). Once mutated,
51-
# the runtime layout intentionally diverges from the static declare_coordinate_specs()
52-
# contract, so the drift guard in build_dataset() must not run.
53-
self._grid_overrides_applied: bool = False
54-
5552
self._builder: MDIODatasetBuilder | None = None
5653
self._dim_sizes: tuple[int, ...] = ()
5754
self._units: dict[str, AllUnitModel] = {}
@@ -114,6 +111,38 @@ def declare_coordinate_specs(self) -> tuple[CoordinateSpec, ...]:
114111
)
115112
return tuple(specs)
116113

114+
def declare_dim_coordinate_types(self) -> DimCoordinateTypes:
115+
"""Declare data types for each dimension coordinate in this template.
116+
117+
Returns:
118+
A dictionary mapping dimension name to ScalarType.
119+
"""
120+
return dict.fromkeys(self.dimension_names, ScalarType.INT32)
121+
122+
def _dim_dtype(self, name: str) -> ScalarType:
123+
"""Return the declared dtype for a dimension coordinate.
124+
125+
Args:
126+
name: The dimension name.
127+
128+
Returns:
129+
The declared ScalarType, defaulting to INT32.
130+
"""
131+
return self.declare_dim_coordinate_types().get(name, ScalarType.INT32)
132+
133+
def _add_dimension_coordinate(self, name: str) -> None:
134+
"""Add a single dimension coordinate.
135+
136+
Args:
137+
name: The dimension name.
138+
"""
139+
self._builder.add_coordinate(
140+
name,
141+
dimensions=(name,),
142+
data_type=self._dim_dtype(name),
143+
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key(name)),
144+
)
145+
117146
def build_dataset(
118147
self,
119148
name: str,
@@ -122,6 +151,12 @@ def build_dataset(
122151
) -> Dataset:
123152
"""Template method that builds the dataset.
124153
154+
.. deprecated:: 1.2
155+
``build_dataset`` is deprecated and is planned for removal in a future release. SEG-Y
156+
ingestion now builds datasets from a resolved schema via the schema-driven
157+
factory (:func:`mdio.ingestion.dataset_factory.build_mdio_dataset`); use
158+
:func:`mdio.segy_to_mdio` for ingestion.
159+
125160
Args:
126161
name: The name of the dataset.
127162
sizes: The sizes of the dimensions.
@@ -133,6 +168,11 @@ def build_dataset(
133168
Raises:
134169
ValueError: If coordinate already exists from subclass override.
135170
"""
171+
logger.warning(
172+
"AbstractDatasetTemplate.build_dataset is deprecated as of 1.2 and is planned for "
173+
"removal in a future release; SEG-Y ingestion builds datasets via the schema-driven factory. "
174+
"Use `mdio.segy_to_mdio` for ingestion."
175+
)
136176
self._dim_sizes = sizes
137177

138178
attributes = self._load_dataset_attributes() or {}
@@ -154,10 +194,7 @@ def build_dataset(
154194
except ValueError as exc: # coordinate may already exist
155195
if "same name twice" not in str(exc):
156196
raise
157-
# Skip the static drift guard when grid overrides have transformed the template: the
158-
# runtime layout no longer matches the declared (override-free) specs by design.
159-
if not self._grid_overrides_applied:
160-
self._validate_declared_coordinate_specs()
197+
self._validate_declared_coordinate_specs()
161198
self._add_variables()
162199
self._add_trace_mask()
163200

@@ -174,30 +211,6 @@ def add_units(self, units: dict[str, AllUnitModel]) -> None:
174211
raise ValueError(msg)
175212
self._units |= units
176213

177-
def apply_resolved_dimensions(
178-
self,
179-
dim_names: tuple[str, ...],
180-
chunk_shape: tuple[int, ...],
181-
) -> None:
182-
"""Update the template's dimension layout from a resolved schema.
183-
184-
Supported entry point for the ingestion pipeline to push back dimension names
185-
and chunk shape after the SchemaResolver has applied grid overrides
186-
(e.g. NonBinned, HasDuplicates), instead of mutating private attributes.
187-
188-
Args:
189-
dim_names: Final ordered dimension names.
190-
chunk_shape: Chunk shape matching ``dim_names`` length.
191-
192-
Raises:
193-
ValueError: If ``len(chunk_shape) != len(dim_names)``.
194-
"""
195-
if len(chunk_shape) != len(dim_names):
196-
msg = f"chunk_shape length {len(chunk_shape)} does not match dim_names length {len(dim_names)}"
197-
raise ValueError(msg)
198-
self._dim_names = tuple(dim_names)
199-
self._var_chunk_shape = tuple(chunk_shape)
200-
201214
def _validate_declared_coordinate_specs(self) -> None:
202215
"""Fail the build if :meth:`declare_coordinate_specs` drifted from the built coordinates.
203216
@@ -206,11 +219,8 @@ def _validate_declared_coordinate_specs(self) -> None:
206219
:meth:`_add_coordinates`, this guard ensures the two never diverge in name, dimensions,
207220
or dtype. The ingestion ``SchemaResolver`` trusts the declared specs, so silent drift
208221
would corrupt resolved schemas. The check runs for every template (built-in and
209-
user-defined) on every ``build_dataset`` call that does not apply grid overrides. Grid
210-
overrides mutate the template in-place (collapsing dims into ``trace`` and adding
211-
coordinates), so the runtime layout intentionally diverges from the declared specs and
212-
the guard is skipped for those builds. It is removed once ``_add_coordinates`` is derived
213-
from the resolved schema and the duplication no longer exists.
222+
user-defined) on every ``build_dataset`` call. It is removed once ``_add_coordinates``
223+
is derived from the resolved schema and the duplication no longer exists.
214224
215225
Raises:
216226
ValueError: If the declared specs do not match the built non-dimension coordinates.
@@ -266,32 +276,32 @@ def trace_domain(self) -> str:
266276
@property
267277
def spatial_dimension_names(self) -> tuple[str, ...]:
268278
"""Returns the names of the dimensions excluding the last axis."""
269-
return copy.deepcopy(self._dim_names[:-1])
279+
return self._dim_names[:-1]
270280

271281
@property
272282
def dimension_names(self) -> tuple[str, ...]:
273283
"""Returns the names of the dimensions."""
274-
return copy.deepcopy(self._dim_names)
284+
return self._dim_names
275285

276286
@property
277287
def calculated_dimension_names(self) -> tuple[str, ...]:
278-
"""Returns the names of the dimensions."""
279-
return copy.deepcopy(self._calculated_dims)
288+
"""Returns the names of the calculated dimensions."""
289+
return self._calculated_dims
280290

281291
@property
282292
def physical_coordinate_names(self) -> tuple[str, ...]:
283293
"""Returns the names of the physical (world) coordinates."""
284-
return copy.deepcopy(self._physical_coord_names)
294+
return self._physical_coord_names
285295

286296
@property
287297
def logical_coordinate_names(self) -> tuple[str, ...]:
288298
"""Returns the names of the logical (grid) coordinates."""
289-
return copy.deepcopy(self._logical_coord_names)
299+
return self._logical_coord_names
290300

291301
@property
292302
def coordinate_names(self) -> tuple[str, ...]:
293303
"""Returns names of all coordinates."""
294-
return copy.deepcopy(self._physical_coord_names + self._logical_coord_names)
304+
return self._physical_coord_names + self._logical_coord_names
295305

296306
@property
297307
def full_chunk_shape(self) -> tuple[int, ...]:
@@ -354,6 +364,15 @@ def _load_dataset_attributes(self) -> dict[str, Any]:
354364
The dataset attributes as a dictionary
355365
"""
356366

367+
@property
368+
def units(self) -> dict[str, AllUnitModel]:
369+
"""Return a copy of the template's configured units.
370+
371+
Read-only view for collaborators (e.g. ingestion unit resolution) so they do not
372+
reach into the private ``_units`` mapping.
373+
"""
374+
return dict(self._units)
375+
357376
def get_unit_by_key(self, key: str) -> AllUnitModel | None:
358377
"""Get units by variable/dimension/coordinate name. Returns None if not found."""
359378
return self._units.get(key, None)
@@ -375,12 +394,7 @@ def _add_coordinates(self) -> None:
375394
"""
376395
# Add dimension coordinates
377396
for name in self._dim_names:
378-
self._builder.add_coordinate(
379-
name,
380-
dimensions=(name,),
381-
data_type=ScalarType.INT32,
382-
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key(name)),
383-
)
397+
self._add_dimension_coordinate(name)
384398

385399
# Add non-dimension coordinates
386400
# Note: coordinate_names may be modified at runtime by grid overrides,
@@ -400,7 +414,7 @@ def _add_coordinates(self) -> None:
400414
raise
401415

402416
def _add_trace_mask(self) -> None:
403-
"""Add trace mask variables."""
417+
"""Add trace mask variable."""
404418
self._builder.add_variable(
405419
name="trace_mask",
406420
dimensions=self.spatial_dimension_names,
@@ -410,7 +424,7 @@ def _add_trace_mask(self) -> None:
410424
)
411425

412426
def _add_trace_headers(self, header_dtype: StructuredType) -> None:
413-
"""Add trace mask variables."""
427+
"""Add trace headers variable."""
414428
chunk_grid = RegularChunkGrid(configuration=RegularChunkShape(chunk_shape=self.full_chunk_shape[:-1]))
415429
self._builder.add_variable(
416430
name="headers",

src/mdio/builder/templates/seismic_2d_cdp.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,18 @@ def _add_coordinates(self) -> None:
4848
self._builder.add_coordinate(
4949
"cdp",
5050
dimensions=("cdp",),
51-
data_type=ScalarType.INT32,
51+
data_type=self._dim_dtype("cdp"),
5252
)
5353
self._builder.add_coordinate(
5454
self._gather_domain,
5555
dimensions=(self._gather_domain,),
56-
data_type=ScalarType.INT32,
56+
data_type=self._dim_dtype(self._gather_domain),
5757
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key(self._gather_domain)),
5858
)
5959
self._builder.add_coordinate(
6060
self.trace_domain,
6161
dimensions=(self.trace_domain,),
62-
data_type=ScalarType.INT32,
62+
data_type=self._dim_dtype(self.trace_domain),
6363
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key(self.trace_domain)),
6464
)
6565

src/mdio/builder/templates/seismic_2d_streamer_shot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def _add_coordinates(self) -> None:
4444
self._builder.add_coordinate(
4545
name,
4646
dimensions=(name,),
47-
data_type=ScalarType.INT32,
47+
data_type=self._dim_dtype(name),
4848
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key(name)),
4949
)
5050

src/mdio/builder/templates/seismic_3d_cdp.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,23 @@ def _add_coordinates(self) -> None:
4848
self._builder.add_coordinate(
4949
"inline",
5050
dimensions=("inline",),
51-
data_type=ScalarType.INT32,
51+
data_type=self._dim_dtype("inline"),
5252
)
5353
self._builder.add_coordinate(
5454
"crossline",
5555
dimensions=("crossline",),
56-
data_type=ScalarType.INT32,
56+
data_type=self._dim_dtype("crossline"),
5757
)
5858
self._builder.add_coordinate(
5959
self._gather_domain,
6060
dimensions=(self._gather_domain,),
61-
data_type=ScalarType.INT32,
61+
data_type=self._dim_dtype(self._gather_domain),
6262
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key(self._gather_domain)),
6363
)
6464
self._builder.add_coordinate(
6565
self.trace_domain,
6666
dimensions=(self.trace_domain,),
67-
data_type=ScalarType.INT32,
67+
data_type=self._dim_dtype(self.trace_domain),
6868
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key(self.trace_domain)),
6969
)
7070

src/mdio/builder/templates/seismic_3d_coca.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from mdio.builder.schemas.v1.variable import CoordinateMetadata
88
from mdio.builder.templates.base import AbstractDatasetTemplate
99
from mdio.builder.templates.types import CoordinateSpec
10+
from mdio.builder.templates.types import DimCoordinateTypes
1011
from mdio.builder.templates.types import SeismicDataDomain
1112

1213

@@ -34,34 +35,44 @@ def declare_coordinate_specs(self) -> tuple[CoordinateSpec, ...]:
3435
CoordinateSpec(name="cdp_y", dimensions=("inline", "crossline"), dtype=ScalarType.FLOAT64),
3536
)
3637

38+
def declare_dim_coordinate_types(self) -> DimCoordinateTypes:
39+
"""Declare the data types for each dimension coordinate in this template."""
40+
return {
41+
"inline": ScalarType.INT32,
42+
"crossline": ScalarType.INT32,
43+
"offset": ScalarType.INT32,
44+
"azimuth": ScalarType.FLOAT32,
45+
self._data_domain: ScalarType.INT32,
46+
}
47+
3748
def _add_coordinates(self) -> None:
3849
# Add dimension coordinates
3950
self._builder.add_coordinate(
4051
"inline",
4152
dimensions=("inline",),
42-
data_type=ScalarType.INT32,
53+
data_type=self._dim_dtype("inline"),
4354
)
4455
self._builder.add_coordinate(
4556
"crossline",
4657
dimensions=("crossline",),
47-
data_type=ScalarType.INT32,
58+
data_type=self._dim_dtype("crossline"),
4859
)
4960
self._builder.add_coordinate(
5061
"offset",
5162
dimensions=("offset",),
52-
data_type=ScalarType.INT32,
63+
data_type=self._dim_dtype("offset"),
5364
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key("offset")), # same unit as X/Y
5465
)
5566
self._builder.add_coordinate(
5667
"azimuth",
5768
dimensions=("azimuth",),
58-
data_type=ScalarType.FLOAT32,
69+
data_type=self._dim_dtype("azimuth"),
5970
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key("azimuth")),
6071
)
6172
self._builder.add_coordinate(
6273
self.trace_domain,
6374
dimensions=(self.trace_domain,),
64-
data_type=ScalarType.INT32,
75+
data_type=self._dim_dtype(self.trace_domain),
6576
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key(self.trace_domain)),
6677
)
6778

0 commit comments

Comments
 (0)