wavespectra 4.0.2__tar.gz → 4.1.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 (72) hide show
  1. {wavespectra-4.0.2/wavespectra.egg-info → wavespectra-4.1.0}/PKG-INFO +5 -1
  2. {wavespectra-4.0.2 → wavespectra-4.1.0}/README.rst +4 -0
  3. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/__init__.py +1 -1
  4. wavespectra-4.1.0/wavespectra/input/awac.py +496 -0
  5. wavespectra-4.1.0/wavespectra/input/datawell.py +178 -0
  6. wavespectra-4.1.0/wavespectra/input/spotter.py +294 -0
  7. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/partition/specpart/specpartc.c +1 -1
  8. {wavespectra-4.0.2 → wavespectra-4.1.0/wavespectra.egg-info}/PKG-INFO +5 -1
  9. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra.egg-info/SOURCES.txt +2 -0
  10. wavespectra-4.0.2/wavespectra/input/spotter.py +0 -478
  11. {wavespectra-4.0.2 → wavespectra-4.1.0}/AUTHORS.rst +0 -0
  12. {wavespectra-4.0.2 → wavespectra-4.1.0}/LICENSE.txt +0 -0
  13. {wavespectra-4.0.2 → wavespectra-4.1.0}/pyproject.toml +0 -0
  14. {wavespectra-4.0.2 → wavespectra-4.1.0}/setup.cfg +0 -0
  15. {wavespectra-4.0.2 → wavespectra-4.1.0}/setup.py +0 -0
  16. {wavespectra-4.0.2 → wavespectra-4.1.0}/tests/test_cli.py +0 -0
  17. {wavespectra-4.0.2 → wavespectra-4.1.0}/tests/test_cookiecutter_template.py +0 -0
  18. {wavespectra-4.0.2 → wavespectra-4.1.0}/tests/test_partition.py +0 -0
  19. {wavespectra-4.0.2 → wavespectra-4.1.0}/tests/test_specarray.py +0 -0
  20. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/cli.py +0 -0
  21. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/construct/__init__.py +0 -0
  22. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/construct/direction.py +0 -0
  23. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/construct/frequency.py +0 -0
  24. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/core/__init__.py +0 -0
  25. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/core/attributes.py +0 -0
  26. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/core/attributes.yml +0 -0
  27. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/core/fitting.py +0 -0
  28. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/core/npstats.py +0 -0
  29. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/core/select.py +0 -0
  30. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/core/swan.py +0 -0
  31. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/core/utils.py +0 -0
  32. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/core/xrstats.py +0 -0
  33. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/input/__init__.py +0 -0
  34. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/input/dataset.py +0 -0
  35. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/input/era5.py +0 -0
  36. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/input/funwave.py +0 -0
  37. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/input/json.py +0 -0
  38. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/input/ncswan.py +0 -0
  39. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/input/ndbc.py +0 -0
  40. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/input/ndbc_ascii.py +0 -0
  41. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/input/netcdf.py +0 -0
  42. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/input/obscape.py +0 -0
  43. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/input/octopus.py +0 -0
  44. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/input/swan.py +0 -0
  45. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/input/triaxys.py +0 -0
  46. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/input/wavespectra.py +0 -0
  47. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/input/ww3.py +0 -0
  48. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/input/ww3_station.py +0 -0
  49. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/input/wwm.py +0 -0
  50. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/input/xwaves.py +0 -0
  51. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/output/__init__.py +0 -0
  52. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/output/funwave.py +0 -0
  53. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/output/json.py +0 -0
  54. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/output/netcdf.py +0 -0
  55. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/output/octopus.py +0 -0
  56. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/output/orcaflex.py +0 -0
  57. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/output/swan.py +0 -0
  58. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/output/ww3.py +0 -0
  59. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/output/ww3.yml +0 -0
  60. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/partition/__init__.py +0 -0
  61. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/partition/hanson_and_phillips_2001.py +0 -0
  62. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/partition/partition.py +0 -0
  63. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/partition/specpart/specpartc.h +0 -0
  64. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/partition/specpart/specpartc_wrap.c +0 -0
  65. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/partition/tracking.py +0 -0
  66. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/plot.py +0 -0
  67. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/specarray.py +0 -0
  68. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra/specdataset.py +0 -0
  69. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra.egg-info/dependency_links.txt +0 -0
  70. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra.egg-info/entry_points.txt +0 -0
  71. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra.egg-info/requires.txt +0 -0
  72. {wavespectra-4.0.2 → wavespectra-4.1.0}/wavespectra.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wavespectra
3
- Version: 4.0.2
3
+ Version: 4.1.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>
@@ -95,6 +95,10 @@ Python library for ocean wave spectra.
95
95
  .. image:: https://img.shields.io/pypi/dm/wavespectra
96
96
  :target: https://pypistats.org/packages/wavespectra
97
97
  :alt: PyPI - Downloads
98
+ .. image:: https://anaconda.org/conda-forge/wavespectra/badges/version.svg
99
+ :target: https://anaconda.org/conda-forge/wavespectra
100
+ .. image:: https://anaconda.org/conda-forge/wavespectra/badges/platforms.svg
101
+ :target: https://anaconda.org/conda-forge/wavespectra
98
102
  .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
99
103
  :target: https://github.com/python/black
100
104
  .. image:: https://img.shields.io/pypi/pyversions/wavespectra
@@ -16,6 +16,10 @@ Python library for ocean wave spectra.
16
16
  .. image:: https://img.shields.io/pypi/dm/wavespectra
17
17
  :target: https://pypistats.org/packages/wavespectra
18
18
  :alt: PyPI - Downloads
19
+ .. image:: https://anaconda.org/conda-forge/wavespectra/badges/version.svg
20
+ :target: https://anaconda.org/conda-forge/wavespectra
21
+ .. image:: https://anaconda.org/conda-forge/wavespectra/badges/platforms.svg
22
+ :target: https://anaconda.org/conda-forge/wavespectra
19
23
  .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
20
24
  :target: https://github.com/python/black
21
25
  .. image:: https://img.shields.io/pypi/pyversions/wavespectra
@@ -13,7 +13,7 @@ except ImportError:
13
13
  warnings.warn("Cannot import accessors at the main module level")
14
14
 
15
15
 
16
- __version__ = "4.0.2"
16
+ __version__ = "4.1.0"
17
17
 
18
18
 
19
19
  def _import_functions(pkgname="input", prefix="read"):
@@ -0,0 +1,496 @@
1
+ """Reader for AWAC spectra from NMEA data
2
+
3
+ https://www.nortekgroup.com/
4
+
5
+ The spectra are reconstructed from the Fourier coefficients using the maximum entropy method to remove negative energy.
6
+ Normalization is applied per frequency to keep energy over directions identical to energy in non-directional spectrum
7
+ The 2D spectral data is scaled to the significant wave height (Hs) given in the wave parameters.
8
+
9
+ The time is returned as read from the timestamps. The time-zone depends on the configuration of the device and is not included in the output.
10
+
11
+ revision history:
12
+ -----------------
13
+ 2024-08-15 : First version - Ruben de Bruin
14
+
15
+
16
+ Data description and example data:
17
+ -------------
18
+
19
+
20
+ Fourier coeffcients:
21
+
22
+ Field Description Form
23
+ 0 Identifier “$PNORF”
24
+ 1 Fourier coefficient flag (A1/B1/A2/B2) "CC"
25
+ 2 Date MMDDYY
26
+ 3 Time hhmmss.s
27
+ 4 Spectrum basis type (0=pressure, 1=velocity,
28
+ 3=AST) n
29
+ 5 Start Frequency (Hz) d.dd
30
+ 6 Step Frequency (Hz) d.dd
31
+ 7 Number of Frequencies N nnn
32
+ 8 Fourier Coefficient CC [frequency 1] d.dddd
33
+ 9 Fourier Coefficient CC [frequency 2] d.dddd
34
+ N+7 Fourier Coefficient CC [frequency N] d.dddd
35
+ N+8 Checksum (hex) *hh
36
+
37
+ $PNORF,A1,050120,000101,3,0.02,0.01,48,-0.1370,-0.1413,-0.2548,-0.7437,-0.9243,-0.9156,-0.9092,-0.9027,-0.8771,-0.8651,-0.8662,-0.8520,-0.8440,-0.8249,-0.8166,-0.8455,-0.8518,-0.8404,-0.8077,-0.7297,-0.6421,-0.5878,-0.5803,-0.5477,-0.5559,-0.5445,-0.3028,-0.2844,-0.1277,-0.2942,-0.2004,-0.1690,-0.1242,-0.0984,-0.0428,-0.0352,-0.0103,-0.0138,0.0057,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000*06
38
+ $PNORF,B1,050120,000101,3,0.02,0.01,48,0.2317,0.2715,0.2530,0.2815,0.3104,0.3728,0.3932,0.3831,0.3758,0.3861,0.3942,0.4068,0.3551,0.3375,0.2897,0.2033,0.1489,0.0765,0.1139,0.1978,0.2613,0.1794,0.2080,0.0510,0.0194,-0.0606,0.0258,0.1475,0.1210,0.0400,0.0183,-0.0247,0.0287,0.0026,-0.0161,-0.0041,0.0134,0.0183,0.0113,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000*05
39
+ $PNORF,A2,050120,000101,3,0.02,0.01,48,0.0298,-0.0182,0.0292,0.5327,0.7465,0.6843,0.6601,0.6511,0.5770,0.5368,0.5383,0.5018,0.5014,0.4659,0.4671,0.5400,0.5509,0.5443,0.4952,0.3610,0.2616,0.2780,0.3188,0.2016,0.1471,0.2066,0.1387,0.0577,0.0479,0.0829,0.0135,0.0651,-0.0119,0.0106,-0.0576,-0.0748,-0.0762,-0.0359,-0.0048,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000*2B
40
+ $PNORF,B2,050120,000101,3,0.02,0.01,48,-0.0619,-0.0377,-0.0514,-0.3674,-0.5502,-0.6668,-0.7015,-0.6633,-0.6189,-0.6227,-0.6373,-0.6422,-0.5306,-0.4705,-0.3596,-0.2469,-0.1640,-0.0557,-0.0666,-0.1313,-0.1363,-0.0540,-0.0295,0.0234,0.0481,0.0893,0.0843,-0.0011,0.0024,0.0494,0.1149,0.1554,0.0620,-0.0601,-0.0865,-0.0371,-0.0058,-0.0191,-0.0185,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000,-9.0000*0C
41
+
42
+ Spectra:
43
+
44
+ Field Description Form
45
+ 0 Identifier “$PNORE”
46
+ 1 Date MMDDYY
47
+ 2 Time hhmmss.s
48
+ 3 Spectrum basis type (0=pressure, 1=velocity,3=AST) n
49
+ 4 Start Frequency (Hz) d.dd
50
+ 5 Step Frequency (Hz) d.dd
51
+ 6 Number of Frequencies N nnn
52
+ 7 Energy Density [frequency 1] (cm2/Hz) dddd.ddd
53
+ 8 Energy Density [frequency 2] (cm2/Hz) dddd.ddd
54
+ N+6 Energy Density [frequency N] (cm2/Hz) dddd.ddd
55
+
56
+ $PNORE,050120,000101,3,0.02,0.01,98,0.063,0.029,0.020,0.182,2.671,5.169,6.311,4.587,2.876,1.802,1.817,1.
57
+ 442,1.163,0.878,0.653,0.575,0.549,0.409,0.306,0.195,0.184,0.167,0.159,0.119,0.105,0.085,0.072,0.052,0.053,0.045,0.034,0.025,0.022,0.018,0.018,0.018,0.014,0.011,0.008,0.008,0.008,0.007,0.006,0.006,0.005,0.005,0.005,0.005,0.005,0.004,0.003,0.003,0.002,0.003,0.002,0.002,0.002,0.001,0.002,0.002,0.002,0.002,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.001,0.000,0.000,0.000,0.000,0.000,0.000*75
58
+
59
+ Wave Parameters:
60
+
61
+ Field Description Form
62
+ 0 Identifier “$PNORW”
63
+ 1 Date MMDDYY
64
+ 2 Time hhmmss
65
+ 3 Spectrum basis type (0=pressure, 1=velocity,3=AST)
66
+ 4 Processing method (1=PUV, 2=SUV, 3=MLM,4=MLMST)
67
+ 5 Hm0 (m) dd.dd
68
+ 6 H3 (m) dd.dd
69
+ 7 H10 (m) dd.dd
70
+ 8 Hmax (m) dd.dd
71
+ 9 Tm02 (s) dd.dd
72
+ 10 Tp (s) dd.dd
73
+ 11 Tz (s) dd.dd
74
+ 12 DirTp (deg) ddd.dd
75
+ 13 SprTp (deg) ddd.dd
76
+ 14 Main Direction (deg) ddd.dd
77
+ 15 Unidirectivity Index dd.dd
78
+ 16 Mean pressure (dbar) dd.dd
79
+ 17 Number of no detects n
80
+ 18 Number of bad detects n
81
+ 19 Near surface Current speed (m/s) dd.dd
82
+ 20 Near surface Current direction (deg) ddd.dd
83
+ 21 Error Code hhhh
84
+ 22 Checksum (hex) *hh
85
+
86
+ Example:
87
+ $PNORW,073010,051001,3,4,0.55,0.51,0.63,0.82,2.76,3.33,2.97,55.06,78.91,337.62,0.48,22.35,0,1,0.
88
+ 27,129.11,0000*4E
89
+
90
+ Reconstructing a spectrum from these coefficients
91
+ --------------------------------------------------
92
+
93
+ DirRes = 4;
94
+ freq = 0.02:0.01:(48*0.01 + 0.01);
95
+ dDir = 0:pi*DirRes/180:(2*pi-pi*DirRes/180);
96
+ Spec = S;
97
+
98
+ % (conversion from Cart to Compass)
99
+ A1temp = A1;
100
+ B1temp= B1;
101
+ A1 = -B1temp;
102
+ B1= -A1temp;
103
+ A2 = -A2;
104
+ B2 = B2;
105
+
106
+ % Direction From
107
+ dr = pi;
108
+ A1new = cos(dr)*A1 + sin(dr)*B1;
109
+ B1new = -sin(dr)*A1 + cos(dr)*B1;
110
+ A2new = cos(2*dr)*A2 + sin(2*dr)*B2;
111
+ B2new = -sin(2*dr)*A2 + cos(2*dr)*B2;
112
+
113
+ % Now apply maximum entropy method to remove negative energy
114
+ for j=1:length(freq)
115
+
116
+ % This little gem switches from cart2compass
117
+ for dd = 1:length(dDir)
118
+ dNewdir = pi/2 - dDir(dd);
119
+
120
+ %Pure Fourier coef soln w/o MEM
121
+ DirFieldOLD(j,dd) = (A1new(j)*cos(dNewdir) + B1new(j)*sin(dNewdir) + A2new(j)*cos(2*dNewdir) + B2new(j)*sin(2*dNewdir))*Spec(ii,j);
122
+ end
123
+ end
124
+
125
+ --OR--
126
+
127
+
128
+ % Now apply maximum entropy method to remove negative energy
129
+ for j=1:length(freq)
130
+ c1 = A1new(j) + 1i*B1new(j);
131
+ c2 = A2new(j) + 1i*B2new(j);
132
+ %c1 = A1new(ii,j) + i*B1new(ii,j);
133
+ %c2 = A2new(ii,j) + i*B2new(ii,j);
134
+
135
+ fi1 = (c1 - c2*conj(c1)) / (1 - abs(c1)*abs(c1));
136
+ fi2 = c2 - c1*fi1;
137
+
138
+ % This little gem switches from cart2compass
139
+ for dd = 1:length(dDir)
140
+ dNewdir = pi/2 - dDir(dd);
141
+ %dNewdir = dDir(dd);
142
+
143
+ numer = 1 - fi1*conj(c1) - fi2*conj(c2);
144
+ %denom = 1 - fi1*exp(-i*dDir(dd)) - fi2*exp(-2.0*i*dDir(dd));
145
+ denom = 1 - fi1*exp(-1i*dNewdir) - fi2*exp(-2.0*1i*dNewdir);
146
+ DirField(j,dd) = real(numer/(abs(denom)*abs(denom)))/(2.0*pi)*Spec(ii,j)*pi*DirRes/180;
147
+
148
+ %Pure Fourier coef soln w/o ME
149
+ DirFieldOLD(j,dd) = (A1new(j)*cos(dNewdir) + B1new(j)*sin(dNewdir) + A2new(j)*cos(2*dNewdir) + B2new(j)*sin(2*dNewdir))*Spec(ii,j);
150
+ end
151
+ end
152
+
153
+
154
+ """
155
+
156
+ import datetime
157
+ import warnings
158
+
159
+ import numpy as np
160
+
161
+ import xarray as xr
162
+ from wavespectra.core.attributes import set_spec_attributes
163
+
164
+
165
+ def read_awac(filename):
166
+ """Read Spectra from Nortec AWAC NMEA file.
167
+
168
+ The spectra are reconstructed from the Fourier coefficients using the maximum
169
+ entropy method to remove negative energy. Normalisation is applied per frequency to
170
+ keep energy over directions identical to energy in non-directional spectrum. The 2D
171
+ spectral data is scaled to the significant wave height (Hs) given in the wave
172
+ parameters.
173
+
174
+ Args:
175
+ - filename (str): path to AWAC file.
176
+
177
+ Returns:
178
+ - dset (SpecDataset): spectra dataset object read from file.
179
+
180
+ """
181
+ with open(filename, "r") as f:
182
+ lines = [line for line in f.read().split("\n")]
183
+ return read_awac_strings(lines)
184
+
185
+
186
+ def parse_awac_nmnea_wave_parameters(lines):
187
+ for line in lines:
188
+ if line.startswith("$PNORW"):
189
+ blocks = line.split(",")
190
+ timestamp = blocks[1] + blocks[2]
191
+ Hm0 = float(blocks[5])
192
+ H3 = float(blocks[6])
193
+ H10 = float(blocks[7])
194
+ Hmax = float(blocks[8])
195
+ Tm02 = float(blocks[9])
196
+ Tp = float(blocks[10])
197
+ Tz = float(blocks[11])
198
+ DirTp = float(blocks[12])
199
+ SprTp = float(blocks[13])
200
+ MainDir = float(blocks[14])
201
+ UnidirectivityIndex = float(blocks[15])
202
+ MeanPressure = float(blocks[16])
203
+ NearSurfaceCurrentSpeed = float(blocks[19])
204
+ NearSurfaceCurrentDirection = float(blocks[20])
205
+
206
+ # yield as dict
207
+ yield {
208
+ "timestamp": timestamp,
209
+ "Hm0": Hm0,
210
+ "H3": H3,
211
+ "H10": H10,
212
+ "Hmax": Hmax,
213
+ "Tm02": Tm02,
214
+ "Tp": Tp,
215
+ "Tz": Tz,
216
+ "DirTp": DirTp,
217
+ "SprTp": SprTp,
218
+ "MainDir": MainDir,
219
+ "UnidirectivityIndex": UnidirectivityIndex,
220
+ "MeanPressure": MeanPressure,
221
+ "NearSurfaceCurrentSpeed": NearSurfaceCurrentSpeed,
222
+ "NearSurfaceCurrentDirection": NearSurfaceCurrentDirection,
223
+ }
224
+
225
+
226
+ def parse_awac_nmea(lines):
227
+
228
+ # scan to find frequency grid
229
+ freq = None
230
+
231
+ # read spectral frequencies from PNORF
232
+ for line in lines:
233
+ if line.startswith("$PNORF"):
234
+ blocks = line.split(",")
235
+ fstart = float(blocks[5])
236
+ fstep = float(blocks[6])
237
+ fn = int(blocks[7])
238
+
239
+ freq = fstart + np.linspace(start=0, stop=fn * fstep, num=fn, endpoint=True)
240
+
241
+ break
242
+
243
+ if freq is None:
244
+ raise ValueError("Can not determine frequency grid, $PNORF field not found")
245
+
246
+ # read spectral frequencies from PNORE
247
+ ok = False
248
+ for line in lines:
249
+ if line.startswith("$PNORE"):
250
+ blocks = line.split(",")
251
+ fstart2 = float(blocks[4])
252
+ fstep2 = float(blocks[5])
253
+
254
+ # assert that the frequency grid is the same
255
+ assert (
256
+ fstart == fstart2
257
+ ), "Frequency start does not match between $PNORF and $PNORE"
258
+ assert (
259
+ fstep == fstep2
260
+ ), "Frequency step does not match between $PNORF and $PNORE"
261
+
262
+ ok = True
263
+ break
264
+ if not ok:
265
+ raise ValueError("Can not determine frequency grid, $PNORE field not found")
266
+
267
+ A1 = None
268
+ A2 = None
269
+ B1 = None
270
+ B2 = None
271
+ S = None
272
+ Hs = None
273
+ timestamp = None
274
+
275
+ for line in lines:
276
+ line = line.split("*")[0]
277
+
278
+ # new timestamp?
279
+ TS = None
280
+ if line.startswith("$PNORF"):
281
+ TS = line[10:16] + line[17:23]
282
+ elif line.startswith("$PNORE"):
283
+ TS = line[7:13] + line[14:20]
284
+ elif line.startswith("$PNORW"):
285
+ blocks = line.split(",")
286
+ TS = blocks[1] + blocks[2]
287
+
288
+ if TS is not None:
289
+ if TS != timestamp:
290
+ if A1 is not None:
291
+ yield timestamp, freq, A1, A2, B1, B2, S, Hs
292
+
293
+ timestamp = TS
294
+ A1 = None
295
+ A2 = None
296
+ B1 = None
297
+ B2 = None
298
+ S = None
299
+ Hs = None
300
+
301
+ if line.startswith("$PNORF"):
302
+ # remove the checksum
303
+ blocks = line.split(",")
304
+ if blocks[1] == "A1":
305
+ if A1 is not None:
306
+ warnings.warn(
307
+ f"Duplicate A1 spectrum found for timestamp {timestamp}"
308
+ )
309
+ A1 = np.array([float(x) for x in blocks[8:]])
310
+ elif blocks[1] == "A2":
311
+ if A2 is not None:
312
+ warnings.warn(
313
+ f"Duplicate A2 spectrum found for timestamp {timestamp}"
314
+ )
315
+ A2 = np.array([float(x) for x in blocks[8:]])
316
+ elif blocks[1] == "B1":
317
+ if B1 is not None:
318
+ warnings.warn(
319
+ f"Duplicate B1 spectrum found for timestamp {timestamp}"
320
+ )
321
+ B1 = np.array([float(x) for x in blocks[8:]])
322
+ elif blocks[1] == "B2":
323
+ if B2 is not None:
324
+ warnings.warn(
325
+ f"Duplicate B2 spectrum found for timestamp {timestamp}"
326
+ )
327
+ B2 = np.array([float(x) for x in blocks[8:]])
328
+ elif line.startswith("$PNORE"):
329
+ blocks = line.split(",")
330
+ n = int(blocks[6])
331
+ S = np.array([float(x) for x in blocks[7:]])
332
+ if len(S) != n:
333
+ warnings.warn(
334
+ f"Number of frequencies in spectrum does not match number of frequencies expected {n} got {len(S)}"
335
+ )
336
+ elif line.startswith("$PNORW"):
337
+ blocks = line.split(",")
338
+ Hs = float(blocks[5])
339
+
340
+ if A1 is not None:
341
+ yield timestamp, freq, A1, A2, B1, B2, S, Hs
342
+
343
+
344
+ def read_awac_strings(
345
+ lines,
346
+ nDirs=90,
347
+ ):
348
+ """Parses AWAC NMEA data into directional spectra."""
349
+
350
+ data = [R for R in parse_awac_nmea(lines)]
351
+
352
+ # each line of data contains a timestamp, freq, A1, A2, B1, B2, S
353
+ # the first and last line may contain Nones for A1, A2, B1, B2, S if the file was incomplete. In that case we remove them
354
+
355
+ if any(x is None for x in data[0]):
356
+ warnings.warn(
357
+ f"First spectrum of AWAC data incomplete, removed spectrum with timestamp {data[0][0]}"
358
+ )
359
+ data = data[1:]
360
+ if any(x is None for x in data[-1]):
361
+ warnings.warn(
362
+ f"Last spectrum of AWAC data incomplete, removed with timestamp {data[0][0]}"
363
+ )
364
+ data = data[:-1]
365
+
366
+ # convert to directional spectra
367
+ dDir = np.linspace(0, 2 * np.pi, num=nDirs, endpoint=False)
368
+ dir_step = dDir[1] - dDir[0]
369
+
370
+ efth = []
371
+ times = []
372
+
373
+ for timestamp, freq, aA1, aA2, aB1, aB2, S, Hs in data:
374
+ # %(conversion from Cart to Compass)
375
+ # A1temp = A1.copy()
376
+ # B1temp = B1.copy()
377
+ #
378
+ # print('A1 = [' + ','.join([str(a) for a in aA1]) + ']')
379
+ # print('A2 = [' + ','.join([str(a) for a in aA2]) + ']')
380
+ # print('B1 = [' + ','.join([str(a) for a in aB1]) + ']')
381
+ # print('B2 = [' + ','.join([str(a) for a in aB2]) + ']')
382
+ # print('S = [' + ','.join([str(a) for a in S]) + ']')
383
+
384
+ A1 = -aB1.copy()
385
+ B1 = -aA1.copy()
386
+ A2 = -aA2.copy()
387
+ B2 = aB2.copy()
388
+
389
+ # Direction From
390
+ dr = np.pi
391
+ A1new = np.cos(dr) * A1 + np.sin(dr) * B1
392
+ B1new = -np.sin(dr) * A1 + np.cos(dr) * B1
393
+ A2new = np.cos(2 * dr) * A2 + np.sin(2 * dr) * B2
394
+ B2new = -np.sin(2 * dr) * A2 + np.cos(2 * dr) * B2
395
+
396
+ # Now apply maximum entropy method to remove negative energy
397
+ # Initialize the directional field
398
+ DirField = np.zeros((len(freq), len(dDir)))
399
+
400
+ # Apply maximum entropy method to remove negative energy
401
+ for iFreq in range(len(freq)):
402
+
403
+ c1 = A1new[iFreq] + 1j * B1new[iFreq]
404
+ c2 = A2new[iFreq] + 1j * B2new[iFreq]
405
+
406
+ fi1 = (c1 - c2 * np.conj(c1)) / (1 - np.abs(c1) * np.abs(c1))
407
+ fi2 = c2 - c1 * fi1
408
+
409
+ # print(f"c1 = {c1}")
410
+ # print(f"c2 = {c2}")
411
+ # print(f"fi1 = {fi1}")
412
+ # print(f"fi2 = {fi2}")
413
+
414
+ factors = []
415
+ for dd in range(len(dDir)):
416
+
417
+ dNewdir = np.pi / 2 - dDir[dd]
418
+ numer = 1 - fi1 * np.conj(c1) - fi2 * np.conj(c2)
419
+ denom = (
420
+ 1 - fi1 * np.exp(-1j * dNewdir) - fi2 * np.exp(-2.0 * 1j * dNewdir)
421
+ )
422
+
423
+ dir_factor = np.real(numer / (np.abs(denom) * np.abs(denom)))
424
+
425
+ scale1 = 1 / (2 * np.pi)
426
+ scale2 = dir_step
427
+
428
+ scale = scale1 * scale2
429
+
430
+ factor = scale * dir_factor
431
+
432
+ DirField[iFreq, dd] = factor * S[iFreq]
433
+ #
434
+ # print("======================")
435
+ # print(f"dd = {dd + 1}")
436
+ # print(f"dNewdir = {dNewdir}")
437
+ # print(f"numer = {numer}")
438
+ # print(f"denom = {denom}")
439
+ # print(f"dir_factor = {dir_factor}")
440
+
441
+ factors.append(factor)
442
+
443
+ # all factors should add up to 1.0, scale accordingly
444
+ # print(f"Sum of factors = {np.sum(factors)}")
445
+ # if np.sum(factors) > 1.1:
446
+ # print('Warning: sum of factors is larger than 1.0')
447
+
448
+ directional_scaling = 1.0 / np.sum(factors)
449
+ DirField[iFreq] *= directional_scaling
450
+
451
+ # Calculate m0 of the spectrum
452
+ Sd = [np.sum(DirField[iFreq]) for iFreq in range(len(freq))]
453
+ m0 = np.trapz(Sd, freq)
454
+ Hs_spectrum = 4 * np.sqrt(m0)
455
+
456
+ # scale the spectrum to the given Hs IF Hs_spectrum is not zero
457
+ if Hs_spectrum == 0:
458
+ if Hs > 0:
459
+ warnings.warn(
460
+ f"Spectrum with timestamp {timestamp} has zero significant wave height, can not scale to Hs = {Hs}m"
461
+ )
462
+ else:
463
+ DirField *= (Hs / Hs_spectrum) ** 2
464
+
465
+ # in wavespecrta we work in:
466
+ # m
467
+ # Hz
468
+ # degrees
469
+ # m2/Hz so that is already ok
470
+ dir_step_deg = np.degrees(dir_step)
471
+ efth.append(DirField / dir_step_deg)
472
+
473
+ ts = datetime.datetime.strptime(timestamp, "%m%d%y%H%M%S")
474
+ assert ts.tzinfo is None
475
+ times.append(ts)
476
+
477
+ # from matplotlib import pyplot as plt
478
+ # plt.plot(Hss, label = 'integrated directional spectra')
479
+
480
+ dir_degrees = np.degrees(dDir)
481
+ freq = data[0][1]
482
+
483
+ ds = xr.DataArray(
484
+ data=efth,
485
+ coords={"time": times, "freq": freq, "dir": dir_degrees},
486
+ dims=("time", "freq", "dir"),
487
+ name="efth",
488
+ ).to_dataset()
489
+
490
+ # Set attributes
491
+ set_spec_attributes(ds)
492
+
493
+ # add site dimension
494
+ ds["site"] = [0]
495
+
496
+ return ds