Skip to content

Commit b895d09

Browse files
authored
Merge pull request #1250 from OSOceanAcoustics/dev
Release/v0.8.3
2 parents 06ce50a + 79bb587 commit b895d09

File tree

15 files changed

+241
-160
lines changed

15 files changed

+241
-160
lines changed

docs/source/whats-new.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,26 @@ What's new
44
See [GitHub releases page](https://github.com/OSOceanAcoustics/echopype/releases) for the complete history.
55

66

7+
# v0.8.3 (2024 December 24)
8+
9+
## Overview
10+
11+
This release includes a bug fix for changes from the previous release and a few functionality enhancements.
12+
13+
## Enhancements
14+
- Add parser support for EK80 MRU1 datagram (#1242)
15+
- Add support for `consolidate` subpackage functions to accept both in-memory or stored datasets (#1216)
16+
- Add test for ES60 spare field decoding issue (#1233)
17+
- Add test for EK80 missing `receiver_sampling_freq` error (#1234)
18+
19+
## Bug fixes
20+
- Fixed reshape bug in `pad_shorter_ping` that was remnant from `use_swap` full refactoring (#1234)
21+
22+
23+
24+
25+
26+
727
# v0.8.2 (2023 November 20)
828

929
## Overview

echopype/consolidate/api.py

Lines changed: 63 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datetime
22
import pathlib
3+
from pathlib import Path
34
from typing import Optional, Union
45

56
import numpy as np
@@ -8,14 +9,14 @@
89
from ..calibrate.ek80_complex import get_filter_coeff
910
from ..echodata import EchoData
1011
from ..echodata.simrad import retrieve_correct_beam_group
11-
from ..utils.io import validate_source_ds_da
12+
from ..utils.io import get_file_format, open_source
1213
from ..utils.prov import add_processing_level
13-
from .split_beam_angle import add_angle_to_ds, get_angle_complex_samples, get_angle_power_samples
14+
from .split_beam_angle import get_angle_complex_samples, get_angle_power_samples
1415

1516
POSITION_VARIABLES = ["latitude", "longitude"]
1617

1718

18-
def swap_dims_channel_frequency(ds: xr.Dataset) -> xr.Dataset:
19+
def swap_dims_channel_frequency(ds: Union[xr.Dataset, str, pathlib.Path]) -> xr.Dataset:
1920
"""
2021
Use frequency_nominal in place of channel to be dataset dimension and coorindate.
2122
@@ -24,8 +25,9 @@ def swap_dims_channel_frequency(ds: xr.Dataset) -> xr.Dataset:
2425
2526
Parameters
2627
----------
27-
ds : xr.Dataset
28-
Dataset for which the dimension will be swapped
28+
ds : xr.Dataset or str or pathlib.Path
29+
Dataset or path to a file containing the Dataset
30+
for which the dimension will be swapped
2931
3032
Returns
3133
-------
@@ -35,6 +37,7 @@ def swap_dims_channel_frequency(ds: xr.Dataset) -> xr.Dataset:
3537
-----
3638
This operation is only possible when there are no duplicated frequencies present in the file.
3739
"""
40+
ds = open_source(ds, "dataset", {})
3841
# Only possible if no duplicated frequencies
3942
if np.unique(ds["frequency_nominal"]).size == ds["frequency_nominal"].size:
4043
return (
@@ -50,7 +53,7 @@ def swap_dims_channel_frequency(ds: xr.Dataset) -> xr.Dataset:
5053

5154

5255
def add_depth(
53-
ds: xr.Dataset,
56+
ds: Union[xr.Dataset, str, pathlib.Path],
5457
depth_offset: float = 0,
5558
tilt: float = 0,
5659
downward: bool = True,
@@ -64,8 +67,9 @@ def add_depth(
6467
6568
Parameters
6669
----------
67-
ds : xr.Dataset
68-
Source Sv dataset to which a depth variable will be added.
70+
ds : xr.Dataset or str or pathlib.Path
71+
Source Sv dataset or path to a file containing the Source Sv dataset
72+
to which a depth variable will be added.
6973
Must contain `echo_range`.
7074
depth_offset : float
7175
Offset along the vertical (depth) dimension to account for actual transducer
@@ -114,6 +118,7 @@ def add_depth(
114118
# else:
115119
# tilt = 0
116120

121+
ds = open_source(ds, "dataset", {})
117122
# Multiplication factor depending on if transducers are pointing downward
118123
mult = 1 if downward else -1
119124

@@ -132,7 +137,11 @@ def add_depth(
132137

133138

134139
@add_processing_level("L2A")
135-
def add_location(ds: xr.Dataset, echodata: EchoData = None, nmea_sentence: Optional[str] = None):
140+
def add_location(
141+
ds: Union[xr.Dataset, str, pathlib.Path],
142+
echodata: Optional[Union[EchoData, str, pathlib.Path]],
143+
nmea_sentence: Optional[str] = None,
144+
):
136145
"""
137146
Add geographical location (latitude/longitude) to the Sv dataset.
138147
@@ -142,10 +151,12 @@ def add_location(ds: xr.Dataset, echodata: EchoData = None, nmea_sentence: Optio
142151
143152
Parameters
144153
----------
145-
ds : xr.Dataset
146-
An Sv or MVBS dataset for which the geographical locations will be added to
147-
echodata
148-
An `EchoData` object holding the raw data
154+
ds : xr.Dataset or str or pathlib.Path
155+
An Sv or MVBS dataset or path to a file containing the Sv or MVBS
156+
dataset for which the geographical locations will be added to
157+
echodata : EchoData or str or pathlib.Path
158+
An ``EchoData`` object or path to a file containing the ``EchoData``
159+
object holding the raw data
149160
nmea_sentence
150161
NMEA sentence to select a subset of location data (optional)
151162
@@ -174,6 +185,9 @@ def sel_interp(var, time_dim_name):
174185
# Values may be nan if there are ping_time values outside the time_dim_name range
175186
return position_var.interp(**{time_dim_name: ds["ping_time"]})
176187

188+
ds = open_source(ds, "dataset", {})
189+
echodata = open_source(echodata, "echodata", {})
190+
177191
if "longitude" not in echodata["Platform"] or echodata["Platform"]["longitude"].isnull().all():
178192
raise ValueError("Coordinate variables not present or all nan")
179193

@@ -198,12 +212,12 @@ def sel_interp(var, time_dim_name):
198212

199213
def add_splitbeam_angle(
200214
source_Sv: Union[xr.Dataset, str, pathlib.Path],
201-
echodata: EchoData,
215+
echodata: Union[EchoData, str, pathlib.Path],
202216
waveform_mode: str,
203217
encode_mode: str,
204218
pulse_compression: bool = False,
205219
storage_options: dict = {},
206-
return_dataset: bool = True,
220+
to_disk: bool = True,
207221
) -> xr.Dataset:
208222
"""
209223
Add split-beam (alongship/athwartship) angles into the Sv dataset.
@@ -218,8 +232,9 @@ def add_splitbeam_angle(
218232
source_Sv: xr.Dataset or str or pathlib.Path
219233
The Sv Dataset or path to a file containing the Sv Dataset,
220234
to which the split-beam angles will be added
221-
echodata: EchoData
222-
An ``EchoData`` object holding the raw data
235+
echodata: EchoData or str or pathlib.Path
236+
An ``EchoData`` object or path to a file containing the ``EchoData``
237+
object holding the raw data
223238
waveform_mode : {"CW", "BB"}
224239
Type of transmit waveform
225240
@@ -240,19 +255,20 @@ def add_splitbeam_angle(
240255
storage_options: dict, default={}
241256
Any additional parameters for the storage backend, corresponding to the
242257
path provided for ``source_Sv``
243-
return_dataset: bool, default=True
244-
If ``True``, ``source_Sv`` with split-beam angles added will be returned.
245-
``return_dataset=False`` is useful when ``source_Sv`` is a path and
258+
to_disk: bool, default=True
259+
If ``False``, ``to_disk`` with split-beam angles added will be returned.
260+
``to_disk=True`` is useful when ``source_Sv`` is a path and
246261
users only want to write the split-beam angle data to this path.
247262
248263
Returns
249264
-------
250265
xr.Dataset or None
251-
If ``return_dataset=False``, nothing will be returned.
252-
If ``return_dataset=True``, either the input dataset ``source_Sv``
266+
If ``to_disk=False``, nothing will be returned.
267+
If ``to_disk=True``, either the input dataset ``source_Sv``
253268
or a lazy-loaded Dataset (from the path ``source_Sv``)
254269
with split-beam angles added will be returned.
255270
271+
256272
Raises
257273
------
258274
ValueError
@@ -279,6 +295,19 @@ def add_splitbeam_angle(
279295
`echodata`` will be identical. If this is not the case, only angle data corresponding
280296
to channels existing in ``source_Sv`` will be added.
281297
"""
298+
# ensure that when source_Sv is a Dataset then to_disk should be False
299+
if not isinstance(source_Sv, (str, Path)) and to_disk:
300+
raise ValueError(
301+
"The input source_Sv must be a path when to_disk=True, "
302+
"so that the split-beam angles can be written to disk!"
303+
)
304+
305+
# obtain the file format of source_Sv if it is a path
306+
if isinstance(source_Sv, (str, Path)):
307+
source_Sv_type = get_file_format(source_Sv)
308+
309+
source_Sv = open_source(source_Sv, "dataset", storage_options)
310+
echodata = open_source(echodata, "echodata", storage_options)
282311

283312
# ensure that echodata was produced by EK60 or EK80-like sensors
284313
if echodata.sonar_model not in ["EK60", "ES70", "EK80", "ES80", "EA640"]:
@@ -287,22 +316,6 @@ def add_splitbeam_angle(
287316
"transducers, split-beam angles cannot be added to source_Sv!"
288317
)
289318

290-
# validate the source_Sv type or path (if it is provided)
291-
source_Sv, file_type = validate_source_ds_da(source_Sv, storage_options)
292-
293-
# initialize source_Sv_path
294-
source_Sv_path = None
295-
296-
if isinstance(source_Sv, str):
297-
# store source_Sv path so we can use it to write to later
298-
source_Sv_path = source_Sv
299-
300-
# TODO: In the future we can improve this by obtaining the variable names, channels,
301-
# and dimension lengths directly from source_Sv using zarr or netcdf4. This would
302-
# prevent the unnecessary loading in of the coordinates, which the below statement does.
303-
# open up Dataset using source_Sv path
304-
source_Sv = xr.open_dataset(source_Sv, engine=file_type, chunks={}, **storage_options)
305-
306319
# raise not implemented error if source_Sv corresponds to MVBS
307320
if source_Sv.attrs["processing_function"] == "commongrid.compute_MVBS":
308321
raise NotImplementedError("Adding split-beam data to MVBS has not been implemented!")
@@ -364,9 +377,18 @@ def add_splitbeam_angle(
364377
theta, phi = get_angle_complex_samples(ds_beam, angle_params)
365378

366379
# add theta and phi to source_Sv input
367-
source_Sv = add_angle_to_ds(
368-
theta, phi, source_Sv, return_dataset, source_Sv_path, file_type, storage_options
369-
)
380+
theta.attrs["long_name"] = "split-beam alongship angle"
381+
phi.attrs["long_name"] = "split-beam athwartship angle"
382+
383+
# add the split-beam angles to the provided Dataset
384+
source_Sv["angle_alongship"] = theta
385+
source_Sv["angle_athwartship"] = phi
386+
if to_disk:
387+
if source_Sv_type == "netcdf4":
388+
source_Sv.to_netcdf(mode="a", **storage_options)
389+
else:
390+
source_Sv.to_zarr(mode="a", **storage_options)
391+
source_Sv = open_source(source_Sv, "dataset", storage_options)
370392

371393
# Add history attribute
372394
history_attr = (

echopype/consolidate/split_beam_angle.py

Lines changed: 1 addition & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Contains functions necessary to compute the split-beam (alongship/athwartship)
33
angles and add them to a Dataset.
44
"""
5-
from typing import List, Optional, Tuple
5+
from typing import List, Tuple
66

77
import numpy as np
88
import xarray as xr
@@ -245,79 +245,3 @@ def get_angle_complex_samples(
245245
)
246246

247247
return theta, phi
248-
249-
250-
def add_angle_to_ds(
251-
theta: xr.Dataset,
252-
phi: xr.Dataset,
253-
ds: xr.Dataset,
254-
return_dataset: bool,
255-
source_ds_path: Optional[str] = None,
256-
file_type: Optional[str] = None,
257-
storage_options: dict = {},
258-
) -> Optional[xr.Dataset]:
259-
"""
260-
Adds the split-beam angle data to the provided input ``ds``.
261-
262-
Parameters
263-
----------
264-
theta: xr.Dataset
265-
The calculated split-beam alongship angle
266-
phi: xr.Dataset
267-
The calculated split-beam athwartship angle
268-
ds: xr.Dataset
269-
The Dataset that ``theta`` and ``phi`` will be added to
270-
return_dataset: bool
271-
Whether a dataset will be returned or not
272-
source_ds_path: str, optional
273-
The path to the file corresponding to ``ds``, if it exists
274-
file_type: {"netcdf4", "zarr"}, optional
275-
The file type corresponding to ``source_ds_path``
276-
storage_options: dict, default={}
277-
Any additional parameters for the storage backend, corresponding to the
278-
path ``source_ds_path``
279-
280-
Returns
281-
-------
282-
xr.Dataset or None
283-
If ``return_dataset=False``, nothing will be returned. If ``return_dataset=True``
284-
either the input dataset ``ds`` or a lazy-loaded Dataset (obtained from
285-
the path provided by ``source_ds_path``) with the split-beam angle data added
286-
will be returned.
287-
"""
288-
289-
# TODO: do we want to add anymore attributes to these variables?
290-
# add appropriate attributes to theta and phi
291-
theta.attrs["long_name"] = "split-beam alongship angle"
292-
phi.attrs["long_name"] = "split-beam athwartship angle"
293-
294-
if source_ds_path is not None:
295-
# put the variables into a Dataset, so they can be written at the same time
296-
# add ds attributes to splitb_ds since they will be overwritten by to_netcdf/zarr
297-
splitb_ds = xr.Dataset(
298-
data_vars={"angle_alongship": theta, "angle_athwartship": phi},
299-
coords=theta.coords,
300-
attrs=ds.attrs,
301-
)
302-
303-
# release any resources linked to ds (necessary for to_netcdf)
304-
ds.close()
305-
306-
# write the split-beam angle data to the provided path
307-
if file_type == "netcdf4":
308-
splitb_ds.to_netcdf(path=source_ds_path, mode="a", **storage_options)
309-
else:
310-
splitb_ds.to_zarr(store=source_ds_path, mode="a", **storage_options)
311-
312-
if return_dataset:
313-
# open up and return Dataset in source_ds_path
314-
return xr.open_dataset(source_ds_path, engine=file_type, chunks={}, **storage_options)
315-
316-
else:
317-
# add the split-beam angles to the provided Dataset
318-
ds["angle_alongship"] = theta
319-
ds["angle_athwartship"] = phi
320-
321-
if return_dataset:
322-
# return input dataset with split-beam angle data
323-
return ds

echopype/convert/parse_base.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -600,8 +600,14 @@ def pad_shorter_ping(data_list) -> np.ndarray:
600600
lens = np.array([len(item) for item in data_list])
601601
if np.unique(lens).size != 1: # if some pings have different lengths along range
602602
if data_list[0].ndim == 2:
603-
# Angle data have an extra dimension for alongship and athwartship samples
604-
mask = lens[:, None, None] > np.array([np.arange(lens.max())] * 2).T
603+
# Data may have an extra dimension:
604+
# - Angle data have an extra dimension for alongship and athwartship samples
605+
# - Complex data have an extra dimension for different transducer sectors
606+
mask = (
607+
lens[:, None, None]
608+
> np.array([np.arange(lens.max())] * data_list[0].shape[1]).T
609+
)
610+
605611
else:
606612
mask = lens[:, None] > np.arange(lens.max())
607613

echopype/convert/set_groups_ek80.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,17 @@ def set_platform(self) -> xr.Dataset:
406406
"standard_name": "sound_frequency",
407407
},
408408
),
409+
"heading": (
410+
["time2"],
411+
np.array(self.parser_obj.mru.get("heading", [np.nan])),
412+
{
413+
"long_name": "Platform heading (true)",
414+
"standard_name": "platform_orientation",
415+
"units": "degrees_north",
416+
"valid_min": 0.0,
417+
"valid_max": 360.0,
418+
},
419+
),
409420
},
410421
coords={
411422
"channel": (

0 commit comments

Comments
 (0)