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/rsi/helpers.py
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
# Mar-2026, Claude and Pat Welch, pat@mousebrains.com
|
|
2
|
+
"""Shared helpers for loading instrument data and preparing profiles.
|
|
3
|
+
|
|
4
|
+
These functions bridge PFile/NetCDF reading with spectral processing.
|
|
5
|
+
Originally part of dissipation.py, extracted so that both epsilon and
|
|
6
|
+
chi packages can use them without circular dependencies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
import warnings
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TYPE_CHECKING, Any, TypedDict
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import numpy.typing as npt
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
import xarray as xr
|
|
22
|
+
|
|
23
|
+
from odas_tpw.rsi.p_file import PFile
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ChannelsDict(TypedDict, total=False):
|
|
29
|
+
"""Type for the dict returned by :func:`load_channels`."""
|
|
30
|
+
|
|
31
|
+
shear: list[tuple[str, np.ndarray]]
|
|
32
|
+
accel: list[tuple[str, np.ndarray]]
|
|
33
|
+
P: np.ndarray
|
|
34
|
+
T: np.ndarray
|
|
35
|
+
t_fast: np.ndarray
|
|
36
|
+
t_slow: np.ndarray
|
|
37
|
+
fs_fast: float
|
|
38
|
+
fs_slow: float
|
|
39
|
+
is_profile: bool
|
|
40
|
+
metadata: dict[str, str]
|
|
41
|
+
vehicle: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Channel name patterns for RSI instruments
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
SH_PATTERN = re.compile(r"^sh\d+$")
|
|
49
|
+
AC_PATTERN = re.compile(r"^A[xyz]$")
|
|
50
|
+
T_PATTERN = re.compile(r"^T\d+$")
|
|
51
|
+
DT_PATTERN = re.compile(r"^T\d+_dT\d+$")
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Channel loading
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_channels(
|
|
59
|
+
source: PFile | str | Path,
|
|
60
|
+
shear_pattern: str = r"^sh\d+$",
|
|
61
|
+
accel_pattern: str = r"^A[xyz]$",
|
|
62
|
+
pressure_name: str = "P",
|
|
63
|
+
temperature_name: str = "T1",
|
|
64
|
+
) -> ChannelsDict:
|
|
65
|
+
"""Load channel data from any supported source.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
source : PFile, str, or Path
|
|
70
|
+
A PFile object, .p file path, full-record .nc path,
|
|
71
|
+
or per-profile .nc path.
|
|
72
|
+
shear_pattern : str
|
|
73
|
+
Regex pattern matching shear channel names.
|
|
74
|
+
accel_pattern : str
|
|
75
|
+
Regex pattern matching accelerometer channel names.
|
|
76
|
+
pressure_name : str
|
|
77
|
+
Name of the pressure channel.
|
|
78
|
+
temperature_name : str
|
|
79
|
+
Name of the temperature channel.
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
dict with keys:
|
|
84
|
+
shear : list of (name, ndarray) — shear probe signals
|
|
85
|
+
accel : list of (name, ndarray) — accelerometer signals
|
|
86
|
+
P : ndarray — pressure (slow)
|
|
87
|
+
T : ndarray — temperature (slow)
|
|
88
|
+
t_fast : ndarray — fast time vector
|
|
89
|
+
t_slow : ndarray — slow time vector
|
|
90
|
+
fs_fast : float
|
|
91
|
+
fs_slow : float
|
|
92
|
+
is_profile : bool — whether source is a per-profile file
|
|
93
|
+
metadata : dict
|
|
94
|
+
"""
|
|
95
|
+
from odas_tpw.rsi.p_file import PFile
|
|
96
|
+
|
|
97
|
+
if isinstance(source, PFile):
|
|
98
|
+
return _channels_from_pfile(
|
|
99
|
+
source, shear_pattern, accel_pattern, pressure_name, temperature_name
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
source = Path(source)
|
|
103
|
+
if source.suffix.lower() == ".p":
|
|
104
|
+
pf = PFile(source)
|
|
105
|
+
return _channels_from_pfile(
|
|
106
|
+
pf, shear_pattern, accel_pattern, pressure_name, temperature_name
|
|
107
|
+
)
|
|
108
|
+
elif source.suffix.lower() == ".nc":
|
|
109
|
+
return _channels_from_nc(
|
|
110
|
+
source, shear_pattern, accel_pattern, pressure_name, temperature_name
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
raise ValueError(f"Unsupported file type: {source.suffix}")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _channels_from_pfile(
|
|
117
|
+
pf: PFile, sh_pat: str, ac_pat: str, p_name: str, t_name: str
|
|
118
|
+
) -> ChannelsDict:
|
|
119
|
+
sh_re = re.compile(sh_pat) if sh_pat != SH_PATTERN.pattern else SH_PATTERN
|
|
120
|
+
ac_re = re.compile(ac_pat) if ac_pat != AC_PATTERN.pattern else AC_PATTERN
|
|
121
|
+
shear = sorted(
|
|
122
|
+
[(n, pf.channels[n]) for n in pf._fast_channels if sh_re.match(n)],
|
|
123
|
+
key=lambda x: x[0],
|
|
124
|
+
)
|
|
125
|
+
accel = sorted(
|
|
126
|
+
[(n, pf.channels[n]) for n in pf._fast_channels if ac_re.match(n)],
|
|
127
|
+
key=lambda x: x[0],
|
|
128
|
+
)
|
|
129
|
+
out: dict[str, Any] = {
|
|
130
|
+
"shear": shear,
|
|
131
|
+
"accel": accel,
|
|
132
|
+
"P": pf.channels[p_name],
|
|
133
|
+
"T": pf.channels[t_name],
|
|
134
|
+
"t_fast": pf.t_fast,
|
|
135
|
+
"t_slow": pf.t_slow,
|
|
136
|
+
"fs_fast": pf.fs_fast,
|
|
137
|
+
"fs_slow": pf.fs_slow,
|
|
138
|
+
"is_profile": False,
|
|
139
|
+
"vehicle": pf.config["instrument_info"].get("vehicle", "").lower(),
|
|
140
|
+
"metadata": {
|
|
141
|
+
"source": str(pf.filepath),
|
|
142
|
+
"instrument": pf.config["instrument_info"].get("model", ""),
|
|
143
|
+
"sn": pf.config["instrument_info"].get("sn", ""),
|
|
144
|
+
"start_time": pf.start_time.isoformat(),
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
# Carry through the perturb-injected speed channel(s) if present.
|
|
148
|
+
if "speed_fast" in pf.channels:
|
|
149
|
+
out["speed_fast"] = pf.channels["speed_fast"]
|
|
150
|
+
if "W_slow" in pf.channels:
|
|
151
|
+
out["W_slow"] = pf.channels["W_slow"]
|
|
152
|
+
return out
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _channels_from_nc(
|
|
156
|
+
nc_path: Path, sh_pat: str, ac_pat: str, p_name: str, t_name: str
|
|
157
|
+
) -> ChannelsDict:
|
|
158
|
+
import netCDF4 as nc
|
|
159
|
+
|
|
160
|
+
ds = nc.Dataset(str(nc_path), "r")
|
|
161
|
+
sh_re = re.compile(sh_pat) if sh_pat != SH_PATTERN.pattern else SH_PATTERN
|
|
162
|
+
ac_re = re.compile(ac_pat) if ac_pat != AC_PATTERN.pattern else AC_PATTERN
|
|
163
|
+
|
|
164
|
+
fs_fast = float(ds.fs_fast)
|
|
165
|
+
fs_slow = float(ds.fs_slow)
|
|
166
|
+
is_profile = hasattr(ds, "profile_number")
|
|
167
|
+
|
|
168
|
+
t_fast = ds.variables["t_fast"][:].data
|
|
169
|
+
t_slow = ds.variables["t_slow"][:].data
|
|
170
|
+
P = ds.variables[p_name][:].data.astype(np.float64)
|
|
171
|
+
T = ds.variables[t_name][:].data.astype(np.float64)
|
|
172
|
+
|
|
173
|
+
shear = []
|
|
174
|
+
accel = []
|
|
175
|
+
for vname in sorted(ds.variables.keys()):
|
|
176
|
+
var = ds.variables[vname]
|
|
177
|
+
if var.dimensions == ("time_fast",):
|
|
178
|
+
data = var[:].data.astype(np.float64)
|
|
179
|
+
if sh_re.match(vname):
|
|
180
|
+
shear.append((vname, data))
|
|
181
|
+
elif ac_re.match(vname):
|
|
182
|
+
accel.append((vname, data))
|
|
183
|
+
|
|
184
|
+
metadata = {"source": str(nc_path)}
|
|
185
|
+
for attr in ("instrument_model", "instrument_sn", "source_file", "start_time"):
|
|
186
|
+
if hasattr(ds, attr):
|
|
187
|
+
metadata[attr] = getattr(ds, attr)
|
|
188
|
+
|
|
189
|
+
vehicle = ""
|
|
190
|
+
if hasattr(ds, "vehicle"):
|
|
191
|
+
vehicle = ds.vehicle.lower()
|
|
192
|
+
elif hasattr(ds, "instrument_model"):
|
|
193
|
+
model = ds.instrument_model.lower()
|
|
194
|
+
if "vmp" in model:
|
|
195
|
+
vehicle = "vmp"
|
|
196
|
+
elif "xmp" in model:
|
|
197
|
+
vehicle = "xmp"
|
|
198
|
+
elif "mr" in model or "microrider" in model:
|
|
199
|
+
vehicle = "slocum_glider"
|
|
200
|
+
|
|
201
|
+
speed_fast = None
|
|
202
|
+
W_slow = None
|
|
203
|
+
if "speed_fast" in ds.variables:
|
|
204
|
+
speed_fast = ds.variables["speed_fast"][:].data.astype(np.float64)
|
|
205
|
+
if "W_slow" in ds.variables:
|
|
206
|
+
W_slow = ds.variables["W_slow"][:].data.astype(np.float64)
|
|
207
|
+
|
|
208
|
+
ds.close()
|
|
209
|
+
|
|
210
|
+
out: dict[str, Any] = {
|
|
211
|
+
"shear": shear,
|
|
212
|
+
"accel": accel,
|
|
213
|
+
"P": P,
|
|
214
|
+
"T": T,
|
|
215
|
+
"t_fast": t_fast,
|
|
216
|
+
"t_slow": t_slow,
|
|
217
|
+
"fs_fast": fs_fast,
|
|
218
|
+
"fs_slow": fs_slow,
|
|
219
|
+
"is_profile": is_profile,
|
|
220
|
+
"vehicle": vehicle,
|
|
221
|
+
"metadata": metadata,
|
|
222
|
+
}
|
|
223
|
+
if speed_fast is not None:
|
|
224
|
+
out["speed_fast"] = speed_fast
|
|
225
|
+
if W_slow is not None:
|
|
226
|
+
out["W_slow"] = W_slow
|
|
227
|
+
return out
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
# Profile preparation
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def prepare_profiles(
|
|
236
|
+
data: ChannelsDict | dict[str, Any],
|
|
237
|
+
speed: float | None,
|
|
238
|
+
direction: str,
|
|
239
|
+
salinity: npt.ArrayLike | None,
|
|
240
|
+
tau: float | None = None,
|
|
241
|
+
speed_cutout: float = 0.05,
|
|
242
|
+
vehicle: str | None = None,
|
|
243
|
+
) -> tuple | None:
|
|
244
|
+
"""Profile detection, speed computation, and salinity interpolation.
|
|
245
|
+
|
|
246
|
+
Matches the ODAS odas_p2mat.m speed pipeline:
|
|
247
|
+
1. W = gradient(P) filtered with Butterworth at 0.68/tau
|
|
248
|
+
2. speed = abs(W)
|
|
249
|
+
3. speed filtered again with Butterworth at 0.68/tau
|
|
250
|
+
4. speed clamped to speed_cutout minimum
|
|
251
|
+
|
|
252
|
+
``tau=None`` (default) resolves the smoothing time constant from the
|
|
253
|
+
vehicle table; pass an explicit value to override it.
|
|
254
|
+
|
|
255
|
+
Returns (profiles_slow, speed_fast, P_fast, T_fast, sal_fast, fs_fast,
|
|
256
|
+
fs_slow, ratio, t_fast).
|
|
257
|
+
"""
|
|
258
|
+
from odas_tpw.rsi.vehicle import resolve_direction, resolve_tau
|
|
259
|
+
|
|
260
|
+
fs_fast = data["fs_fast"]
|
|
261
|
+
fs_slow = data["fs_slow"]
|
|
262
|
+
ratio = round(fs_fast / fs_slow)
|
|
263
|
+
|
|
264
|
+
P_slow = data["P"]
|
|
265
|
+
T_slow = data["T"]
|
|
266
|
+
t_fast = data["t_fast"]
|
|
267
|
+
t_slow = data["t_slow"]
|
|
268
|
+
|
|
269
|
+
# Resolve vehicle, direction, and tau
|
|
270
|
+
if vehicle is None:
|
|
271
|
+
vehicle = data.get("vehicle", "")
|
|
272
|
+
direction = resolve_direction(direction, vehicle)
|
|
273
|
+
if tau is None:
|
|
274
|
+
tau = resolve_tau(vehicle)
|
|
275
|
+
|
|
276
|
+
if data["is_profile"]:
|
|
277
|
+
profiles_slow = [(0, len(P_slow) - 1)]
|
|
278
|
+
else:
|
|
279
|
+
from odas_tpw.rsi.profile import _smooth_fall_rate, get_profiles
|
|
280
|
+
|
|
281
|
+
W_slow = _smooth_fall_rate(P_slow, fs_slow, tau=tau)
|
|
282
|
+
profiles_slow = get_profiles(P_slow, W_slow, fs_slow, direction=direction)
|
|
283
|
+
if not profiles_slow:
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
# Prefer a precomputed ``speed_fast`` channel if the caller (perturb
|
|
287
|
+
# pipeline post-hotel-merge) has injected one; otherwise fall back to
|
|
288
|
+
# the historical ODAS pressure-rate path.
|
|
289
|
+
precomputed = data.get("speed_fast") if hasattr(data, "get") else None
|
|
290
|
+
if speed is not None:
|
|
291
|
+
speed_fast = np.full(len(t_fast), abs(speed))
|
|
292
|
+
elif precomputed is not None and len(precomputed) == len(t_fast):
|
|
293
|
+
speed_fast = np.asarray(precomputed, dtype=np.float64)
|
|
294
|
+
else:
|
|
295
|
+
from odas_tpw.scor160.profile import compute_speed_fast
|
|
296
|
+
|
|
297
|
+
if direction in ("glide", "horizontal"):
|
|
298
|
+
# ODAS uses a flight model (glide) or EM current meter /
|
|
299
|
+
# hotel speed (horizontal) for these vehicles; |dP/dt|
|
|
300
|
+
# underestimates the flow past the sensors, and epsilon has
|
|
301
|
+
# roughly U^4 leverage on the speed through the shear
|
|
302
|
+
# conversion and wavenumber transform.
|
|
303
|
+
warnings.warn(
|
|
304
|
+
f"Vehicle direction '{direction}' but speed is being computed "
|
|
305
|
+
"from |dP/dt|; provide an explicit speed or a precomputed "
|
|
306
|
+
"speed_fast channel for glider/horizontal platforms — "
|
|
307
|
+
"epsilon scales as ~U^4 and will be strongly biased",
|
|
308
|
+
stacklevel=2,
|
|
309
|
+
)
|
|
310
|
+
speed_fast, _W_slow = compute_speed_fast(
|
|
311
|
+
P_slow,
|
|
312
|
+
t_fast,
|
|
313
|
+
t_slow,
|
|
314
|
+
fs_fast,
|
|
315
|
+
fs_slow,
|
|
316
|
+
tau=tau,
|
|
317
|
+
speed_min=speed_cutout,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Floor (and NaN-scrub) speed once at this choke point so every consumer
|
|
321
|
+
# gets a clean, positive speed: shear normalization (shear /= speed**2) and
|
|
322
|
+
# the chi wavenumber / fp07 axis both blow up on a zero/NaN speed. The
|
|
323
|
+
# fixed-speed and precomputed branches are otherwise unfloored;
|
|
324
|
+
# compute_speed_fast already floors, so this is a no-op there.
|
|
325
|
+
speed_fast = np.maximum(
|
|
326
|
+
np.nan_to_num(np.asarray(speed_fast, dtype=np.float64), nan=speed_cutout),
|
|
327
|
+
speed_cutout,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
P_fast = np.interp(t_fast, t_slow, P_slow)
|
|
331
|
+
T_fast = np.interp(t_fast, t_slow, T_slow)
|
|
332
|
+
|
|
333
|
+
if salinity is not None:
|
|
334
|
+
salinity = np.asarray(salinity, dtype=float)
|
|
335
|
+
if salinity.ndim > 0:
|
|
336
|
+
if len(salinity) == len(t_slow):
|
|
337
|
+
sal_fast = np.interp(t_fast, t_slow, salinity)
|
|
338
|
+
elif len(salinity) == len(t_fast):
|
|
339
|
+
sal_fast = salinity
|
|
340
|
+
else:
|
|
341
|
+
raise ValueError(
|
|
342
|
+
f"salinity array length {len(salinity)} doesn't match "
|
|
343
|
+
f"slow ({len(t_slow)}) or fast ({len(t_fast)}) time series"
|
|
344
|
+
)
|
|
345
|
+
else:
|
|
346
|
+
sal_fast = float(salinity)
|
|
347
|
+
else:
|
|
348
|
+
sal_fast = None
|
|
349
|
+
|
|
350
|
+
return (profiles_slow, speed_fast, P_fast, T_fast, sal_fast, fs_fast, fs_slow, ratio, t_fast)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# ---------------------------------------------------------------------------
|
|
354
|
+
# L1Data construction from load_channels output
|
|
355
|
+
# ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _build_l1data_from_channels(
|
|
359
|
+
data: ChannelsDict | dict[str, Any],
|
|
360
|
+
s_fast: int,
|
|
361
|
+
e_fast: int,
|
|
362
|
+
speed_fast: np.ndarray,
|
|
363
|
+
P_fast: np.ndarray,
|
|
364
|
+
T_fast: np.ndarray,
|
|
365
|
+
direction: str = "down",
|
|
366
|
+
*,
|
|
367
|
+
therm_list: list[tuple[str, np.ndarray]] | None = None,
|
|
368
|
+
diff_gains: list[float] | None = None,
|
|
369
|
+
) -> Any:
|
|
370
|
+
"""Build L1Data from load_channels output and interpolated profile arrays.
|
|
371
|
+
|
|
372
|
+
Parameters
|
|
373
|
+
----------
|
|
374
|
+
data : dict
|
|
375
|
+
Output of load_channels().
|
|
376
|
+
s_fast, e_fast : int
|
|
377
|
+
Fast-rate slice indices for this profile.
|
|
378
|
+
speed_fast : ndarray
|
|
379
|
+
Full-length speed array (fast rate).
|
|
380
|
+
P_fast, T_fast : ndarray
|
|
381
|
+
Full-length pressure and temperature (interpolated to fast rate).
|
|
382
|
+
direction : str
|
|
383
|
+
Profile direction.
|
|
384
|
+
therm_list : list of (name, array), optional
|
|
385
|
+
Thermistor data for chi computation.
|
|
386
|
+
diff_gains : list of float, optional
|
|
387
|
+
Per-thermistor differentiator gains.
|
|
388
|
+
"""
|
|
389
|
+
from odas_tpw.scor160.io import L1Data
|
|
390
|
+
|
|
391
|
+
shear_arrays = [s[1] for s in data["shear"]]
|
|
392
|
+
accel_arrays = [a[1] for a in data["accel"]]
|
|
393
|
+
fs_fast = data["fs_fast"]
|
|
394
|
+
n = e_fast - s_fast
|
|
395
|
+
|
|
396
|
+
# Defensive floor (0.05 m/s cutout): callers normally pass a speed already
|
|
397
|
+
# floored by prepare_profiles, but a zero/tiny speed here would blow up the
|
|
398
|
+
# shear/speed**2 normalization. Matches adapter.pfile_to_l1data and speed.py.
|
|
399
|
+
speed_prof = np.maximum(speed_fast[s_fast:e_fast], 0.05)
|
|
400
|
+
|
|
401
|
+
# Shear: normalize by speed^2
|
|
402
|
+
if shear_arrays:
|
|
403
|
+
shear = np.stack(
|
|
404
|
+
[arr[s_fast:e_fast] / speed_prof**2 for arr in shear_arrays],
|
|
405
|
+
axis=0,
|
|
406
|
+
)
|
|
407
|
+
else:
|
|
408
|
+
shear = np.zeros((0, n), dtype=np.float64)
|
|
409
|
+
|
|
410
|
+
# Vibration/accelerometer
|
|
411
|
+
if accel_arrays:
|
|
412
|
+
vib = np.stack([arr[s_fast:e_fast] for arr in accel_arrays], axis=0)
|
|
413
|
+
vib_type = "ACC"
|
|
414
|
+
else:
|
|
415
|
+
vib = np.zeros((0, n), dtype=np.float64)
|
|
416
|
+
vib_type = "NONE"
|
|
417
|
+
|
|
418
|
+
# Optional fast temperature
|
|
419
|
+
if therm_list:
|
|
420
|
+
tf = np.stack([arr[s_fast:e_fast] for _, arr in therm_list], axis=0)
|
|
421
|
+
else:
|
|
422
|
+
tf = np.zeros((0, 0), dtype=np.float64)
|
|
423
|
+
|
|
424
|
+
return L1Data(
|
|
425
|
+
time=data["t_fast"][s_fast:e_fast],
|
|
426
|
+
pres=P_fast[s_fast:e_fast],
|
|
427
|
+
shear=shear,
|
|
428
|
+
vib=vib,
|
|
429
|
+
vib_type=vib_type,
|
|
430
|
+
fs_fast=fs_fast,
|
|
431
|
+
f_AA=98.0,
|
|
432
|
+
vehicle=data.get("vehicle", ""),
|
|
433
|
+
profile_dir=direction,
|
|
434
|
+
time_reference_year=2000,
|
|
435
|
+
pspd_rel=speed_prof,
|
|
436
|
+
temp=T_fast[s_fast:e_fast],
|
|
437
|
+
temp_fast=tf,
|
|
438
|
+
diff_gains=diff_gains or [],
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# ---------------------------------------------------------------------------
|
|
443
|
+
# Shared file-level output
|
|
444
|
+
# ---------------------------------------------------------------------------
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def write_profile_results(
|
|
448
|
+
results: list,
|
|
449
|
+
source_path: Path,
|
|
450
|
+
output_dir: Path,
|
|
451
|
+
suffix: str,
|
|
452
|
+
) -> list[Path]:
|
|
453
|
+
"""Write per-profile xarray Datasets to NetCDF files.
|
|
454
|
+
|
|
455
|
+
Parameters
|
|
456
|
+
----------
|
|
457
|
+
results : list of xr.Dataset
|
|
458
|
+
Per-profile results.
|
|
459
|
+
source_path : Path
|
|
460
|
+
Original source file (stem used for naming).
|
|
461
|
+
output_dir : Path
|
|
462
|
+
Output directory (created if needed).
|
|
463
|
+
suffix : str
|
|
464
|
+
File suffix, e.g. 'eps' or 'chi'.
|
|
465
|
+
|
|
466
|
+
Returns
|
|
467
|
+
-------
|
|
468
|
+
list of Path
|
|
469
|
+
Paths to output files written.
|
|
470
|
+
"""
|
|
471
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
472
|
+
output_paths: list[Path] = []
|
|
473
|
+
for i, ds in enumerate(results):
|
|
474
|
+
if len(results) == 1:
|
|
475
|
+
out_name = f"{source_path.stem}_{suffix}.nc"
|
|
476
|
+
else:
|
|
477
|
+
out_name = f"{source_path.stem}_prof{i + 1:03d}_{suffix}.nc"
|
|
478
|
+
out_path = output_dir / out_name
|
|
479
|
+
ds.to_netcdf(out_path)
|
|
480
|
+
output_paths.append(out_path)
|
|
481
|
+
logger.info(
|
|
482
|
+
f"{out_path.name}: {ds.sizes['time']} estimates, "
|
|
483
|
+
f"P={float(ds.P_mean.min()):.0f}-{float(ds.P_mean.max()):.0f} dbar"
|
|
484
|
+
)
|
|
485
|
+
return output_paths
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
# ---------------------------------------------------------------------------
|
|
489
|
+
# Shared dataset builder
|
|
490
|
+
# ---------------------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _build_result_dataset(
|
|
494
|
+
variables: list[tuple[str, list[str], np.ndarray, dict]],
|
|
495
|
+
probe_names: list[str],
|
|
496
|
+
t_out: np.ndarray,
|
|
497
|
+
probe_long_name: str,
|
|
498
|
+
global_attrs: dict,
|
|
499
|
+
) -> xr.Dataset:
|
|
500
|
+
"""Build an xarray Dataset from a list of variable specs.
|
|
501
|
+
|
|
502
|
+
Parameters
|
|
503
|
+
----------
|
|
504
|
+
variables : list of (name, dims, data, attrs)
|
|
505
|
+
Variable definitions.
|
|
506
|
+
probe_names : list of str
|
|
507
|
+
Probe/sensor coordinate labels.
|
|
508
|
+
t_out : ndarray
|
|
509
|
+
Time coordinate values.
|
|
510
|
+
probe_long_name : str
|
|
511
|
+
Long name for the probe coordinate.
|
|
512
|
+
global_attrs : dict
|
|
513
|
+
Dataset-level attributes.
|
|
514
|
+
"""
|
|
515
|
+
import xarray as xr
|
|
516
|
+
|
|
517
|
+
data_vars = {name: (dims, data, attrs) for name, dims, data, attrs in variables}
|
|
518
|
+
ds = xr.Dataset(
|
|
519
|
+
data_vars,
|
|
520
|
+
coords={
|
|
521
|
+
"probe": probe_names,
|
|
522
|
+
"t": (["time"], t_out),
|
|
523
|
+
},
|
|
524
|
+
attrs=global_attrs,
|
|
525
|
+
)
|
|
526
|
+
ds.coords["probe"].attrs["long_name"] = probe_long_name
|
|
527
|
+
return ds
|