Skip to content

Commit 8f11edf

Browse files
committed
Fix geometry detection in analyze_lines_for_guns to handle Type A cases correctly. Ensure all shot lines are processed and unique_guns_per_line is populated to prevent KeyError. Add tests for multiline Type A geometry scenarios.
1 parent 4dfe09d commit 8f11edf

3 files changed

Lines changed: 127 additions & 7 deletions

File tree

src/mdio/segy/geometry.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ def analyze_lines_for_guns(
185185
num_guns = unique_guns_in_line.shape[0]
186186
unique_guns_per_line[str(line_val)] = list(unique_guns_in_line)
187187

188+
# Skip geometry detection if we already know it's Type A
189+
if geom_type == ShotGunGeometryType.A:
190+
continue
191+
188192
for gun in unique_guns_in_line:
189193
gun_mask = gun_current == gun
190194
shots_for_gun = shot_current[gun_mask]
@@ -194,7 +198,7 @@ def analyze_lines_for_guns(
194198
msg = "%s %s has %s shots; div by %s guns gives %s unique mod shots."
195199
logger.info(msg, line_field, line_val, num_shots, num_guns, len(np.unique(mod_shots)))
196200
geom_type = ShotGunGeometryType.A
197-
return unique_lines, unique_guns_per_line, geom_type
201+
break # No need to check more guns for this line
198202

199203
return unique_lines, unique_guns_per_line, geom_type
200204

@@ -611,18 +615,29 @@ def transform(
611615
logger.info("%s: %s has guns: %s", line_field, line_val, guns)
612616
max_num_guns = max(len(guns), max_num_guns)
613617

614-
# Only calculate shot_index when shot points are interleaved across guns (Type B)
615-
if geom_type == ShotGunGeometryType.B:
616-
shot_index = np.empty(len(index_headers), dtype="uint32")
617-
# Use .base if available (view of another array), otherwise use the array directly
618-
base_array = index_headers.base if index_headers.base is not None else index_headers
619-
index_headers = rfn.append_fields(base_array, "shot_index", shot_index)
618+
# Always calculate shot_index - the OBN template requires it
619+
shot_index = np.empty(len(index_headers), dtype="uint32")
620+
# Use .base if available (view of another array), otherwise use the array directly
621+
base_array = index_headers.base if index_headers.base is not None else index_headers
622+
index_headers = rfn.append_fields(base_array, "shot_index", shot_index)
620623

624+
if geom_type == ShotGunGeometryType.B:
625+
# Type B: shot points are interleaved across guns, divide to get dense index
621626
for line_val in unique_lines:
622627
line_idxs = np.where(index_headers[line_field][:] == line_val)
623628
index_headers["shot_index"][line_idxs] = np.floor(index_headers["shot_point"][line_idxs] / max_num_guns)
624629
# Make shot index zero-based PER line
625630
index_headers["shot_index"][line_idxs] -= index_headers["shot_index"][line_idxs].min()
631+
else:
632+
# Type A: shot points are already unique per gun, create 0-based index from unique values
633+
for line_val in unique_lines:
634+
line_idxs = np.where(index_headers[line_field][:] == line_val)[0]
635+
shot_points = index_headers["shot_point"][line_idxs]
636+
unique_shots = np.sort(np.unique(shot_points))
637+
# Map each shot_point to its 0-based index
638+
shot_to_idx = {sp: i for i, sp in enumerate(unique_shots)}
639+
for i, idx in enumerate(line_idxs):
640+
index_headers["shot_index"][idx] = shot_to_idx[shot_points[i]]
626641

627642
return index_headers
628643

tests/integration/conftest.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,43 @@ def segy_mock_obn_no_component(fake_segy_tmp: Path) -> Path:
342342
components=None, # No component header
343343
filename_suffix="no_component",
344344
)
345+
346+
347+
@pytest.fixture(scope="module")
348+
def segy_mock_obn_multiline_type_a(fake_segy_tmp: Path) -> Path:
349+
"""Generate mock OBN SEG-Y file with multiple shot lines and Type A geometry.
350+
351+
This fixture tests the scenario where:
352+
- Multiple shot lines exist (3 lines)
353+
- Shot points are NOT interleaved across guns (Type A geometry)
354+
- Each gun has the same dense shot point values
355+
356+
This specifically tests the fix for the bug where an early return in
357+
analyze_lines_for_guns() would skip populating unique_guns_per_line for
358+
lines after the first Type A detection, causing KeyError when later
359+
code tried to access those lines.
360+
"""
361+
num_samples = 25
362+
receivers = [101, 102]
363+
shot_lines = [1, 2, 3] # Multiple lines to test all are processed
364+
guns = [1, 2]
365+
components = [1] # Single component for simplicity
366+
367+
# Non-interleaved (Type A): both guns have the same shot point values
368+
# This triggers Type A detection because floor(shot_point / num_guns)
369+
# produces duplicates when shot points are dense per gun
370+
shot_points_per_gun = {
371+
1: [1, 2, 3], # gun 1: dense shot points
372+
2: [1, 2, 3], # gun 2: same dense shot points (Type A)
373+
}
374+
375+
return create_segy_mock_obn(
376+
fake_segy_tmp,
377+
num_samples=num_samples,
378+
receivers=receivers,
379+
shot_lines=shot_lines,
380+
guns=guns,
381+
shot_points_per_gun=shot_points_per_gun,
382+
components=components,
383+
filename_suffix="multiline_type_a",
384+
)

tests/integration/test_import_obn_grid_overrides.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,68 @@ def test_import_obn_without_calculate_shot_index_raises(
158158
error_message = str(exc_info.value)
159159
assert "shot_index" in error_message
160160
assert "ObnReceiverGathers3D" in error_message
161+
162+
163+
class TestImportObnMultilineTypeA:
164+
"""Test OBN SEG-Y import with multiple shot lines and Type A geometry.
165+
166+
This test class verifies the fix for a bug where analyze_lines_for_guns()
167+
would return early upon detecting Type A geometry, leaving the
168+
unique_guns_per_line dictionary incomplete. This caused KeyError when
169+
CalculateShotIndex.transform() tried to access shot lines that weren't
170+
in the dictionary.
171+
172+
Regression test for: KeyError when ingesting OBN data with multiple shot
173+
lines where Type A geometry is detected on an earlier line.
174+
"""
175+
176+
def test_import_obn_multiline_type_a_all_lines_processed(
177+
self,
178+
segy_mock_obn_multiline_type_a: Path,
179+
zarr_tmp: Path,
180+
) -> None:
181+
"""Test that all shot lines are processed with Type A geometry.
182+
183+
This test verifies that:
184+
1. CalculateShotIndex works with Type A geometry (non-interleaved shots)
185+
2. All shot lines are included in the output, not just the first one
186+
3. shot_index is correctly calculated for Type A (0-based from unique values)
187+
"""
188+
segy_spec = get_segy_mock_obn_spec(include_component=True)
189+
grid_override = {"CalculateShotIndex": True}
190+
191+
segy_to_mdio(
192+
segy_spec=segy_spec,
193+
mdio_template=TemplateRegistry().get("ObnReceiverGathers3D"),
194+
input_path=segy_mock_obn_multiline_type_a,
195+
output_path=zarr_tmp,
196+
overwrite=True,
197+
grid_overrides=grid_override,
198+
)
199+
200+
ds = open_mdio(zarr_tmp)
201+
202+
# Verify ALL shot lines are present (the bug would cause lines to be missing)
203+
expected_shot_lines = [1, 2, 3]
204+
xrt.assert_duckarray_equal(ds["shot_line"], expected_shot_lines)
205+
206+
# Verify guns are present
207+
expected_guns = [1, 2]
208+
xrt.assert_duckarray_equal(ds["gun"], expected_guns)
209+
210+
# Verify shot_index is calculated correctly for Type A geometry
211+
# Type A: shot points [1, 2, 3] are already unique per gun
212+
# shot_index should be 0-based indices: [0, 1, 2]
213+
expected_shot_index = [0, 1, 2]
214+
xrt.assert_duckarray_equal(ds["shot_index"], expected_shot_index)
215+
216+
# Verify other dimensions
217+
expected_receivers = [101, 102]
218+
xrt.assert_duckarray_equal(ds["receiver"], expected_receivers)
219+
220+
expected_components = [1]
221+
xrt.assert_duckarray_equal(ds["component"], expected_components)
222+
223+
# Verify shot_point is preserved as a coordinate
224+
assert "shot_point" in ds.coords
225+
assert ds["shot_point"].dims == ("shot_line", "gun", "shot_index")

0 commit comments

Comments
 (0)