wavespectra 4.7.0__tar.gz → 4.9.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.
- {wavespectra-4.7.0 → wavespectra-4.9.0}/AUTHORS.rst +4 -0
- {wavespectra-4.7.0/wavespectra.egg-info → wavespectra-4.9.0}/PKG-INFO +3 -1
- {wavespectra-4.7.0 → wavespectra-4.9.0}/pyproject.toml +7 -16
- {wavespectra-4.7.0 → wavespectra-4.9.0}/tests/test_partition.py +308 -1
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/__init__.py +1 -1
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/core/npstats.py +50 -5
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/partition/partition.py +304 -14
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/specarray.py +8 -3
- {wavespectra-4.7.0 → wavespectra-4.9.0/wavespectra.egg-info}/PKG-INFO +3 -1
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra.egg-info/requires.txt +1 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/LICENSE.txt +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/README.rst +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/setup.cfg +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/setup.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/tests/test_cli.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/tests/test_combine_hp01.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/tests/test_cookiecutter_template.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/tests/test_reader_registry.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/tests/test_specarray.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/tests/test_specdataset.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/cli.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/construct/__init__.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/construct/direction.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/construct/frequency.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/core/__init__.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/core/attributes.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/core/attributes.yml +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/core/fitting.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/core/options.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/core/select.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/core/swan.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/core/utils.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/core/xrstats.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/__init__.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/awac.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/dataset.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/datawell.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/era5.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/funwave.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/funwave_new.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/json.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/ncswan.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/ndbc.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/ndbc_ascii.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/netcdf.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/obscape.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/octopus.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/spotter.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/swan.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/triaxys.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/wavespectra.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/ww3.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/ww3_station.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/wwm.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/input/xwaves.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/output/__init__.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/output/funwave.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/output/json.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/output/netcdf.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/output/octopus.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/output/orcaflex.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/output/swan.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/output/ww3.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/output/ww3.yml +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/partition/__init__.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/partition/hanson_and_phillips_2001.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/partition/specpart/specpart.c +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/partition/specpart/specpart.h +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/partition/specpart/specpart_wrap.c +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/partition/tracking.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/plot.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra/specdataset.py +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra.egg-info/SOURCES.txt +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra.egg-info/dependency_links.txt +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra.egg-info/entry_points.txt +0 -0
- {wavespectra-4.7.0 → wavespectra-4.9.0}/wavespectra.egg-info/top_level.txt +0 -0
|
@@ -26,9 +26,11 @@ Contributors
|
|
|
26
26
|
|
|
27
27
|
* `Matthew Iannucci`_
|
|
28
28
|
* `Carlos A. Michelén Ströfer`_
|
|
29
|
+
* `eddylr`_
|
|
29
30
|
* `Henrique Rapizo`_
|
|
30
31
|
* `John Harrington`_
|
|
31
32
|
* `Jorge Perez`_
|
|
33
|
+
* `Joris de Vroom`_
|
|
32
34
|
* `Paul Branson`_
|
|
33
35
|
* `Ruben de Bruin`_
|
|
34
36
|
* `Ryan Coe`_
|
|
@@ -36,9 +38,11 @@ Contributors
|
|
|
36
38
|
|
|
37
39
|
.. _`Matthew Iannucci`: https://github.com/mpiannucci
|
|
38
40
|
.. _`Carlos A. Michelén Ströfer`: https://github.com/cmichelenstrofer
|
|
41
|
+
.. _`eddylr`: https://github.com/eddylr
|
|
39
42
|
.. _`Henrique Rapizo`: https://github.com/hrapizo
|
|
40
43
|
.. _`John Harrington`: https://github.com/JohnCHarrington
|
|
41
44
|
.. _`Jorge Perez`: https://github.com/jorgeperezg
|
|
45
|
+
.. _`Joris de Vroom`: https://github.com/jdevroom
|
|
42
46
|
.. _`Paul Branson`: https://github.com/pbranson
|
|
43
47
|
.. _`Ruben de Bruin`: https://github.com/RubendeBruin
|
|
44
48
|
.. _`Ryan Coe`: https://github.com/ryancoe
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavespectra
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.9.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>
|
|
@@ -42,6 +42,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
42
42
|
Classifier: Programming Language :: Python :: 3.11
|
|
43
43
|
Classifier: Programming Language :: Python :: 3.12
|
|
44
44
|
Classifier: Programming Language :: Python :: 3.13
|
|
45
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
45
46
|
Requires-Python: >=3.9
|
|
46
47
|
Description-Content-Type: text/x-rst
|
|
47
48
|
License-File: LICENSE.txt
|
|
@@ -49,6 +50,7 @@ License-File: AUTHORS.rst
|
|
|
49
50
|
Requires-Dist: click
|
|
50
51
|
Requires-Dist: dask
|
|
51
52
|
Requires-Dist: matplotlib
|
|
53
|
+
Requires-Dist: numpy
|
|
52
54
|
Requires-Dist: python-dateutil
|
|
53
55
|
Requires-Dist: pyyaml
|
|
54
56
|
Requires-Dist: sortedcontainers
|
|
@@ -30,12 +30,14 @@ classifiers = [
|
|
|
30
30
|
"Programming Language :: Python :: 3.11",
|
|
31
31
|
"Programming Language :: Python :: 3.12",
|
|
32
32
|
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Programming Language :: Python :: 3.14",
|
|
33
34
|
]
|
|
34
35
|
requires-python = ">=3.9"
|
|
35
36
|
dependencies = [
|
|
36
37
|
"click",
|
|
37
38
|
"dask",
|
|
38
39
|
"matplotlib",
|
|
40
|
+
"numpy",
|
|
39
41
|
"python-dateutil",
|
|
40
42
|
"pyyaml",
|
|
41
43
|
"sortedcontainers",
|
|
@@ -99,8 +101,6 @@ docs = [
|
|
|
99
101
|
]
|
|
100
102
|
|
|
101
103
|
[tool.setuptools.packages.find]
|
|
102
|
-
# Explicit include so stray top-level directories created at build time
|
|
103
|
-
# (e.g. cibuildwheel's wheelhouse/) don't break flat-layout auto-discovery.
|
|
104
104
|
include = ["wavespectra*"]
|
|
105
105
|
|
|
106
106
|
[tool.setuptools.package-data]
|
|
@@ -112,15 +112,9 @@ include = ["wavespectra*"]
|
|
|
112
112
|
version = {attr = "wavespectra.__version__"}
|
|
113
113
|
|
|
114
114
|
[tool.cibuildwheel]
|
|
115
|
-
|
|
116
|
-
build = ["cp39-*", "cp310-*", "cp311-*", "cp312-*", "cp313-*"]
|
|
117
|
-
# 64-bit only: 32-bit Windows wheels would be built by default, but core
|
|
118
|
-
# dependencies (e.g. matplotlib) no longer ship win32 wheels to test against.
|
|
115
|
+
build = ["cp39-*", "cp310-*", "cp311-*", "cp312-*", "cp313-*", "cp314-*"]
|
|
119
116
|
archs = ["auto64"]
|
|
120
|
-
# Skip musl-based linux; can be added later if there is demand.
|
|
121
117
|
skip = ["*-musllinux*"]
|
|
122
|
-
# Test every built wheel by running the partition test module, which
|
|
123
|
-
# exercises the compiled specpart extension end to end.
|
|
124
118
|
test-requires = ["pytest", "netCDF4"]
|
|
125
119
|
test-command = "pytest {project}/tests/test_partition.py -q"
|
|
126
120
|
build-verbosity = 1
|
|
@@ -128,9 +122,6 @@ build-verbosity = 1
|
|
|
128
122
|
[tool.pytest.ini_options]
|
|
129
123
|
norecursedirs = [".*", "build", "dist", "CVS", "_darcs", "{arch}", "*.egg", "venv", "broken"]
|
|
130
124
|
filterwarnings = [
|
|
131
|
-
# netCDF4-python sets the .shape attribute on numpy arrays, which numpy
|
|
132
|
-
# 2.5 deprecated; it surfaces through xarray's netCDF4 backend whenever a
|
|
133
|
-
# netCDF file is written. Third-party issue, nothing to fix in wavespectra.
|
|
134
125
|
"ignore:Setting the shape on a NumPy array has been deprecated:DeprecationWarning",
|
|
135
126
|
]
|
|
136
127
|
|
|
@@ -143,9 +134,9 @@ indent-style = "space"
|
|
|
143
134
|
line-ending = "auto"
|
|
144
135
|
|
|
145
136
|
[tool.ruff.lint]
|
|
146
|
-
#
|
|
147
|
-
#
|
|
148
|
-
|
|
137
|
+
# Pin the rule set explicitly so linting does not change with the ruff release.
|
|
138
|
+
# E4, E7, E9 and F were the defaults up to ruff 0.15.
|
|
139
|
+
select = ["E4", "E7", "E9", "F", "B015", "B018"]
|
|
149
140
|
|
|
150
141
|
[tool.ruff.lint.extend-per-file-ignores]
|
|
151
142
|
"wavespectra/__init__.py" = ["F401"]
|
|
@@ -173,7 +164,7 @@ extend-select = ["B015", "B018"]
|
|
|
173
164
|
github_url = "https://github.com/wavespectra/wavespectra/"
|
|
174
165
|
|
|
175
166
|
[tool.tbump.version]
|
|
176
|
-
current = "4.
|
|
167
|
+
current = "4.9.0"
|
|
177
168
|
regex = '''
|
|
178
169
|
(?P<major>\d+)
|
|
179
170
|
\.(?P<minor>\d+)
|
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
import numpy as np
|
|
4
|
+
import xarray as xr
|
|
4
5
|
|
|
5
6
|
import wavespectra
|
|
6
7
|
from wavespectra import read_ww3
|
|
8
|
+
from wavespectra.construct import construct_partition
|
|
7
9
|
from wavespectra.partition.partition import (
|
|
10
|
+
DEFAULTS,
|
|
8
11
|
Partition,
|
|
9
12
|
np_ptm1,
|
|
10
13
|
np_ptm2,
|
|
11
14
|
np_ptm3,
|
|
15
|
+
np_steepness,
|
|
12
16
|
np_hp01,
|
|
13
17
|
)
|
|
14
|
-
from wavespectra.core.utils import waveage
|
|
18
|
+
from wavespectra.core.utils import waveage, create_frequencies, to_coords
|
|
15
19
|
from wavespectra.core import npstats
|
|
16
20
|
from wavespectra.partition.tracking import track_partitions
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
HERE = Path(__file__).parent
|
|
20
24
|
|
|
25
|
+
# Well resolved spectral grid for constructing synthetic spectra
|
|
26
|
+
FREQ = to_coords(create_frequencies(0.03, 40, 1.1), "freq")
|
|
27
|
+
DIR = to_coords(np.arange(0, 360, 10.0), "dir")
|
|
28
|
+
|
|
21
29
|
|
|
22
30
|
@pytest.fixture(autouse=True)
|
|
23
31
|
def dataset_transforms():
|
|
@@ -374,6 +382,292 @@ class TestHP01(BasePTM):
|
|
|
374
382
|
assert np.allclose(hs.values, self.dset.spec.hs().values, rtol=1e-2)
|
|
375
383
|
|
|
376
384
|
|
|
385
|
+
class TestSteepness(BasePTM):
|
|
386
|
+
def setup_class(self):
|
|
387
|
+
super().setup_class(self)
|
|
388
|
+
self.dpt = self.dset.isel(time=0, site=0).dpt.values
|
|
389
|
+
self.scut = 0.025
|
|
390
|
+
self.swells = 3
|
|
391
|
+
|
|
392
|
+
def _exec(self, **kwargs):
|
|
393
|
+
out = np_steepness(
|
|
394
|
+
spectrum=self.efth,
|
|
395
|
+
spectrum_smooth=self.efth,
|
|
396
|
+
freq=self.freq,
|
|
397
|
+
dir=self.dir,
|
|
398
|
+
dpt=kwargs.get("dpt", self.dpt),
|
|
399
|
+
scut=kwargs.get("scut", self.scut),
|
|
400
|
+
tail=kwargs.get("tail", False),
|
|
401
|
+
swells=kwargs.get("swells", self.swells),
|
|
402
|
+
)
|
|
403
|
+
return out
|
|
404
|
+
|
|
405
|
+
def test_defaults(self):
|
|
406
|
+
self.out = self._exec()
|
|
407
|
+
assert self.out.shape[0] == self.swells + 1
|
|
408
|
+
assert self.hs_full == pytest.approx(self.hs_from_partitions)
|
|
409
|
+
|
|
410
|
+
def test_default_swells_is_one(self):
|
|
411
|
+
"""The default returns a single wind sea and a single swell."""
|
|
412
|
+
assert DEFAULTS["steepness_swells"] == 1
|
|
413
|
+
self.out = np_steepness(
|
|
414
|
+
spectrum=self.efth,
|
|
415
|
+
spectrum_smooth=self.efth,
|
|
416
|
+
freq=self.freq,
|
|
417
|
+
dir=self.dir,
|
|
418
|
+
dpt=self.dpt,
|
|
419
|
+
)
|
|
420
|
+
assert self.out.shape[0] == 2
|
|
421
|
+
assert self.hs_full == pytest.approx(self.hs_from_partitions)
|
|
422
|
+
|
|
423
|
+
def test_default_swells_is_one_class(self):
|
|
424
|
+
"""The Partition wrapper defaults to one wind sea and one swell."""
|
|
425
|
+
dspart = self.pt.steepness(dpt=self.dset.dpt)
|
|
426
|
+
assert dspart.part.size == 2
|
|
427
|
+
hs_dspart = np.sqrt((dspart.spec.hs() ** 2).sum("part"))
|
|
428
|
+
assert np.allclose(hs_dspart.values, self.dset.spec.hs().values, rtol=1e-2)
|
|
429
|
+
|
|
430
|
+
def test_request_all_partitions(self):
|
|
431
|
+
self.out = self._exec(swells=None)
|
|
432
|
+
assert self.hs_full == pytest.approx(self.hs_from_partitions)
|
|
433
|
+
|
|
434
|
+
def test_request_all_partitions_class(self):
|
|
435
|
+
"""swells=None detects the required number of partitions."""
|
|
436
|
+
dspart = self.pt.steepness(dpt=self.dset.dpt, swells=None)
|
|
437
|
+
hs_dspart = np.sqrt((dspart.spec.hs() ** 2).sum("part"))
|
|
438
|
+
assert np.allclose(hs_dspart.values, self.dset.spec.hs().values, rtol=1e-2)
|
|
439
|
+
|
|
440
|
+
def test_request_less_partitions(self):
|
|
441
|
+
swells = 1
|
|
442
|
+
self.out = self._exec(swells=swells)
|
|
443
|
+
assert len(self.out) == swells + 1
|
|
444
|
+
|
|
445
|
+
def test_partition_class(self):
|
|
446
|
+
swells = 2
|
|
447
|
+
self.out = self._exec(swells=swells)
|
|
448
|
+
dspart = self.pt.steepness(dpt=self.dset.dpt, swells=swells)
|
|
449
|
+
hs_dspart = np.sqrt(np.sum(dspart.isel(time=0, site=0).spec.hs().values ** 2))
|
|
450
|
+
assert hs_dspart == pytest.approx(self.hs_from_partitions, rel=1e-2)
|
|
451
|
+
|
|
452
|
+
def test_no_wind_required(self):
|
|
453
|
+
"""The method runs from a DataArray with no wind or depth available."""
|
|
454
|
+
dspart = Partition(self.dset.efth).steepness(swells=2)
|
|
455
|
+
assert isinstance(dspart, xr.DataArray)
|
|
456
|
+
assert dspart.part.size == 3
|
|
457
|
+
assert bool((dspart.spec.hs() > 0).any())
|
|
458
|
+
|
|
459
|
+
def test_no_depth_deep_water_default(self):
|
|
460
|
+
"""Depth is optional, the deep water approximation is used without it."""
|
|
461
|
+
deep = self._exec(dpt=np.nan)
|
|
462
|
+
prescribed = self._exec(dpt=1e5)
|
|
463
|
+
assert np.allclose(deep, prescribed)
|
|
464
|
+
|
|
465
|
+
def test_depth_makes_partitions_steeper(self):
|
|
466
|
+
"""Shoaling in shallow water pushes more energy into the wind sea.
|
|
467
|
+
|
|
468
|
+
The steepness is evaluated with the local wavelength when the depth is
|
|
469
|
+
prescribed, so the same swell is classified as wind sea in shallow
|
|
470
|
+
enough water.
|
|
471
|
+
"""
|
|
472
|
+
swell = construct_partition(
|
|
473
|
+
freq_kwargs=dict(freq=FREQ, fp=1 / 9.0, hs=2.0, gamma=3.3),
|
|
474
|
+
dir_kwargs=dict(dir=DIR, dm=270.0, dspr=25.0),
|
|
475
|
+
).transpose("freq", "dir")
|
|
476
|
+
|
|
477
|
+
def hs_windsea(dpt):
|
|
478
|
+
out = np_steepness(
|
|
479
|
+
spectrum=swell.values,
|
|
480
|
+
spectrum_smooth=swell.values,
|
|
481
|
+
freq=FREQ.values,
|
|
482
|
+
dir=DIR.values,
|
|
483
|
+
dpt=dpt,
|
|
484
|
+
swells=2,
|
|
485
|
+
)
|
|
486
|
+
return npstats.hs(out[0], FREQ.values, DIR.values)
|
|
487
|
+
|
|
488
|
+
assert hs_windsea(np.nan) == 0.0
|
|
489
|
+
assert hs_windsea(15.0) == 0.0
|
|
490
|
+
assert hs_windsea(10.0) == pytest.approx(2.0, rel=1e-2)
|
|
491
|
+
|
|
492
|
+
def test_lower_threshold_grows_windsea(self):
|
|
493
|
+
"""A lower scut threshold classifies more energy as wind sea."""
|
|
494
|
+
lo = self.pt.steepness(dpt=self.dset.dpt, scut=0.01, swells=2)
|
|
495
|
+
hi = self.pt.steepness(dpt=self.dset.dpt, scut=0.06, swells=2)
|
|
496
|
+
hs_sea_lo = lo.isel(part=0).spec.hs()
|
|
497
|
+
hs_sea_hi = hi.isel(part=0).spec.hs()
|
|
498
|
+
assert bool((hs_sea_lo >= hs_sea_hi).all())
|
|
499
|
+
assert bool((hs_sea_lo > hs_sea_hi).any())
|
|
500
|
+
|
|
501
|
+
def test_unreachable_threshold_leaves_no_windsea(self):
|
|
502
|
+
"""Partition 0 is null when no partition reaches scut."""
|
|
503
|
+
dspart = self.pt.steepness(dpt=self.dset.dpt, scut=1.0, swells=2)
|
|
504
|
+
assert bool((dspart.isel(part=0).spec.hs() == 0).all())
|
|
505
|
+
|
|
506
|
+
def test_tail_grows_windsea(self):
|
|
507
|
+
"""The tail correction recovers wind seas truncated by the freq grid.
|
|
508
|
+
|
|
509
|
+
The sample file frequency grid stops at 0.406 Hz so the light wind seas
|
|
510
|
+
in it are truncated and their steepness is underestimated.
|
|
511
|
+
"""
|
|
512
|
+
assert self.freq[-1] < 0.5
|
|
513
|
+
notail = self.pt.steepness(dpt=self.dset.dpt, swells=2)
|
|
514
|
+
tail = self.pt.steepness(dpt=self.dset.dpt, swells=2, tail=True)
|
|
515
|
+
hs_sea_notail = notail.isel(part=0).spec.hs()
|
|
516
|
+
hs_sea_tail = tail.isel(part=0).spec.hs()
|
|
517
|
+
assert bool((hs_sea_tail >= hs_sea_notail).all())
|
|
518
|
+
assert bool((hs_sea_tail > hs_sea_notail).any())
|
|
519
|
+
|
|
520
|
+
def test_tail_inert_for_swells(self):
|
|
521
|
+
"""The tail correction cannot turn a swell into a wind sea.
|
|
522
|
+
|
|
523
|
+
A watershed swell partition carries no energy at the last frequency so
|
|
524
|
+
the correction is inert for it.
|
|
525
|
+
"""
|
|
526
|
+
efth = self._sea_and_swell()
|
|
527
|
+
kwargs = dict(
|
|
528
|
+
spectrum=efth.values,
|
|
529
|
+
spectrum_smooth=efth.values,
|
|
530
|
+
freq=FREQ.values,
|
|
531
|
+
dir=DIR.values,
|
|
532
|
+
swells=2,
|
|
533
|
+
)
|
|
534
|
+
notail = np_steepness(**kwargs, tail=False)
|
|
535
|
+
tail = np_steepness(**kwargs, tail=True)
|
|
536
|
+
assert notail[0][-1].sum() > 0 # wind sea reaches the last frequency
|
|
537
|
+
assert notail[1][-1].sum() == 0 # swell does not
|
|
538
|
+
assert np.array_equal(notail[1], tail[1])
|
|
539
|
+
|
|
540
|
+
@staticmethod
|
|
541
|
+
def _sea_and_swell():
|
|
542
|
+
"""Steep 6s wind sea and gentle 14s swell, opposing directions."""
|
|
543
|
+
sea = construct_partition(
|
|
544
|
+
freq_kwargs=dict(freq=FREQ, fp=1 / 6.0, hs=2.5, gamma=3.3),
|
|
545
|
+
dir_kwargs=dict(dir=DIR, dm=90.0, dspr=25.0),
|
|
546
|
+
)
|
|
547
|
+
swell = construct_partition(
|
|
548
|
+
freq_kwargs=dict(freq=FREQ, fp=1 / 14.0, hs=1.5, gamma=3.3),
|
|
549
|
+
dir_kwargs=dict(dir=DIR, dm=270.0, dspr=15.0),
|
|
550
|
+
)
|
|
551
|
+
return (sea + swell).transpose("freq", "dir")
|
|
552
|
+
|
|
553
|
+
def test_sea_and_swell_are_recovered(self):
|
|
554
|
+
"""A steep wind sea and a gentle swell are classified from steepness."""
|
|
555
|
+
efth = self._sea_and_swell()
|
|
556
|
+
out = np_steepness(
|
|
557
|
+
spectrum=efth.values,
|
|
558
|
+
spectrum_smooth=efth.values,
|
|
559
|
+
freq=FREQ.values,
|
|
560
|
+
dir=DIR.values,
|
|
561
|
+
swells=2,
|
|
562
|
+
)
|
|
563
|
+
hs_parts = [npstats.hs(part, FREQ.values, DIR.values) for part in out]
|
|
564
|
+
assert hs_parts[0] == pytest.approx(2.5, rel=1e-2)
|
|
565
|
+
assert hs_parts[1] == pytest.approx(1.5, rel=1e-2)
|
|
566
|
+
assert hs_parts[2] == 0.0
|
|
567
|
+
|
|
568
|
+
def test_smoothing(self):
|
|
569
|
+
swells = 2
|
|
570
|
+
ds_nosmoothing = self.pt.steepness(dpt=self.dset.dpt, swells=swells)
|
|
571
|
+
ds_smoothing = self.pt.steepness(dpt=self.dset.dpt, swells=swells, smooth=True)
|
|
572
|
+
assert not ds_nosmoothing.spec.hs().equals(ds_smoothing.spec.hs())
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
class TestSteepnessSwellMerge:
|
|
576
|
+
"""Coverage for `swell_merge`, exercising the path where more swells are
|
|
577
|
+
detected than the requested `swells` slots. The ww3file.nc fixture used by
|
|
578
|
+
TestSteepness never detects more than 3 swells so it can't reach this path;
|
|
579
|
+
a synthetic four-swell spectrum with no wind sea is built here instead.
|
|
580
|
+
"""
|
|
581
|
+
|
|
582
|
+
def setup_class(self):
|
|
583
|
+
self.freq = np.linspace(0.03, 0.5, 40)
|
|
584
|
+
self.dir = np.linspace(0, 350, 36)
|
|
585
|
+
self.dpt = np.nan # np_steepness's sentinel for "no depth" (deep water)
|
|
586
|
+
|
|
587
|
+
def peak(fp, hsig, dp, spread=15):
|
|
588
|
+
E = npstats.jonswap(self.freq, fp, hsig)
|
|
589
|
+
dfac = np.exp(-0.5 * ((self.dir - dp + 180) % 360 - 180) ** 2 / spread**2)
|
|
590
|
+
dfac = dfac / (dfac.sum() * (self.dir[1] - self.dir[0]))
|
|
591
|
+
return np.outer(E, dfac)
|
|
592
|
+
|
|
593
|
+
# Four distinct, gentle (sub-threshold) swell systems, all classified
|
|
594
|
+
# as swell rather than wind sea, so requesting fewer than 4 swell
|
|
595
|
+
# slots always exercises the merge path.
|
|
596
|
+
self.efth = (
|
|
597
|
+
peak(0.07, 1.8, 200)
|
|
598
|
+
+ peak(0.09, 1.4, 260)
|
|
599
|
+
+ peak(0.11, 1.0, 30)
|
|
600
|
+
+ peak(0.14, 0.6, 100)
|
|
601
|
+
)
|
|
602
|
+
self.hs_full = npstats.hs(self.efth, self.freq, self.dir)
|
|
603
|
+
|
|
604
|
+
def _exec(self, swells, swell_merge):
|
|
605
|
+
return np_steepness(
|
|
606
|
+
spectrum=self.efth,
|
|
607
|
+
spectrum_smooth=self.efth,
|
|
608
|
+
freq=self.freq,
|
|
609
|
+
dir=self.dir,
|
|
610
|
+
dpt=self.dpt,
|
|
611
|
+
scut=0.025,
|
|
612
|
+
swells=swells,
|
|
613
|
+
swell_merge=swell_merge,
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
def _hs_from_partitions(self, out):
|
|
617
|
+
hs_partitions = [npstats.hs(efth, self.freq, self.dir) for efth in out]
|
|
618
|
+
return np.sqrt(np.sum(np.power(hs_partitions, 2)))
|
|
619
|
+
|
|
620
|
+
@pytest.mark.parametrize("swell_merge", ["sum", "hp01"])
|
|
621
|
+
@pytest.mark.parametrize("swells", [1, 2, 3])
|
|
622
|
+
def test_merge_conserves_energy_and_count(self, swells, swell_merge):
|
|
623
|
+
"""Merging (either strategy) always yields exactly `swells` swell
|
|
624
|
+
partitions and loses no energy, unlike the old discard behaviour."""
|
|
625
|
+
out = self._exec(swells=swells, swell_merge=swell_merge)
|
|
626
|
+
assert out.shape[0] == swells + 1
|
|
627
|
+
assert self.hs_full == pytest.approx(self._hs_from_partitions(out), rel=1e-2)
|
|
628
|
+
|
|
629
|
+
def test_swells_one_merges_everything(self):
|
|
630
|
+
"""swells=1 collapses every detected swell into the single output slot."""
|
|
631
|
+
out = self._exec(swells=1, swell_merge="sum")
|
|
632
|
+
assert out.shape[0] == 2
|
|
633
|
+
hs_swell = npstats.hs(out[1], self.freq, self.dir)
|
|
634
|
+
assert hs_swell == pytest.approx(self.hs_full, rel=1e-2)
|
|
635
|
+
|
|
636
|
+
def test_default_swell_merge_is_sum(self):
|
|
637
|
+
assert DEFAULTS["swell_merge"] == "sum"
|
|
638
|
+
|
|
639
|
+
def test_invalid_swell_merge_raises(self):
|
|
640
|
+
with pytest.raises(ValueError):
|
|
641
|
+
self._exec(swells=2, swell_merge="bogus")
|
|
642
|
+
|
|
643
|
+
def test_sum_and_hp01_can_disagree(self):
|
|
644
|
+
"""The two strategies are free to group swells differently as long as
|
|
645
|
+
both conserve energy - sanity-check they actually differ on this
|
|
646
|
+
deliberately-ambiguous four-swell case rather than one silently
|
|
647
|
+
falling back to the other."""
|
|
648
|
+
hs_sum = sorted(
|
|
649
|
+
npstats.hs(e, self.freq, self.dir)
|
|
650
|
+
for e in self._exec(swells=2, swell_merge="sum")[1:]
|
|
651
|
+
)
|
|
652
|
+
hs_hp01 = sorted(
|
|
653
|
+
npstats.hs(e, self.freq, self.dir)
|
|
654
|
+
for e in self._exec(swells=2, swell_merge="hp01")[1:]
|
|
655
|
+
)
|
|
656
|
+
assert hs_sum != pytest.approx(hs_hp01)
|
|
657
|
+
|
|
658
|
+
def test_partition_class_default_merge(self):
|
|
659
|
+
"""The Dataset-level `Partition.steepness()` wrapper defaults to the
|
|
660
|
+
same 'sum' merge behaviour as the numpy core function."""
|
|
661
|
+
ds = xr.Dataset(
|
|
662
|
+
{"efth": (("freq", "dir"), self.efth)},
|
|
663
|
+
coords={"freq": self.freq, "dir": self.dir},
|
|
664
|
+
)
|
|
665
|
+
dspart = ds.spec.partition.steepness(swells=2)
|
|
666
|
+
assert dspart.sizes["part"] == 3
|
|
667
|
+
hs_dspart = np.sqrt((dspart.spec.hs() ** 2).sum())
|
|
668
|
+
assert float(hs_dspart) == pytest.approx(self.hs_full, rel=1e-2)
|
|
669
|
+
|
|
670
|
+
|
|
377
671
|
class TestBbox(BasePTM):
|
|
378
672
|
def test_default(self):
|
|
379
673
|
bboxes = [
|
|
@@ -441,6 +735,19 @@ class TestPartitionAndTrack(BasePTM):
|
|
|
441
735
|
dspart = dspart.load()
|
|
442
736
|
assert (dspart.ntracks.values > 0).all()
|
|
443
737
|
|
|
738
|
+
def test_track_steepness(self):
|
|
739
|
+
self._track("steepness", wspd=self.dset.wspd, dpt=self.dset.dpt)
|
|
740
|
+
|
|
741
|
+
def test_track_steepness_no_wind(self):
|
|
742
|
+
"""Without wind the steepness partitions are matched as swells.
|
|
743
|
+
|
|
744
|
+
Wind variables are resolved from the dataset so partitioning from
|
|
745
|
+
the DataArray is required to exercise the no-wind path.
|
|
746
|
+
"""
|
|
747
|
+
pt = Partition(self.dset.efth)
|
|
748
|
+
dspart = pt.track(method="steepness", swells=2).load()
|
|
749
|
+
assert (dspart.ntracks.values > 0).all()
|
|
750
|
+
|
|
444
751
|
def test_track_hp01(self):
|
|
445
752
|
self._track("hp01", wspd=self.dset.wspd, wdir=self.dset.wdir, dpt=self.dset.dpt)
|
|
446
753
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
from scipy.constants import g, pi
|
|
5
5
|
|
|
6
|
-
from wavespectra.core.utils import R2D
|
|
6
|
+
from wavespectra.core.utils import R2D, wavelen
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def mom1(spectrum, dir, theta=90.0):
|
|
@@ -27,19 +27,21 @@ def mom1(spectrum, dir, theta=90.0):
|
|
|
27
27
|
return msin, mcos
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def dm(spectrum, dir):
|
|
30
|
+
def dm(spectrum, dir, freq):
|
|
31
31
|
"""Mean wave direction Dm.
|
|
32
32
|
|
|
33
33
|
Args:
|
|
34
|
-
- spectrum (2darray): wave spectrum array.
|
|
34
|
+
- spectrum (2darray): wave spectrum array E(f, d).
|
|
35
35
|
- dir (1darray): wave direction array.
|
|
36
|
+
- freq (1darray): wave frequency array.
|
|
36
37
|
|
|
37
38
|
Returns:
|
|
38
|
-
- dm (float): Mean
|
|
39
|
+
- dm (float): Mean wave direction.
|
|
39
40
|
|
|
40
41
|
"""
|
|
41
42
|
moms, momc = mom1(spectrum, dir)
|
|
42
|
-
|
|
43
|
+
df = np.gradient(freq) if np.size(freq) > 1 else np.ones(1)
|
|
44
|
+
dm = np.arctan2((moms * df).sum(axis=0), (momc * df).sum(axis=0))
|
|
43
45
|
dm = (270 - R2D * dm) % 360.0
|
|
44
46
|
return dm
|
|
45
47
|
|
|
@@ -69,6 +71,49 @@ def hs(spectrum, freq, dir=None, tail=True):
|
|
|
69
71
|
return 4.0 * np.sqrt(Etot)
|
|
70
72
|
|
|
71
73
|
|
|
74
|
+
def steepness(spectrum, freq, dpt=None, tail=False):
|
|
75
|
+
"""Wave steepness Hm0 / L evaluated at the energy period Tm-1,0.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
- spectrum (1darray): Direction-integrated wave spectrum array E(f).
|
|
79
|
+
- freq (1darray): Wave frequency array.
|
|
80
|
+
- dpt (float): Water depth, deep water approximation used by default.
|
|
81
|
+
- tail (bool): If True, add the contribution of an f^-5 high-frequency
|
|
82
|
+
tail beyond the last frequency to the spectral moments.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
- steepness (float): Wave steepness Hm0 / L, used as the wind sea
|
|
86
|
+
criterion in the `steepness` partitioning method.
|
|
87
|
+
|
|
88
|
+
Note:
|
|
89
|
+
- The steepness of a fully developed Pierson-Moskowitz sea is 0.035 in
|
|
90
|
+
deep water regardless of the wind speed, and it increases as the sea
|
|
91
|
+
gets younger, so this is a wave age proxy that requires no wind input.
|
|
92
|
+
- The `tail` correction is applied to both m0 and m-1 so it is
|
|
93
|
+
consistent with the energy period. It matters for wind sea partitions
|
|
94
|
+
whose peak sits close to the upper limit of the frequency grid, which
|
|
95
|
+
are otherwise truncated and appear less steep than they are. Swell
|
|
96
|
+
partitions carry no energy at the last frequency so the correction is
|
|
97
|
+
inert for them.
|
|
98
|
+
- The steepness is evaluated with the local wavelength when `dpt` is
|
|
99
|
+
prescribed, so shoaling waves become steeper in shallow water.
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
df = np.abs(freq[1:] - freq[:-1])
|
|
103
|
+
add_tail = tail and freq[-1] > 0.333
|
|
104
|
+
m0 = 0.5 * np.sum(df * (spectrum[1:] + spectrum[:-1]))
|
|
105
|
+
if add_tail:
|
|
106
|
+
m0 += 0.25 * spectrum[-1] * freq[-1]
|
|
107
|
+
if m0 < 1e-6:
|
|
108
|
+
return 0.0
|
|
109
|
+
m_1 = 0.5 * np.sum(df * (spectrum[1:] / freq[1:] + spectrum[:-1] / freq[:-1]))
|
|
110
|
+
if add_tail:
|
|
111
|
+
m_1 += spectrum[-1] / 5.0
|
|
112
|
+
hm0 = 4.0 * np.sqrt(m0)
|
|
113
|
+
fm_10 = m0 / m_1
|
|
114
|
+
return float(hm0 / wavelen(fm_10, dpt))
|
|
115
|
+
|
|
116
|
+
|
|
72
117
|
def dpm(ipeak, momsin, momcos):
|
|
73
118
|
"""Mean direction at the peak wave period Dpm.
|
|
74
119
|
|
|
@@ -38,6 +38,9 @@ DEFAULTS = {
|
|
|
38
38
|
"window": 3,
|
|
39
39
|
"wscut": 0.3333,
|
|
40
40
|
"agefac": 1.7,
|
|
41
|
+
"scut": 0.025,
|
|
42
|
+
"steepness_swells": 1,
|
|
43
|
+
"swell_merge": "sum",
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
|
|
@@ -77,6 +80,11 @@ class Partition:
|
|
|
77
80
|
freely propogating swell (i.e. unforced by the wind). This is similar to the
|
|
78
81
|
method commonly used to generate wind-sea and swell from the WAM model.
|
|
79
82
|
- ptm5: PTM5 splits spectra into wind sea and swell based on a user defined static cutoff.
|
|
83
|
+
- steepness: STEEPNESS works like PTM1 but identifies wind sea from the steepness Hm0 / L of each
|
|
84
|
+
topographic partition, evaluated at the energy period Tm-1,0, rather than from a wind speed and
|
|
85
|
+
direction based wave age criterion. Partitions at or above a minimum steepness are aggregated
|
|
86
|
+
and assigned to the wind-sea component and the remaining partitions are assigned as swell
|
|
87
|
+
components in order of decreasing wave height. No wind input is required.
|
|
80
88
|
- hp01: HP01 partitions the spectra and merges wind-sea components as in the PTM1 method, then it combines
|
|
81
89
|
adjacent swells belonging to the same wave system following the criteria outlined in Hanson and
|
|
82
90
|
Phillips (2001) and Hanson et al. (2009). Useful for noisy measured spectra which the watershed
|
|
@@ -638,6 +646,169 @@ class Partition:
|
|
|
638
646
|
|
|
639
647
|
return self._wrap_output(dsout.fillna(0.0))
|
|
640
648
|
|
|
649
|
+
def steepness(
|
|
650
|
+
self,
|
|
651
|
+
dpt=None,
|
|
652
|
+
scut=DEFAULTS["scut"],
|
|
653
|
+
tail=False,
|
|
654
|
+
swells=DEFAULTS["steepness_swells"],
|
|
655
|
+
swell_merge=DEFAULTS["swell_merge"],
|
|
656
|
+
smooth=DEFAULTS["smooth"],
|
|
657
|
+
freq_window=DEFAULTS["window"],
|
|
658
|
+
dir_window=DEFAULTS["window"],
|
|
659
|
+
ihmax=DEFAULTS["ihmax"],
|
|
660
|
+
):
|
|
661
|
+
"""STEEPNESS watershed partitioning.
|
|
662
|
+
|
|
663
|
+
STEEPNESS works like PTM1 but identifies wind sea from the steepness
|
|
664
|
+
Hm0 / L of each topographic partition, evaluated at the energy period
|
|
665
|
+
Tm-1,0, rather than from the wind speed and direction based wave age
|
|
666
|
+
criterion used by the other methods. Partitions with steepness at or
|
|
667
|
+
above `scut` are aggregated and assigned to the wind-sea component
|
|
668
|
+
(e.g., the first partition). The remaining partitions are assigned as
|
|
669
|
+
swell components in order of decreasing wave height, merged down to
|
|
670
|
+
`swells` partitions if more are detected (see `swell_merge`) so that no
|
|
671
|
+
swell energy is discarded. The default `swells=1` therefore returns a
|
|
672
|
+
single wind sea and a single swell holding all the non wind-sea energy.
|
|
673
|
+
Because no wind input is used, this method is useful when reliable wind
|
|
674
|
+
speed and direction are not available, or when the misalignment between
|
|
675
|
+
wave and wind direction is not a desired classification factor.
|
|
676
|
+
|
|
677
|
+
Args:
|
|
678
|
+
- dpt (xr.DataArray): Depth DataArray, taken from the `dpt` variable
|
|
679
|
+
in the underlying dataset if not provided, deep water approximation
|
|
680
|
+
used if not available from either source.
|
|
681
|
+
- scut (float): Wave steepness Hm0 / L cutoff, partitions with
|
|
682
|
+
steepness at or above this value are classified as wind sea.
|
|
683
|
+
- tail (bool): If True, account for an f^-5 high-frequency tail
|
|
684
|
+
beyond the last frequency when evaluating the steepness.
|
|
685
|
+
- swells (int): Number of swell partitions to compute, the default
|
|
686
|
+
of 1 merges all detected swells into a single partition. If None,
|
|
687
|
+
the number required to hold all swells detected from all spectra
|
|
688
|
+
is used, which doubles the compute time and triggers an eager
|
|
689
|
+
computation on dask datasets.
|
|
690
|
+
- swell_merge (str): How to reduce more than `swells` detected swells
|
|
691
|
+
down to `swells` partitions. "sum" (default) keeps the `swells - 1`
|
|
692
|
+
largest swells individually and adds the spectral energy of the
|
|
693
|
+
rest into the last (smallest) kept partition. "hp01" instead merges
|
|
694
|
+
excess swells onto their closest neighbour using the adjacency,
|
|
695
|
+
saddle-point and angle criteria from the `hp01` method (Hanson and
|
|
696
|
+
Phillips, 2001). Either way spectral energy is conserved rather
|
|
697
|
+
than discarded. Ignored if `swells` is None.
|
|
698
|
+
- smooth (bool): Compute watershed boundaries from smoothed spectra
|
|
699
|
+
as described in Portilla et al., 2009.
|
|
700
|
+
- freq_window (int): Size of running window for smoothing spectra.
|
|
701
|
+
- dir_window (int): Size of running window for smoothing spectra.
|
|
702
|
+
- ihmax (int): Number of discrete spectral levels in WW3 Watershed code.
|
|
703
|
+
|
|
704
|
+
Returns:
|
|
705
|
+
- dspart (xr.DataArray, xr.Dataset): Partitioned spectra with extra `part` dimension
|
|
706
|
+
where the 0th index are the wind sea and remaining indices are the swells
|
|
707
|
+
sorted by descending order of Hs.
|
|
708
|
+
|
|
709
|
+
Note:
|
|
710
|
+
- The steepness of a fully developed Pierson-Moskowitz sea is 0.035
|
|
711
|
+
in deep water regardless of the wind speed, and it grows as the sea
|
|
712
|
+
gets younger, so a single `scut` threshold applies across the
|
|
713
|
+
range of wind speeds. The default of 0.025 sits below the fully
|
|
714
|
+
developed value to allow for the energy a topographic partition
|
|
715
|
+
loses at its watershed boundaries.
|
|
716
|
+
- The steepness of a wind sea partition is underestimated when its
|
|
717
|
+
peak is close to the upper limit of the frequency grid, since the
|
|
718
|
+
truncated energy is missing from Hm0. This affects light wind seas
|
|
719
|
+
on the coarse frequency grids typical of wave model output, which
|
|
720
|
+
may then fall below `scut` and be classified as swell. Set
|
|
721
|
+
`tail=True` to compensate for the truncated energy; the correction
|
|
722
|
+
is inert for swell partitions, which carry no energy at the last
|
|
723
|
+
frequency.
|
|
724
|
+
- The steepness is evaluated with the local wavelength when the depth
|
|
725
|
+
is available, so shoaling waves become steeper in shallow water and
|
|
726
|
+
swells may be classified as wind sea on that basis alone. Prescribe
|
|
727
|
+
a larger `scut` or partition from the DataArray accessor without
|
|
728
|
+
a `dpt` argument to classify from the deep water steepness in
|
|
729
|
+
shallow water applications.
|
|
730
|
+
|
|
731
|
+
References:
|
|
732
|
+
- Hanson and Phillips (2001).
|
|
733
|
+
- Portilla et al. (2009).
|
|
734
|
+
- Tracy et al. (2007).
|
|
735
|
+
- Vincent et al. (1991).
|
|
736
|
+
- WW3 documentation (https://github.com/NOAA-EMC/WW3).
|
|
737
|
+
|
|
738
|
+
"""
|
|
739
|
+
dpt = self._resolve(dpt, attrs.DEPNAME)
|
|
740
|
+
if dpt is None:
|
|
741
|
+
# Plain nan sentinel so apply_ufunc can broadcast it like the other
|
|
742
|
+
# scalar arguments; translated back to the deep water approximation
|
|
743
|
+
# inside np_steepness.
|
|
744
|
+
dpt = np.nan
|
|
745
|
+
if smooth:
|
|
746
|
+
dset_smooth = smooth_spec(self.dset, freq_window, dir_window)
|
|
747
|
+
else:
|
|
748
|
+
dset_smooth = self.dset
|
|
749
|
+
|
|
750
|
+
args = [
|
|
751
|
+
self.dset,
|
|
752
|
+
dset_smooth,
|
|
753
|
+
self.dset.freq,
|
|
754
|
+
self.dset.dir,
|
|
755
|
+
dpt,
|
|
756
|
+
scut,
|
|
757
|
+
tail,
|
|
758
|
+
]
|
|
759
|
+
input_core_dims = [
|
|
760
|
+
["freq", "dir"],
|
|
761
|
+
["freq", "dir"],
|
|
762
|
+
["freq"],
|
|
763
|
+
["dir"],
|
|
764
|
+
[],
|
|
765
|
+
[],
|
|
766
|
+
[],
|
|
767
|
+
[],
|
|
768
|
+
[],
|
|
769
|
+
[],
|
|
770
|
+
]
|
|
771
|
+
|
|
772
|
+
# Detect the number of swells required if not prescribed
|
|
773
|
+
if swells is None:
|
|
774
|
+
swells = self._detect_nparts(
|
|
775
|
+
np_steepness,
|
|
776
|
+
*args,
|
|
777
|
+
None,
|
|
778
|
+
swell_merge,
|
|
779
|
+
ihmax,
|
|
780
|
+
input_core_dims=input_core_dims,
|
|
781
|
+
nparts_fixed=1,
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
# Partitioning full spectra
|
|
785
|
+
dsout = xr.apply_ufunc(
|
|
786
|
+
np_steepness,
|
|
787
|
+
*args,
|
|
788
|
+
swells,
|
|
789
|
+
swell_merge,
|
|
790
|
+
ihmax,
|
|
791
|
+
input_core_dims=input_core_dims,
|
|
792
|
+
output_core_dims=[["part", "freq", "dir"]],
|
|
793
|
+
vectorize=True,
|
|
794
|
+
dask="parallelized",
|
|
795
|
+
output_dtypes=["float32"],
|
|
796
|
+
dask_gufunc_kwargs={
|
|
797
|
+
"allow_rechunk": True,
|
|
798
|
+
"output_sizes": {"part": swells + 1},
|
|
799
|
+
},
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
# Finalise output
|
|
803
|
+
dsout = self._set_metadata(dsout)
|
|
804
|
+
parts_description = {
|
|
805
|
+
"part0": "wind sea",
|
|
806
|
+
"part1-n": "swells in descending order of hs",
|
|
807
|
+
}
|
|
808
|
+
dsout.attrs.update(parts_description)
|
|
809
|
+
|
|
810
|
+
return self._wrap_output(dsout.transpose("part", ...))
|
|
811
|
+
|
|
641
812
|
def hp01(
|
|
642
813
|
self,
|
|
643
814
|
wspd=None,
|
|
@@ -936,14 +1107,14 @@ class Partition:
|
|
|
936
1107
|
|
|
937
1108
|
Args:
|
|
938
1109
|
- wspd (xr.DataArray): Wind speed DataArray, required by the ptm1
|
|
939
|
-
and ptm2 methods and optional for hp01. Taken from
|
|
940
|
-
variable in the underlying dataset if not provided.
|
|
1110
|
+
and ptm2 methods and optional for hp01 and steepness. Taken from
|
|
1111
|
+
the `wspd` variable in the underlying dataset if not provided.
|
|
941
1112
|
- wdir (xr.DataArray): Wind direction DataArray, as above.
|
|
942
1113
|
- dpt (xr.DataArray): Depth DataArray, as above.
|
|
943
1114
|
- method (str): Partitioning method to track partitions from,
|
|
944
|
-
one of "ptm1", "ptm2", "ptm3" or "hp01". The ptm4,
|
|
945
|
-
methods define partitions as fixed spectral regions
|
|
946
|
-
identity is already continuous in time, so there is nothing
|
|
1115
|
+
one of "ptm1", "ptm2", "ptm3", "steepness" or "hp01". The ptm4,
|
|
1116
|
+
ptm5 and bbox methods define partitions as fixed spectral regions
|
|
1117
|
+
whose identity is already continuous in time, so there is nothing
|
|
947
1118
|
to track.
|
|
948
1119
|
- ddpm_sea_max (float): Maximum peak direction difference for wind sea
|
|
949
1120
|
partitions. Default is 30 degrees.
|
|
@@ -963,8 +1134,8 @@ class Partition:
|
|
|
963
1134
|
must span to be included in the `systems=True` output. The
|
|
964
1135
|
default of 1 keeps all tracked systems.
|
|
965
1136
|
- kwargs: Further arguments passed to the partitioning method, e.g.
|
|
966
|
-
`swells`, `agefac`, `wscut`, `smooth` or the
|
|
967
|
-
parameters.
|
|
1137
|
+
`swells`, `agefac`, `wscut`, `scut`, `smooth` or the
|
|
1138
|
+
hp01 combining parameters.
|
|
968
1139
|
|
|
969
1140
|
Returns:
|
|
970
1141
|
- dspart (xr.DataArray, xr.Dataset): Partitioned spectra with extra `part` dimension
|
|
@@ -977,11 +1148,14 @@ class Partition:
|
|
|
977
1148
|
in the `systems=False` output.
|
|
978
1149
|
|
|
979
1150
|
Note:
|
|
980
|
-
- Wind sea partitions (partition 0 in ptm1 and
|
|
981
|
-
and 1 in ptm2) are matched with wind-sea thresholds
|
|
982
|
-
remaining partitions with swell thresholds. The ptm3
|
|
983
|
-
are not classified and are all matched with swell
|
|
984
|
-
which makes wind inputs optional for that method.
|
|
1151
|
+
- Wind sea partitions (partition 0 in ptm1, hp01 and steepness,
|
|
1152
|
+
partitions 0 and 1 in ptm2) are matched with wind-sea thresholds
|
|
1153
|
+
and the remaining partitions with swell thresholds. The ptm3
|
|
1154
|
+
partitions are not classified and are all matched with swell
|
|
1155
|
+
thresholds, which makes wind inputs optional for that method.
|
|
1156
|
+
The steepness method classifies the wind sea without wind but the
|
|
1157
|
+
wind speed still defines the wind-sea matching threshold, so
|
|
1158
|
+
without it all its partitions are matched with swell thresholds.
|
|
985
1159
|
- The time step is evaluated for each pair of consecutive spectra
|
|
986
1160
|
so records with gaps or irregular sampling use matching
|
|
987
1161
|
thresholds consistent with the actual time elapsed.
|
|
@@ -1014,10 +1188,15 @@ class Partition:
|
|
|
1014
1188
|
# ptm3 partitions are not classified, tracking does not need wind
|
|
1015
1189
|
wind_kwargs = {}
|
|
1016
1190
|
nsea = 0
|
|
1191
|
+
elif method == "steepness":
|
|
1192
|
+
# steepness classifies the wind sea without wind, which is only
|
|
1193
|
+
# required to match it with wind sea thresholds when tracking
|
|
1194
|
+
wind_kwargs = {"dpt": dpt}
|
|
1195
|
+
nsea = 1 if wspd is not None else 0
|
|
1017
1196
|
else:
|
|
1018
1197
|
raise ValueError(
|
|
1019
|
-
f"Cannot track partitions from method '{method}', "
|
|
1020
|
-
"
|
|
1198
|
+
f"Cannot track partitions from method '{method}', available "
|
|
1199
|
+
"methods are 'ptm1', 'ptm2', 'ptm3', 'steepness' and 'hp01'"
|
|
1021
1200
|
)
|
|
1022
1201
|
|
|
1023
1202
|
# Do the partitioning, on the spectral variable only so the tracking
|
|
@@ -1125,6 +1304,117 @@ def np_ptm1(
|
|
|
1125
1304
|
return np.array([wsea_partition] + swell_partitions)
|
|
1126
1305
|
|
|
1127
1306
|
|
|
1307
|
+
def np_steepness(
|
|
1308
|
+
spectrum,
|
|
1309
|
+
spectrum_smooth,
|
|
1310
|
+
freq,
|
|
1311
|
+
dir,
|
|
1312
|
+
dpt=np.nan,
|
|
1313
|
+
scut=DEFAULTS["scut"],
|
|
1314
|
+
tail=False,
|
|
1315
|
+
swells=DEFAULTS["steepness_swells"],
|
|
1316
|
+
swell_merge=DEFAULTS["swell_merge"],
|
|
1317
|
+
ihmax=DEFAULTS["ihmax"],
|
|
1318
|
+
):
|
|
1319
|
+
"""STEEPNESS spectra partitioning on numpy arrays.
|
|
1320
|
+
|
|
1321
|
+
Args:
|
|
1322
|
+
- spectrum (2darray): Wave spectrum array with shape (nf, nd).
|
|
1323
|
+
- spectrum_smooth (2darray): Smoothed wave spectrum array with shape (nf, nd).
|
|
1324
|
+
- freq (1darray): Wave frequency array with shape (nf).
|
|
1325
|
+
- dir (1darray): Wave direction array with shape (nd).
|
|
1326
|
+
- dpt (float): Water depth, deep water approximation used if nan.
|
|
1327
|
+
- scut (float): Wave steepness Hm0 / L cutoff, evaluated at the energy
|
|
1328
|
+
period Tm-1,0, at or above which a partition is classified as wind sea.
|
|
1329
|
+
- tail (bool): If True, account for an f^-5 high-frequency tail beyond
|
|
1330
|
+
the last frequency when evaluating the steepness.
|
|
1331
|
+
- swells (int): Number of swell partitions to compute, all detected if
|
|
1332
|
+
None. The default of 1 merges all detected swells into a single
|
|
1333
|
+
partition.
|
|
1334
|
+
- swell_merge (str): How to reduce more than `swells` detected swells down
|
|
1335
|
+
to `swells` partitions: "sum" adds the spectral energy of the smallest
|
|
1336
|
+
excess swells into the last (smallest) kept partition; "hp01" merges
|
|
1337
|
+
excess swells onto their closest neighbour using the Hanson and Phillips
|
|
1338
|
+
(2001) adjacency criteria. Ignored if `swells` is None. Either way, no
|
|
1339
|
+
swell energy is discarded.
|
|
1340
|
+
- ihmax (int): Number of discrete spectral levels in WW3 Watershed code.
|
|
1341
|
+
|
|
1342
|
+
Returns:
|
|
1343
|
+
- specpart (3darray): Wave spectrum partitions sorted in decreasing order of Hs
|
|
1344
|
+
with shape (np, nf, nd).
|
|
1345
|
+
|
|
1346
|
+
Note:
|
|
1347
|
+
- The smooth spectrum `spectrum_smooth` is used to define the watershed
|
|
1348
|
+
boundaries which are applied to the original spectrum.
|
|
1349
|
+
- Unlike ptm1, ptm2 and hp01, wind sea is identified from the steepness
|
|
1350
|
+
of each topographic partition rather than from a wind speed and
|
|
1351
|
+
direction based wave age criterion, so no wind input is required.
|
|
1352
|
+
|
|
1353
|
+
"""
|
|
1354
|
+
# Use smooth spectrum to define morphological boundaries
|
|
1355
|
+
watershed_map = specpart.partition(spectrum_smooth, ihmax)
|
|
1356
|
+
nparts = watershed_map.max()
|
|
1357
|
+
dpt = None if np.isnan(dpt) else dpt
|
|
1358
|
+
ddir = abs(dir[1] - dir[0]) if dir.size > 1 else 1.0
|
|
1359
|
+
|
|
1360
|
+
# Classify each topographic partition as wind sea or swell from its steepness
|
|
1361
|
+
wsea_partition = np.zeros_like(spectrum)
|
|
1362
|
+
swell_partitions = []
|
|
1363
|
+
for ipart in range(nparts):
|
|
1364
|
+
part = np.where(watershed_map == ipart + 1, spectrum, 0.0) # start at 1
|
|
1365
|
+
E = ddir * part.sum(axis=1)
|
|
1366
|
+
if npstats.steepness(E, freq, dpt, tail) >= scut:
|
|
1367
|
+
wsea_partition += part
|
|
1368
|
+
else:
|
|
1369
|
+
swell_partitions.append(part)
|
|
1370
|
+
|
|
1371
|
+
# Sort swells by Hs
|
|
1372
|
+
if swell_partitions:
|
|
1373
|
+
isort = np.argsort(
|
|
1374
|
+
[-npstats.hs(swell, freq, dir) for swell in swell_partitions]
|
|
1375
|
+
)
|
|
1376
|
+
swell_partitions = list(np.array(swell_partitions)[isort])
|
|
1377
|
+
|
|
1378
|
+
# Dealing with the number of swells
|
|
1379
|
+
if swells is None:
|
|
1380
|
+
# Exclude null swell partitions if the number of output swells is undefined
|
|
1381
|
+
swell_partitions = [swell for swell in swell_partitions if swell.sum() > 0]
|
|
1382
|
+
else:
|
|
1383
|
+
if len(swell_partitions) > swells:
|
|
1384
|
+
if swell_merge == "sum":
|
|
1385
|
+
# Keep the `swells - 1` largest swells individually, sum the
|
|
1386
|
+
# spectral energy of the rest into the last kept partition
|
|
1387
|
+
n_individual = max(swells - 1, 0)
|
|
1388
|
+
kept = list(swell_partitions[:n_individual])
|
|
1389
|
+
excess = swell_partitions[n_individual:]
|
|
1390
|
+
if excess:
|
|
1391
|
+
kept.append(np.sum(excess, axis=0))
|
|
1392
|
+
swell_partitions = kept
|
|
1393
|
+
elif swell_merge == "hp01":
|
|
1394
|
+
# Merge excess swells onto their closest neighbour using HP01's
|
|
1395
|
+
# own adjacency/saddle/angle criteria instead of a plain sum
|
|
1396
|
+
swell_partitions = combine_partitions_hp01(
|
|
1397
|
+
partitions=swell_partitions,
|
|
1398
|
+
freq=freq,
|
|
1399
|
+
dir=dir,
|
|
1400
|
+
swells=swells,
|
|
1401
|
+
combine_extra_swells=True,
|
|
1402
|
+
)
|
|
1403
|
+
else:
|
|
1404
|
+
raise ValueError(
|
|
1405
|
+
f"swell_merge must be 'sum' or 'hp01', got {swell_merge!r}"
|
|
1406
|
+
)
|
|
1407
|
+
if len(swell_partitions) < swells:
|
|
1408
|
+
# Extend partitions list with null spectra -- either fewer than
|
|
1409
|
+
# `swells` were detected in the first place, or hp01 merging
|
|
1410
|
+
# combined some of them down below the requested count
|
|
1411
|
+
n = swells - len(swell_partitions)
|
|
1412
|
+
for i in range(n):
|
|
1413
|
+
swell_partitions.append(np.zeros_like(spectrum))
|
|
1414
|
+
|
|
1415
|
+
return np.array([wsea_partition] + swell_partitions)
|
|
1416
|
+
|
|
1417
|
+
|
|
1128
1418
|
def np_ptm2(
|
|
1129
1419
|
spectrum,
|
|
1130
1420
|
spectrum_smooth,
|
|
@@ -479,13 +479,18 @@ class SpecArray(object):
|
|
|
479
479
|
return tm02.rename(self._my_name())
|
|
480
480
|
|
|
481
481
|
def dm(self):
|
|
482
|
-
"""Mean wave direction from the 1st spectral moment Dm.
|
|
482
|
+
"""Mean wave direction from the 1st spectral moment Dm.
|
|
483
|
+
|
|
484
|
+
Defined from the sin and cosine components of the first directional
|
|
485
|
+
moment integrated over frequency and direction.
|
|
486
|
+
|
|
487
|
+
"""
|
|
483
488
|
if self.dir is None:
|
|
484
489
|
raise ValueError("Cannot calculate dm from 1d, frequency spectra.")
|
|
485
490
|
moms, momc = self.momd(1)
|
|
486
491
|
dm = np.arctan2(
|
|
487
|
-
moms.sum(dim=attrs.FREQNAME, skipna=False),
|
|
488
|
-
momc.sum(dim=attrs.FREQNAME, skipna=False),
|
|
492
|
+
(moms * self.df).sum(dim=attrs.FREQNAME, skipna=False),
|
|
493
|
+
(momc * self.df).sum(dim=attrs.FREQNAME, skipna=False),
|
|
489
494
|
)
|
|
490
495
|
dm = (270 - R2D * dm) % 360.0
|
|
491
496
|
dm.attrs.update(self._get_cf_attributes(self._my_name()))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavespectra
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.9.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>
|
|
@@ -42,6 +42,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
42
42
|
Classifier: Programming Language :: Python :: 3.11
|
|
43
43
|
Classifier: Programming Language :: Python :: 3.12
|
|
44
44
|
Classifier: Programming Language :: Python :: 3.13
|
|
45
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
45
46
|
Requires-Python: >=3.9
|
|
46
47
|
Description-Content-Type: text/x-rst
|
|
47
48
|
License-File: LICENSE.txt
|
|
@@ -49,6 +50,7 @@ License-File: AUTHORS.rst
|
|
|
49
50
|
Requires-Dist: click
|
|
50
51
|
Requires-Dist: dask
|
|
51
52
|
Requires-Dist: matplotlib
|
|
53
|
+
Requires-Dist: numpy
|
|
52
54
|
Requires-Dist: python-dateutil
|
|
53
55
|
Requires-Dist: pyyaml
|
|
54
56
|
Requires-Dist: sortedcontainers
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|