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
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
# Jun-2026, Claude and Pat Welch, pat@mousebrains.com
|
|
2
|
+
"""Mixing viewer for Rockland Scientific profiler data.
|
|
3
|
+
|
|
4
|
+
Interactive multi-panel viewer focused on the diapycnal-mixing quantities the
|
|
5
|
+
pipeline derives from epsilon, chi, and the background stratification: the
|
|
6
|
+
buoyancy frequency N², the background temperature gradient dT/dz, the
|
|
7
|
+
Osborn-Cox heat diffusivity K_T, the measured Osborn mixing coefficient
|
|
8
|
+
Γ = N²χ / (2ε(dT/dz)²), and the Osborn diapycnal diffusivity K_rho.
|
|
9
|
+
|
|
10
|
+
Where ``diss_look`` shows only the dimensional χ/ε ratio, this viewer
|
|
11
|
+
computes the *proper* Γ by Thorpe-sorting each window to a stable profile
|
|
12
|
+
(removing overturns) for the background N² and dT/dz, exactly as the
|
|
13
|
+
``perturb``/``rsi`` pipelines do in their mixing step.
|
|
14
|
+
|
|
15
|
+
Layout (2 rows x 4 columns):
|
|
16
|
+
Row 0 (profile panels, pressure y-axis linked):
|
|
17
|
+
Overview | N² profile | K_T & K_rho profiles | Γ profile
|
|
18
|
+
Row 1:
|
|
19
|
+
dT/dz profile | ε profile | χ profile | K_T vs K_rho scatter
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import warnings
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
from odas_tpw.processing.mixing import mixing_coefficients, sorted_stratification
|
|
27
|
+
from odas_tpw.rsi.p_file import PFile
|
|
28
|
+
from odas_tpw.rsi.viewer_base import (
|
|
29
|
+
ProfileViewer,
|
|
30
|
+
compute_depth_spectra,
|
|
31
|
+
compute_windowed_diss,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Color cycle: shear-probe (epsilon) and thermistor (chi) families, matching
|
|
35
|
+
# the conventions used by quick_look / diss_look.
|
|
36
|
+
_SHEAR_COLORS = ["C0", "C1", "C4", "C5"]
|
|
37
|
+
_THERM_COLORS = ["C3", "C1", "C4", "C5"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MixingLookViewer(ProfileViewer):
|
|
41
|
+
"""Interactive mixing viewer with Prev/Next navigation."""
|
|
42
|
+
|
|
43
|
+
_nrows = 3 # extra row for the epsilon/chi spectra and FM/FOM panels
|
|
44
|
+
|
|
45
|
+
def __init__(self, pf, salinity=None, **kwargs):
|
|
46
|
+
super().__init__(pf, **kwargs)
|
|
47
|
+
self._P_win = np.array([])
|
|
48
|
+
self._strat = None
|
|
49
|
+
self._mix = None
|
|
50
|
+
self._eps_mix = np.array([])
|
|
51
|
+
self._chi_mix = np.array([])
|
|
52
|
+
|
|
53
|
+
# Slow-rate practical salinity for the background stratification:
|
|
54
|
+
# an explicit override wins; otherwise the profile's own JAC C/T (when
|
|
55
|
+
# both are present and aligned with the slow time base); otherwise None
|
|
56
|
+
# (-> 35 PSU assumed by sorted_stratification). Mirrors the pipeline's
|
|
57
|
+
# measured > supplied > 35 preference.
|
|
58
|
+
self.salinity: float | np.ndarray | None
|
|
59
|
+
if salinity is not None:
|
|
60
|
+
self.salinity = float(salinity)
|
|
61
|
+
else:
|
|
62
|
+
self.salinity = self._measured_salinity(pf)
|
|
63
|
+
|
|
64
|
+
# Colorbar for the pressure-colored K_T-vs-K_rho scatter; removed and
|
|
65
|
+
# rebuilt each draw so its stolen space never compounds.
|
|
66
|
+
self._scatter_cbar = None
|
|
67
|
+
|
|
68
|
+
def _measured_salinity(self, pf) -> np.ndarray | None:
|
|
69
|
+
"""Slow-rate practical salinity from JAC C/T, or None when unavailable."""
|
|
70
|
+
jac_c = pf.channels.get("JAC_C")
|
|
71
|
+
jac_t = pf.channels.get("JAC_T")
|
|
72
|
+
n_slow = len(self.t_slow)
|
|
73
|
+
if (
|
|
74
|
+
jac_c is None
|
|
75
|
+
or jac_t is None
|
|
76
|
+
or len(jac_c) != n_slow
|
|
77
|
+
or len(jac_t) != n_slow
|
|
78
|
+
or len(self.P) != n_slow
|
|
79
|
+
):
|
|
80
|
+
return None
|
|
81
|
+
import gsw
|
|
82
|
+
|
|
83
|
+
with warnings.catch_warnings():
|
|
84
|
+
warnings.simplefilter("ignore", RuntimeWarning)
|
|
85
|
+
sp = gsw.SP_from_C(np.asarray(jac_c), np.asarray(jac_t), np.asarray(self.P))
|
|
86
|
+
sp = np.asarray(sp, dtype=np.float64)
|
|
87
|
+
return sp if np.isfinite(sp).any() else None
|
|
88
|
+
|
|
89
|
+
# ------------------------------------------------------------------
|
|
90
|
+
# Axis layout
|
|
91
|
+
# ------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
def _setup_axes(self):
|
|
94
|
+
p_ref = self.axes[0, 0]
|
|
95
|
+
# Pressure-profile panels share the y-axis. Independent panels: the
|
|
96
|
+
# K_T-vs-K_rho scatter (1, 3) and the two wavenumber spectra (2, 0/1).
|
|
97
|
+
for col in range(1, 4):
|
|
98
|
+
self.axes[0, col].sharey(p_ref)
|
|
99
|
+
for col in range(3): # (1,0) dT/dz, (1,1) eps, (1,2) chi
|
|
100
|
+
self.axes[1, col].sharey(p_ref)
|
|
101
|
+
self.axes[2, 2].sharey(p_ref) # FM profile
|
|
102
|
+
self.axes[2, 3].sharey(p_ref) # chi FOM profile
|
|
103
|
+
p_ref.invert_yaxis()
|
|
104
|
+
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
# Per-window combine helpers (match pipeline epsi_final / chi_final)
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def _window_center_times(self, sel_fast) -> np.ndarray:
|
|
110
|
+
"""Window-center times aligned 1:1 with compute_windowed_diss windows.
|
|
111
|
+
|
|
112
|
+
Mirrors the windowing in :func:`compute_windowed_diss` exactly (same
|
|
113
|
+
overlap, step, count, and early-break) so each center time lines up with
|
|
114
|
+
the corresponding ``P_windows`` / epsilon / chi column. The caller
|
|
115
|
+
asserts the lengths match as a drift tripwire.
|
|
116
|
+
"""
|
|
117
|
+
diss = self.diss_length
|
|
118
|
+
overlap = diss // 2
|
|
119
|
+
step = diss - overlap
|
|
120
|
+
seg_start, seg_end = sel_fast.start, sel_fast.stop
|
|
121
|
+
n_windows = max(0, 1 + (seg_end - seg_start - diss) // step)
|
|
122
|
+
t_win = np.full(n_windows, np.nan)
|
|
123
|
+
for idx in range(n_windows):
|
|
124
|
+
s = seg_start + idx * step
|
|
125
|
+
e = s + diss
|
|
126
|
+
if e > seg_end:
|
|
127
|
+
break
|
|
128
|
+
t_win[idx] = float(np.mean(self.t_fast[s:e]))
|
|
129
|
+
return t_win
|
|
130
|
+
|
|
131
|
+
def _mixing_epsilon(self, d) -> np.ndarray:
|
|
132
|
+
"""Per-window epsilon for mixing: FM<1.15 geometric mean, nanmean fallback.
|
|
133
|
+
|
|
134
|
+
Reproduces the pipeline's QC-then-combine: average the probes that pass
|
|
135
|
+
the Lueck (2022) FM<1.15 quality cut (geometric mean, like
|
|
136
|
+
``_compute_epsi_final``); if no probe passes a window, fall back to the
|
|
137
|
+
geometric mean of all finite positive probes so the window still yields a
|
|
138
|
+
mixing estimate.
|
|
139
|
+
"""
|
|
140
|
+
eps = d["epsilon"] # (n_shear, n_win)
|
|
141
|
+
FM = d["FM"]
|
|
142
|
+
n_shear, n_win = eps.shape
|
|
143
|
+
out = np.full(n_win, np.nan)
|
|
144
|
+
if n_shear == 0 or n_win == 0:
|
|
145
|
+
return out
|
|
146
|
+
for j in range(n_win):
|
|
147
|
+
col = eps[:, j]
|
|
148
|
+
# Keep probes that pass the Lueck (2022) reject criterion FM <= 1.15
|
|
149
|
+
# (reject FM > 1.15), matching the pipeline's fom_limit convention.
|
|
150
|
+
passed = col[np.isfinite(FM[:, j]) & (FM[:, j] <= 1.15) & np.isfinite(col) & (col > 0)]
|
|
151
|
+
pool = passed if passed.size else col[np.isfinite(col) & (col > 0)]
|
|
152
|
+
if pool.size:
|
|
153
|
+
out[j] = float(np.exp(np.mean(np.log(pool))))
|
|
154
|
+
return out
|
|
155
|
+
|
|
156
|
+
def _mixing_chi(self, d) -> np.ndarray:
|
|
157
|
+
"""Per-window chi for mixing: mean(Batchelor, Kraichnan) per thermistor,
|
|
158
|
+
then geometric mean across thermistors.
|
|
159
|
+
|
|
160
|
+
Diagnostic approximation only: this pools finite, positive probes but
|
|
161
|
+
does NOT apply the fom / K_max_ratio spectral QC that the published
|
|
162
|
+
``L4ChiData.chi_final`` now uses (2026-07-03 review, Gemini C), so it can
|
|
163
|
+
differ from the reported chi for windows with a poorly-fit thermistor.
|
|
164
|
+
"""
|
|
165
|
+
chi_b = d["chi_batchelor"] # (n_therm, n_win)
|
|
166
|
+
chi_k = d["chi_kraichnan"]
|
|
167
|
+
n_therm, n_win = chi_b.shape
|
|
168
|
+
out = np.full(n_win, np.nan)
|
|
169
|
+
if n_therm == 0 or n_win == 0:
|
|
170
|
+
return out
|
|
171
|
+
per = np.where(
|
|
172
|
+
np.isfinite(chi_b) & np.isfinite(chi_k),
|
|
173
|
+
(chi_b + chi_k) / 2.0,
|
|
174
|
+
np.where(np.isfinite(chi_b), chi_b, chi_k),
|
|
175
|
+
)
|
|
176
|
+
for j in range(n_win):
|
|
177
|
+
col = per[:, j]
|
|
178
|
+
pool = col[np.isfinite(col) & (col > 0)]
|
|
179
|
+
if pool.size:
|
|
180
|
+
out[j] = float(np.exp(np.mean(np.log(pool))))
|
|
181
|
+
return out
|
|
182
|
+
|
|
183
|
+
# ------------------------------------------------------------------
|
|
184
|
+
# Draw
|
|
185
|
+
# ------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
def _draw(self):
|
|
188
|
+
s_slow, e_slow = self.profiles[self.profile_idx]
|
|
189
|
+
sel_fast = self._slow_to_fast_slice(s_slow, e_slow)
|
|
190
|
+
self._spec_bin_width = self._spec_bin_dbar(s_slow, e_slow)
|
|
191
|
+
sel_spec = self._spec_fast_slice(sel_fast)
|
|
192
|
+
|
|
193
|
+
if self._scatter_cbar is not None:
|
|
194
|
+
self._scatter_cbar.remove()
|
|
195
|
+
self._scatter_cbar = None
|
|
196
|
+
|
|
197
|
+
with warnings.catch_warnings():
|
|
198
|
+
warnings.simplefilter("ignore", UserWarning)
|
|
199
|
+
for ax in self.axes.flat:
|
|
200
|
+
ax.clear()
|
|
201
|
+
self.axes[0, 0].invert_yaxis()
|
|
202
|
+
|
|
203
|
+
d = compute_windowed_diss(
|
|
204
|
+
self.shear,
|
|
205
|
+
self.accel,
|
|
206
|
+
self.therm_fast,
|
|
207
|
+
self.diff_gains,
|
|
208
|
+
self.P_fast,
|
|
209
|
+
self.T,
|
|
210
|
+
self.speed_fast,
|
|
211
|
+
self.fs_fast,
|
|
212
|
+
sel_fast,
|
|
213
|
+
self.fft_length,
|
|
214
|
+
self.f_AA,
|
|
215
|
+
self.goodman,
|
|
216
|
+
diss_length=self.diss_length,
|
|
217
|
+
)
|
|
218
|
+
self._cached_diss = d
|
|
219
|
+
P = d["P_windows"]
|
|
220
|
+
self._P_win = P
|
|
221
|
+
|
|
222
|
+
# Spectra at the selected depth for the row-2 spectral panels.
|
|
223
|
+
self._cached_spec = compute_depth_spectra(
|
|
224
|
+
self.shear,
|
|
225
|
+
self.accel,
|
|
226
|
+
self.therm_fast,
|
|
227
|
+
self.diff_gains,
|
|
228
|
+
self.P_fast,
|
|
229
|
+
self.T,
|
|
230
|
+
self.speed_fast,
|
|
231
|
+
self.fs_fast,
|
|
232
|
+
sel_spec,
|
|
233
|
+
self.fft_length,
|
|
234
|
+
self.f_AA,
|
|
235
|
+
self.goodman,
|
|
236
|
+
diss_length=self.diss_length,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Background stratification on the Thorpe-sorted slow profile, then the
|
|
240
|
+
# mixing coefficients on the same window grid.
|
|
241
|
+
t_win = self._window_center_times(sel_fast)
|
|
242
|
+
assert len(t_win) == len(P), "window-time grid drifted from compute_windowed_diss"
|
|
243
|
+
half_w = 0.5 * self.diss_length / self.fs_fast
|
|
244
|
+
with warnings.catch_warnings():
|
|
245
|
+
warnings.simplefilter("ignore", RuntimeWarning)
|
|
246
|
+
self._strat = sorted_stratification(
|
|
247
|
+
t_win, half_w, self.t_slow, self.P, self.T, S=self.salinity
|
|
248
|
+
)
|
|
249
|
+
self._eps_mix = self._mixing_epsilon(d)
|
|
250
|
+
self._chi_mix = self._mixing_chi(d)
|
|
251
|
+
self._mix = mixing_coefficients(
|
|
252
|
+
self._eps_mix, self._chi_mix, self._strat.N2, self._strat.dTdz
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Row 0
|
|
256
|
+
self._draw_overview(self.axes[0, 0], s_slow, e_slow)
|
|
257
|
+
self._draw_n2(self.axes[0, 1])
|
|
258
|
+
self._draw_diffusivities(self.axes[0, 2])
|
|
259
|
+
self._draw_gamma(self.axes[0, 3])
|
|
260
|
+
|
|
261
|
+
# Row 1
|
|
262
|
+
self._draw_dtdz(self.axes[1, 0])
|
|
263
|
+
self._draw_eps_profile(self.axes[1, 1], P, d["epsilon"])
|
|
264
|
+
self._draw_chi_profile(self.axes[1, 2])
|
|
265
|
+
self._draw_kt_krho_scatter(self.axes[1, 3])
|
|
266
|
+
|
|
267
|
+
# Row 2: spectra at depth + Lueck FM / chi FOM (shared base panels)
|
|
268
|
+
self._draw_eps_spectra(self.axes[2, 0], sel_spec, self._cached_spec)
|
|
269
|
+
self._draw_chi_m1m2_spectra(self.axes[2, 1], sel_spec)
|
|
270
|
+
self._draw_fm_profile(self.axes[2, 2])
|
|
271
|
+
self._draw_chi_fom_profile(self.axes[2, 3])
|
|
272
|
+
|
|
273
|
+
pressure_axes = (
|
|
274
|
+
[self.axes[0, c] for c in range(4)]
|
|
275
|
+
+ [self.axes[1, c] for c in range(3)]
|
|
276
|
+
+ [self.axes[2, 2], self.axes[2, 3]]
|
|
277
|
+
)
|
|
278
|
+
self._finish_draw(s_slow, e_slow, pressure_axes)
|
|
279
|
+
|
|
280
|
+
# ------------------------------------------------------------------
|
|
281
|
+
# Panels
|
|
282
|
+
# ------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
@staticmethod
|
|
285
|
+
def _no_data(ax, msg):
|
|
286
|
+
ax.text(0.5, 0.5, msg, transform=ax.transAxes, ha="center", va="center")
|
|
287
|
+
|
|
288
|
+
def _draw_n2(self, ax):
|
|
289
|
+
"""Panel (0,1): background N² vs pressure (log; only N² > 0)."""
|
|
290
|
+
P = self._P_win
|
|
291
|
+
N2 = self._strat.N2 if self._strat is not None else np.array([])
|
|
292
|
+
valid = np.isfinite(N2) & (N2 > 0) & np.isfinite(P)
|
|
293
|
+
if not np.any(valid):
|
|
294
|
+
self._no_data(ax, "No stratified N²")
|
|
295
|
+
return
|
|
296
|
+
ax.plot(N2[valid], P[valid], "C2o-", markersize=3, linewidth=0.8)
|
|
297
|
+
ax.set_xscale("log")
|
|
298
|
+
ax.set_xlabel("N² [s⁻²]")
|
|
299
|
+
ax.set_ylabel("Pressure [dbar]")
|
|
300
|
+
ax.set_title("N² (Thorpe-sorted)", fontsize=9)
|
|
301
|
+
ax.grid(True, alpha=0.3, which="both")
|
|
302
|
+
|
|
303
|
+
def _draw_dtdz(self, ax):
|
|
304
|
+
"""Panel (1,0): background dT/dz vs pressure (linear; positive down)."""
|
|
305
|
+
P = self._P_win
|
|
306
|
+
dTdz = self._strat.dTdz if self._strat is not None else np.array([])
|
|
307
|
+
valid = np.isfinite(dTdz) & np.isfinite(P)
|
|
308
|
+
if not np.any(valid):
|
|
309
|
+
self._no_data(ax, "No dT/dz")
|
|
310
|
+
return
|
|
311
|
+
ax.plot(dTdz[valid], P[valid], "C5o-", markersize=3, linewidth=0.8)
|
|
312
|
+
ax.axvline(0.0, color="k", linewidth=0.7, alpha=0.5)
|
|
313
|
+
ax.set_xlabel("dT/dz [K m⁻¹]")
|
|
314
|
+
ax.set_ylabel("Pressure [dbar]")
|
|
315
|
+
ax.set_title("Background dT/dz", fontsize=9)
|
|
316
|
+
ax.grid(True, alpha=0.3)
|
|
317
|
+
|
|
318
|
+
def _draw_diffusivities(self, ax):
|
|
319
|
+
"""Panel (0,2): K_T (Osborn-Cox) and K_rho (Osborn) vs pressure (log)."""
|
|
320
|
+
P = self._P_win
|
|
321
|
+
if self._mix is None:
|
|
322
|
+
self._no_data(ax, "No data")
|
|
323
|
+
return
|
|
324
|
+
has = False
|
|
325
|
+
for vals, color, label in (
|
|
326
|
+
(self._mix.K_T, "C0", r"$K_T$ (Osborn-Cox)"),
|
|
327
|
+
(self._mix.K_rho, "C3", r"$K_\rho$ (Osborn)"),
|
|
328
|
+
):
|
|
329
|
+
valid = np.isfinite(vals) & (vals > 0) & np.isfinite(P)
|
|
330
|
+
if np.any(valid):
|
|
331
|
+
ax.plot(vals[valid], P[valid], "o-", color=color, markersize=3,
|
|
332
|
+
linewidth=0.8, label=label)
|
|
333
|
+
has = True
|
|
334
|
+
if not has:
|
|
335
|
+
self._no_data(ax, "No valid K")
|
|
336
|
+
return
|
|
337
|
+
ax.set_xscale("log")
|
|
338
|
+
ax.set_xlabel("K [m² s⁻¹]")
|
|
339
|
+
ax.set_ylabel("Pressure [dbar]")
|
|
340
|
+
ax.legend(fontsize=6, loc="lower left")
|
|
341
|
+
ax.set_title(r"Diffusivity: $K_T$ vs $K_\rho$", fontsize=9)
|
|
342
|
+
ax.grid(True, alpha=0.3, which="both")
|
|
343
|
+
|
|
344
|
+
def _draw_gamma(self, ax):
|
|
345
|
+
"""Panel (0,3): measured mixing coefficient Γ vs pressure (log; ref 0.2)."""
|
|
346
|
+
P = self._P_win
|
|
347
|
+
Gamma = self._mix.Gamma if self._mix is not None else np.array([])
|
|
348
|
+
valid = np.isfinite(Gamma) & (Gamma > 0) & np.isfinite(P)
|
|
349
|
+
if not np.any(valid):
|
|
350
|
+
self._no_data(ax, "No valid Γ")
|
|
351
|
+
return
|
|
352
|
+
ax.plot(Gamma[valid], P[valid], "C4o-", markersize=3, linewidth=0.8)
|
|
353
|
+
ax.axvline(0.2, color="k", linestyle="--", linewidth=0.9, alpha=0.7, label="Γ = 0.2")
|
|
354
|
+
ax.set_xscale("log")
|
|
355
|
+
ax.set_xlabel("Γ = N²χ / (2ε(dT/dz)²)")
|
|
356
|
+
ax.set_ylabel("Pressure [dbar]")
|
|
357
|
+
ax.legend(fontsize=6, loc="lower left")
|
|
358
|
+
ax.set_title("Mixing coefficient Γ", fontsize=9)
|
|
359
|
+
ax.grid(True, alpha=0.3, which="both")
|
|
360
|
+
|
|
361
|
+
def _draw_chi_profile(self, ax):
|
|
362
|
+
"""Panel (1,2): chi per thermistor vs pressure (mean of Batchelor & Kraichnan)."""
|
|
363
|
+
d = self._cached_diss
|
|
364
|
+
P = self._P_win
|
|
365
|
+
if d is None or len(P) == 0:
|
|
366
|
+
self._no_data(ax, "No data")
|
|
367
|
+
return
|
|
368
|
+
has = False
|
|
369
|
+
for i, (name, _) in enumerate(self.therm_fast):
|
|
370
|
+
c = _THERM_COLORS[i % len(_THERM_COLORS)]
|
|
371
|
+
chi_b = d["chi_batchelor"][i]
|
|
372
|
+
chi_k = d["chi_kraichnan"][i]
|
|
373
|
+
chi_mean = np.where(
|
|
374
|
+
np.isfinite(chi_b) & np.isfinite(chi_k),
|
|
375
|
+
(chi_b + chi_k) / 2.0,
|
|
376
|
+
np.where(np.isfinite(chi_b), chi_b, chi_k),
|
|
377
|
+
)
|
|
378
|
+
valid = np.isfinite(chi_mean) & (chi_mean > 0)
|
|
379
|
+
if np.any(valid):
|
|
380
|
+
ax.plot(chi_mean[valid], P[valid], f"{c}o-", markersize=3,
|
|
381
|
+
linewidth=0.8, label=name)
|
|
382
|
+
has = True
|
|
383
|
+
if not has:
|
|
384
|
+
self._no_data(ax, "No valid χ")
|
|
385
|
+
return
|
|
386
|
+
ax.set_xscale("log")
|
|
387
|
+
ax.set_xlabel("χ [K²/s]")
|
|
388
|
+
ax.set_ylabel("Pressure [dbar]")
|
|
389
|
+
ax.legend(fontsize=6, loc="lower left")
|
|
390
|
+
ax.set_title("χ profile (Batch. + Kraich.)", fontsize=9)
|
|
391
|
+
ax.grid(True, alpha=0.3, which="both")
|
|
392
|
+
|
|
393
|
+
def _draw_kt_krho_scatter(self, ax):
|
|
394
|
+
"""Panel (1,3): K_T vs K_rho (log-log, 1:1 line), colored by pressure.
|
|
395
|
+
|
|
396
|
+
Points on the 1:1 line have measured Γ ≈ 0.2 (K_T ≈ K_rho); the pressure
|
|
397
|
+
color shows how the agreement varies with depth.
|
|
398
|
+
"""
|
|
399
|
+
if self._mix is None:
|
|
400
|
+
self._no_data(ax, "No data")
|
|
401
|
+
return
|
|
402
|
+
K_T = self._mix.K_T
|
|
403
|
+
K_rho = self._mix.K_rho
|
|
404
|
+
valid = (
|
|
405
|
+
np.isfinite(K_T) & (K_T > 0) & np.isfinite(K_rho) & (K_rho > 0)
|
|
406
|
+
& np.isfinite(self._P_win)
|
|
407
|
+
)
|
|
408
|
+
if not np.any(valid):
|
|
409
|
+
self._no_data(ax, "No valid K_T/K_rho")
|
|
410
|
+
return
|
|
411
|
+
x = K_rho[valid]
|
|
412
|
+
y = K_T[valid]
|
|
413
|
+
pcol = self._P_win[valid]
|
|
414
|
+
ax.set_xscale("log")
|
|
415
|
+
ax.set_yscale("log")
|
|
416
|
+
sc = ax.scatter(x, y, c=pcol, cmap="viridis", s=26, alpha=0.85, edgecolors="none")
|
|
417
|
+
assert self.fig is not None
|
|
418
|
+
cbar = self.fig.colorbar(sc, ax=ax, pad=0.02)
|
|
419
|
+
cbar.set_label("Pressure [dbar]", fontsize=7)
|
|
420
|
+
cbar.ax.tick_params(labelsize=6)
|
|
421
|
+
cbar.ax.invert_yaxis() # shallow at top, deep at bottom (matches profiles)
|
|
422
|
+
self._scatter_cbar = cbar
|
|
423
|
+
lo = float(min(x.min(), y.min()))
|
|
424
|
+
hi = float(max(x.max(), y.max()))
|
|
425
|
+
ax.plot([lo, hi], [lo, hi], "k--", linewidth=0.9, alpha=0.7, label="1:1 (Γ = 0.2)")
|
|
426
|
+
ax.set_xlabel(r"$K_\rho$ (Osborn) [m² s⁻¹]")
|
|
427
|
+
ax.set_ylabel(r"$K_T$ (Osborn-Cox) [m² s⁻¹]")
|
|
428
|
+
ax.legend(fontsize=6, loc="upper left")
|
|
429
|
+
ax.set_title(r"$K_T$ vs $K_\rho$ (color = P)", fontsize=9)
|
|
430
|
+
ax.grid(True, alpha=0.3, which="both")
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# ---------------------------------------------------------------------------
|
|
434
|
+
# Entry point
|
|
435
|
+
# ---------------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def mixing_look(
|
|
439
|
+
source,
|
|
440
|
+
fft_length=1024,
|
|
441
|
+
f_AA=98.0,
|
|
442
|
+
goodman=True,
|
|
443
|
+
direction="auto",
|
|
444
|
+
P_min=0.5,
|
|
445
|
+
W_min=0.3,
|
|
446
|
+
min_duration=7.0,
|
|
447
|
+
spec_P_range=None,
|
|
448
|
+
diss_length=None,
|
|
449
|
+
vehicle=None,
|
|
450
|
+
salinity=None,
|
|
451
|
+
) -> MixingLookViewer:
|
|
452
|
+
"""Open an interactive mixing viewer for a .p file.
|
|
453
|
+
|
|
454
|
+
Displays the background stratification (N², dT/dz) and the derived
|
|
455
|
+
diapycnal-mixing quantities (K_T, Γ, K_rho), computed exactly as the
|
|
456
|
+
pipeline does: per-window epsilon and chi with the background N²/dT/dz
|
|
457
|
+
from a Thorpe-sorted (overturn-removed) profile.
|
|
458
|
+
|
|
459
|
+
Parameters
|
|
460
|
+
----------
|
|
461
|
+
source : str or Path
|
|
462
|
+
Path to a Rockland .p file.
|
|
463
|
+
fft_length : int
|
|
464
|
+
FFT segment length for spectral estimation (default: 1024).
|
|
465
|
+
f_AA : float
|
|
466
|
+
Anti-aliasing filter cutoff [Hz].
|
|
467
|
+
goodman : bool
|
|
468
|
+
Apply Goodman coherent noise removal to shear spectra.
|
|
469
|
+
direction : str
|
|
470
|
+
Profile direction: 'auto', 'up', 'down', 'glide', or 'horizontal'.
|
|
471
|
+
P_min : float
|
|
472
|
+
Minimum pressure for profile detection [dbar].
|
|
473
|
+
W_min : float
|
|
474
|
+
Minimum fall rate for profile detection [dbar/s].
|
|
475
|
+
min_duration : float
|
|
476
|
+
Minimum profile duration [s].
|
|
477
|
+
spec_P_range : tuple of (float, float) or None
|
|
478
|
+
Pressure range (P_min, P_max) in dbar to highlight on the profiles.
|
|
479
|
+
diss_length : int or None
|
|
480
|
+
Dissipation window length [samples]. Default: 4 * fft_length.
|
|
481
|
+
vehicle : str or None
|
|
482
|
+
Vehicle type override (e.g. 'slocum_glider'). If None, read from
|
|
483
|
+
instrument config.
|
|
484
|
+
salinity : float or None
|
|
485
|
+
Fixed practical salinity [PSU] for the stratification. If None, the
|
|
486
|
+
profile's own JAC C/T conductivity is used when available, else 35.
|
|
487
|
+
|
|
488
|
+
Returns
|
|
489
|
+
-------
|
|
490
|
+
MixingLookViewer
|
|
491
|
+
The viewer instance.
|
|
492
|
+
"""
|
|
493
|
+
pf = PFile(source)
|
|
494
|
+
viewer = MixingLookViewer(
|
|
495
|
+
pf,
|
|
496
|
+
fft_length=fft_length,
|
|
497
|
+
f_AA=f_AA,
|
|
498
|
+
goodman=goodman,
|
|
499
|
+
direction=direction,
|
|
500
|
+
P_min=P_min,
|
|
501
|
+
W_min=W_min,
|
|
502
|
+
min_duration=min_duration,
|
|
503
|
+
spec_P_range=spec_P_range,
|
|
504
|
+
diss_length=diss_length,
|
|
505
|
+
vehicle=vehicle,
|
|
506
|
+
salinity=salinity,
|
|
507
|
+
)
|
|
508
|
+
viewer.show()
|
|
509
|
+
return viewer
|