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/chi/l2_chi.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""L2 chi: cleaned temperature and vibration for chi estimation.
|
|
2
|
+
|
|
3
|
+
Despikes fast temperature, HP-filters vibration for Goodman cleaning,
|
|
4
|
+
and computes temperature gradient (dT/dz) from the cleaned signals.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
from scipy.signal import butter, filtfilt
|
|
13
|
+
|
|
14
|
+
from odas_tpw.scor160.despike import despike
|
|
15
|
+
from odas_tpw.scor160.io import L1Data, L2Data
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class L2ChiData:
|
|
20
|
+
"""Level-2 cleaned data for chi estimation."""
|
|
21
|
+
|
|
22
|
+
time: np.ndarray # (N_TIME,)
|
|
23
|
+
pres: np.ndarray # (N_TIME,), dbar
|
|
24
|
+
temp: np.ndarray # (N_TIME,), slow-rate temperature [°C] (for viscosity)
|
|
25
|
+
temp_fast: np.ndarray # (N_TEMP, N_TIME), despiked temperature [°C]
|
|
26
|
+
gradt: np.ndarray # (N_TEMP, N_TIME), temperature gradient [°C/m]
|
|
27
|
+
vib: np.ndarray # (N_VIB, N_TIME), HP-filtered vibration
|
|
28
|
+
pspd_rel: np.ndarray # (N_TIME,), profiling speed [m/s]
|
|
29
|
+
section_number: np.ndarray # (N_TIME,), 0 = excluded
|
|
30
|
+
diff_gains: list[float]
|
|
31
|
+
fs_fast: float
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def n_temp(self) -> int:
|
|
35
|
+
"""Number of fast thermistor channels."""
|
|
36
|
+
return self.temp_fast.shape[0] if self.temp_fast.ndim == 2 else 0
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def n_vib(self) -> int:
|
|
40
|
+
"""Number of vibration/accelerometer channels."""
|
|
41
|
+
return int(self.vib.shape[0])
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def n_time(self) -> int:
|
|
45
|
+
"""Number of fast-rate time samples."""
|
|
46
|
+
return int(self.time.shape[0])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class L2ChiParams:
|
|
51
|
+
"""Processing parameters for L2 chi cleaning."""
|
|
52
|
+
|
|
53
|
+
HP_cut: float = 0.25 # HP cutoff for vibration [Hz]
|
|
54
|
+
despike_T: np.ndarray = field(
|
|
55
|
+
default_factory=lambda: np.array([10.0, 0.5, 0.04]),
|
|
56
|
+
) # [threshold, smooth_freq, half_width_fraction]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def process_l2_chi(
|
|
60
|
+
l1: L1Data,
|
|
61
|
+
l2: L2Data,
|
|
62
|
+
params: L2ChiParams | None = None,
|
|
63
|
+
) -> L2ChiData:
|
|
64
|
+
"""Clean L1 temperature and vibration for chi estimation.
|
|
65
|
+
|
|
66
|
+
Steps:
|
|
67
|
+
1. Despike fast temperature within sections.
|
|
68
|
+
2. HP-filter vibration for Goodman coherent noise removal.
|
|
69
|
+
3. Compute temperature gradient: dT/dz = fs * diff(T) / speed.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
l1 : L1Data
|
|
74
|
+
Level-1 data with raw temp_fast and vib.
|
|
75
|
+
l2 : L2Data
|
|
76
|
+
Level-2 data for section_number and pspd_rel.
|
|
77
|
+
params : L2ChiParams, optional
|
|
78
|
+
Cleaning parameters. Uses defaults if None.
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
L2ChiData
|
|
83
|
+
"""
|
|
84
|
+
if not l1.has_temp_fast:
|
|
85
|
+
raise ValueError("L1Data has no temp_fast — cannot compute chi")
|
|
86
|
+
|
|
87
|
+
if params is None:
|
|
88
|
+
params = L2ChiParams()
|
|
89
|
+
|
|
90
|
+
fs = l1.fs_fast
|
|
91
|
+
n_temp = l1.n_temp
|
|
92
|
+
diff_gains = l1.diff_gains if l1.diff_gains else [0.94] * n_temp
|
|
93
|
+
|
|
94
|
+
# 1. Despike temperature within sections
|
|
95
|
+
temp_fast = np.array(l1.temp_fast, dtype=np.float64)
|
|
96
|
+
t_thresh, t_smooth, t_hw = params.despike_T
|
|
97
|
+
|
|
98
|
+
if np.isfinite(t_thresh):
|
|
99
|
+
sections = np.unique(l2.section_number)
|
|
100
|
+
sections = sections[sections > 0]
|
|
101
|
+
for sec_id in sections:
|
|
102
|
+
mask = l2.section_number == sec_id
|
|
103
|
+
for ci in range(n_temp):
|
|
104
|
+
N_t = round(t_hw * fs) if t_hw < 1 else int(t_hw)
|
|
105
|
+
temp_fast[ci, mask], _, _, _ = despike(
|
|
106
|
+
temp_fast[ci, mask],
|
|
107
|
+
fs,
|
|
108
|
+
thresh=t_thresh,
|
|
109
|
+
smooth=t_smooth,
|
|
110
|
+
N=N_t,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# 2. HP-filter vibration for Goodman cleaning
|
|
114
|
+
vib = l1.vib.copy() if l1.vib.size > 0 else l1.vib
|
|
115
|
+
if vib.size > 0 and params.HP_cut > 0:
|
|
116
|
+
b, a = butter(1, params.HP_cut / (fs / 2), btype="high")
|
|
117
|
+
for vi in range(l1.n_vib):
|
|
118
|
+
vib[vi] = filtfilt(b, a, vib[vi])
|
|
119
|
+
|
|
120
|
+
# 3. Compute temperature gradient: dT/dz = fs * diff(T) / speed
|
|
121
|
+
spd_safe = np.maximum(np.abs(l2.pspd_rel), 0.01)
|
|
122
|
+
gradt = np.empty_like(temp_fast)
|
|
123
|
+
for ci in range(n_temp):
|
|
124
|
+
dTdt = np.diff(temp_fast[ci]) * fs
|
|
125
|
+
# A length-1 (or empty) temperature row has no finite-difference
|
|
126
|
+
# gradient; emit zeros so dTdt[-1] doesn't IndexError on the empty diff.
|
|
127
|
+
dTdt = (
|
|
128
|
+
np.append(dTdt, dTdt[-1])
|
|
129
|
+
if dTdt.size
|
|
130
|
+
else np.zeros(temp_fast[ci].shape[0])
|
|
131
|
+
)
|
|
132
|
+
gradt[ci] = dTdt / spd_safe
|
|
133
|
+
|
|
134
|
+
# Temperature for viscosity
|
|
135
|
+
if l1.temp.size == l1.n_time:
|
|
136
|
+
temp_slow = l1.temp
|
|
137
|
+
elif n_temp > 0:
|
|
138
|
+
temp_slow = temp_fast[0]
|
|
139
|
+
else:
|
|
140
|
+
temp_slow = np.full(l1.n_time, 10.0)
|
|
141
|
+
|
|
142
|
+
return L2ChiData(
|
|
143
|
+
time=l1.time.copy(),
|
|
144
|
+
pres=l1.pres.copy(),
|
|
145
|
+
temp=temp_slow,
|
|
146
|
+
temp_fast=temp_fast,
|
|
147
|
+
gradt=gradt,
|
|
148
|
+
vib=vib,
|
|
149
|
+
pspd_rel=l2.pspd_rel.copy(),
|
|
150
|
+
section_number=l2.section_number.copy(),
|
|
151
|
+
diff_gains=diff_gains,
|
|
152
|
+
fs_fast=fs,
|
|
153
|
+
)
|
odas_tpw/chi/l3_chi.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""L3 chi: temperature gradient spectra for chi estimation.
|
|
2
|
+
|
|
3
|
+
Computes Welch-method spectra of temperature gradient, applies Goodman
|
|
4
|
+
coherent-noise removal, first-difference and bilinear corrections, and
|
|
5
|
+
converts to wavenumber units.
|
|
6
|
+
|
|
7
|
+
Reads cleaned temperature gradient and HP-filtered vibration from L2ChiData.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import warnings
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from odas_tpw.chi.chi import _bilinear_correction
|
|
18
|
+
from odas_tpw.chi.fp07 import (
|
|
19
|
+
default_tau_model,
|
|
20
|
+
fp07_tau_batch,
|
|
21
|
+
fp07_transfer_batch,
|
|
22
|
+
gradT_noise_batch,
|
|
23
|
+
)
|
|
24
|
+
from odas_tpw.chi.l2_chi import L2ChiData
|
|
25
|
+
from odas_tpw.scor160.goodman import clean_shear_spec_batch
|
|
26
|
+
from odas_tpw.scor160.io import L3Params
|
|
27
|
+
from odas_tpw.scor160.ocean import kappa_T as kappa_T_TSP
|
|
28
|
+
from odas_tpw.scor160.ocean import visc, visc35
|
|
29
|
+
from odas_tpw.scor160.spectral import csd_matrix_batch
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class _SectionResult:
|
|
34
|
+
"""Accumulator for per-window results within a section."""
|
|
35
|
+
|
|
36
|
+
times: list = field(default_factory=list)
|
|
37
|
+
pres: list = field(default_factory=list)
|
|
38
|
+
temp: list = field(default_factory=list)
|
|
39
|
+
speed: list = field(default_factory=list)
|
|
40
|
+
section: list = field(default_factory=list)
|
|
41
|
+
gradt_spec: list = field(default_factory=list)
|
|
42
|
+
noise_spec: list = field(default_factory=list)
|
|
43
|
+
kcyc: list = field(default_factory=list)
|
|
44
|
+
H2: list = field(default_factory=list)
|
|
45
|
+
tau0: list = field(default_factory=list)
|
|
46
|
+
nu: list = field(default_factory=list)
|
|
47
|
+
kappa_T: list = field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class L3ChiData:
|
|
52
|
+
"""Level-3 temperature gradient spectra for chi estimation."""
|
|
53
|
+
|
|
54
|
+
time: np.ndarray # (N_SPECTRA,), center time per window
|
|
55
|
+
pres: np.ndarray # (N_SPECTRA,), mean pressure per window
|
|
56
|
+
temp: np.ndarray # (N_SPECTRA,), mean temperature per window
|
|
57
|
+
pspd_rel: np.ndarray # (N_SPECTRA,), mean speed per window
|
|
58
|
+
section_number: np.ndarray # (N_SPECTRA,)
|
|
59
|
+
nu: np.ndarray # (N_SPECTRA,), kinematic viscosity
|
|
60
|
+
kappa_T: np.ndarray # (N_SPECTRA,), molecular thermal diffusivity [m^2/s]
|
|
61
|
+
|
|
62
|
+
kcyc: np.ndarray # (N_WAVENUMBER, N_SPECTRA)
|
|
63
|
+
freq: np.ndarray # (N_FREQ,), frequency vector [Hz]
|
|
64
|
+
gradt_spec: np.ndarray # (N_GRADT, N_WAVENUMBER, N_SPECTRA)
|
|
65
|
+
noise_spec: np.ndarray # (N_GRADT, N_WAVENUMBER, N_SPECTRA)
|
|
66
|
+
H2: np.ndarray # (N_SPECTRA, N_FREQ), FP07 transfer function
|
|
67
|
+
tau0: np.ndarray # (N_SPECTRA,), FP07 time constant
|
|
68
|
+
|
|
69
|
+
diff_gains: list[float] = field(default_factory=list)
|
|
70
|
+
fp07_model: str = "single_pole"
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def n_spectra(self) -> int:
|
|
74
|
+
"""Number of spectral windows."""
|
|
75
|
+
return int(self.time.shape[0])
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def n_wavenumber(self) -> int:
|
|
79
|
+
"""Number of wavenumber bins."""
|
|
80
|
+
return int(self.kcyc.shape[0])
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def n_gradt(self) -> int:
|
|
84
|
+
"""Number of temperature gradient channels."""
|
|
85
|
+
return int(self.gradt_spec.shape[0])
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _process_section_chi(
|
|
89
|
+
l2_chi: L2ChiData,
|
|
90
|
+
sec_id: int,
|
|
91
|
+
diss_length: int,
|
|
92
|
+
diss_step: int,
|
|
93
|
+
nfft: int,
|
|
94
|
+
fs: float,
|
|
95
|
+
n_temp: int,
|
|
96
|
+
n_freq: int,
|
|
97
|
+
do_goodman: bool,
|
|
98
|
+
fd_correction: np.ndarray,
|
|
99
|
+
bl_corrections: list[np.ndarray],
|
|
100
|
+
F_const: np.ndarray,
|
|
101
|
+
fp07_model: str,
|
|
102
|
+
salinity: float | np.ndarray | None,
|
|
103
|
+
diff_gains: list[float],
|
|
104
|
+
therm_cal: list[dict] | None,
|
|
105
|
+
acc: _SectionResult,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Process a single section, appending results to *acc*."""
|
|
108
|
+
mask = l2_chi.section_number == sec_id
|
|
109
|
+
idx = np.where(mask)[0]
|
|
110
|
+
if len(idx) < diss_length:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
sec_start = idx[0]
|
|
114
|
+
sec_end = idx[-1] + 1
|
|
115
|
+
sec_len = sec_end - sec_start
|
|
116
|
+
|
|
117
|
+
n_windows = (sec_len - diss_length) // diss_step + 1
|
|
118
|
+
if n_windows < 1:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
starts = sec_start + np.arange(n_windows) * diss_step
|
|
122
|
+
indices = starts[:, np.newaxis] + np.arange(diss_length)[np.newaxis, :]
|
|
123
|
+
|
|
124
|
+
gradt_windows = np.stack(
|
|
125
|
+
[l2_chi.gradt[ci][indices] for ci in range(n_temp)],
|
|
126
|
+
axis=-1,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Goodman cleaning
|
|
130
|
+
n_vib = l2_chi.n_vib
|
|
131
|
+
if do_goodman:
|
|
132
|
+
vib_windows = np.stack(
|
|
133
|
+
[l2_chi.vib[vi][indices] for vi in range(n_vib)],
|
|
134
|
+
axis=-1,
|
|
135
|
+
)
|
|
136
|
+
clean_TT, _ = clean_shear_spec_batch(vib_windows, gradt_windows, nfft, fs)
|
|
137
|
+
clean_spectra = np.real(np.diagonal(clean_TT, axis1=2, axis2=3)).copy()
|
|
138
|
+
else:
|
|
139
|
+
TT, _, _, _ = csd_matrix_batch(
|
|
140
|
+
gradt_windows,
|
|
141
|
+
None,
|
|
142
|
+
nfft,
|
|
143
|
+
fs,
|
|
144
|
+
overlap=nfft // 2,
|
|
145
|
+
detrend="linear",
|
|
146
|
+
)
|
|
147
|
+
clean_spectra = np.real(np.diagonal(TT, axis1=2, axis2=3)).copy()
|
|
148
|
+
|
|
149
|
+
# Per-window means. nanmean (not mean) so a window with a few NaN samples
|
|
150
|
+
# (routine near the surface) averages its finite samples instead of
|
|
151
|
+
# collapsing the whole window: np.mean returns NaN on a single NaN, which
|
|
152
|
+
# for speed then floored the window to 0.01 m/s (#58) and for temperature
|
|
153
|
+
# NaN'd its viscosity. An ALL-NaN window then needs a deliberate fallback
|
|
154
|
+
# rather than numpy's NaN + "Mean of empty slice" warning (#60), so the
|
|
155
|
+
# nanmeans run under suppressed RuntimeWarnings.
|
|
156
|
+
with warnings.catch_warnings():
|
|
157
|
+
warnings.simplefilter("ignore", RuntimeWarning)
|
|
158
|
+
# nan_to_num before the floor: an all-NaN speed window -> 0.01 m/s floor
|
|
159
|
+
# instead of propagating NaN into fp07_tau and the wavenumber axis.
|
|
160
|
+
speed_means = np.maximum(
|
|
161
|
+
np.nan_to_num(
|
|
162
|
+
np.nanmean(np.abs(l2_chi.pspd_rel[indices]), axis=1), nan=0.01
|
|
163
|
+
),
|
|
164
|
+
0.01,
|
|
165
|
+
)
|
|
166
|
+
T_means = np.nanmean(l2_chi.temp[indices], axis=1)
|
|
167
|
+
P_means = np.mean(l2_chi.pres[indices], axis=1)
|
|
168
|
+
t_means = np.mean(l2_chi.time[indices], axis=1)
|
|
169
|
+
# All-NaN-temperature windows: substitute 10 degC for viscosity (mirrors the
|
|
170
|
+
# epsilon side, scor160/l4.py) so chi is computed rather than silently
|
|
171
|
+
# dropped to NaN; warn once if any window needed it (#57).
|
|
172
|
+
if not np.all(np.isfinite(T_means)):
|
|
173
|
+
warnings.warn(
|
|
174
|
+
"Non-finite window temperature(s); using 10 degC for viscosity "
|
|
175
|
+
"in those chi windows",
|
|
176
|
+
stacklevel=2,
|
|
177
|
+
)
|
|
178
|
+
T_means = np.where(np.isfinite(T_means), T_means, 10.0)
|
|
179
|
+
|
|
180
|
+
# Viscosity
|
|
181
|
+
if salinity is not None:
|
|
182
|
+
if np.ndim(salinity) > 0:
|
|
183
|
+
with warnings.catch_warnings():
|
|
184
|
+
warnings.simplefilter("ignore", RuntimeWarning)
|
|
185
|
+
sal_means = np.nanmean(np.asarray(salinity)[indices], axis=1)
|
|
186
|
+
# All-NaN-salinity windows: fall back to 35 PSU (the visc35 reference
|
|
187
|
+
# salinity) so viscosity -> chi is not silently NaN'd, mirroring the
|
|
188
|
+
# 10 degC temperature fallback above (#57, #60).
|
|
189
|
+
sal_means = np.where(np.isfinite(sal_means), sal_means, 35.0)
|
|
190
|
+
else:
|
|
191
|
+
sal_means = np.full(n_windows, float(salinity))
|
|
192
|
+
nu_all = np.asarray(
|
|
193
|
+
[visc(T_means[i], sal_means[i], P_means[i]) for i in range(n_windows)],
|
|
194
|
+
dtype=np.float64,
|
|
195
|
+
)
|
|
196
|
+
# Molecular thermal diffusivity per window from the same T/S/P. chi is
|
|
197
|
+
# linear in kappa_T (chi = 6*kappa_T*I) and epsilon_T ∝ kappa_T², so the
|
|
198
|
+
# T-dependence (~+8% warm vs the old fixed 1.4e-7) propagates directly.
|
|
199
|
+
kappa_T_all = np.asarray(
|
|
200
|
+
kappa_T_TSP(T_means, sal_means, P_means), dtype=np.float64
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
nu_all = np.asarray(
|
|
204
|
+
[visc35(T_means[i]) for i in range(n_windows)],
|
|
205
|
+
dtype=np.float64,
|
|
206
|
+
)
|
|
207
|
+
# No salinity supplied: use the S=35, P=0 reference (matches visc35).
|
|
208
|
+
kappa_T_all = np.asarray(kappa_T_TSP(T_means), dtype=np.float64)
|
|
209
|
+
|
|
210
|
+
K_all = F_const[:, np.newaxis] / speed_means[np.newaxis, :]
|
|
211
|
+
|
|
212
|
+
# Apply corrections
|
|
213
|
+
for ci in range(n_temp):
|
|
214
|
+
clean_spectra[:, :, ci] *= (
|
|
215
|
+
speed_means[:, np.newaxis]
|
|
216
|
+
* fd_correction[np.newaxis, :]
|
|
217
|
+
* bl_corrections[ci][np.newaxis, :]
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# FP07 transfer function
|
|
221
|
+
tau_model = default_tau_model(fp07_model)
|
|
222
|
+
tau0_all = fp07_tau_batch(speed_means, model=tau_model)
|
|
223
|
+
H2_all = fp07_transfer_batch(F_const, tau0_all, model=fp07_model)
|
|
224
|
+
|
|
225
|
+
# Noise spectra
|
|
226
|
+
noise_all = np.empty((n_temp, n_windows, n_freq))
|
|
227
|
+
for ci in range(n_temp):
|
|
228
|
+
cal_ci = therm_cal[ci] if therm_cal and ci < len(therm_cal) else {}
|
|
229
|
+
noise_kwargs = {
|
|
230
|
+
k: v
|
|
231
|
+
for k, v in cal_ci.items()
|
|
232
|
+
if k in ("e_b", "gain", "b", "beta_1", "beta_2", "T_0", "adc_fs", "adc_bits")
|
|
233
|
+
}
|
|
234
|
+
noise_all[ci] = gradT_noise_batch(
|
|
235
|
+
F_const,
|
|
236
|
+
T_means,
|
|
237
|
+
speed_means,
|
|
238
|
+
fs=fs,
|
|
239
|
+
diff_gain=diff_gains[ci],
|
|
240
|
+
**noise_kwargs,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Append results
|
|
244
|
+
for w in range(n_windows):
|
|
245
|
+
acc.times.append(t_means[w])
|
|
246
|
+
acc.pres.append(P_means[w])
|
|
247
|
+
acc.temp.append(T_means[w])
|
|
248
|
+
acc.speed.append(speed_means[w])
|
|
249
|
+
acc.section.append(sec_id)
|
|
250
|
+
acc.nu.append(nu_all[w])
|
|
251
|
+
acc.kappa_T.append(kappa_T_all[w])
|
|
252
|
+
acc.kcyc.append(K_all[:, w])
|
|
253
|
+
acc.H2.append(H2_all[w])
|
|
254
|
+
acc.tau0.append(tau0_all[w])
|
|
255
|
+
acc.gradt_spec.append(clean_spectra[w].T)
|
|
256
|
+
acc.noise_spec.append(noise_all[:, w, :])
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def process_l3_chi(
|
|
260
|
+
l2_chi: L2ChiData,
|
|
261
|
+
params: L3Params,
|
|
262
|
+
*,
|
|
263
|
+
fp07_model: str = "single_pole",
|
|
264
|
+
salinity: float | np.ndarray | None = None,
|
|
265
|
+
therm_cal: list[dict] | None = None,
|
|
266
|
+
) -> L3ChiData:
|
|
267
|
+
"""Compute L3 temperature gradient spectra from L2ChiData.
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
l2_chi : L2ChiData
|
|
272
|
+
Level-2 cleaned chi data (despiked temperature gradient,
|
|
273
|
+
HP-filtered vibration, section info).
|
|
274
|
+
params : L3Params
|
|
275
|
+
Spectral parameters (fft_length, diss_length, overlap).
|
|
276
|
+
fp07_model : str
|
|
277
|
+
FP07 transfer function model ('single_pole' or 'double_pole').
|
|
278
|
+
salinity : float or array or None
|
|
279
|
+
Practical salinity for viscosity.
|
|
280
|
+
therm_cal : list of dict, optional
|
|
281
|
+
Per-thermistor calibration for noise model.
|
|
282
|
+
|
|
283
|
+
Returns
|
|
284
|
+
-------
|
|
285
|
+
L3ChiData
|
|
286
|
+
"""
|
|
287
|
+
fs = params.fs_fast
|
|
288
|
+
nfft = params.fft_length
|
|
289
|
+
diss_length = params.diss_length
|
|
290
|
+
diss_overlap = params.overlap
|
|
291
|
+
# Clamp to >= 1: overlap == diss_length would make diss_step 0 and the
|
|
292
|
+
# n_windows division below raise ZeroDivisionError.
|
|
293
|
+
diss_step = max(diss_length - diss_overlap, 1)
|
|
294
|
+
n_freq = nfft // 2 + 1
|
|
295
|
+
n_temp = l2_chi.n_temp
|
|
296
|
+
diff_gains = l2_chi.diff_gains
|
|
297
|
+
|
|
298
|
+
# Frequency vector (constant)
|
|
299
|
+
F_const = np.arange(n_freq) * fs / nfft
|
|
300
|
+
|
|
301
|
+
# First-difference correction (constant across windows/probes)
|
|
302
|
+
fd_correction = np.ones(n_freq)
|
|
303
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
304
|
+
fd_correction[1:] = (np.pi * F_const[1:] / (fs * np.sin(np.pi * F_const[1:] / fs))) ** 2
|
|
305
|
+
fd_correction = np.where(np.isfinite(fd_correction), fd_correction, 1.0)
|
|
306
|
+
|
|
307
|
+
# Bilinear corrections per probe
|
|
308
|
+
bl_corrections = [_bilinear_correction(F_const, dg, fs) for dg in diff_gains]
|
|
309
|
+
|
|
310
|
+
do_goodman = params.goodman and l2_chi.n_vib > 0
|
|
311
|
+
sections = np.unique(l2_chi.section_number)
|
|
312
|
+
sections = sections[sections > 0]
|
|
313
|
+
|
|
314
|
+
acc = _SectionResult()
|
|
315
|
+
|
|
316
|
+
for sec_id in sections:
|
|
317
|
+
_process_section_chi(
|
|
318
|
+
l2_chi,
|
|
319
|
+
sec_id,
|
|
320
|
+
diss_length,
|
|
321
|
+
diss_step,
|
|
322
|
+
nfft,
|
|
323
|
+
fs,
|
|
324
|
+
n_temp,
|
|
325
|
+
n_freq,
|
|
326
|
+
do_goodman,
|
|
327
|
+
fd_correction,
|
|
328
|
+
bl_corrections,
|
|
329
|
+
F_const,
|
|
330
|
+
fp07_model,
|
|
331
|
+
salinity,
|
|
332
|
+
diff_gains,
|
|
333
|
+
therm_cal,
|
|
334
|
+
acc,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if not acc.times:
|
|
338
|
+
return L3ChiData(
|
|
339
|
+
time=np.array([]),
|
|
340
|
+
pres=np.array([]),
|
|
341
|
+
temp=np.array([]),
|
|
342
|
+
pspd_rel=np.array([]),
|
|
343
|
+
section_number=np.array([]),
|
|
344
|
+
nu=np.array([]),
|
|
345
|
+
kappa_T=np.array([]),
|
|
346
|
+
kcyc=np.zeros((n_freq, 0)),
|
|
347
|
+
freq=F_const,
|
|
348
|
+
gradt_spec=np.zeros((n_temp, n_freq, 0)),
|
|
349
|
+
noise_spec=np.zeros((n_temp, n_freq, 0)),
|
|
350
|
+
H2=np.zeros((0, n_freq)),
|
|
351
|
+
tau0=np.array([]),
|
|
352
|
+
diff_gains=diff_gains,
|
|
353
|
+
fp07_model=fp07_model,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Assemble output arrays
|
|
357
|
+
time_out = np.array(acc.times)
|
|
358
|
+
pres_out = np.array(acc.pres)
|
|
359
|
+
temp_out = np.array(acc.temp)
|
|
360
|
+
speed_out = np.array(acc.speed)
|
|
361
|
+
section_out = np.array(acc.section)
|
|
362
|
+
nu_out = np.array(acc.nu)
|
|
363
|
+
kappa_T_out = np.array(acc.kappa_T)
|
|
364
|
+
tau0_out = np.array(acc.tau0)
|
|
365
|
+
|
|
366
|
+
kcyc_out = np.column_stack(acc.kcyc) # (n_freq, n_spec)
|
|
367
|
+
H2_out = np.stack(acc.H2, axis=0) # (n_spec, n_freq)
|
|
368
|
+
|
|
369
|
+
gradt_spec_out = np.stack(acc.gradt_spec, axis=-1) # (n_temp, n_freq, n_spec)
|
|
370
|
+
noise_spec_out = np.stack(acc.noise_spec, axis=-1) # (n_temp, n_freq, n_spec)
|
|
371
|
+
|
|
372
|
+
return L3ChiData(
|
|
373
|
+
time=time_out,
|
|
374
|
+
pres=pres_out,
|
|
375
|
+
temp=temp_out,
|
|
376
|
+
pspd_rel=speed_out,
|
|
377
|
+
section_number=section_out,
|
|
378
|
+
nu=nu_out,
|
|
379
|
+
kappa_T=kappa_T_out,
|
|
380
|
+
kcyc=kcyc_out,
|
|
381
|
+
freq=F_const,
|
|
382
|
+
gradt_spec=gradt_spec_out,
|
|
383
|
+
noise_spec=noise_spec_out,
|
|
384
|
+
H2=H2_out,
|
|
385
|
+
tau0=tau0_out,
|
|
386
|
+
diff_gains=diff_gains,
|
|
387
|
+
fp07_model=fp07_model,
|
|
388
|
+
)
|