wavespectra 4.6.0__tar.gz → 4.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. {wavespectra-4.6.0/wavespectra.egg-info → wavespectra-4.7.0}/PKG-INFO +1 -1
  2. {wavespectra-4.6.0 → wavespectra-4.7.0}/pyproject.toml +1 -1
  3. {wavespectra-4.6.0 → wavespectra-4.7.0}/tests/test_partition.py +16 -1
  4. wavespectra-4.7.0/tests/test_specdataset.py +192 -0
  5. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/__init__.py +3 -1
  6. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/construct/__init__.py +1 -1
  7. wavespectra-4.7.0/wavespectra/core/options.py +57 -0
  8. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/core/utils.py +30 -0
  9. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/swan.py +43 -24
  10. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/triaxys.py +4 -2
  11. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/output/octopus.py +1 -1
  12. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/partition/partition.py +122 -38
  13. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/specarray.py +38 -6
  14. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/specdataset.py +78 -3
  15. {wavespectra-4.6.0 → wavespectra-4.7.0/wavespectra.egg-info}/PKG-INFO +1 -1
  16. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra.egg-info/SOURCES.txt +2 -0
  17. {wavespectra-4.6.0 → wavespectra-4.7.0}/AUTHORS.rst +0 -0
  18. {wavespectra-4.6.0 → wavespectra-4.7.0}/LICENSE.txt +0 -0
  19. {wavespectra-4.6.0 → wavespectra-4.7.0}/README.rst +0 -0
  20. {wavespectra-4.6.0 → wavespectra-4.7.0}/setup.cfg +0 -0
  21. {wavespectra-4.6.0 → wavespectra-4.7.0}/setup.py +0 -0
  22. {wavespectra-4.6.0 → wavespectra-4.7.0}/tests/test_cli.py +0 -0
  23. {wavespectra-4.6.0 → wavespectra-4.7.0}/tests/test_combine_hp01.py +0 -0
  24. {wavespectra-4.6.0 → wavespectra-4.7.0}/tests/test_cookiecutter_template.py +0 -0
  25. {wavespectra-4.6.0 → wavespectra-4.7.0}/tests/test_reader_registry.py +0 -0
  26. {wavespectra-4.6.0 → wavespectra-4.7.0}/tests/test_specarray.py +0 -0
  27. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/cli.py +0 -0
  28. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/construct/direction.py +0 -0
  29. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/construct/frequency.py +0 -0
  30. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/core/__init__.py +0 -0
  31. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/core/attributes.py +0 -0
  32. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/core/attributes.yml +0 -0
  33. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/core/fitting.py +0 -0
  34. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/core/npstats.py +0 -0
  35. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/core/select.py +0 -0
  36. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/core/swan.py +0 -0
  37. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/core/xrstats.py +0 -0
  38. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/__init__.py +0 -0
  39. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/awac.py +0 -0
  40. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/dataset.py +0 -0
  41. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/datawell.py +0 -0
  42. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/era5.py +0 -0
  43. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/funwave.py +0 -0
  44. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/funwave_new.py +0 -0
  45. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/json.py +0 -0
  46. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/ncswan.py +0 -0
  47. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/ndbc.py +0 -0
  48. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/ndbc_ascii.py +0 -0
  49. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/netcdf.py +0 -0
  50. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/obscape.py +0 -0
  51. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/octopus.py +0 -0
  52. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/spotter.py +0 -0
  53. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/wavespectra.py +0 -0
  54. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/ww3.py +0 -0
  55. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/ww3_station.py +0 -0
  56. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/wwm.py +0 -0
  57. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/input/xwaves.py +0 -0
  58. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/output/__init__.py +0 -0
  59. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/output/funwave.py +0 -0
  60. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/output/json.py +0 -0
  61. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/output/netcdf.py +0 -0
  62. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/output/orcaflex.py +0 -0
  63. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/output/swan.py +0 -0
  64. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/output/ww3.py +0 -0
  65. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/output/ww3.yml +0 -0
  66. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/partition/__init__.py +0 -0
  67. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/partition/hanson_and_phillips_2001.py +0 -0
  68. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/partition/specpart/specpart.c +0 -0
  69. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/partition/specpart/specpart.h +0 -0
  70. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/partition/specpart/specpart_wrap.c +0 -0
  71. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/partition/tracking.py +0 -0
  72. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra/plot.py +0 -0
  73. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra.egg-info/dependency_links.txt +0 -0
  74. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra.egg-info/entry_points.txt +0 -0
  75. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra.egg-info/requires.txt +0 -0
  76. {wavespectra-4.6.0 → wavespectra-4.7.0}/wavespectra.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavespectra
3
- Version: 4.6.0
3
+ Version: 4.7.0
4
4
  Summary: Library for ocean wave spectra
5
5
  Author-email: Wavespectra Developers <r.guedes@oceanum.science>
6
6
  Maintainer-email: Rafael Guedes <r.guedes@oceanum.science>, Tom Durrant <t.durrant@oceanum.science>
@@ -173,7 +173,7 @@ extend-select = ["B015", "B018"]
173
173
  github_url = "https://github.com/wavespectra/wavespectra/"
174
174
 
175
175
  [tool.tbump.version]
176
- current = "4.6.0"
176
+ current = "4.7.0"
177
177
  regex = '''
178
178
  (?P<major>\d+)
179
179
  \.(?P<minor>\d+)
@@ -2,6 +2,7 @@ import pytest
2
2
  from pathlib import Path
3
3
  import numpy as np
4
4
 
5
+ import wavespectra
5
6
  from wavespectra import read_ww3
6
7
  from wavespectra.partition.partition import (
7
8
  Partition,
@@ -18,6 +19,13 @@ from wavespectra.partition.tracking import track_partitions
18
19
  HERE = Path(__file__).parent
19
20
 
20
21
 
22
+ @pytest.fixture(autouse=True)
23
+ def dataset_transforms():
24
+ """Run under the future dataset transforms behaviour in this module."""
25
+ with wavespectra.set_options(dataset_transforms=True):
26
+ yield
27
+
28
+
21
29
  class BasePTM:
22
30
  def setup_class(self):
23
31
  self.dset = read_ww3(HERE / "sample_files/ww3file.nc")
@@ -437,11 +445,18 @@ class TestPartitionAndTrack(BasePTM):
437
445
  self._track("hp01", wspd=self.dset.wspd, wdir=self.dset.wdir, dpt=self.dset.dpt)
438
446
 
439
447
  def test_track_hp01_no_wind(self):
448
+ # Wind variables are resolved from the dataset so partitioning from
449
+ # the DataArray is required to exercise the no-wind path
450
+ pt = Partition(self.dset.efth)
440
451
  with pytest.warns(UserWarning):
441
- dspart = self.pt.track(method="hp01", swells=2)
452
+ dspart = pt.track(method="hp01", swells=2)
442
453
  dspart = dspart.load()
443
454
  assert (dspart.ntracks.values > 0).all()
444
455
 
456
+ def test_track_hp01_wind_from_dataset(self):
457
+ dspart = self.pt.track(method="hp01", swells=2).load()
458
+ assert (dspart.ntracks.values > 0).all()
459
+
445
460
  def test_track_invalid_method(self):
446
461
  with pytest.raises(ValueError):
447
462
  self.pt.track(method="ptm4")
@@ -0,0 +1,192 @@
1
+ """Testing the dataset-aware behaviour of the SpecDataset accessor."""
2
+
3
+ import os
4
+
5
+ import numpy as np
6
+ import pytest
7
+ import xarray as xr
8
+
9
+ from wavespectra import read_ww3, set_options, get_options
10
+ from wavespectra.partition.partition import Partition
11
+
12
+
13
+ FILES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "sample_files")
14
+
15
+
16
+ @pytest.fixture(autouse=True)
17
+ def dataset_transforms():
18
+ """Opt in to the future dataset transforms behaviour in this module."""
19
+ with set_options(dataset_transforms=True):
20
+ yield
21
+
22
+
23
+ TRANSFORMS = {
24
+ "oned": {},
25
+ "split": {"fmin": 0.05, "fmax": 0.4},
26
+ "smooth": {},
27
+ "rotate": {"angle": 30},
28
+ "interp": {"freq": np.arange(0.04, 0.4, 0.02)},
29
+ "to_energy": {},
30
+ "scale_by_hs": {"expr": "2 * hs"},
31
+ }
32
+
33
+
34
+ @pytest.fixture(scope="module")
35
+ def dset():
36
+ yield read_ww3(os.path.join(FILES_DIR, "ww3file.nc"))
37
+
38
+
39
+ @pytest.mark.parametrize("method, kwargs", TRANSFORMS.items())
40
+ def test_transform_returns_dataset_with_variables(dset, method, kwargs):
41
+ """Dataset accessor transforms return Dataset with non-spectral variables."""
42
+ dsout = getattr(dset.spec, method)(**kwargs)
43
+ assert isinstance(dsout, xr.Dataset)
44
+ for name in ["wspd", "wdir", "dpt", "lat", "lon"]:
45
+ assert name in dsout.data_vars
46
+
47
+
48
+ @pytest.mark.parametrize("method, kwargs", TRANSFORMS.items())
49
+ def test_transform_dataarray_accessor_unchanged(dset, method, kwargs):
50
+ """DataArray accessor transforms still return a DataArray with same values."""
51
+ darr = getattr(dset.efth.spec, method)(**kwargs)
52
+ assert isinstance(darr, xr.DataArray)
53
+ dsout = getattr(dset.spec, method)(**kwargs)
54
+ specname = "energy" if method == "to_energy" else "efth"
55
+ assert dsout[specname].values == pytest.approx(darr.values, nan_ok=True)
56
+
57
+
58
+ def test_interp_like_returns_dataset(dset):
59
+ dsout = dset.spec.interp_like(dset.efth)
60
+ assert isinstance(dsout, xr.Dataset)
61
+ assert "wspd" in dsout.data_vars
62
+ assert isinstance(dset.efth.spec.interp_like(dset.efth), xr.DataArray)
63
+
64
+
65
+ def test_transform_drops_spectral_variables(dset):
66
+ """Extra variables that depend on spectral dims are dropped."""
67
+ ds = dset.copy()
68
+ ds["extra"] = ds.efth * 2
69
+ dsout = ds.spec.smooth()
70
+ assert "extra" not in dsout
71
+ assert "wspd" in dsout.data_vars
72
+
73
+
74
+ def test_transform_keeps_dataset_attrs(dset):
75
+ ds = dset.copy()
76
+ ds.attrs["title"] = "test title"
77
+ assert ds.spec.oned().attrs["title"] == "test title"
78
+
79
+
80
+ def test_scalar_params_still_dataarray(dset):
81
+ assert isinstance(dset.spec.hs(), xr.DataArray)
82
+ assert isinstance(dset.spec.stats(["hs", "tp"]), xr.Dataset)
83
+
84
+
85
+ def test_transform_chaining(dset):
86
+ dsout = dset.spec.split(fmax=0.2).spec.oned()
87
+ assert isinstance(dsout, xr.Dataset)
88
+ assert "wspd" in dsout.data_vars
89
+
90
+
91
+ class TestPartitionDatasetAware:
92
+ def test_partition_returns_dataset(self, dset):
93
+ dspart = dset.spec.partition.ptm1(swells=2)
94
+ assert isinstance(dspart, xr.Dataset)
95
+ for name in ["wspd", "wdir", "dpt"]:
96
+ assert name in dspart.data_vars
97
+ assert "part" in dspart.efth.dims
98
+
99
+ def test_partition_wind_defaults_from_dataset(self, dset):
100
+ implicit = dset.spec.partition.ptm1(swells=2)
101
+ explicit = dset.spec.partition.ptm1(
102
+ wspd=dset.wspd, wdir=dset.wdir, dpt=dset.dpt, swells=2
103
+ )
104
+ xr.testing.assert_allclose(implicit, explicit)
105
+
106
+ @pytest.mark.parametrize("method", ["ptm2", "ptm4"])
107
+ def test_partition_wind_defaults_other_methods(self, dset, method):
108
+ dspart = getattr(dset.spec.partition, method)()
109
+ assert isinstance(dspart, xr.Dataset)
110
+
111
+ def test_partition_dataarray_returns_dataarray(self, dset):
112
+ dspart = dset.efth.spec.partition.ptm1(
113
+ wspd=dset.wspd, wdir=dset.wdir, dpt=dset.dpt, swells=2
114
+ )
115
+ assert isinstance(dspart, xr.DataArray)
116
+
117
+ def test_partition_wind_required_without_dataset(self, dset):
118
+ with pytest.raises(ValueError):
119
+ dset.efth.spec.partition.ptm1()
120
+
121
+ def test_partition_no_wind_dims(self, dset):
122
+ dspart = dset.spec.partition.ptm5(fcut=0.1)
123
+ assert isinstance(dspart, xr.Dataset)
124
+ assert "wspd" in dspart.data_vars
125
+
126
+ def test_track_returns_dataset_with_variables(self, dset):
127
+ dsout = dset.spec.partition.track(swells=2)
128
+ assert isinstance(dsout, xr.Dataset)
129
+ for name in ["efth", "track_id", "ntracks", "wspd", "wdir", "dpt"]:
130
+ assert name in dsout.data_vars
131
+
132
+ def test_track_dataarray_no_extra_variables(self, dset):
133
+ dsout = Partition(dset.efth).track(
134
+ wspd=dset.wspd, wdir=dset.wdir, dpt=dset.dpt, swells=2
135
+ )
136
+ assert isinstance(dsout, xr.Dataset)
137
+ assert "wspd" not in dsout.data_vars
138
+
139
+
140
+ class TestDefaultBehaviour:
141
+ """Without the dataset_transforms option the old behaviour is preserved."""
142
+
143
+ def test_transform_warns_and_returns_dataarray(self, dset):
144
+ with set_options(dataset_transforms=False):
145
+ with pytest.warns(FutureWarning, match="oned"):
146
+ dsout = dset.spec.oned()
147
+ assert isinstance(dsout, xr.DataArray)
148
+
149
+ def test_transform_values_unchanged(self, dset):
150
+ with set_options(dataset_transforms=False):
151
+ with pytest.warns(FutureWarning):
152
+ darr = dset.spec.smooth()
153
+ xr.testing.assert_allclose(darr, dset.efth.spec.smooth())
154
+
155
+ def test_dataarray_accessor_does_not_warn(self, dset):
156
+ import warnings
157
+
158
+ with set_options(dataset_transforms=False):
159
+ with warnings.catch_warnings():
160
+ warnings.simplefilter("error", FutureWarning)
161
+ darr = dset.efth.spec.oned()
162
+ assert isinstance(darr, xr.DataArray)
163
+
164
+ def test_partition_warns_and_returns_dataarray(self, dset):
165
+ with set_options(dataset_transforms=False):
166
+ with pytest.warns(FutureWarning, match="Partitioning"):
167
+ dspart = dset.spec.partition.ptm1(
168
+ wspd=dset.wspd, wdir=dset.wdir, dpt=dset.dpt, swells=2
169
+ )
170
+ assert isinstance(dspart, xr.DataArray)
171
+
172
+ def test_hp01_does_not_default_wind_from_dataset(self, dset):
173
+ with set_options(dataset_transforms=False):
174
+ with pytest.warns((FutureWarning, UserWarning)):
175
+ dspart = dset.spec.partition.hp01(swells=2)
176
+ assert isinstance(dspart, xr.DataArray)
177
+ # No wind classification, partition 0 is null
178
+ assert float(dspart.isel(part=0).sum()) == 0
179
+
180
+
181
+ class TestSetOptions:
182
+ def test_option_toggles_and_restores(self, dset):
183
+ with set_options(dataset_transforms=False):
184
+ assert get_options()["dataset_transforms"] is False
185
+ with set_options(dataset_transforms=True):
186
+ assert get_options()["dataset_transforms"] is True
187
+ assert isinstance(dset.spec.oned(), xr.Dataset)
188
+ assert get_options()["dataset_transforms"] is False
189
+
190
+ def test_invalid_option_raises(self):
191
+ with pytest.raises(ValueError):
192
+ set_options(not_an_option=True)
@@ -7,6 +7,8 @@
7
7
 
8
8
  import warnings
9
9
 
10
+ from wavespectra.core.options import set_options, get_options
11
+
10
12
  try:
11
13
  from wavespectra.specdataset import SpecDataset
12
14
  from wavespectra.specarray import SpecArray
@@ -14,7 +16,7 @@ except ImportError as exc:
14
16
  warnings.warn(f"Cannot import accessors at the main module level:\n{exc}")
15
17
 
16
18
 
17
- __version__ = "4.6.0"
19
+ __version__ = "4.7.0"
18
20
 
19
21
 
20
22
  def _import_functions(pkgname="input", prefix="read"):
@@ -145,7 +145,7 @@ def partition_and_reconstruct(
145
145
  if v not in dset.data_vars:
146
146
  raise ValueError(f"Missing variable '{v}' in dset for partitioning.")
147
147
  kw = dict(wspd=dset.wspd, wdir=dset.wdir, dpt=dset.dpt, swells=parts - 1)
148
- dspart = getattr(dset.spec.partition, partition_method)(**kw)
148
+ dspart = getattr(dset[attrs.SPECNAME].spec.partition, partition_method)(**kw)
149
149
 
150
150
  # Calculating parameters
151
151
  stats = list(set(STATS) - set(use_defaults))
@@ -0,0 +1,57 @@
1
+ """Package-level options."""
2
+
3
+ DATASET_TRANSFORMS = "dataset_transforms"
4
+
5
+ OPTIONS = {
6
+ DATASET_TRANSFORMS: False,
7
+ }
8
+
9
+
10
+ class set_options:
11
+ """Set package-level options for wavespectra.
12
+
13
+ Can be used either as a function to set options globally or as a context
14
+ manager to set them within a controlled block, restoring the previous
15
+ values when exiting the block.
16
+
17
+ Currently supported options:
18
+
19
+ - ``dataset_transforms``: If True, methods that transform the spectral
20
+ variable such as ``interp``, ``smooth``, ``split``, ``oned`` and the
21
+ partitioning methods return a Dataset preserving the non-spectral
22
+ variables when called from the Dataset accessor, and the ``wspd``,
23
+ ``wdir`` and ``dpt`` arguments of the ``hp01`` and ``track``
24
+ partitioning methods default to the dataset variables with those names.
25
+ If False (default), those methods return a bare spectral DataArray and
26
+ emit a FutureWarning. This will become the default behaviour in
27
+ wavespectra 5.0.
28
+
29
+ Examples:
30
+ >>> import wavespectra
31
+ >>> wavespectra.set_options(dataset_transforms=True) # global
32
+ >>> with wavespectra.set_options(dataset_transforms=True):
33
+ ... dsout = dset.spec.oned() # within context only
34
+
35
+ """
36
+
37
+ def __init__(self, **kwargs):
38
+ self.old = {}
39
+ for key, value in kwargs.items():
40
+ if key not in OPTIONS:
41
+ raise ValueError(
42
+ f"Argument '{key}' is not in the set of valid options "
43
+ f"{set(OPTIONS)}"
44
+ )
45
+ self.old[key] = OPTIONS[key]
46
+ OPTIONS.update(kwargs)
47
+
48
+ def __enter__(self):
49
+ return
50
+
51
+ def __exit__(self, exc_type, exc_value, traceback):
52
+ OPTIONS.update(self.old)
53
+
54
+
55
+ def get_options():
56
+ """Return a dictionary with the current package-level options."""
57
+ return dict(OPTIONS)
@@ -280,6 +280,36 @@ def check_same_coordinates(*args):
280
280
  )
281
281
 
282
282
 
283
+ def dataset_from_transform(transformed, dset):
284
+ """Merge transformed spectral variable back into its source dataset.
285
+
286
+ Args:
287
+ - transformed (DataArray, Dataset): Transformed spectral variable(s)
288
+ calculated from the spectral variable in dset.
289
+ - dset (Dataset): Source dataset the transform was calculated from.
290
+
291
+ Returns:
292
+ - dsout (Dataset): Dataset with the transformed spectral variable(s)
293
+ and the non-spectral variables from dset.
294
+
295
+ Note:
296
+ - Variables in dset that depend on the spectral dims (freq, dir) are
297
+ dropped since their coordinates may no longer be consistent with
298
+ those in the transformed spectra.
299
+
300
+ """
301
+ if isinstance(transformed, xr.DataArray):
302
+ transformed = transformed.to_dataset(name=transformed.name or attrs.SPECNAME)
303
+ drop_dims = [d for d in (attrs.FREQNAME, attrs.DIRNAME) if d in dset.dims]
304
+ others = dset.drop_dims(drop_dims)
305
+ others = others.drop_vars(
306
+ [v for v in others.data_vars if v in transformed.data_vars]
307
+ )
308
+ dsout = xr.merge([transformed, others])
309
+ dsout.attrs = dset.attrs
310
+ return dsout
311
+
312
+
283
313
  def load_function(module_name, func_name, prefix=None):
284
314
  """Returns a function object from string.
285
315
 
@@ -176,8 +176,11 @@ def read_swans(
176
176
 
177
177
  Args:
178
178
  - fileglob (str, list): glob pattern specifying files to read.
179
- - ndays (float): number of days to keep from each file, choose None to
180
- keep entire period.
179
+ - ndays (float): number of days to keep from the start of each file,
180
+ choose None to keep the entire period. Useful for constructing
181
+ continuous timeseries from files of overlapping forecast cycles.
182
+ Reading stops once the period is read and an exception is raised
183
+ if a file does not span the requested period.
181
184
  - int_freq (ndarray, bool): frequency array for interpolating onto:
182
185
 
183
186
  - ndarray: 1d array specifying frequencies to interpolate onto.
@@ -192,8 +195,11 @@ def read_swans(
192
195
  - False: No interpolation performed in direction space.
193
196
 
194
197
  - dirorder (bool): if True ensures directions are sorted.
195
- - ntimes (int): use it to read only specific number of times, useful
196
- for checking headers only.
198
+ - ntimes (int): number of times to read from the start of each file,
199
+ choose None to read all times. Reading stops early once the records
200
+ are read which makes it useful for quickly inspecting the first few
201
+ records of large files. Can be combined with ndays, whichever limit
202
+ is reached first stops the reading.
197
203
 
198
204
  Returns:
199
205
  - dset (SpecDataset): spectra dataset object read from file with
@@ -207,6 +213,8 @@ def read_swans(
207
213
  have same number of sites.
208
214
  - Either all or none of the spectra in fileglob must have tabfile
209
215
  associated to provide wind/depth data.
216
+ - Winds and depth from tab files are sliced at the times actually
217
+ read when the ndays or ntimes options are used.
210
218
  - Concatenation is done with numpy arrays for efficiency.
211
219
 
212
220
  """
@@ -249,16 +257,40 @@ def read_swans(
249
257
  freqs = swanfile.freqs
250
258
  dirs = swanfile.dirs
251
259
 
252
- if ntimes is None:
253
- spec_list = [s for s in swanfile.readall()]
254
- else:
255
- spec_list = [swanfile.read() for itime in range(ntimes)]
260
+ # Read spectra records, stopping early once ntimes records or ndays
261
+ # worth of times have been read so the rest of the file is not parsed
262
+ spec_list = []
263
+ tend = None
264
+ truncated = False
265
+ while ntimes is None or len(spec_list) < ntimes:
266
+ sset = swanfile.read()
267
+ if not sset:
268
+ break
269
+ if ndays is not None:
270
+ if tend is None:
271
+ tend = times[0] + datetime.timedelta(days=ndays)
272
+ if times[-1] > tend:
273
+ # Ignore the time parsed beyond the period and stop reading
274
+ times.pop()
275
+ truncated = True
276
+ break
277
+ spec_list.append(sset)
278
+
279
+ # Ensure file spans the requested period unless ntimes stopped first
280
+ ntimes_reached = ntimes is not None and len(spec_list) == ntimes
281
+ if tend is not None and not truncated and not ntimes_reached:
282
+ if times[-1] < tend:
283
+ raise OSError(
284
+ "Times in %s does not extend for %0.2f days" % (filename, ndays)
285
+ )
256
286
 
257
- # Read tab files for winds / depth
287
+ # Read tab files for winds / depth, slicing at the times actually read
288
+ tab = pd.DataFrame()
258
289
  if swanfile.is_tab:
259
290
  try:
260
291
  tab = read_tab(swanfile.tabfile).rename(columns={"dep": attrs.DEPNAME})
261
- if len(swanfile.times) == tab.index.size:
292
+ if isinstance(times, list) and set(times).issubset(tab.index):
293
+ tab = tab.loc[times]
262
294
  if "X-wsp" in tab and "Y-wsp" in tab:
263
295
  tab[attrs.WSPDNAME], tab[attrs.WDIRNAME] = uv_to_spddir(
264
296
  tab["X-wsp"], tab["Y-wsp"], coming_from=True
@@ -277,23 +309,10 @@ def read_swans(
277
309
  )
278
310
  ]
279
311
  except Exception as exc:
312
+ tab = pd.DataFrame()
280
313
  warnings.warn(
281
314
  f"Cannot parse depth and winds from {swanfile.tabfile}:\n{exc}"
282
315
  )
283
- else:
284
- tab = pd.DataFrame()
285
-
286
- # Shrinking times
287
- if ndays is not None:
288
- tend = times[0] + datetime.timedelta(days=ndays)
289
- if tend > times[-1]:
290
- raise OSError(
291
- "Times in %s does not extend for %0.2f days" % (filename, ndays)
292
- )
293
- iend = times.index(min(times, key=lambda d: abs(d - tend)))
294
- times = times[0 : iend + 1]
295
- spec_list = spec_list[0 : iend + 1]
296
- tab = tab.loc[times[0] : tend] if tab is not None else tab
297
316
 
298
317
  spec_list = flatten_list(spec_list, [])
299
318
 
@@ -10,7 +10,7 @@ import xarray as xr
10
10
 
11
11
  from wavespectra.specdataset import SpecDataset
12
12
  from wavespectra.core.attributes import attrs, set_spec_attributes
13
- from wavespectra.core.utils import interp_spec
13
+ from wavespectra.core.utils import interp_spec, dataset_from_transform
14
14
 
15
15
 
16
16
  def read_triaxys(
@@ -47,7 +47,9 @@ def read_triaxys(
47
47
  dset = dset.assign_coords({"dir": dirs + magnetic_variation})
48
48
  # Regrid
49
49
  if regrid_dir:
50
- dset = dset.spec.interp(dir=dirs)
50
+ dset = dataset_from_transform(
51
+ dset[attrs.SPECNAME].spec.interp(dir=dirs), dset
52
+ )
51
53
  return dset
52
54
 
53
55
 
@@ -107,7 +107,7 @@ def to_octopus(
107
107
  dset.spec.momf(mom=1).rename("momf1"),
108
108
  dset.spec.momf(mom=2).rename("momf2"),
109
109
  dset.spec.momd(mom=0)[0].rename("momd"),
110
- dset.spec.to_energy(),
110
+ dset[attrs.SPECNAME].spec.to_energy(),
111
111
  (dset.efth.spec.df * dset.spec.momd(mom=0)[0])
112
112
  .transpose(attrs.TIMENAME, attrs.SITENAME, attrs.FREQNAME)
113
113
  .rename("fSpec"),