Skip to content

Commit 0d2ae00

Browse files
committed
Add OBN Data Import documentation and remove single-component template
- Introduced a new documentation file for OBN Data Import detailing the `ObnReceiverGathers3D` template and its usage. - Updated the main index documentation to include a reference to the new OBN Data Import guide. - Removed the `Seismic3DObnSingleComponentGathersTemplate` as it is no longer needed. - Adjusted the template registry to reflect the removal of the single-component template. - Enhanced the `Seismic3DObnReceiverGathersTemplate` to clarify the handling of the `shot_index` dimension and component synthesis during data import. - Updated related tests to ensure proper functionality with the new structure.
1 parent c1b6dd4 commit 0d2ae00

14 files changed

Lines changed: 856 additions & 397 deletions

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ configuration
2424
:caption: Learning and Support
2525
2626
tutorials/index
27+
obn_data_import
2728
api_reference
2829
```
2930

docs/obn_data_import.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# OBN Data Import
2+
3+
This guide covers the `ObnReceiverGathers3D` template for importing Ocean Bottom Node (OBN) seismic data into MDIO.
4+
5+
## Template Overview
6+
7+
The `ObnReceiverGathers3D` template organizes data with the following dimensions:
8+
9+
| Dimension | Description |
10+
| -------------- | ---------------------------------------------------------------------------------- |
11+
| `component` | Sensor component (e.g., 1=X, 2=Y, 3=Z, 4=Hydrophone) |
12+
| `receiver` | Ocean bottom node receiver ID |
13+
| `shot_line` | Shot line identifier |
14+
| `gun` | Gun identifier for multi-gun sources |
15+
| `shot_index` | Calculated dense index for shots (see [AutoShotWrap](#autoshotwrap-grid-override)) |
16+
| `time`/`depth` | Vertical sample axis |
17+
18+
### Coordinates
19+
20+
- **Logical coordinates**: `shot_point` (original values), `orig_field_record_num`
21+
- **Physical coordinates**: `group_coord_x`, `group_coord_y`, `source_coord_x`, `source_coord_y`
22+
23+
```{note}
24+
The `shot_index` dimension is calculated (0 to N-1) from `shot_point` values during ingestion. Original `shot_point` values are preserved as a coordinate indexed by `(shot_line, gun, shot_index)`.
25+
```
26+
27+
## Special Behaviors
28+
29+
### AutoShotWrap Grid Override
30+
31+
The `AutoShotWrap` grid override handles multi-gun acquisition where shot points are interleaved across guns. It calculates a dense `shot_index` from sparse `shot_point` values:
32+
33+
```
34+
Before (interleaved shot_point):
35+
Gun 1: 1, 3, 5, 7, ...
36+
Gun 2: 2, 4, 6, 8, ...
37+
38+
After (dense shot_index):
39+
Gun 1: 0, 1, 2, 3, ...
40+
Gun 2: 0, 1, 2, 3, ...
41+
```
42+
43+
For `ObnReceiverGathers3D`, the override uses `shot_line` as the line field and requires `shot_line`, `gun`, and `shot_point` headers.
44+
45+
### Component Synthesis
46+
47+
When the SEG-Y spec does not include a `component` field, MDIO automatically synthesizes it with value `1` for all traces. This allows single-component data (e.g., hydrophone-only) to use the same template without modification.
48+
49+
```{note}
50+
A warning is logged when component is synthesized:
51+
> SEG-Y headers do not contain 'component' field required by template 'ObnReceiverGathers3D'.
52+
> Synthesizing 'component' dimension with constant value 1 for all traces.
53+
```
54+
55+
## Usage
56+
57+
### Basic Import
58+
59+
```python
60+
from segy.schema import HeaderField
61+
from segy.standards import get_segy_standard
62+
63+
from mdio import segy_to_mdio
64+
from mdio.builder.template_registry import get_template
65+
66+
# Define SEG-Y header mapping
67+
obn_headers = [
68+
HeaderField(name="orig_field_record_num", byte=9, format="int32"),
69+
HeaderField(name="receiver", byte=13, format="int32"),
70+
HeaderField(name="shot_point", byte=17, format="int32"),
71+
HeaderField(name="shot_line", byte=133, format="int16"),
72+
HeaderField(name="gun", byte=171, format="int16"),
73+
HeaderField(name="component", byte=189, format="int16"),
74+
HeaderField(name="coordinate_scalar", byte=71, format="int16"),
75+
HeaderField(name="source_coord_x", byte=73, format="int32"),
76+
HeaderField(name="source_coord_y", byte=77, format="int32"),
77+
HeaderField(name="group_coord_x", byte=81, format="int32"),
78+
HeaderField(name="group_coord_y", byte=85, format="int32"),
79+
]
80+
81+
obn_spec = get_segy_standard(1.0).customize(trace_header_fields=obn_headers)
82+
83+
segy_to_mdio(
84+
input_path="obn_data.sgy",
85+
output_path="obn_data.mdio",
86+
segy_spec=obn_spec,
87+
mdio_template=get_template("ObnReceiverGathers3D"),
88+
grid_overrides={"AutoShotWrap": True},
89+
overwrite=True,
90+
)
91+
```
92+
93+
### Single-Component Data
94+
95+
For data without a `component` header field, simply omit it from the spec:
96+
97+
```python
98+
# Same as above, but without the component field
99+
obn_headers = [
100+
HeaderField(name="orig_field_record_num", byte=9, format="int32"),
101+
HeaderField(name="receiver", byte=13, format="int32"),
102+
HeaderField(name="shot_point", byte=17, format="int32"),
103+
HeaderField(name="shot_line", byte=133, format="int16"),
104+
HeaderField(name="gun", byte=171, format="int16"),
105+
# component omitted - will be synthesized
106+
HeaderField(name="coordinate_scalar", byte=71, format="int16"),
107+
HeaderField(name="source_coord_x", byte=73, format="int32"),
108+
HeaderField(name="source_coord_y", byte=77, format="int32"),
109+
HeaderField(name="group_coord_x", byte=81, format="int32"),
110+
HeaderField(name="group_coord_y", byte=85, format="int32"),
111+
]
112+
```
113+
114+
### Exploring the Data
115+
116+
```python
117+
from mdio import open_mdio
118+
119+
ds = open_mdio("obn_data.mdio")
120+
121+
# View dimensions
122+
print(ds.sizes)
123+
# {'component': 4, 'receiver': 100, 'shot_line': 10, 'gun': 2, 'shot_index': 500, 'time': 2001}
124+
125+
# Access original shot_point values (preserved as coordinate)
126+
print(ds["shot_point"].dims) # ('shot_line', 'gun', 'shot_index')
127+
128+
# Select a receiver gather
129+
receiver_gather = ds.sel(receiver=150, component=4)
130+
receiver_gather["amplitude"].plot()
131+
```
132+
133+
## Required Header Fields
134+
135+
| Field | Required | Notes |
136+
| ----------------------- | -------- | ----------------------------------- |
137+
| `receiver` | Yes | |
138+
| `shot_line` | Yes | |
139+
| `gun` | Yes | |
140+
| `shot_point` | Yes | |
141+
| `component` | No | Synthesized with value 1 if missing |
142+
| `coordinate_scalar` | Yes | |
143+
| `source_coord_x` | Yes | |
144+
| `source_coord_y` | Yes | |
145+
| `group_coord_x` | Yes | |
146+
| `group_coord_y` | Yes | |
147+
| `orig_field_record_num` | Yes | |
148+
149+
## See Also
150+
151+
- [Template Registry](template_registry.md)
152+
- [Quickstart Tutorial](tutorials/quickstart.ipynb)

src/mdio/builder/template_registry.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
from mdio.builder.templates.seismic_3d_cdp import Seismic3DCdpGathersTemplate
2727
from mdio.builder.templates.seismic_3d_coca import Seismic3DCocaGathersTemplate
2828
from mdio.builder.templates.seismic_3d_obn import Seismic3DObnReceiverGathersTemplate
29-
from mdio.builder.templates.seismic_3d_obn_single_component import Seismic3DObnSingleComponentGathersTemplate
3029
from mdio.builder.templates.seismic_3d_poststack import Seismic3DPostStackTemplate
3130
from mdio.builder.templates.seismic_3d_shot_receiver_line import Seismic3DShotReceiverLineGathersTemplate
3231
from mdio.builder.templates.seismic_3d_streamer_field import Seismic3DStreamerFieldRecordsTemplate
@@ -143,7 +142,6 @@ def _register_default_templates(self) -> None:
143142

144143
# OBN (Ocean Bottom Node) data
145144
self.register(Seismic3DObnReceiverGathersTemplate())
146-
self.register(Seismic3DObnSingleComponentGathersTemplate())
147145

148146
# Land/OBC shot-receiver data
149147
self.register(Seismic3DShotReceiverLineGathersTemplate())

src/mdio/builder/templates/seismic_3d_obn.py

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,37 @@
99

1010

1111
class Seismic3DObnReceiverGathersTemplate(AbstractDatasetTemplate):
12-
"""Seismic 3D OBN (Ocean Bottom Node) receiver gathers template."""
12+
"""Seismic 3D OBN (Ocean Bottom Node) receiver gathers template.
13+
14+
This template uses shot_index as a calculated dimension (similar to StreamerFieldRecords3D).
15+
The shot_index is computed from shot_point values during SEG-Y import using the
16+
AutoShotWrap grid override. AutoShotWrap is template-aware and automatically detects
17+
that this template uses shot_line (not sail_line) based on the dimension names.
18+
The original shot_point values are preserved as a coordinate indexed by
19+
(shot_line, gun, shot_index).
20+
21+
This design handles OBN data where shot points may be interleaved across multiple guns.
22+
23+
Special handling for component dimension:
24+
If the SEG-Y spec does not contain a 'component' field, the ingestion process
25+
will automatically synthesize a component dimension with constant value 1 for
26+
all traces. A warning is logged when this occurs. This is handled explicitly
27+
in GridOverrider._synthesize_obn_component().
28+
"""
1329

1430
def __init__(self, data_domain: SeismicDataDomain = "time"):
1531
super().__init__(data_domain=data_domain)
1632

17-
self._spatial_dim_names = ("component", "receiver", "shot_line", "gun", "shot_point")
33+
self._spatial_dim_names = ("component", "receiver", "shot_line", "gun", "shot_index")
34+
self._calculated_dims = ("shot_index",)
1835
self._dim_names = (*self._spatial_dim_names, self._data_domain)
1936
self._physical_coord_names = (
2037
"group_coord_x",
2138
"group_coord_y",
2239
"source_coord_x",
2340
"source_coord_y",
2441
)
25-
self._logical_coord_names = ("orig_field_record_num",)
42+
self._logical_coord_names = ("shot_point", "orig_field_record_num")
2643
self._var_chunk_shape = (1, 1, 1, 3, 128, 4096)
2744

2845
@property
@@ -34,6 +51,7 @@ def _load_dataset_attributes(self) -> dict[str, Any]:
3451

3552
def _add_coordinates(self) -> None:
3653
# Add dimension coordinates
54+
# EXCLUDE: `shot_index` since it's 0-N (calculated dimension)
3755
self._builder.add_coordinate(
3856
"component",
3957
dimensions=("component",),
@@ -54,11 +72,6 @@ def _add_coordinates(self) -> None:
5472
dimensions=("gun",),
5573
data_type=ScalarType.UINT8,
5674
)
57-
self._builder.add_coordinate(
58-
"shot_point",
59-
dimensions=("shot_point",),
60-
data_type=ScalarType.UINT32,
61-
)
6275
self._builder.add_coordinate(
6376
self._data_domain,
6477
dimensions=(self._data_domain,),
@@ -79,20 +92,25 @@ def _add_coordinates(self) -> None:
7992
data_type=ScalarType.FLOAT64,
8093
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key("group_coord_y")),
8194
)
95+
self._builder.add_coordinate(
96+
"shot_point",
97+
dimensions=("shot_line", "gun", "shot_index"),
98+
data_type=ScalarType.UINT32,
99+
)
100+
self._builder.add_coordinate(
101+
"orig_field_record_num",
102+
dimensions=("shot_line", "gun", "shot_index"),
103+
data_type=ScalarType.UINT32,
104+
)
82105
self._builder.add_coordinate(
83106
"source_coord_x",
84-
dimensions=("shot_line", "gun", "shot_point"),
107+
dimensions=("shot_line", "gun", "shot_index"),
85108
data_type=ScalarType.FLOAT64,
86109
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key("source_coord_x")),
87110
)
88111
self._builder.add_coordinate(
89112
"source_coord_y",
90-
dimensions=("shot_line", "gun", "shot_point"),
113+
dimensions=("shot_line", "gun", "shot_index"),
91114
data_type=ScalarType.FLOAT64,
92115
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key("source_coord_y")),
93116
)
94-
self._builder.add_coordinate(
95-
"orig_field_record_num",
96-
dimensions=("shot_line", "gun", "shot_point"),
97-
data_type=ScalarType.UINT32,
98-
)

src/mdio/builder/templates/seismic_3d_obn_single_component.py

Lines changed: 0 additions & 93 deletions
This file was deleted.

src/mdio/converters/segy.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,14 @@ def _validate_spec_in_template(segy_spec: SegySpec, mdio_template: AbstractDatas
665665

666666
required_fields = set(mdio_template.spatial_dimension_names) | set(mdio_template.coordinate_names)
667667
required_fields = required_fields - set(mdio_template.calculated_dimension_names) # remove to be calculated
668+
669+
# For OBN template: 'component' is optional (will be synthesized if missing)
670+
# Import here to avoid circular imports at module load time
671+
from mdio.builder.templates.seismic_3d_obn import Seismic3DObnReceiverGathersTemplate # noqa: PLC0415
672+
673+
if isinstance(mdio_template, Seismic3DObnReceiverGathersTemplate):
674+
required_fields.discard("component")
675+
668676
required_fields = required_fields | {"coordinate_scalar"} # ensure coordinate scalar is always present
669677
missing_fields = required_fields - header_fields
670678

0 commit comments

Comments
 (0)