microstructure-tpw 0.3.0__py3-none-any.whl
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.
- microstructure_tpw-0.3.0.dist-info/METADATA +234 -0
- microstructure_tpw-0.3.0.dist-info/RECORD +119 -0
- microstructure_tpw-0.3.0.dist-info/WHEEL +5 -0
- microstructure_tpw-0.3.0.dist-info/entry_points.txt +7 -0
- microstructure_tpw-0.3.0.dist-info/licenses/LICENSE +674 -0
- microstructure_tpw-0.3.0.dist-info/top_level.txt +1 -0
- odas_tpw/__init__.py +18 -0
- odas_tpw/chi/__init__.py +48 -0
- odas_tpw/chi/batchelor.py +189 -0
- odas_tpw/chi/chi.py +857 -0
- odas_tpw/chi/fp07.py +649 -0
- odas_tpw/chi/l2_chi.py +153 -0
- odas_tpw/chi/l3_chi.py +388 -0
- odas_tpw/chi/l4_chi.py +391 -0
- odas_tpw/config_base.py +423 -0
- odas_tpw/perturb/__init__.py +18 -0
- odas_tpw/perturb/adcp.py +274 -0
- odas_tpw/perturb/atomic_io.py +47 -0
- odas_tpw/perturb/binning.py +761 -0
- odas_tpw/perturb/cli.py +591 -0
- odas_tpw/perturb/combo.py +346 -0
- odas_tpw/perturb/config.py +1046 -0
- odas_tpw/perturb/ctd.py +424 -0
- odas_tpw/perturb/diag/__init__.py +12 -0
- odas_tpw/perturb/diag/_common.py +140 -0
- odas_tpw/perturb/diag/chi.py +42 -0
- odas_tpw/perturb/diag/cli.py +67 -0
- odas_tpw/perturb/diag/data.py +662 -0
- odas_tpw/perturb/diag/epsilon.py +45 -0
- odas_tpw/perturb/diag/inspector.py +342 -0
- odas_tpw/perturb/diag/mixing.py +94 -0
- odas_tpw/perturb/diag/render.py +543 -0
- odas_tpw/perturb/discover.py +40 -0
- odas_tpw/perturb/fp07_cal.py +354 -0
- odas_tpw/perturb/gps.py +458 -0
- odas_tpw/perturb/hotel.py +572 -0
- odas_tpw/perturb/logging_setup.py +181 -0
- odas_tpw/perturb/merge.py +320 -0
- odas_tpw/perturb/mk_sections.py +310 -0
- odas_tpw/perturb/netcdf_schema.py +520 -0
- odas_tpw/perturb/pipeline.py +2654 -0
- odas_tpw/perturb/plot/__init__.py +15 -0
- odas_tpw/perturb/plot/cli.py +107 -0
- odas_tpw/perturb/plot/diagnostics.py +195 -0
- odas_tpw/perturb/plot/eps_chi.py +524 -0
- odas_tpw/perturb/plot/figure.py +679 -0
- odas_tpw/perturb/plot/gamma_scaling.py +1097 -0
- odas_tpw/perturb/plot/grid.py +112 -0
- odas_tpw/perturb/plot/layout.py +361 -0
- odas_tpw/perturb/plot/overview.py +547 -0
- odas_tpw/perturb/plot/presets/eps-chi.yaml +16 -0
- odas_tpw/perturb/plot/presets/profiles.yaml +32 -0
- odas_tpw/perturb/plot/presets/scalar.yaml +21 -0
- odas_tpw/perturb/plot/profiles.py +652 -0
- odas_tpw/perturb/plot/scalar.py +391 -0
- odas_tpw/perturb/plot/sections.py +506 -0
- odas_tpw/perturb/plot/xaxis.py +396 -0
- odas_tpw/perturb/qc_gate.py +177 -0
- odas_tpw/perturb/qc_rules.py +373 -0
- odas_tpw/perturb/resolve.py +306 -0
- odas_tpw/perturb/seawater.py +76 -0
- odas_tpw/perturb/speed.py +14 -0
- odas_tpw/perturb/trim.py +313 -0
- odas_tpw/processing/__init__.py +62 -0
- odas_tpw/processing/bottom.py +215 -0
- odas_tpw/processing/chi_combine.py +300 -0
- odas_tpw/processing/ct_align.py +172 -0
- odas_tpw/processing/epsilon_combine.py +210 -0
- odas_tpw/processing/mixing.py +630 -0
- odas_tpw/processing/thorpe.py +458 -0
- odas_tpw/processing/top_trim.py +274 -0
- odas_tpw/pyturb/__init__.py +3 -0
- odas_tpw/pyturb/__main__.py +5 -0
- odas_tpw/pyturb/_compat.py +204 -0
- odas_tpw/pyturb/_profind.py +124 -0
- odas_tpw/pyturb/bin.py +113 -0
- odas_tpw/pyturb/cli.py +175 -0
- odas_tpw/pyturb/eps.py +353 -0
- odas_tpw/pyturb/merge.py +101 -0
- odas_tpw/pyturb/p2nc.py +76 -0
- odas_tpw/rsi/__init__.py +31 -0
- odas_tpw/rsi/adapter.py +314 -0
- odas_tpw/rsi/bench.py +904 -0
- odas_tpw/rsi/binning.py +193 -0
- odas_tpw/rsi/channels.py +376 -0
- odas_tpw/rsi/chi_io.py +980 -0
- odas_tpw/rsi/cli.py +1411 -0
- odas_tpw/rsi/combine.py +120 -0
- odas_tpw/rsi/config.py +126 -0
- odas_tpw/rsi/config_patch.py +790 -0
- odas_tpw/rsi/convert.py +651 -0
- odas_tpw/rsi/deconvolve.py +119 -0
- odas_tpw/rsi/diss_look.py +343 -0
- odas_tpw/rsi/dissipation.py +669 -0
- odas_tpw/rsi/helpers.py +527 -0
- odas_tpw/rsi/mixing_look.py +509 -0
- odas_tpw/rsi/p_file.py +771 -0
- odas_tpw/rsi/pipeline.py +924 -0
- odas_tpw/rsi/profile.py +624 -0
- odas_tpw/rsi/quick_look.py +758 -0
- odas_tpw/rsi/sensor_inventory.py +684 -0
- odas_tpw/rsi/shear_noise.py +128 -0
- odas_tpw/rsi/speed.py +221 -0
- odas_tpw/rsi/vehicle.py +74 -0
- odas_tpw/rsi/viewer_base.py +1071 -0
- odas_tpw/rsi/window.py +459 -0
- odas_tpw/scor160/__init__.py +36 -0
- odas_tpw/scor160/cli.py +242 -0
- odas_tpw/scor160/compare.py +484 -0
- odas_tpw/scor160/despike.py +227 -0
- odas_tpw/scor160/goodman.py +288 -0
- odas_tpw/scor160/io.py +690 -0
- odas_tpw/scor160/l2.py +311 -0
- odas_tpw/scor160/l3.py +279 -0
- odas_tpw/scor160/l4.py +850 -0
- odas_tpw/scor160/nasmyth.py +208 -0
- odas_tpw/scor160/ocean.py +287 -0
- odas_tpw/scor160/profile.py +231 -0
- odas_tpw/scor160/spectral.py +482 -0
odas_tpw/scor160/io.py
ADDED
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
"""I/O for ATOMIX-format NetCDF benchmark files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import NamedTuple
|
|
8
|
+
|
|
9
|
+
import netCDF4
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class L1Data:
|
|
15
|
+
"""Level-1 converted time series in physical units."""
|
|
16
|
+
|
|
17
|
+
time: np.ndarray # (N_TIME,), decimal days
|
|
18
|
+
pres: np.ndarray # (N_TIME,), dbar
|
|
19
|
+
shear: np.ndarray # (N_SHEAR, N_TIME), s⁻¹
|
|
20
|
+
# Vibration/acceleration — whichever the instrument provides
|
|
21
|
+
vib: np.ndarray # (N_VIB, N_TIME)
|
|
22
|
+
vib_type: str # "ACC" or "VIB"
|
|
23
|
+
|
|
24
|
+
fs_fast: float # fast sampling rate [Hz]
|
|
25
|
+
f_AA: float # anti-alias filter cutoff [Hz]
|
|
26
|
+
vehicle: str # "vmp", "mss", etc.
|
|
27
|
+
profile_dir: str # "down", "up", or "horizontal"
|
|
28
|
+
time_reference_year: int
|
|
29
|
+
|
|
30
|
+
# Optional: pre-computed speed (e.g. Epsifish, MSS provide this in L1)
|
|
31
|
+
pspd_rel: np.ndarray = field(default_factory=lambda: np.array([]))
|
|
32
|
+
|
|
33
|
+
# Optional slow-rate arrays (may be empty)
|
|
34
|
+
time_slow: np.ndarray = field(default_factory=lambda: np.array([]))
|
|
35
|
+
pres_slow: np.ndarray = field(default_factory=lambda: np.array([]))
|
|
36
|
+
pitch: np.ndarray = field(default_factory=lambda: np.array([]))
|
|
37
|
+
roll: np.ndarray = field(default_factory=lambda: np.array([]))
|
|
38
|
+
temp: np.ndarray = field(default_factory=lambda: np.array([]))
|
|
39
|
+
fs_slow: float = 0.0
|
|
40
|
+
|
|
41
|
+
# Optional fast-rate FP07 temperature (for chi computation)
|
|
42
|
+
temp_fast: np.ndarray = field(default_factory=lambda: np.zeros((0, 0)))
|
|
43
|
+
diff_gains: list[float] = field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
# Optional fast-rate practical salinity, derived from CTD conductivity when
|
|
46
|
+
# the .p file carries a JAC C/T pair (VMP). Empty for instruments without
|
|
47
|
+
# conductivity at the .p level (e.g. MicroRiders), in which case the
|
|
48
|
+
# stratification falls back to a supplied/assumed salinity.
|
|
49
|
+
salinity: np.ndarray = field(default_factory=lambda: np.array([]))
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def n_shear(self) -> int:
|
|
53
|
+
"""Number of shear probe channels."""
|
|
54
|
+
return self.shear.shape[0]
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def n_vib(self) -> int:
|
|
58
|
+
"""Number of vibration/accelerometer channels."""
|
|
59
|
+
return self.vib.shape[0]
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def n_time(self) -> int:
|
|
63
|
+
"""Number of fast-rate time samples."""
|
|
64
|
+
return self.time.shape[0]
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def has_speed(self) -> bool:
|
|
68
|
+
"""Whether pre-computed profiling speed is available."""
|
|
69
|
+
return self.pspd_rel.size > 0
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def n_temp(self) -> int:
|
|
73
|
+
"""Number of fast thermistor channels."""
|
|
74
|
+
return self.temp_fast.shape[0] if self.temp_fast.ndim == 2 else 0
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def has_temp_fast(self) -> bool:
|
|
78
|
+
"""Whether fast-rate temperature data is available."""
|
|
79
|
+
return self.temp_fast.size > 0 and self.temp_fast.ndim == 2
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class L2Params:
|
|
84
|
+
"""Processing parameters for L1→L2, read from L2_cleaned group attributes."""
|
|
85
|
+
|
|
86
|
+
HP_cut: float # high-pass cutoff frequency [Hz]
|
|
87
|
+
despike_sh: np.ndarray # [threshold, smooth_freq, half_width_frac]
|
|
88
|
+
despike_A: np.ndarray # same for accelerometers
|
|
89
|
+
profile_min_W: float # minimum speed [m/s]
|
|
90
|
+
profile_min_P: float # minimum pressure [dbar]
|
|
91
|
+
profile_min_duration: float # minimum section duration [s]
|
|
92
|
+
speed_tau: float # speed smoothing time constant [s] (ODAS default 1.5)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class L2Data:
|
|
97
|
+
"""Level-2 cleaned time series."""
|
|
98
|
+
|
|
99
|
+
time: np.ndarray
|
|
100
|
+
shear: np.ndarray # (N_SHEAR, N_TIME), despiked + HP-filtered
|
|
101
|
+
vib: np.ndarray # (N_VIB, N_TIME), despiked + HP-filtered
|
|
102
|
+
vib_type: str
|
|
103
|
+
pspd_rel: np.ndarray # (N_TIME,), profiling speed [m/s]
|
|
104
|
+
section_number: np.ndarray # (N_TIME,), 0 = excluded
|
|
105
|
+
# Despike bookkeeping for ATOMIX QC flags (empty when despiking was
|
|
106
|
+
# disabled or for benchmark reference reads): per-sample
|
|
107
|
+
# replaced-by-despike masks, and pass counts per (channel, section)
|
|
108
|
+
# — the benchmark's PASS_COUNT_SH layout, with the section ids in
|
|
109
|
+
# ``despike_section_ids``.
|
|
110
|
+
despike_mask_sh: np.ndarray = field(default_factory=lambda: np.zeros((0, 0), dtype=bool))
|
|
111
|
+
despike_passes_sh: np.ndarray = field(
|
|
112
|
+
default_factory=lambda: np.zeros((0, 0), dtype=np.int64)
|
|
113
|
+
)
|
|
114
|
+
despike_mask_A: np.ndarray = field(default_factory=lambda: np.zeros((0, 0), dtype=bool))
|
|
115
|
+
despike_passes_A: np.ndarray = field(
|
|
116
|
+
default_factory=lambda: np.zeros((0, 0), dtype=np.int64)
|
|
117
|
+
)
|
|
118
|
+
despike_section_ids: np.ndarray = field(
|
|
119
|
+
default_factory=lambda: np.zeros(0, dtype=np.int64)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class L3Params:
|
|
125
|
+
"""Processing parameters for L2→L3, read from L3_spectra group attributes."""
|
|
126
|
+
|
|
127
|
+
fft_length: int # FFT segment length [samples]
|
|
128
|
+
diss_length: int # dissipation window length [samples]
|
|
129
|
+
overlap: int # dissipation window overlap [samples]
|
|
130
|
+
HP_cut: float # high-pass cutoff frequency [Hz]
|
|
131
|
+
fs_fast: float # fast sampling rate [Hz]
|
|
132
|
+
goodman: bool # whether Goodman cleaning was applied
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class L3Data:
|
|
137
|
+
"""Level-3 wavenumber spectra."""
|
|
138
|
+
|
|
139
|
+
time: np.ndarray # (N_SPECTRA,), center time of each window
|
|
140
|
+
pres: np.ndarray # (N_SPECTRA,), mean pressure per window
|
|
141
|
+
temp: np.ndarray # (N_SPECTRA,), mean temperature per window
|
|
142
|
+
pspd_rel: np.ndarray # (N_SPECTRA,), mean speed per window
|
|
143
|
+
section_number: np.ndarray # (N_SPECTRA,)
|
|
144
|
+
kcyc: np.ndarray # (N_WAVENUMBER, N_SPECTRA), wavenumber [cpm]
|
|
145
|
+
sh_spec: np.ndarray # (N_SHEAR, N_WAVENUMBER, N_SPECTRA), raw spectrum
|
|
146
|
+
sh_spec_clean: np.ndarray # (N_SHEAR, N_WAVENUMBER, N_SPECTRA), Goodman-cleaned
|
|
147
|
+
# Despike bookkeeping for ATOMIX QC flags (empty for benchmark
|
|
148
|
+
# reference reads): per-window fraction of shear samples replaced by
|
|
149
|
+
# despiking, and the pass count of each window's section.
|
|
150
|
+
despike_fraction: np.ndarray = field(
|
|
151
|
+
default_factory=lambda: np.zeros((0, 0))
|
|
152
|
+
) # (N_SHEAR, N_SPECTRA)
|
|
153
|
+
despike_passes: np.ndarray = field(
|
|
154
|
+
default_factory=lambda: np.zeros((0, 0), dtype=np.int64)
|
|
155
|
+
) # (N_SHEAR, N_SPECTRA)
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def n_spectra(self) -> int:
|
|
159
|
+
"""Number of spectral windows."""
|
|
160
|
+
return self.time.shape[0]
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def n_wavenumber(self) -> int:
|
|
164
|
+
"""Number of wavenumber bins."""
|
|
165
|
+
return self.kcyc.shape[0]
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def n_shear(self) -> int:
|
|
169
|
+
"""Number of shear probe channels."""
|
|
170
|
+
return self.sh_spec.shape[0]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass
|
|
174
|
+
class L4Params:
|
|
175
|
+
"""Processing parameters for L3→L4, read from L4_dissipation group attributes."""
|
|
176
|
+
|
|
177
|
+
fft_length: int # FFT segment length [samples]
|
|
178
|
+
diss_length: int # dissipation window length [samples]
|
|
179
|
+
overlap: int # dissipation window overlap [samples]
|
|
180
|
+
fs_fast: float # fast sampling rate [Hz]
|
|
181
|
+
fit_order: int # polynomial order for spectral minimum detection
|
|
182
|
+
f_AA: float # anti-alias filter cutoff [Hz]
|
|
183
|
+
FOM_limit: float # QC threshold for figure of merit
|
|
184
|
+
variance_resolved_limit: float # QC threshold for variance resolved
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class L4Data:
|
|
189
|
+
"""Level-4 dissipation estimates."""
|
|
190
|
+
|
|
191
|
+
time: np.ndarray # (N_SPECTRA,)
|
|
192
|
+
pres: np.ndarray # (N_SPECTRA,)
|
|
193
|
+
pspd_rel: np.ndarray # (N_SPECTRA,)
|
|
194
|
+
section_number: np.ndarray # (N_SPECTRA,)
|
|
195
|
+
epsi: np.ndarray # (N_SHEAR, N_SPECTRA), epsilon per probe
|
|
196
|
+
epsi_final: np.ndarray # (N_SPECTRA,), combined epsilon
|
|
197
|
+
epsi_flags: np.ndarray # (N_SHEAR, N_SPECTRA), QC flags
|
|
198
|
+
# NOTE: dual-use depending on the constructor. _read_l4 (benchmark reference
|
|
199
|
+
# load) fills this with the benchmark FOM = ATOMIX/Lueck (2022)
|
|
200
|
+
# MAD-normalized statistic; the compute path (process_l4) fills it with the
|
|
201
|
+
# obs/Nasmyth variance-ratio fom. They are different statistics — check the
|
|
202
|
+
# source before thresholding (the 1.15 limit belongs to the Lueck FM).
|
|
203
|
+
fom: np.ndarray # (N_SHEAR, N_SPECTRA), FOM — see note (benchmark FM vs variance-ratio fom)
|
|
204
|
+
mad: np.ndarray # (N_SHEAR, N_SPECTRA), mean absolute deviation
|
|
205
|
+
kmax: np.ndarray # (N_SHEAR, N_SPECTRA), upper integration limit
|
|
206
|
+
method: np.ndarray # (N_SHEAR, N_SPECTRA), 0=variance, 1=ISR
|
|
207
|
+
var_resolved: np.ndarray # (N_SHEAR, N_SPECTRA), fraction of variance resolved
|
|
208
|
+
FM: np.ndarray | None = None # (N_SHEAR, N_SPECTRA), Lueck (2022) MAD-based FM statistic
|
|
209
|
+
despike_fraction: np.ndarray | None = None # (N_SHEAR, N_SPECTRA), per-window
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def n_spectra(self) -> int:
|
|
213
|
+
"""Number of spectral windows."""
|
|
214
|
+
return self.time.shape[0]
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def n_shear(self) -> int:
|
|
218
|
+
"""Number of shear probe channels."""
|
|
219
|
+
return self.epsi.shape[0]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class AtomixData(NamedTuple):
|
|
223
|
+
"""All data from an ATOMIX benchmark NetCDF file."""
|
|
224
|
+
|
|
225
|
+
l1: L1Data
|
|
226
|
+
l2_params: L2Params
|
|
227
|
+
l2_ref: L2Data
|
|
228
|
+
l3_params: L3Params
|
|
229
|
+
l3_ref: L3Data
|
|
230
|
+
l4_ref: L4Data
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def read_l4_qc_limits(path: str | Path) -> dict[str, float]:
|
|
234
|
+
"""Read the dataset's own QC thresholds from the L4 group attributes.
|
|
235
|
+
|
|
236
|
+
The ATOMIX benchmark files store per-dataset QC limits
|
|
237
|
+
(``FOM_limit``, ``variance_resolved_limit``,
|
|
238
|
+
``despike_shear_fraction_limit``, ``despike_shear_iterations_limit``,
|
|
239
|
+
``diss_ratio_limit``) plus ``diss_length``/``fs_fast``; comparing
|
|
240
|
+
computed flags against the reference is only meaningful with the
|
|
241
|
+
same limits. Missing attributes fall back to the package defaults.
|
|
242
|
+
"""
|
|
243
|
+
from odas_tpw.scor160.l4 import (
|
|
244
|
+
DEFAULT_DESPIKE_FRACTION_LIMIT,
|
|
245
|
+
DEFAULT_DESPIKE_PASSES_LIMIT,
|
|
246
|
+
DEFAULT_DISS_RATIO_LIMIT,
|
|
247
|
+
DEFAULT_FOM_LIMIT,
|
|
248
|
+
DEFAULT_VAR_RESOLVED_LIMIT,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
defaults = {
|
|
252
|
+
"FOM_limit": DEFAULT_FOM_LIMIT,
|
|
253
|
+
"variance_resolved_limit": DEFAULT_VAR_RESOLVED_LIMIT,
|
|
254
|
+
"despike_shear_fraction_limit": DEFAULT_DESPIKE_FRACTION_LIMIT,
|
|
255
|
+
"despike_shear_iterations_limit": float(DEFAULT_DESPIKE_PASSES_LIMIT),
|
|
256
|
+
"diss_ratio_limit": DEFAULT_DISS_RATIO_LIMIT,
|
|
257
|
+
}
|
|
258
|
+
out: dict[str, float] = {}
|
|
259
|
+
ds = netCDF4.Dataset(str(path), "r")
|
|
260
|
+
try:
|
|
261
|
+
g = ds.groups.get("L4_dissipation")
|
|
262
|
+
for name, default in defaults.items():
|
|
263
|
+
val = getattr(g, name, None) if g is not None else None
|
|
264
|
+
out[name] = float(val) if val is not None else default
|
|
265
|
+
# diss window duration [s] for the sigma_ln model
|
|
266
|
+
diss_len = getattr(g, "diss_length", None) if g is not None else None
|
|
267
|
+
fs = getattr(g, "fs_fast", None) if g is not None else None
|
|
268
|
+
out["diss_length_s"] = (
|
|
269
|
+
float(diss_len) / float(fs) if diss_len is not None and fs else 0.0
|
|
270
|
+
)
|
|
271
|
+
finally:
|
|
272
|
+
ds.close()
|
|
273
|
+
return out
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def read_atomix(path: str | Path) -> AtomixData:
|
|
277
|
+
"""Read an ATOMIX benchmark NetCDF file.
|
|
278
|
+
|
|
279
|
+
Returns L1 data, L2 processing parameters, reference L2 result,
|
|
280
|
+
L3 processing parameters, reference L3 result, and reference L4 result.
|
|
281
|
+
"""
|
|
282
|
+
path = Path(path)
|
|
283
|
+
ds = netCDF4.Dataset(str(path), "r")
|
|
284
|
+
try:
|
|
285
|
+
l1 = _read_l1(ds)
|
|
286
|
+
l2_params = _read_l2_params(ds, l1)
|
|
287
|
+
l2_ref = _read_l2(ds)
|
|
288
|
+
l3_params = _read_l3_params(ds)
|
|
289
|
+
l3_ref = _read_l3(ds)
|
|
290
|
+
l4_ref = _read_l4(ds)
|
|
291
|
+
|
|
292
|
+
# If L1 has no speed but L2 does (e.g. Nemo: speed from hotel file),
|
|
293
|
+
# copy L2 reference speed into L1 so process_l2 can use it.
|
|
294
|
+
if not l1.has_speed and l2_ref.pspd_rel.shape[0] == l1.n_time:
|
|
295
|
+
l1.pspd_rel = l2_ref.pspd_rel.copy()
|
|
296
|
+
finally:
|
|
297
|
+
ds.close()
|
|
298
|
+
return AtomixData(l1, l2_params, l2_ref, l3_params, l3_ref, l4_ref)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
# Helpers for resolving attributes across groups
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _resolve_attr(ds: netCDF4.Dataset, name: str, default=None):
|
|
307
|
+
"""Look for an attribute in L1_converted, then L2_cleaned, then root."""
|
|
308
|
+
for source in [ds.groups["L1_converted"], ds.groups["L2_cleaned"], ds]:
|
|
309
|
+
if name in {a for a in source.ncattrs()}:
|
|
310
|
+
return source.getncattr(name)
|
|
311
|
+
return default
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ---------------------------------------------------------------------------
|
|
315
|
+
# L1 reader
|
|
316
|
+
# ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _read_l1(ds: netCDF4.Dataset) -> L1Data:
|
|
320
|
+
g = ds.groups["L1_converted"]
|
|
321
|
+
|
|
322
|
+
time = np.asarray(g.variables["TIME"][:], dtype=np.float64)
|
|
323
|
+
shear = np.asarray(g.variables["SHEAR"][:], dtype=np.float64)
|
|
324
|
+
|
|
325
|
+
# Pressure may be on a slower grid (e.g. Epsifish: TIME_CTD)
|
|
326
|
+
pres_var = g.variables["PRES"]
|
|
327
|
+
pres_raw = np.asarray(pres_var[:], dtype=np.float64)
|
|
328
|
+
if pres_raw.shape[0] == len(time):
|
|
329
|
+
pres = pres_raw
|
|
330
|
+
else:
|
|
331
|
+
pres_dim = pres_var.dimensions[0]
|
|
332
|
+
if pres_dim in g.variables:
|
|
333
|
+
time_pres = np.asarray(g.variables[pres_dim][:], dtype=np.float64)
|
|
334
|
+
pres = np.interp(time, time_pres, pres_raw)
|
|
335
|
+
else:
|
|
336
|
+
pres = np.interp(
|
|
337
|
+
np.linspace(0, 1, len(time)),
|
|
338
|
+
np.linspace(0, 1, len(pres_raw)),
|
|
339
|
+
pres_raw,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# ACC or VIB
|
|
343
|
+
if "ACC" in g.variables:
|
|
344
|
+
vib = np.asarray(g.variables["ACC"][:], dtype=np.float64)
|
|
345
|
+
vib_type = "ACC"
|
|
346
|
+
elif "VIB" in g.variables:
|
|
347
|
+
vib = np.asarray(g.variables["VIB"][:], dtype=np.float64)
|
|
348
|
+
vib_type = "VIB"
|
|
349
|
+
else:
|
|
350
|
+
vib = np.zeros((0, len(time)), dtype=np.float64)
|
|
351
|
+
vib_type = "NONE"
|
|
352
|
+
|
|
353
|
+
# Pre-computed speed (Epsifish, MSS provide this in L1).
|
|
354
|
+
# May be on a slower time grid (e.g. CTD rate) — interpolate to fast.
|
|
355
|
+
pspd_rel = np.array([])
|
|
356
|
+
if "PSPD_REL" in g.variables:
|
|
357
|
+
pspd_var = g.variables["PSPD_REL"]
|
|
358
|
+
pspd_raw = np.asarray(pspd_var[:], dtype=np.float64)
|
|
359
|
+
if pspd_raw.shape[0] == len(time):
|
|
360
|
+
pspd_rel = pspd_raw
|
|
361
|
+
else:
|
|
362
|
+
# On a different time grid — find the corresponding time variable
|
|
363
|
+
pspd_dims = pspd_var.dimensions
|
|
364
|
+
time_dim = pspd_dims[0] # e.g. "TIME_CTD"
|
|
365
|
+
if time_dim in g.variables:
|
|
366
|
+
time_slow_spd = np.asarray(g.variables[time_dim][:], dtype=np.float64)
|
|
367
|
+
pspd_rel = np.interp(time, time_slow_spd, pspd_raw)
|
|
368
|
+
else:
|
|
369
|
+
# Fallback: assume uniform spacing, just resample
|
|
370
|
+
pspd_rel = np.interp(
|
|
371
|
+
np.linspace(0, 1, len(time)),
|
|
372
|
+
np.linspace(0, 1, len(pspd_raw)),
|
|
373
|
+
pspd_raw,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Optional slow-rate channels
|
|
377
|
+
time_slow = np.array([])
|
|
378
|
+
pres_slow = np.array([])
|
|
379
|
+
pitch = np.array([])
|
|
380
|
+
roll = np.array([])
|
|
381
|
+
temp = np.array([])
|
|
382
|
+
|
|
383
|
+
if "TIME_SLOW" in g.variables:
|
|
384
|
+
time_slow = np.asarray(g.variables["TIME_SLOW"][:], dtype=np.float64)
|
|
385
|
+
if "PRES_SLOW" in g.variables:
|
|
386
|
+
pres_slow = np.asarray(g.variables["PRES_SLOW"][:], dtype=np.float64)
|
|
387
|
+
if "PITCH" in g.variables:
|
|
388
|
+
pitch = np.asarray(g.variables["PITCH"][:], dtype=np.float64)
|
|
389
|
+
if "ROLL" in g.variables:
|
|
390
|
+
roll = np.asarray(g.variables["ROLL"][:], dtype=np.float64)
|
|
391
|
+
if "TEMP" in g.variables:
|
|
392
|
+
temp = np.asarray(g.variables["TEMP"][:], dtype=np.float64)
|
|
393
|
+
# Some datasets (MSS Baltic, VMP2000 Faroe Bank) store TEMP as
|
|
394
|
+
# (N_TEMP_SENSORS, TIME); use the first sensor.
|
|
395
|
+
if temp.ndim == 2:
|
|
396
|
+
temp = temp[0] if temp.shape[0] <= temp.shape[1] else temp[:, 0]
|
|
397
|
+
# TEMP may be sampled on the slow grid; interpolate to the fast
|
|
398
|
+
# time grid so per-window means (and hence viscosity) are defined.
|
|
399
|
+
# Without this, L3 windows get temp=NaN -> visc35(NaN)=NaN -> all
|
|
400
|
+
# epsilon estimates NaN, silently.
|
|
401
|
+
if temp.shape[0] != len(time):
|
|
402
|
+
temp_dims = g.variables["TEMP"].dimensions
|
|
403
|
+
temp_dim = temp_dims[-1] if len(temp_dims) > 1 else temp_dims[0]
|
|
404
|
+
if temp_dim in g.variables:
|
|
405
|
+
time_temp = np.asarray(g.variables[temp_dim][:], dtype=np.float64)
|
|
406
|
+
elif time_slow.size == temp.shape[0]:
|
|
407
|
+
time_temp = time_slow
|
|
408
|
+
else:
|
|
409
|
+
time_temp = np.linspace(time[0], time[-1], temp.shape[0])
|
|
410
|
+
temp = np.interp(time, time_temp, temp)
|
|
411
|
+
|
|
412
|
+
fs_fast = float(_resolve_attr(ds, "fs_fast", 512.0))
|
|
413
|
+
fs_slow = float(_resolve_attr(ds, "fs_slow", 0.0))
|
|
414
|
+
|
|
415
|
+
return L1Data(
|
|
416
|
+
time=time,
|
|
417
|
+
pres=pres,
|
|
418
|
+
shear=shear,
|
|
419
|
+
vib=vib,
|
|
420
|
+
vib_type=vib_type,
|
|
421
|
+
fs_fast=fs_fast,
|
|
422
|
+
f_AA=float(_resolve_attr(ds, "f_AA", 98.0)),
|
|
423
|
+
vehicle=str(_resolve_attr(ds, "vehicle", "unknown")),
|
|
424
|
+
profile_dir=str(_resolve_attr(ds, "profile_dir", "down")),
|
|
425
|
+
time_reference_year=int(_resolve_attr(ds, "time_reference_year", 2000)),
|
|
426
|
+
pspd_rel=pspd_rel,
|
|
427
|
+
time_slow=time_slow,
|
|
428
|
+
pres_slow=pres_slow,
|
|
429
|
+
pitch=pitch,
|
|
430
|
+
roll=roll,
|
|
431
|
+
temp=temp,
|
|
432
|
+
fs_slow=fs_slow,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
# ---------------------------------------------------------------------------
|
|
437
|
+
# L2 params reader
|
|
438
|
+
# ---------------------------------------------------------------------------
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _read_l2_params(ds: netCDF4.Dataset, l1: L1Data) -> L2Params:
|
|
442
|
+
"""Read L2 processing parameters, falling back to sensible defaults.
|
|
443
|
+
|
|
444
|
+
Parameters are read from the L2_cleaned group attributes first, then
|
|
445
|
+
from global attributes (e.g. ``speed_cutout``), then defaults.
|
|
446
|
+
"""
|
|
447
|
+
g = ds.groups["L2_cleaned"]
|
|
448
|
+
attrs = {a: g.getncattr(a) for a in g.ncattrs()}
|
|
449
|
+
gattrs = {a: ds.getncattr(a) for a in ds.ncattrs()}
|
|
450
|
+
|
|
451
|
+
# Defaults per Lueck et al. (2024) recommendations.
|
|
452
|
+
# HP_cut from the global attrs has a different meaning (e.g. 0 for
|
|
453
|
+
# already-differentiated shear on epsifish) so only fall back to it
|
|
454
|
+
# for speed_cutout, not HP_cut.
|
|
455
|
+
HP_cut = float(attrs.get("HP_cut", 0.25))
|
|
456
|
+
despike_sh = np.asarray(attrs.get("despike_sh", [8.0, 0.5, 0.04]), dtype=np.float64)
|
|
457
|
+
despike_A = np.asarray(attrs.get("despike_A", [np.inf, 0.5, 0.04]), dtype=np.float64)
|
|
458
|
+
profile_min_W = float(attrs.get("profile_min_W", gattrs.get("speed_cutout", 0.1)))
|
|
459
|
+
profile_min_P = float(attrs.get("profile_min_P", 1.0))
|
|
460
|
+
profile_min_duration = float(attrs.get("profile_min_duration", 10.0))
|
|
461
|
+
speed_tau = float(attrs.get("speed_tau", gattrs.get("speed_tau", 1.5)))
|
|
462
|
+
|
|
463
|
+
return L2Params(
|
|
464
|
+
HP_cut=HP_cut,
|
|
465
|
+
despike_sh=despike_sh,
|
|
466
|
+
despike_A=despike_A,
|
|
467
|
+
profile_min_W=profile_min_W,
|
|
468
|
+
profile_min_P=profile_min_P,
|
|
469
|
+
profile_min_duration=profile_min_duration,
|
|
470
|
+
speed_tau=speed_tau,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# ---------------------------------------------------------------------------
|
|
475
|
+
# L2 reference reader
|
|
476
|
+
# ---------------------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _read_l2(ds: netCDF4.Dataset) -> L2Data:
|
|
480
|
+
g = ds.groups["L2_cleaned"]
|
|
481
|
+
time = np.asarray(g.variables["TIME"][:], dtype=np.float64)
|
|
482
|
+
|
|
483
|
+
shear = np.asarray(g.variables["SHEAR"][:], dtype=np.float64)
|
|
484
|
+
section_number = np.asarray(g.variables["SECTION_NUMBER"][:], dtype=np.float64)
|
|
485
|
+
|
|
486
|
+
# PSPD_REL may be on a slower time grid (e.g. Epsifish: TIME_CTD)
|
|
487
|
+
pspd_var = g.variables["PSPD_REL"]
|
|
488
|
+
pspd_raw = np.asarray(pspd_var[:], dtype=np.float64)
|
|
489
|
+
if pspd_raw.shape[0] == len(time):
|
|
490
|
+
pspd_rel = pspd_raw
|
|
491
|
+
else:
|
|
492
|
+
pspd_dim = pspd_var.dimensions[0]
|
|
493
|
+
if pspd_dim in g.variables:
|
|
494
|
+
time_pspd = np.asarray(g.variables[pspd_dim][:], dtype=np.float64)
|
|
495
|
+
pspd_rel = np.interp(time, time_pspd, pspd_raw)
|
|
496
|
+
else:
|
|
497
|
+
pspd_rel = np.interp(
|
|
498
|
+
np.linspace(0, 1, len(time)),
|
|
499
|
+
np.linspace(0, 1, len(pspd_raw)),
|
|
500
|
+
pspd_raw,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
if "ACC" in g.variables:
|
|
504
|
+
vib = np.asarray(g.variables["ACC"][:], dtype=np.float64)
|
|
505
|
+
vib_type = "ACC"
|
|
506
|
+
elif "VIB" in g.variables:
|
|
507
|
+
vib = np.asarray(g.variables["VIB"][:], dtype=np.float64)
|
|
508
|
+
vib_type = "VIB"
|
|
509
|
+
else:
|
|
510
|
+
vib = np.zeros((0, len(time)), dtype=np.float64)
|
|
511
|
+
vib_type = "NONE"
|
|
512
|
+
|
|
513
|
+
return L2Data(
|
|
514
|
+
time=time,
|
|
515
|
+
shear=shear,
|
|
516
|
+
vib=vib,
|
|
517
|
+
vib_type=vib_type,
|
|
518
|
+
pspd_rel=pspd_rel,
|
|
519
|
+
section_number=section_number,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
# ---------------------------------------------------------------------------
|
|
524
|
+
# L3 params reader
|
|
525
|
+
# ---------------------------------------------------------------------------
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _read_l3_params(ds: netCDF4.Dataset) -> L3Params:
|
|
529
|
+
"""Read L3 processing parameters from L3_spectra group attributes.
|
|
530
|
+
|
|
531
|
+
Handles three cases:
|
|
532
|
+
1. ODAS-style attributes (fft_length, diss_length, overlap in samples).
|
|
533
|
+
2. Epsifish-style attributes (spectral_fft_length, spectral_disslength,
|
|
534
|
+
overlap as percentage).
|
|
535
|
+
3. No attributes — infer fft_length from the KCYC/SH_SPEC shape.
|
|
536
|
+
"""
|
|
537
|
+
g = ds.groups["L3_spectra"]
|
|
538
|
+
attrs = {a: g.getncattr(a) for a in g.ncattrs()}
|
|
539
|
+
|
|
540
|
+
fs_fast = float(attrs.get("fs_fast", _resolve_attr(ds, "fs_fast", 512.0)))
|
|
541
|
+
|
|
542
|
+
# --- FFT length ---
|
|
543
|
+
if "fft_length" in attrs:
|
|
544
|
+
fft_length = int(attrs["fft_length"])
|
|
545
|
+
elif "spectral_fft_length" in attrs:
|
|
546
|
+
fft_length = int(attrs["spectral_fft_length"])
|
|
547
|
+
elif "KCYC" in g.variables:
|
|
548
|
+
# Infer from wavenumber dimension: n_freq = nfft/2 + 1
|
|
549
|
+
n_freq = g.variables["KCYC"].shape[0]
|
|
550
|
+
fft_length = (n_freq - 1) * 2
|
|
551
|
+
else:
|
|
552
|
+
fft_length = 512
|
|
553
|
+
|
|
554
|
+
# --- Dissipation window length ---
|
|
555
|
+
if "diss_length" in attrs:
|
|
556
|
+
diss_length = int(attrs["diss_length"])
|
|
557
|
+
elif "spectral_disslength" in attrs:
|
|
558
|
+
diss_length = int(attrs["spectral_disslength"])
|
|
559
|
+
else:
|
|
560
|
+
# Infer from reference time spacing if possible
|
|
561
|
+
diss_length = _infer_diss_length(g, fft_length, fs_fast)
|
|
562
|
+
|
|
563
|
+
# --- Overlap ---
|
|
564
|
+
if "overlap" in attrs:
|
|
565
|
+
overlap = int(attrs["overlap"])
|
|
566
|
+
elif "spectral_disslength_overlap" in attrs:
|
|
567
|
+
# Overlap given as percentage
|
|
568
|
+
overlap = int(diss_length * float(attrs["spectral_disslength_overlap"]) / 100)
|
|
569
|
+
else:
|
|
570
|
+
overlap = diss_length // 2
|
|
571
|
+
|
|
572
|
+
return L3Params(
|
|
573
|
+
fft_length=fft_length,
|
|
574
|
+
diss_length=diss_length,
|
|
575
|
+
overlap=overlap,
|
|
576
|
+
HP_cut=float(attrs.get("HP_cut", 0.25)),
|
|
577
|
+
fs_fast=fs_fast,
|
|
578
|
+
goodman=bool(attrs.get("goodman", True)),
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _infer_diss_length(g: netCDF4.Group, fft_length: int, fs_fast: float) -> int:
|
|
583
|
+
"""Infer dissipation window length from reference L3 time spacing."""
|
|
584
|
+
if "TIME" not in g.variables or "SECTION_NUMBER" not in g.variables:
|
|
585
|
+
return fft_length * 4 # fallback
|
|
586
|
+
|
|
587
|
+
time = np.asarray(g.variables["TIME"][:], dtype=np.float64)
|
|
588
|
+
sec = np.asarray(g.variables["SECTION_NUMBER"][:], dtype=np.float64)
|
|
589
|
+
|
|
590
|
+
if len(time) < 2:
|
|
591
|
+
return fft_length * 4
|
|
592
|
+
|
|
593
|
+
# Use within-section time spacing to infer diss_step
|
|
594
|
+
dt_all = []
|
|
595
|
+
for sid in np.unique(sec[sec > 0]):
|
|
596
|
+
t_sec = time[sec == sid]
|
|
597
|
+
if len(t_sec) >= 2:
|
|
598
|
+
dt_all.extend(np.diff(t_sec).tolist())
|
|
599
|
+
if not dt_all:
|
|
600
|
+
return fft_length * 4
|
|
601
|
+
|
|
602
|
+
dt_s = np.median(dt_all) * 86400 # days → seconds
|
|
603
|
+
diss_step = round(dt_s * fs_fast)
|
|
604
|
+
|
|
605
|
+
# Also need total section to infer diss_length.
|
|
606
|
+
# Use the L2_cleaned section to find total samples, then:
|
|
607
|
+
# n_spectra = (sec_len - diss_length) // diss_step + 1
|
|
608
|
+
# → diss_length = sec_len - (n_spectra - 1) * diss_step
|
|
609
|
+
# But we don't have the L2 here. Use a heuristic: diss_length = 2 * diss_step
|
|
610
|
+
# (50% overlap), rounded to a multiple of fft_length.
|
|
611
|
+
# Assume 50% overlap: diss_length = 2 * diss_step.
|
|
612
|
+
diss_length = 2 * diss_step
|
|
613
|
+
return max(2 * fft_length, diss_length)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
# ---------------------------------------------------------------------------
|
|
617
|
+
# L3 reference reader
|
|
618
|
+
# ---------------------------------------------------------------------------
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _read_l3(ds: netCDF4.Dataset) -> L3Data:
|
|
622
|
+
g = ds.groups["L3_spectra"]
|
|
623
|
+
|
|
624
|
+
time = np.asarray(g.variables["TIME"][:], dtype=np.float64)
|
|
625
|
+
pres = np.asarray(g.variables["PRES"][:], dtype=np.float64)
|
|
626
|
+
pspd_rel = np.asarray(g.variables["PSPD_REL"][:], dtype=np.float64)
|
|
627
|
+
section_number = np.asarray(g.variables["SECTION_NUMBER"][:], dtype=np.float64)
|
|
628
|
+
|
|
629
|
+
# NaN marks missing temperature (process_l4 falls back to 10 degC with
|
|
630
|
+
# a warning). A 0 degC default would silently bias viscosity +35 %
|
|
631
|
+
# relative to 10 degC, and epsilon with it.
|
|
632
|
+
temp = np.full_like(time, np.nan)
|
|
633
|
+
if "TEMP" in g.variables:
|
|
634
|
+
temp = np.asarray(g.variables["TEMP"][:], dtype=np.float64)
|
|
635
|
+
|
|
636
|
+
kcyc = np.asarray(g.variables["KCYC"][:], dtype=np.float64)
|
|
637
|
+
sh_spec = np.asarray(g.variables["SH_SPEC"][:], dtype=np.float64)
|
|
638
|
+
|
|
639
|
+
sh_spec_clean = sh_spec.copy()
|
|
640
|
+
if "SH_SPEC_CLEAN" in g.variables:
|
|
641
|
+
sh_spec_clean = np.asarray(g.variables["SH_SPEC_CLEAN"][:], dtype=np.float64)
|
|
642
|
+
|
|
643
|
+
return L3Data(
|
|
644
|
+
time=time,
|
|
645
|
+
pres=pres,
|
|
646
|
+
temp=temp,
|
|
647
|
+
pspd_rel=pspd_rel,
|
|
648
|
+
section_number=section_number,
|
|
649
|
+
kcyc=kcyc,
|
|
650
|
+
sh_spec=sh_spec,
|
|
651
|
+
sh_spec_clean=sh_spec_clean,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
# ---------------------------------------------------------------------------
|
|
656
|
+
# L4 reference reader
|
|
657
|
+
# ---------------------------------------------------------------------------
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _read_l4(ds: netCDF4.Dataset) -> L4Data:
|
|
661
|
+
g = ds.groups["L4_dissipation"]
|
|
662
|
+
|
|
663
|
+
time = np.asarray(g.variables["TIME"][:], dtype=np.float64)
|
|
664
|
+
pres = np.asarray(g.variables["PRES"][:], dtype=np.float64)
|
|
665
|
+
pspd_rel = np.asarray(g.variables["PSPD_REL"][:], dtype=np.float64)
|
|
666
|
+
section_number = np.asarray(g.variables["SECTION_NUMBER"][:], dtype=np.float64)
|
|
667
|
+
|
|
668
|
+
epsi = np.asarray(g.variables["EPSI"][:], dtype=np.float64)
|
|
669
|
+
epsi_final = np.asarray(g.variables["EPSI_FINAL"][:], dtype=np.float64)
|
|
670
|
+
epsi_flags = np.asarray(g.variables["EPSI_FLAGS"][:], dtype=np.float64)
|
|
671
|
+
fom = np.asarray(g.variables["FOM"][:], dtype=np.float64)
|
|
672
|
+
mad = np.asarray(g.variables["MAD"][:], dtype=np.float64)
|
|
673
|
+
kmax = np.asarray(g.variables["KMAX"][:], dtype=np.float64)
|
|
674
|
+
method = np.asarray(g.variables["METHOD"][:], dtype=np.float64)
|
|
675
|
+
var_resolved = np.asarray(g.variables["VAR_RESOLVED"][:], dtype=np.float64)
|
|
676
|
+
|
|
677
|
+
return L4Data(
|
|
678
|
+
time=time,
|
|
679
|
+
pres=pres,
|
|
680
|
+
pspd_rel=pspd_rel,
|
|
681
|
+
section_number=section_number,
|
|
682
|
+
epsi=epsi,
|
|
683
|
+
epsi_final=epsi_final,
|
|
684
|
+
epsi_flags=epsi_flags,
|
|
685
|
+
fom=fom,
|
|
686
|
+
mad=mad,
|
|
687
|
+
kmax=kmax,
|
|
688
|
+
method=method,
|
|
689
|
+
var_resolved=var_resolved,
|
|
690
|
+
)
|