Skip to content

Commit dc8f98d

Browse files
committed
Implement unit tests for refactored helper functions
1 parent b19188c commit dc8f98d

9 files changed

Lines changed: 1186 additions & 0 deletions

tests/unit/ingestion/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Unit tests for ingestion helper modules."""
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
"""Tests for generic coordinate population helpers in ingestion."""
2+
3+
from __future__ import annotations
4+
5+
import numpy as np
6+
import pytest
7+
from xarray import DataArray as xr_DataArray
8+
from xarray import Dataset as xr_Dataset
9+
10+
from mdio.ingestion.coordinates import populate_dim_coordinates
11+
from mdio.ingestion.coordinates import populate_non_dim_coordinates
12+
from tests.unit.ingestion.testing_helpers import make_grid
13+
from tests.unit.ingestion.testing_helpers import make_grid_with_map
14+
15+
16+
class TestPopulateDimCoordinates:
17+
"""Tests for ``populate_dim_coordinates``."""
18+
19+
def test_assigns_coords_for_each_dim(self) -> None:
20+
"""Dim coords should be copied from Grid dims onto the dataset arrays."""
21+
inline_coords = np.array([10, 20, 30], dtype=np.int32)
22+
crossline_coords = np.array([100, 200], dtype=np.int32)
23+
depth_coords = np.array([0, 4, 8, 12], dtype=np.int32)
24+
grid = make_grid(
25+
[
26+
("inline", inline_coords),
27+
("crossline", crossline_coords),
28+
("depth", depth_coords),
29+
]
30+
)
31+
32+
dataset = xr_Dataset(
33+
{
34+
"inline": xr_DataArray(np.zeros(3, dtype=np.int32), dims=["inline"]),
35+
"crossline": xr_DataArray(np.zeros(2, dtype=np.int32), dims=["crossline"]),
36+
"depth": xr_DataArray(np.zeros(4, dtype=np.int32), dims=["depth"]),
37+
}
38+
)
39+
40+
dataset, drop_vars = populate_dim_coordinates(dataset, grid, drop_vars_delayed=[])
41+
42+
np.testing.assert_array_equal(dataset["inline"].values, inline_coords)
43+
np.testing.assert_array_equal(dataset["crossline"].values, crossline_coords)
44+
np.testing.assert_array_equal(dataset["depth"].values, depth_coords)
45+
assert drop_vars == ["inline", "crossline", "depth"]
46+
47+
def test_extends_existing_drop_vars(self) -> None:
48+
"""The drop list should be extended, not replaced."""
49+
grid = make_grid([("x", np.array([1, 2], dtype=np.int32))])
50+
dataset = xr_Dataset({"x": xr_DataArray(np.zeros(2, dtype=np.int32), dims=["x"])})
51+
52+
_, drop_vars = populate_dim_coordinates(dataset, grid, drop_vars_delayed=["already_there"])
53+
54+
assert drop_vars == ["already_there", "x"]
55+
56+
57+
class TestPopulateNonDimCoordinates:
58+
"""Tests for ``populate_non_dim_coordinates``."""
59+
60+
def _make_dataset_with_coord(
61+
self,
62+
coord_name: str,
63+
shape: tuple[int, ...],
64+
dims: tuple[str, ...],
65+
encoding: dict | None,
66+
dtype: np.dtype,
67+
) -> xr_Dataset:
68+
data = xr_DataArray(np.zeros(shape, dtype=dtype), dims=list(dims))
69+
if encoding is not None:
70+
data.encoding.update(encoding)
71+
return xr_Dataset({coord_name: data})
72+
73+
def test_populates_2d_coordinate_with_scaling(self) -> None:
74+
"""Spatial coord ``cdp_x`` should be filled and scaled."""
75+
inline = np.array([1, 2], dtype=np.int32)
76+
crossline = np.array([10, 20, 30], dtype=np.int32)
77+
sample = np.array([0, 4], dtype=np.int32)
78+
# Inline-major live records → trace indices 0..5 populate the full (2, 3) grid.
79+
live = [(1, 10), (1, 20), (1, 30), (2, 10), (2, 20), (2, 30)]
80+
grid = make_grid_with_map(
81+
[("inline", inline), ("crossline", crossline), ("sample", sample)],
82+
live_records=live,
83+
)
84+
85+
coord_values = np.array([100, 200, 300, 400, 500, 600], dtype=np.float64)
86+
coordinates = {"cdp_x": coord_values}
87+
88+
dataset = self._make_dataset_with_coord(
89+
coord_name="cdp_x",
90+
shape=(2, 3),
91+
dims=("inline", "crossline"),
92+
encoding={"_FillValue": np.float64(-1.0)},
93+
dtype=np.float64,
94+
)
95+
96+
dataset, drop_vars = populate_non_dim_coordinates(
97+
dataset,
98+
grid,
99+
coordinates=coordinates,
100+
drop_vars_delayed=[],
101+
spatial_coordinate_scalar=10,
102+
)
103+
104+
expected = (coord_values.reshape((2, 3)) * 10).astype(np.float64)
105+
np.testing.assert_array_equal(dataset["cdp_x"].values, expected)
106+
assert drop_vars == ["cdp_x"]
107+
assert coordinates == {}
108+
109+
def test_uses_fill_value_for_dead_traces(self) -> None:
110+
"""Cells without a live trace should keep the configured fill value."""
111+
inline = np.array([1, 2], dtype=np.int32)
112+
crossline = np.array([10, 20], dtype=np.int32)
113+
sample = np.array([0, 4], dtype=np.int32)
114+
# Only 3 of 4 cells are live; (inline=1, crossline=20) is dead.
115+
live = [(1, 10), (2, 10), (2, 20)]
116+
grid = make_grid_with_map(
117+
[("inline", inline), ("crossline", crossline), ("sample", sample)],
118+
live_records=live,
119+
)
120+
121+
coord_values = np.array([100.0, 200.0, 300.0], dtype=np.float64)
122+
dataset = self._make_dataset_with_coord(
123+
coord_name="cdp_x",
124+
shape=(2, 2),
125+
dims=("inline", "crossline"),
126+
encoding={"_FillValue": np.float64(-9999.0)},
127+
dtype=np.float64,
128+
)
129+
130+
dataset, _ = populate_non_dim_coordinates(
131+
dataset,
132+
grid,
133+
coordinates={"cdp_x": coord_values},
134+
drop_vars_delayed=[],
135+
spatial_coordinate_scalar=1,
136+
)
137+
138+
expected = np.array([[100.0, -9999.0], [200.0, 300.0]], dtype=np.float64)
139+
np.testing.assert_array_equal(dataset["cdp_x"].values, expected)
140+
141+
def test_non_spatial_coordinate_not_scaled(self) -> None:
142+
"""Non-spatial coords (e.g. offset) must not be touched by coord scalar."""
143+
inline = np.array([1, 2], dtype=np.int32)
144+
crossline = np.array([10, 20], dtype=np.int32)
145+
sample = np.array([0, 4], dtype=np.int32)
146+
live = [(1, 10), (1, 20), (2, 10), (2, 20)]
147+
grid = make_grid_with_map(
148+
[("inline", inline), ("crossline", crossline), ("sample", sample)],
149+
live_records=live,
150+
)
151+
152+
coord_values = np.array([5, 6, 7, 8], dtype=np.float64)
153+
dataset = self._make_dataset_with_coord(
154+
coord_name="not_spatial",
155+
shape=(2, 2),
156+
dims=("inline", "crossline"),
157+
encoding={"_FillValue": np.float64(0.0)},
158+
dtype=np.float64,
159+
)
160+
161+
dataset, _ = populate_non_dim_coordinates(
162+
dataset,
163+
grid,
164+
coordinates={"not_spatial": coord_values},
165+
drop_vars_delayed=[],
166+
spatial_coordinate_scalar=100, # would change values if applied
167+
)
168+
169+
np.testing.assert_array_equal(dataset["not_spatial"].values, coord_values.reshape((2, 2)))
170+
171+
def test_reduced_coordinate_uses_slice(self) -> None:
172+
"""A coord declared on a subset of dims should be filled via a sliced map."""
173+
inline = np.array([1, 2], dtype=np.int32)
174+
crossline = np.array([10, 20, 30], dtype=np.int32)
175+
sample = np.array([0, 4], dtype=np.int32)
176+
live = [(1, 10), (1, 20), (1, 30), (2, 10), (2, 20), (2, 30)]
177+
grid = make_grid_with_map(
178+
[("inline", inline), ("crossline", crossline), ("sample", sample)],
179+
live_records=live,
180+
)
181+
182+
# Trace indices along the inline=0 row are 0, 1, 2 so the coord values
183+
# at those positions are taken from coord_values[0:3].
184+
coord_values = np.array([10.0, 20.0, 30.0, 40.0, 50.0, 60.0], dtype=np.float64)
185+
dataset = self._make_dataset_with_coord(
186+
coord_name="offset",
187+
shape=(3,),
188+
dims=("crossline",),
189+
encoding={"_FillValue": np.float64(-1.0)},
190+
dtype=np.float64,
191+
)
192+
193+
dataset, _ = populate_non_dim_coordinates(
194+
dataset,
195+
grid,
196+
coordinates={"offset": coord_values},
197+
drop_vars_delayed=[],
198+
spatial_coordinate_scalar=1,
199+
)
200+
201+
np.testing.assert_array_equal(dataset["offset"].values, coord_values[:3])
202+
203+
def test_default_fill_value_is_nan_when_encoding_missing(self) -> None:
204+
"""When no ``_FillValue`` / ``fill_value`` is set, dead traces become NaN."""
205+
inline = np.array([1, 2], dtype=np.int32)
206+
crossline = np.array([10, 20], dtype=np.int32)
207+
sample = np.array([0, 4], dtype=np.int32)
208+
live = [(1, 10), (2, 10), (2, 20)]
209+
grid = make_grid_with_map(
210+
[("inline", inline), ("crossline", crossline), ("sample", sample)],
211+
live_records=live,
212+
)
213+
214+
coord_values = np.array([1.5, 2.5, 3.5], dtype=np.float64)
215+
dataset = self._make_dataset_with_coord(
216+
coord_name="cdp_x",
217+
shape=(2, 2),
218+
dims=("inline", "crossline"),
219+
encoding=None,
220+
dtype=np.float64,
221+
)
222+
223+
dataset, _ = populate_non_dim_coordinates(
224+
dataset,
225+
grid,
226+
coordinates={"cdp_x": coord_values},
227+
drop_vars_delayed=[],
228+
spatial_coordinate_scalar=1,
229+
)
230+
231+
actual = dataset["cdp_x"].values
232+
assert np.isnan(actual[0, 1])
233+
assert actual[0, 0] == pytest.approx(1.5)
234+
assert actual[1, 0] == pytest.approx(2.5)
235+
assert actual[1, 1] == pytest.approx(3.5)
236+
237+
def test_fill_value_key_in_encoding_is_honored(self) -> None:
238+
"""The lowercase ``fill_value`` encoding key must be honored when ``_FillValue`` is absent."""
239+
inline = np.array([1, 2], dtype=np.int32)
240+
crossline = np.array([10, 20], dtype=np.int32)
241+
sample = np.array([0, 4], dtype=np.int32)
242+
# Dead cell at (inline=1, crossline=20).
243+
live = [(1, 10), (2, 10), (2, 20)]
244+
grid = make_grid_with_map(
245+
[("inline", inline), ("crossline", crossline), ("sample", sample)],
246+
live_records=live,
247+
)
248+
249+
coord_values = np.array([1.0, 2.0, 3.0], dtype=np.float64)
250+
dataset = self._make_dataset_with_coord(
251+
coord_name="cdp_x",
252+
shape=(2, 2),
253+
dims=("inline", "crossline"),
254+
encoding={"fill_value": np.float64(-42.0)},
255+
dtype=np.float64,
256+
)
257+
258+
dataset, _ = populate_non_dim_coordinates(
259+
dataset,
260+
grid,
261+
coordinates={"cdp_x": coord_values},
262+
drop_vars_delayed=[],
263+
spatial_coordinate_scalar=1,
264+
)
265+
266+
expected = np.array([[1.0, -42.0], [2.0, 3.0]], dtype=np.float64)
267+
np.testing.assert_array_equal(dataset["cdp_x"].values, expected)
268+
269+
def test_empty_coordinates_is_noop(self) -> None:
270+
"""An empty coordinates dict should leave the dataset and drop list untouched."""
271+
inline = np.array([1, 2], dtype=np.int32)
272+
crossline = np.array([10, 20], dtype=np.int32)
273+
sample = np.array([0, 4], dtype=np.int32)
274+
live = [(1, 10), (1, 20), (2, 10), (2, 20)]
275+
grid = make_grid_with_map(
276+
[("inline", inline), ("crossline", crossline), ("sample", sample)],
277+
live_records=live,
278+
)
279+
280+
dataset = xr_Dataset()
281+
282+
dataset, drop_vars = populate_non_dim_coordinates(
283+
dataset,
284+
grid,
285+
coordinates={},
286+
drop_vars_delayed=["pre_existing"],
287+
spatial_coordinate_scalar=1,
288+
)
289+
290+
assert drop_vars == ["pre_existing"]
291+
assert len(dataset.data_vars) == 0

0 commit comments

Comments
 (0)