Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions docs/guides/crg_data_import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# CRG Data Import

This guide covers the `ObnContinuousReceiverGathers3D` template for importing OBN
**Continuous Receiver Gathers** (CRG, clock-time) seismic data into MDIO.

In OBN acquisition each ocean-bottom node records continuously. A continuous-receiver-gather
delivery bundles many receivers and splits each receiver's long recording into fixed-length
(e.g. 30 s) segments, one per SEG-Y trace. The template stores those segments along a dense
per-receiver axis while preserving each segment's absolute recording time in the headers.

## Template Overview

The `ObnContinuousReceiverGathers3D` template organizes data with the following dimensions:

| Dimension | Description |
| --------------- | ----------------------------------------------------------------------------- |
| `component` | Sensor component (e.g., 1=X, 2=Y, 3=Z, 4=Hydrophone); synthesized when absent |
| `receiver_line` | Receiver line number |
| `receiver` | Receiver station number |
| `trace` | Dense per-receiver segment index, inserted at ingest (see below) |
| `time`/`depth` | Vertical sample axis (whole trace kept in one chunk) |

### Coordinates

- **Physical coordinates**: `group_coord_x`, `group_coord_y` (receiver X/Y), indexed by
`(receiver_line, receiver)`.

```{note}
The `trace` dimension is **not** declared by the template. It is inserted during ingestion by
the [`HasDuplicates`](grid_overrides.md#hasduplicates) grid override, which appends a dense
per-`(component, receiver_line, receiver)` counter in trace order. This reuses MDIO's existing
duplicate-handling machinery instead of introducing a new calculated dimension.
```

## Required Grid Overrides

### HasDuplicates (Required)

Because many segments share the same `(component, receiver_line, receiver)` index tuple, the
`HasDuplicates` grid override is **required**. It inserts the `trace` segment dimension:

```python
from mdio import GridOverrides

# Whole 30 s trace is 15001 samples; a trace chunk of 1024 gives ~64 MiB chunks.
GridOverrides(has_duplicates=True, chunksize=1024)
```

- `chunksize` sizes the inserted `trace` dimension. It is optional and defaults to `1` (the
legacy behavior), but a production CRG ingest should set it so chunks are a sensible size.
- `trace_dtype` sets the dtype of both the segment counter and the stored `trace` coordinate.
Omitting it keeps the legacy `int16` counter stored as `int32`, which tops out at 32,767
segments per receiver and raises `OverflowError` past that. Use `"uint32"` for
full-campaign products, where a single receiver holds far more segments.

```{note}
Real recording time is **not** encoded into the segment index. The `trace` axis is a dense
positional index in acquisition order; each segment's absolute time is preserved per trace in
the SEG-Y headers (typically an `epoch` field), which rides through to any cut SEG-Y unchanged.
```

## Special Behaviors

### Component Synthesis

When the SEG-Y spec does not include a `component` field, MDIO synthesizes it with value `1`
for all traces, so one template ingests both single- and multi-component CRG data. This is
driven by the template's `synthesize_missing_dims` and handled by `ComponentSynthesisStrategy`.

```{note}
A warning is logged when component is synthesized:
> SEG-Y headers do not contain 'component' field required by template; synthesizing dimension
> with constant value 1 for all traces.
```

## Usage

### Single-Component Import

```python
from segy.schema import HeaderField
from segy.standards import get_segy_standard

from mdio import GridOverrides
from mdio import segy_to_mdio
from mdio.builder.template_registry import get_template

# CRG SEG-Y trace-header mapping (big-endian; bytes past 179 are vendor-defined).
crg_headers = [
HeaderField(name="channel", byte=13, format="int32"),
HeaderField(name="coordinate_scalar", byte=71, format="int16"),
HeaderField(name="group_coord_x", byte=81, format="int32"),
HeaderField(name="group_coord_y", byte=85, format="int32"),
HeaderField(name="receiver_line", byte=137, format="int16"),
HeaderField(name="receiver", byte=139, format="int16"),
HeaderField(name="epoch", byte=189, format="int64"), # per-trace recording time
]

crg_spec = get_segy_standard(1.0).customize(trace_header_fields=crg_headers)

segy_to_mdio(
input_path="crg_data.sgy",
output_path="crg_data.mdio",
segy_spec=crg_spec,
mdio_template=get_template("ObnContinuousReceiverGathers3D"),
grid_overrides=GridOverrides(has_duplicates=True, chunksize=1024, trace_dtype="uint32"),
overwrite=True,
)
```

### Exploring the Data

```python
from mdio import open_mdio

ds = open_mdio("crg_data.mdio")

# View dimensions
print(ds.sizes)
# {'component': 1, 'receiver_line': 1, 'receiver': 10, 'trace': 8172, 'time': 15001}

# Select a whole receiver gather
receiver_gather = ds.sel(receiver_line=4871, receiver=5908, component=1)
receiver_gather["amplitude"].plot()
```

## Required Header Fields

| Field | Required | Notes |
| ------------------- | -------- | ------------------------------------------- |
| `receiver_line` | Yes | |
| `receiver` | Yes | Receiver station |
| `coordinate_scalar` | Yes | |
| `group_coord_x` | Yes | Receiver X |
| `group_coord_y` | Yes | Receiver Y |
| `component` | No | Synthesized with value 1 if missing |
| `epoch` | No | Recommended; per-trace time kept in headers |

## See Also

- [Grid Overrides](grid_overrides.md) - All available grid overrides
- [OBN Data Import](obn_data_import.md) - Shot-indexed OBN receiver gathers
- [Template Registry](../template_registry.md)
48 changes: 44 additions & 4 deletions docs/guides/grid_overrides.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,59 @@ segy_to_mdio(
See [OBN Data Import](obn_data_import.md) for a complete guide on importing OBN data.
```

## HasDuplicates

Inserts a `trace` dimension to disambiguate traces that share the same index tuple, by
appending a dense per-tuple counter (in trace order) as the `trace` axis before the vertical
axis. Used for continuous receiver gathers, where many segments share the same
`(component, receiver_line, receiver)` index.

**Supported Templates:** any template (commonly `ObnContinuousReceiverGathers3D`)

**Parameters:**

| Parameter | Default | Description |
| ------------- | ------- | ------------------------------------------------------------------------- |
| `chunksize` | `1` | Chunk size for the inserted `trace` dimension. |
| `trace_dtype` | - | Integer dtype for the `trace` counter and the coordinate it is stored as. |

Both parameters are optional and backwards compatible: omitting them reproduces the legacy
behavior (chunk size `1`, an `int16` counter stored as an `int32` coordinate). An `int16`
counter tops out at 32,767 traces per index tuple and raises `OverflowError` beyond that, so
set `trace_dtype` for larger gathers. `trace_dtype` applies to `NonBinned` as well, since it
inserts the same `trace` axis.

**Usage:**

```python
from mdio import GridOverrides

# ~64 MiB chunks for a whole-trace (15001-sample) continuous receiver gather.
GridOverrides(has_duplicates=True, chunksize=1024, trace_dtype="uint32")
```

```{note}
See [CRG Data Import](crg_data_import.md) for a complete guide on importing continuous
receiver gathers.
```

## Special Behaviors

Some templates have special behaviors that are applied automatically during import, independent of grid overrides.

### Component Synthesis (OBN)
### Component Synthesis

When using the `ObnReceiverGathers3D` template, if the SEG-Y specification 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.
Templates can declare optional dimensions through `synthesize_missing_dims`. The
`ObnReceiverGathers3D` and `ObnContinuousReceiverGathers3D` templates use this hook for
`component`: if the SEG-Y specification omits that field, MDIO synthesizes it with value
`1` for all traces. This allows single-component data (e.g., hydrophone-only) to use the
same template without modification.

```{note}
A warning is logged when component is synthesized:

> SEG-Y headers do not contain 'component' field required by template 'ObnReceiverGathers3D'.
> Synthesizing 'component' dimension with constant value 1 for all traces.
> SEG-Y headers do not contain 'component' field required by template; synthesizing dimension
> with constant value 1 for all traces.
```

## Error Handling
Expand Down
12 changes: 12 additions & 0 deletions docs/guides/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Welcome to the MDIO guides. This section provides in-depth documentation on adva

grid_overrides
obn_data_import
crg_data_import
```

## Overview
Expand All @@ -29,3 +30,14 @@ Ocean Bottom Node (OBN) data has unique characteristics requiring specialized ha
- Component synthesis for single-component data

See [OBN Data Import](obn_data_import.md) for the complete guide.

### CRG Data Import

Continuous Receiver Gathers (CRG) split each ocean-bottom node's continuous recording into
fixed-length segments. The CRG guide covers:

- The `ObnContinuousReceiverGathers3D` template
- The `HasDuplicates` grid override that builds the per-receiver segment axis
- Component synthesis for single-component data

See [CRG Data Import](crg_data_import.md) for the complete guide.
4 changes: 4 additions & 0 deletions src/mdio/builder/template_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from mdio.builder.templates.seismic_2d_streamer_shot import Seismic2DStreamerShotGathersTemplate
from mdio.builder.templates.seismic_3d_cdp import Seismic3DCdpGathersTemplate
from mdio.builder.templates.seismic_3d_coca import Seismic3DCocaGathersTemplate
from mdio.builder.templates.seismic_3d_crg import Seismic3DCrgReceiverGathersTemplate
from mdio.builder.templates.seismic_3d_obn import Seismic3DObnReceiverGathersTemplate
from mdio.builder.templates.seismic_3d_offset_tiles import Seismic3DOffsetTilesTemplate
from mdio.builder.templates.seismic_3d_poststack import Seismic3DPostStackTemplate
Expand Down Expand Up @@ -152,6 +153,9 @@ def _register_default_templates(self) -> None:
# OBN (Ocean Bottom Node) data
self.register(Seismic3DObnReceiverGathersTemplate())

# OBN Continuous Receiver Gathers (clock time)
self.register(Seismic3DCrgReceiverGathersTemplate())

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

Expand Down
6 changes: 5 additions & 1 deletion src/mdio/builder/templates/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ class AbstractDatasetTemplate(ABC):
to override specific steps.
"""

# Dimensions ingestion may synthesize when the SEG-Y spec does not carry them (e.g.
# `component` for single-component data). Declared on the class, not in `__init__`, so
# ingestion can read the hook off any template; subclasses opt in by setting it.
synthesize_missing_dims: tuple[str, ...] = ()

def __init__(self, data_domain: SeismicDataDomain) -> None:
self._data_domain = data_domain.lower()

Expand All @@ -47,7 +52,6 @@ def __init__(self, data_domain: SeismicDataDomain) -> None:
self._physical_coord_names: tuple[str, ...] = ()
self._logical_coord_names: tuple[str, ...] = ()
self._var_chunk_shape: tuple[int, ...] = ()
self.synthesize_missing_dims: tuple[str, ...] = ()

self._builder: MDIODatasetBuilder | None = None
self._dim_sizes: tuple[int, ...] = ()
Expand Down
102 changes: 102 additions & 0 deletions src/mdio/builder/templates/seismic_3d_crg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""Seismic3DCrgReceiverGathersTemplate MDIO v1 dataset template.

OBN Continuous Receiver Gathers (CRG), clock-time product. Each ocean-bottom node
records continuously; the delivery bundles many receivers, and every receiver's
long recording is segmented into fixed-length (e.g. 30 s) traces.
"""

from typing import Any

from mdio.builder.schemas.dtype import ScalarType
from mdio.builder.schemas.v1.variable import CoordinateMetadata
from mdio.builder.templates.base import AbstractDatasetTemplate
from mdio.builder.templates.types import CoordinateSpec
from mdio.builder.templates.types import DimCoordinateTypes
from mdio.builder.templates.types import SeismicDataDomain


class Seismic3DCrgReceiverGathersTemplate(AbstractDatasetTemplate):
"""Seismic 3D OBN Continuous Receiver Gathers (clock time) template.

The template declares the spatial receiver geometry ``(component, receiver_line,
receiver)`` plus the vertical axis. The per-receiver segment axis is **not** declared
here: it is inserted at ingest as a ``trace`` dimension by the ``HasDuplicates`` grid
override, which appends a per-``(component, receiver_line, receiver)`` counter in trace
order. This reuses the shipping duplicate-handling machinery instead of adding a new
calculated dimension, so it stays backwards compatible with the existing ``trace``
handling downstream. Size the inserted ``trace`` chunk (and, for very long campaigns,
its dtype) through ``GridOverrides(has_duplicates=True, chunksize=..., trace_dtype=...)``.

Real recording time is preserved per trace in ``headers`` (an ``epoch`` field, when the
SEG-Y spec includes it), not baked into the segment index; the segment axis is a dense
positional index in acquisition order.

Special handling for the component dimension:
If the SEG-Y spec does not contain a ``component`` field, the ingestion process
synthesizes a ``component`` dimension with constant value 1 for all traces (a
warning is logged). This is driven by ``synthesize_missing_dims`` and handled by
``ComponentSynthesisStrategy``, so one template ingests both single- and
multi-component CRG data.
"""

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

# The segment axis (`trace`) is inserted at ingest by HasDuplicates, so it is
# deliberately absent from the declared dimensions here.
self._spatial_dim_names = ("component", "receiver_line", "receiver")
self.synthesize_missing_dims = ("component",)
self._dim_names = (*self._spatial_dim_names, self._data_domain)
self._physical_coord_names = ("group_coord_x", "group_coord_y")
self._logical_coord_names = ()
# 3 spatial + vertical. `time` holds the whole trace in one chunk (>= 15001
# samples); the HasDuplicates effect inserts the segment chunk between them.
self._var_chunk_shape = (1, 1, 1, 16384)

@property
def _name(self) -> str:
return "ObnContinuousReceiverGathers3D"

def _load_dataset_attributes(self) -> dict[str, Any]:
return {"surveyType": "3D", "gatherType": "common_receiver"}

def declare_dim_coordinate_types(self) -> DimCoordinateTypes:
"""Declare the data types for each dimension coordinate in this template."""
return {
"component": ScalarType.UINT8,
"receiver_line": ScalarType.UINT32,
"receiver": ScalarType.UINT32,
self._data_domain: ScalarType.INT32,
}

def declare_coordinate_specs(self) -> tuple[CoordinateSpec, ...]:
"""Declare the receiver-indexed physical coordinates for this template."""
receiver_dims = ("receiver_line", "receiver")
return (
CoordinateSpec(name="group_coord_x", dimensions=receiver_dims, dtype=ScalarType.FLOAT64),
CoordinateSpec(name="group_coord_y", dimensions=receiver_dims, dtype=ScalarType.FLOAT64),
)

def _add_coordinates(self) -> None:
# Add dimension coordinates.
for name in ("component", "receiver_line", "receiver"):
self._builder.add_coordinate(
name,
dimensions=(name,),
data_type=self._dim_dtype(name),
)
self._builder.add_coordinate(
self._data_domain,
dimensions=(self._data_domain,),
data_type=self._dim_dtype(self._data_domain),
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key(self._data_domain)),
)

# Add non-dimension coordinates: receiver X/Y indexed by (receiver_line, receiver).
for name in ("group_coord_x", "group_coord_y"):
self._builder.add_coordinate(
name,
dimensions=("receiver_line", "receiver"),
data_type=ScalarType.FLOAT64,
metadata=CoordinateMetadata(units_v1=self.get_unit_by_key(name)),
)
7 changes: 4 additions & 3 deletions src/mdio/commands/segy.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,14 +296,15 @@ def segy_import( # noqa: PLR0913

\b
For dataset with expected duplicate traces we have the following parameterization. This
will use the same logic as NonBinned with a fixed chunksize of 1. The other keys are still
important. The below example allows multiple traces per receiver (i.e. reshoot).
uses the same duplicate-counter logic as NonBinned. The inserted trace dimension defaults
to a chunksize of 1, which can be configured as shown below. The other keys are still
important. This example allows multiple traces per receiver (i.e. reshoot).
\b
--header-locations 9,213,13
--header-names shot,cable,chan
--header-types int32,int16,int32
--chunk-size 8,2,256,512
--grid-overrides '{"HasDuplicates": True}'
--grid-overrides '{"HasDuplicates": True, "chunksize": 16}'
"""
# Lazy import to reduce CLI startup time
from mdio import segy_to_mdio # noqa: PLC0415
Expand Down
Loading
Loading