Skip to content

Commit 0ceba5a

Browse files
committed
Added support for split-beam angle computation with mixed channel beam types in complex mode
1 parent ec28691 commit 0ceba5a

2 files changed

Lines changed: 74 additions & 22 deletions

File tree

echopype/consolidate/split_beam_angle.py

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,28 @@
88
import numpy as np
99
import xarray as xr
1010

11+
from ..utils.log import _init_logger
1112
from ..calibrate.ek80_complex import compress_pulse, get_norm_fac, get_transmit_signal
1213

1314

15+
logger = _init_logger(__name__)
16+
17+
# Beam type identifiers
18+
BEAM_TYPE_SPLIT_4_SECTOR = 1 # 4-sector split-beam (common Simrad type)
19+
BEAM_TYPE_SPLIT_3_SECTOR = 17 # 3-sector
20+
BEAM_TYPE_SPLIT_3_PLUS_CENTER = 49 # 3-sector + center element
21+
BEAM_TYPE_SPLIT_VARIANT_65 = 65 # Another 3+1 variant (vendor-specific)
22+
BEAM_TYPE_SPLIT_VARIANT_81 = 81 # Another 3+1 variant (vendor-specific)
23+
24+
SUPPORTED_BEAM_TYPES = [
25+
BEAM_TYPE_SPLIT_4_SECTOR,
26+
BEAM_TYPE_SPLIT_3_SECTOR,
27+
BEAM_TYPE_SPLIT_3_PLUS_CENTER,
28+
BEAM_TYPE_SPLIT_VARIANT_65,
29+
BEAM_TYPE_SPLIT_VARIANT_81,
30+
]
31+
32+
1433
def _compute_angle_from_complex(
1534
bs: xr.DataArray, beam_type: int, sens: List[xr.DataArray], offset: List[xr.DataArray]
1635
) -> Tuple[xr.DataArray, xr.DataArray]:
@@ -48,7 +67,7 @@ def _compute_angle_from_complex(
4867
"""
4968

5069
# 4-sector transducer
51-
if beam_type == 1:
70+
if beam_type == BEAM_TYPE_SPLIT_4_SECTOR:
5271
bs_fore = (bs.isel(beam=2) + bs.isel(beam=3)) / 2 # forward
5372
bs_aft = (bs.isel(beam=0) + bs.isel(beam=1)) / 2 # aft
5473
bs_star = (bs.isel(beam=0) + bs.isel(beam=3)) / 2 # starboard
@@ -60,9 +79,14 @@ def _compute_angle_from_complex(
6079
phi = np.arctan2(np.imag(bs_phi), np.real(bs_phi)) / np.pi * 180
6180

6281
# 3-sector transducer with or without center element
63-
elif beam_type in [17, 49, 65, 81]:
82+
elif beam_type in [
83+
BEAM_TYPE_SPLIT_3_SECTOR,
84+
BEAM_TYPE_SPLIT_3_PLUS_CENTER,
85+
BEAM_TYPE_SPLIT_VARIANT_65,
86+
BEAM_TYPE_SPLIT_VARIANT_81,
87+
]:
6488
# 3-sector
65-
if beam_type == 17:
89+
if beam_type == BEAM_TYPE_SPLIT_3_SECTOR:
6690
bs_star = bs.isel(beam=0)
6791
bs_port = bs.isel(beam=1)
6892
bs_fore = bs.isel(beam=2)
@@ -209,8 +233,14 @@ def get_angle_complex_samples(
209233
)
210234
else:
211235
# beam_type different for some channels, process each channel separately
212-
theta, phi = [], []
236+
theta_list, phi_list, valid_channels = [], [], []
213237
for ch_id in bs["channel"].data:
238+
beam_type_ch = ds_beam["beam_type"].sel(channel=ch_id).item()
239+
240+
if beam_type_ch not in SUPPORTED_BEAM_TYPES:
241+
logger.warning(f"Skipping channel {ch_id}: unsupported beam_type {beam_type_ch}")
242+
continue
243+
214244
theta_ch, phi_ch = _compute_angle_from_complex(
215245
bs=bs.sel(channel=ch_id),
216246
# beam_type is not time-varying
@@ -224,25 +254,18 @@ def get_angle_complex_samples(
224254
angle_params["angle_offset_athwartship"].sel(channel=ch_id),
225255
],
226256
)
227-
theta.append(theta_ch)
228-
phi.append(phi_ch)
257+
theta_list.append(theta_ch)
258+
phi_list.append(phi_ch)
259+
valid_channels.append(ch_id)
260+
261+
if not theta_list:
262+
raise ValueError("No valid channels found for angle computation.")
229263

230264
# Combine angles from all channels
231-
theta = xr.DataArray(
232-
data=theta,
233-
coords={
234-
"channel": bs["channel"],
235-
"ping_time": bs["ping_time"],
236-
"range_sample": bs["range_sample"],
237-
},
238-
)
239-
phi = xr.DataArray(
240-
data=phi,
241-
coords={
242-
"channel": bs["channel"],
243-
"ping_time": bs["ping_time"],
244-
"range_sample": bs["range_sample"],
245-
},
246-
)
265+
theta = xr.concat(theta_list, dim="channel")
266+
theta = theta.assign_coords(channel=("channel", valid_channels))
267+
268+
phi = xr.concat(phi_list, dim="channel")
269+
phi = phi.assign_coords(channel=("channel", valid_channels))
247270

248271
return theta, phi

echopype/tests/consolidate/test_consolidate_integration.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,5 +335,34 @@ def test_add_splitbeam_angle_BB_pc(test_path):
335335
assert np.allclose(pyel_vals, ep_vals, atol=1e-6)
336336

337337

338+
def test_add_splitbeam_angle_partial_valid_channels(test_path):
339+
raw_file = test_path["EK80_CAL"] / "2018115-D20181213-T094600.raw"
340+
ed = ep.open_raw(raw_file, sonar_model="EK80")
341+
342+
# Manually override beam_type for one channel to simulate single-beam
343+
beam_types = ed["Sonar/Beam_group1"]["beam_type"].values
344+
total_channels = beam_types.shape[0]
345+
346+
# Force the first channel to an unsupported beam_type
347+
beam_types[1] = 0
348+
ed["Sonar/Beam_group1"]["beam_type"].data[:] = beam_types
349+
350+
# Compute Sv
351+
ds_Sv = ep.calibrate.compute_Sv(ed, waveform_mode="CW", encode_mode="complex")
352+
353+
# Add split-beam angles
354+
ds_Sv = ep.consolidate.add_splitbeam_angle(source_Sv=ds_Sv, echodata=ed, waveform_mode="CW",
355+
encode_mode="complex", pulse_compression=False, to_disk=False)
356+
357+
valid_angle_channels = [
358+
ch for ch in ds_Sv.channel.values
359+
if not np.all(np.isnan(ds_Sv["angle_alongship"].sel(channel=ch)))
360+
]
361+
362+
# Verify angles exist for valid channel only
363+
assert "angle_alongship" in ds_Sv
364+
assert "angle_athwartship" in ds_Sv
365+
assert len(valid_angle_channels) == total_channels - 1
366+
338367
# TODO: need a test for power/angle data, with mock EchoData object
339368
# containing some channels with single-beam data and some channels with split-beam data

0 commit comments

Comments
 (0)