Skip to content

Commit a40bdc7

Browse files
committed
Add Seismic3DOffsetTilesTemplate and Seismic3DReceiverGathersTemplate
- Introduced Seismic3DOffsetTilesTemplate for wide-azimuth seismic data organized by offset vector tiles. - Added Seismic3DReceiverGathersTemplate for fixed receiver positions, supporting receiver-side processing. - Updated TemplateRegistry to include new templates. - Added unit tests for both new templates to ensure functionality and correctness.
1 parent 308dcac commit a40bdc7

6 files changed

Lines changed: 397 additions & 52 deletions

File tree

src/mdio/builder/template_registry.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
from mdio.builder.templates.seismic_2d_streamer_shot import Seismic2DStreamerShotGathersTemplate
2626
from mdio.builder.templates.seismic_3d_cdp import Seismic3DCdpGathersTemplate
2727
from mdio.builder.templates.seismic_3d_coca import Seismic3DCocaGathersTemplate
28-
from mdio.builder.templates.seismic_3d_obn_receiver import Seismic3DObnReceiverGathersTemplate
29-
from mdio.builder.templates.seismic_3d_ovt import Seismic3DOffsetVectorTileTemplate
28+
from mdio.builder.templates.seismic_3d_offset_tiles import Seismic3DOffsetTilesTemplate
3029
from mdio.builder.templates.seismic_3d_poststack import Seismic3DPostStackTemplate
30+
from mdio.builder.templates.seismic_3d_receiver_gathers import Seismic3DReceiverGathersTemplate
3131
from mdio.builder.templates.seismic_3d_streamer_field import Seismic3DStreamerFieldRecordsTemplate
3232
from mdio.builder.templates.seismic_3d_streamer_shot import Seismic3DStreamerShotGathersTemplate
3333

@@ -135,13 +135,12 @@ def _register_default_templates(self) -> None:
135135
self.register(Seismic3DCocaGathersTemplate("time"))
136136
self.register(Seismic3DCocaGathersTemplate("depth"))
137137

138-
# OBN/OBC Receiver Gathers
139-
self.register(Seismic3DObnReceiverGathersTemplate("time"))
140-
self.register(Seismic3DObnReceiverGathersTemplate("depth"))
138+
# Receiver Gathers (OBN, OBC, land fixed-spread) - time domain only
139+
self.register(Seismic3DReceiverGathersTemplate())
141140

142-
# Offset Vector Tile (OVT) Gathers
143-
self.register(Seismic3DOffsetVectorTileTemplate("time"))
144-
self.register(Seismic3DOffsetVectorTileTemplate("depth"))
141+
# Offset Tiles
142+
self.register(Seismic3DOffsetTilesTemplate("time"))
143+
self.register(Seismic3DOffsetTilesTemplate("depth"))
145144

146145
# Field (shot) data
147146
self.register(Seismic2DStreamerShotGathersTemplate())

src/mdio/builder/templates/seismic_3d_ovt.py renamed to src/mdio/builder/templates/seismic_3d_offset_tiles.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Seismic3DOffsetVectorTileTemplate MDIO v1 dataset templates."""
1+
"""Seismic3DOffsetTilesTemplate MDIO v1 dataset templates."""
22

33
from typing import Any
44

@@ -9,8 +9,8 @@
99
from mdio.builder.templates.types import SeismicDataDomain
1010

1111

12-
class Seismic3DOffsetVectorTileTemplate(AbstractDatasetTemplate):
13-
"""Seismic Offset Vector Tile (OVT) pre-stack 3D Dataset template.
12+
class Seismic3DOffsetTilesTemplate(AbstractDatasetTemplate):
13+
"""Seismic Offset Tiles pre-stack 3D Dataset template.
1414
1515
A 5D template for wide-azimuth seismic data organized by offset vector tiles.
1616
Data is binned by CDP location (inline, crossline) with offset vector
@@ -53,10 +53,10 @@ def __init__(self, data_domain: SeismicDataDomain = "time"):
5353

5454
@property
5555
def _name(self) -> str:
56-
return f"OffsetVectorTileGathers3D{self._data_domain.capitalize()}"
56+
return f"OffsetTiles3D{self._data_domain.capitalize()}"
5757

5858
def _load_dataset_attributes(self) -> dict[str, Any]:
59-
return {"surveyType": "3D", "gatherType": "offset_vector_tile"}
59+
return {"surveyType": "3D", "gatherType": "offset_tiles"}
6060

6161
def _add_coordinates(self) -> None:
6262
# Add dimension coordinates

src/mdio/builder/templates/seismic_3d_obn_receiver.py renamed to src/mdio/builder/templates/seismic_3d_receiver_gathers.py

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,60 @@
1-
"""Seismic3DObnReceiverGathersTemplate MDIO v1 dataset templates."""
1+
"""Seismic3DReceiverGathersTemplate MDIO v1 dataset templates."""
22

33
from typing import Any
44

55
from mdio.builder.schemas import compressors
66
from mdio.builder.schemas.dtype import ScalarType
77
from mdio.builder.schemas.v1.variable import CoordinateMetadata
88
from mdio.builder.templates.base import AbstractDatasetTemplate
9-
from mdio.builder.templates.types import SeismicDataDomain
109

1110

12-
class Seismic3DObnReceiverGathersTemplate(AbstractDatasetTemplate):
13-
"""Seismic OBN/OBC receiver-domain pre-stack 3D Dataset template.
11+
class Seismic3DReceiverGathersTemplate(AbstractDatasetTemplate):
12+
"""Seismic receiver gather pre-stack 3D Dataset template.
1413
15-
A template for Ocean Bottom Node (OBN) or Ocean Bottom Cable (OBC) surveys where
16-
receivers are deployed at fixed positions and shots are organized by shot lines.
17-
Data is indexed by receiver position, enabling efficient receiver-side processing.
14+
A template for surveys with fixed receiver positions (OBN, OBC, land fixed-spread)
15+
where data is organized by receiver position for receiver-side processing.
16+
Shots are organized by shot lines with a calculated shot index.
17+
18+
This template is time-domain only as receiver gathers are typically used
19+
for early-stage processing before depth conversion.
1820
1921
Dimensions:
2022
- receiver: Index of the receiver node/station
2123
- shot_line: Shot line or swath identifier
22-
- shot_point: Shot point within the line
23-
- time/depth: Sample dimension
24+
- shot_index: Sequential index of shots within the line (calculated, 0-N)
25+
- time: Sample dimension
2426
2527
This organization is optimal for:
2628
- Receiver-side wavefield separation (up/down)
2729
- Receiver-consistent deconvolution
2830
- Multi-component processing
29-
30-
Args:
31-
data_domain: The domain of the dataset ('time' or 'depth').
31+
- Mirror imaging from OBN
3232
"""
3333

34-
def __init__(self, data_domain: SeismicDataDomain = "time"):
35-
super().__init__(data_domain=data_domain)
34+
def __init__(self) -> None:
35+
super().__init__(data_domain="time")
3636

37-
self._dim_names = ("receiver", "shot_line", "shot_point", self._data_domain)
37+
self._dim_names = ("receiver", "shot_line", "shot_index", "time")
38+
self._calculated_dims = ("shot_index",)
3839
self._physical_coord_names = (
3940
"receiver_x",
4041
"receiver_y",
4142
"source_coord_x",
4243
"source_coord_y",
4344
)
44-
self._logical_coord_names = ("receiver_depth",)
45+
self._logical_coord_names = ("shot_point",)
4546
self._var_chunk_shape = (1, 1, 512, 4096)
4647

4748
@property
4849
def _name(self) -> str:
49-
return f"ObnReceiverGathers3D{self._data_domain.capitalize()}"
50+
return "ReceiverGathers3D"
5051

5152
def _load_dataset_attributes(self) -> dict[str, Any]:
52-
return {"surveyType": "3D", "gatherType": "common_receiver", "acquisitionType": "obn"}
53+
return {"surveyType": "3D", "gatherType": "receiver_gathers"}
5354

5455
def _add_coordinates(self) -> None:
5556
# Add dimension coordinates
57+
# Note: shot_index is calculated (0-N), so we don't add a coordinate for it
5658
self._builder.add_coordinate(
5759
"receiver",
5860
dimensions=("receiver",),
@@ -65,12 +67,6 @@ def _add_coordinates(self) -> None:
6567
data_type=ScalarType.UINT32,
6668
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key("shot_line")),
6769
)
68-
self._builder.add_coordinate(
69-
"shot_point",
70-
dimensions=("shot_point",),
71-
data_type=ScalarType.UINT32,
72-
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key("shot_point")),
73-
)
7470
self._builder.add_coordinate(
7571
self.trace_domain,
7672
dimensions=(self.trace_domain,),
@@ -96,25 +92,27 @@ def _add_coordinates(self) -> None:
9692
compressor=compressor,
9793
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key("receiver_y")),
9894
)
95+
96+
# Shot point coordinate (actual shot point numbers, varies by shot_line and shot_index)
9997
self._builder.add_coordinate(
100-
"receiver_depth",
101-
dimensions=("receiver",),
102-
data_type=ScalarType.FLOAT32,
98+
"shot_point",
99+
dimensions=("shot_line", "shot_index"),
100+
data_type=ScalarType.UINT32,
103101
compressor=compressor,
104-
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key("receiver_depth")),
102+
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key("shot_point")),
105103
)
106104

107-
# Source coordinates (vary by shot_line and shot_point)
105+
# Source coordinates (vary by shot_line and shot_index)
108106
self._builder.add_coordinate(
109107
"source_coord_x",
110-
dimensions=("shot_line", "shot_point"),
108+
dimensions=("shot_line", "shot_index"),
111109
data_type=ScalarType.FLOAT64,
112110
compressor=compressor,
113111
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key("source_coord_x")),
114112
)
115113
self._builder.add_coordinate(
116114
"source_coord_y",
117-
dimensions=("shot_line", "shot_point"),
115+
dimensions=("shot_line", "shot_index"),
118116
data_type=ScalarType.FLOAT64,
119117
compressor=compressor,
120118
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key("source_coord_y")),
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"""Unit tests for Seismic3DOffsetTilesTemplate."""
2+
3+
import pytest
4+
from tests.unit.v1.helpers import validate_variable
5+
6+
from mdio.builder.schemas.chunk_grid import RegularChunkGrid
7+
from mdio.builder.schemas.compressors import Blosc
8+
from mdio.builder.schemas.compressors import BloscCname
9+
from mdio.builder.schemas.dtype import ScalarType
10+
from mdio.builder.schemas.dtype import StructuredType
11+
from mdio.builder.schemas.v1.dataset import Dataset
12+
from mdio.builder.schemas.v1.units import LengthUnitEnum
13+
from mdio.builder.schemas.v1.units import LengthUnitModel
14+
from mdio.builder.schemas.v1.units import TimeUnitEnum
15+
from mdio.builder.schemas.v1.units import TimeUnitModel
16+
from mdio.builder.templates.seismic_3d_offset_tiles import Seismic3DOffsetTilesTemplate
17+
from mdio.builder.templates.types import SeismicDataDomain
18+
19+
UNITS_METER = LengthUnitModel(length=LengthUnitEnum.METER)
20+
UNITS_SECOND = TimeUnitModel(time=TimeUnitEnum.SECOND)
21+
22+
23+
def _validate_coordinates_headers_trace_mask(dataset: Dataset, headers: StructuredType, domain: str) -> None:
24+
"""Validate the coordinate, headers, trace_mask variables in the dataset."""
25+
# Verify variables
26+
# 5 dim coords + 2 non-dim coords + 1 data + 1 trace mask + 1 headers = 10 variables
27+
assert len(dataset.variables) == 10
28+
29+
# Verify trace headers
30+
validate_variable(
31+
dataset,
32+
name="headers",
33+
dims=[("inline", 256), ("crossline", 256), ("inline_offset_tile", 12), ("crossline_offset_tile", 12)],
34+
coords=["cdp_x", "cdp_y"],
35+
dtype=headers,
36+
)
37+
38+
validate_variable(
39+
dataset,
40+
name="trace_mask",
41+
dims=[("inline", 256), ("crossline", 256), ("inline_offset_tile", 12), ("crossline_offset_tile", 12)],
42+
coords=["cdp_x", "cdp_y"],
43+
dtype=ScalarType.BOOL,
44+
)
45+
46+
# Verify dimension coordinate variables
47+
validate_variable(
48+
dataset,
49+
name="inline",
50+
dims=[("inline", 256)],
51+
coords=["inline"],
52+
dtype=ScalarType.INT32,
53+
)
54+
55+
validate_variable(
56+
dataset,
57+
name="crossline",
58+
dims=[("crossline", 256)],
59+
coords=["crossline"],
60+
dtype=ScalarType.INT32,
61+
)
62+
63+
validate_variable(
64+
dataset,
65+
name="inline_offset_tile",
66+
dims=[("inline_offset_tile", 12)],
67+
coords=["inline_offset_tile"],
68+
dtype=ScalarType.INT16,
69+
)
70+
71+
validate_variable(
72+
dataset,
73+
name="crossline_offset_tile",
74+
dims=[("crossline_offset_tile", 12)],
75+
coords=["crossline_offset_tile"],
76+
dtype=ScalarType.INT16,
77+
)
78+
79+
domain_var = validate_variable(
80+
dataset,
81+
name=domain,
82+
dims=[(domain, 2048)],
83+
coords=[domain],
84+
dtype=ScalarType.INT32,
85+
)
86+
assert domain_var.metadata.units_v1 in (UNITS_METER, UNITS_SECOND)
87+
88+
# Verify non-dimension coordinate variables
89+
cdp_x = validate_variable(
90+
dataset,
91+
name="cdp_x",
92+
dims=[("inline", 256), ("crossline", 256)],
93+
coords=["cdp_x"],
94+
dtype=ScalarType.FLOAT64,
95+
)
96+
assert cdp_x.metadata.units_v1 == UNITS_METER
97+
98+
cdp_y = validate_variable(
99+
dataset,
100+
name="cdp_y",
101+
dims=[("inline", 256), ("crossline", 256)],
102+
coords=["cdp_y"],
103+
dtype=ScalarType.FLOAT64,
104+
)
105+
assert cdp_y.metadata.units_v1 == UNITS_METER
106+
107+
108+
@pytest.mark.parametrize("data_domain", ["depth", "time"])
109+
class TestSeismic3DOffsetTilesTemplate:
110+
"""Unit tests for Seismic3DOffsetTilesTemplate."""
111+
112+
def test_configuration(self, data_domain: SeismicDataDomain) -> None:
113+
"""Unit tests for Seismic3DOffsetTilesTemplate configuration."""
114+
t = Seismic3DOffsetTilesTemplate(data_domain=data_domain)
115+
116+
# Template attributes
117+
assert t._dim_names == ("inline", "crossline", "inline_offset_tile", "crossline_offset_tile", data_domain)
118+
assert t._physical_coord_names == ("cdp_x", "cdp_y")
119+
assert t._logical_coord_names == ()
120+
assert t.full_chunk_shape == (4, 4, 6, 6, 4096)
121+
122+
# Variables instantiated when build_dataset() is called
123+
assert t._builder is None
124+
assert t._dim_sizes == ()
125+
126+
# Verify dataset attributes
127+
attrs = t._load_dataset_attributes()
128+
assert attrs == {"surveyType": "3D", "gatherType": "offset_tiles"}
129+
assert t.default_variable_name == "amplitude"
130+
131+
def test_build_dataset(self, data_domain: SeismicDataDomain, structured_headers: StructuredType) -> None:
132+
"""Unit tests for Seismic3DOffsetTilesTemplate build."""
133+
t = Seismic3DOffsetTilesTemplate(data_domain=data_domain)
134+
t.add_units({"cdp_x": UNITS_METER, "cdp_y": UNITS_METER})
135+
t.add_units({"time": UNITS_SECOND, "depth": UNITS_METER})
136+
137+
dataset = t.build_dataset(
138+
"Wide Azimuth Offset Tiles",
139+
sizes=(256, 256, 12, 12, 2048),
140+
header_dtype=structured_headers,
141+
)
142+
143+
assert dataset.metadata.name == "Wide Azimuth Offset Tiles"
144+
assert dataset.metadata.attributes["surveyType"] == "3D"
145+
assert dataset.metadata.attributes["gatherType"] == "offset_tiles"
146+
147+
_validate_coordinates_headers_trace_mask(dataset, structured_headers, data_domain)
148+
149+
# Verify seismic variable
150+
seismic = validate_variable(
151+
dataset,
152+
name="amplitude",
153+
dims=[
154+
("inline", 256),
155+
("crossline", 256),
156+
("inline_offset_tile", 12),
157+
("crossline_offset_tile", 12),
158+
(data_domain, 2048),
159+
],
160+
coords=["cdp_x", "cdp_y"],
161+
dtype=ScalarType.FLOAT32,
162+
)
163+
assert isinstance(seismic.compressor, Blosc)
164+
assert seismic.compressor.cname == BloscCname.zstd
165+
assert isinstance(seismic.metadata.chunk_grid, RegularChunkGrid)
166+
assert seismic.metadata.chunk_grid.configuration.chunk_shape == (4, 4, 6, 6, 4096)
167+
assert seismic.metadata.stats_v1 is None
168+
169+
170+
@pytest.mark.parametrize("data_domain", ["Time", "DePTh"])
171+
def test_domain_case_handling(data_domain: str) -> None:
172+
"""Test that domain parameter handles different cases correctly."""
173+
template = Seismic3DOffsetTilesTemplate(data_domain=data_domain)
174+
assert template._data_domain == data_domain.lower()
175+
176+
data_domain_suffix = data_domain.lower().capitalize()
177+
assert template.name == f"OffsetTiles3D{data_domain_suffix}"

0 commit comments

Comments
 (0)