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,15 @@
|
|
|
1
|
+
# May-2026, Claude and Pat Welch, pat@mousebrains.com
|
|
2
|
+
"""Aggregated-output plotting for perturb runs.
|
|
3
|
+
|
|
4
|
+
The :mod:`odas_tpw.perturb.plot` subpackage hosts the figures produced
|
|
5
|
+
from the ``*_combo_NN/`` outputs of a perturb pipeline. They are
|
|
6
|
+
exposed as subcommands of the ``perturb-plot`` console script (see
|
|
7
|
+
``pyproject.toml``); ``perturb-plot --help`` lists what is available.
|
|
8
|
+
|
|
9
|
+
Modules
|
|
10
|
+
-------
|
|
11
|
+
- :mod:`odas_tpw.perturb.plot.layout` — shared depth/cast layout helpers.
|
|
12
|
+
- :mod:`odas_tpw.perturb.plot.eps_chi` — pcolor of log10(epsilon),
|
|
13
|
+
log10(chi) and log10(chi/epsilon) vs depth and cast number.
|
|
14
|
+
- :mod:`odas_tpw.perturb.plot.cli` — argparse dispatcher.
|
|
15
|
+
"""
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# May-2026, Claude and Pat Welch, pat@mousebrains.com
|
|
2
|
+
"""``perturb-plot`` console-script entry point.
|
|
3
|
+
|
|
4
|
+
Top-level dispatcher for the figures produced from a perturb run's
|
|
5
|
+
aggregated outputs. Every plot lives in its own sibling module under
|
|
6
|
+
:mod:`odas_tpw.perturb.plot` and is wired in here as a subcommand so
|
|
7
|
+
``perturb-plot --help`` is the single source of truth.
|
|
8
|
+
|
|
9
|
+
Adding a new plot
|
|
10
|
+
-----------------
|
|
11
|
+
1. Create ``odas_tpw/perturb/plot/<name>.py`` exposing
|
|
12
|
+
``add_arguments(parser)`` and ``run(args) -> str`` (returns the
|
|
13
|
+
output path).
|
|
14
|
+
2. Register it in :data:`_SUBCOMMANDS` below.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
|
|
21
|
+
from odas_tpw.perturb.plot import (
|
|
22
|
+
eps_chi,
|
|
23
|
+
figure,
|
|
24
|
+
gamma_scaling,
|
|
25
|
+
overview,
|
|
26
|
+
profiles,
|
|
27
|
+
scalar,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Map subcommand name -> (description, add_arguments, run).
|
|
31
|
+
_SUBCOMMANDS = {
|
|
32
|
+
"figure": (
|
|
33
|
+
"Render many figures from one YAML spec (presets scalar/profiles/epsilon/"
|
|
34
|
+
"chi/mixing/eps-chi), resolving directories from a perturb config.",
|
|
35
|
+
figure.add_arguments,
|
|
36
|
+
figure.run,
|
|
37
|
+
),
|
|
38
|
+
"eps-chi": (
|
|
39
|
+
"Pcolor of log10(epsilon), log10(chi) and log10(chi/epsilon) "
|
|
40
|
+
"vs depth and cast number.",
|
|
41
|
+
eps_chi.add_arguments,
|
|
42
|
+
eps_chi.run,
|
|
43
|
+
),
|
|
44
|
+
"overview": (
|
|
45
|
+
"Per-section overview: epsilon and chi (two full-width rows) over a "
|
|
46
|
+
"context row chosen by --bottom (ctd: dT/dz, CT, salinity; mixing: "
|
|
47
|
+
"K_rho, K_T, Gamma), all vs depth and the section x-axis.",
|
|
48
|
+
overview.add_arguments,
|
|
49
|
+
overview.run,
|
|
50
|
+
),
|
|
51
|
+
"scalar": (
|
|
52
|
+
"Depth-vs-x scalar sections (T/S/density/...) from the CTD combo, "
|
|
53
|
+
"with time/latitude/longitude/distance x-axis methods.",
|
|
54
|
+
scalar.add_arguments,
|
|
55
|
+
scalar.run,
|
|
56
|
+
),
|
|
57
|
+
"profiles": (
|
|
58
|
+
"Depth-vs-x sections of binned slow channels (T1/T2/N2/dTdz) per cast.",
|
|
59
|
+
profiles.PROFILES.add_arguments,
|
|
60
|
+
profiles.PROFILES.run,
|
|
61
|
+
),
|
|
62
|
+
"epsilon": (
|
|
63
|
+
"Depth-vs-x sections of binned epsilon (dissipation) per cast.",
|
|
64
|
+
profiles.EPSILON.add_arguments,
|
|
65
|
+
profiles.EPSILON.run,
|
|
66
|
+
),
|
|
67
|
+
"chi": (
|
|
68
|
+
"Depth-vs-x sections of binned chi (thermal-variance dissipation) per cast.",
|
|
69
|
+
profiles.CHI.add_arguments,
|
|
70
|
+
profiles.CHI.run,
|
|
71
|
+
),
|
|
72
|
+
"mixing": (
|
|
73
|
+
"Depth-vs-x sections of binned mixing (K_T/Gamma/K_rho) per cast.",
|
|
74
|
+
profiles.MIXING.add_arguments,
|
|
75
|
+
profiles.MIXING.run,
|
|
76
|
+
),
|
|
77
|
+
"gamma-scaling": (
|
|
78
|
+
"Mixing-efficiency scaling scatter (Lewin et al. 2025 Fig. 5): "
|
|
79
|
+
"Gamma vs R_OT, Re_b, and (with --adcp) Ri_g per window, plus a "
|
|
80
|
+
"Thorpe-route comparison figure.",
|
|
81
|
+
gamma_scaling.add_arguments,
|
|
82
|
+
gamma_scaling.run,
|
|
83
|
+
),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
88
|
+
parser = argparse.ArgumentParser(
|
|
89
|
+
prog="perturb-plot",
|
|
90
|
+
description="Aggregated-output plotting for perturb runs.",
|
|
91
|
+
)
|
|
92
|
+
sub = parser.add_subparsers(dest="command", required=True, metavar="subcommand")
|
|
93
|
+
for name, (descr, add_args, _run) in _SUBCOMMANDS.items():
|
|
94
|
+
sp = sub.add_parser(name, help=descr, description=descr)
|
|
95
|
+
add_args(sp)
|
|
96
|
+
return parser
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def main(argv: list[str] | None = None) -> int:
|
|
100
|
+
args = build_parser().parse_args(argv)
|
|
101
|
+
_, _, run_fn = _SUBCOMMANDS[args.command]
|
|
102
|
+
run_fn(args)
|
|
103
|
+
return 0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__": # pragma: no cover
|
|
107
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Jun-2026, Claude and Pat Welch, pat@mousebrains.com
|
|
2
|
+
"""Plot-time diagnostic pseudo-variables for ``perturb-plot profiles``.
|
|
3
|
+
|
|
4
|
+
Shear / vibration / temperature-gradient *variance* is not stored in any binned
|
|
5
|
+
product — it lives in the raw fast channels of the per-profile files
|
|
6
|
+
(``profiles_NN/*_prof*.nc``). This module computes, on demand, a
|
|
7
|
+
``(bin, profile)`` grid of the per-pressure-bin variance of a raw fast channel,
|
|
8
|
+
aligned to a combo's profiles by ``stime``.
|
|
9
|
+
|
|
10
|
+
Fidelity: the channel is preprocessed with the same two steps the epsilon path
|
|
11
|
+
uses — a first-order Butterworth high-pass (``_hp_filter``, removing platform
|
|
12
|
+
motion / the mean gradient swept through a bin) followed by the iterative
|
|
13
|
+
despike (removing spikes) — then the time-variance is taken in each pressure
|
|
14
|
+
bin. It is applied over the *whole* cast record, NOT within the epsilon path's
|
|
15
|
+
speed/pressure-selected sections, so cast-start/turnaround transients are
|
|
16
|
+
included; this is a QC indicator, so the approximation is acceptable. These
|
|
17
|
+
are **contamination/activity diagnostics**, not
|
|
18
|
+
turbulence quantities: ``Ax``/``Ay`` are raw piezo *counts* (so their variance
|
|
19
|
+
is instrument-relative), and the resolved-band variance of shear is related to
|
|
20
|
+
but not equal to epsilon.
|
|
21
|
+
|
|
22
|
+
Requested via ``--var <channel>_var`` (e.g. ``sh1_var``, ``Ax_var``,
|
|
23
|
+
``T1_dT1_var``).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import glob
|
|
29
|
+
import os
|
|
30
|
+
|
|
31
|
+
import numpy as np
|
|
32
|
+
|
|
33
|
+
# pseudo-var -> (raw fast channel, colorbar label (mathtext units), cmocean cmap)
|
|
34
|
+
_DIAG_INFO: dict[str, tuple[str, str, str]] = {
|
|
35
|
+
"sh1_var": ("sh1", r"sh1 HP/despiked variance (s$^{-2}$)", "amp"),
|
|
36
|
+
"sh2_var": ("sh2", r"sh2 HP/despiked variance (s$^{-2}$)", "amp"),
|
|
37
|
+
"Ax_var": ("Ax", r"Ax vibration variance (counts$^2$)", "amp"),
|
|
38
|
+
"Ay_var": ("Ay", r"Ay vibration variance (counts$^2$)", "amp"),
|
|
39
|
+
"T1_dT1_var": ("T1_dT1", r"T1 gradient-channel variance (K$^2$)", "amp"),
|
|
40
|
+
"T2_dT2_var": ("T2_dT2", r"T2 gradient-channel variance (K$^2$)", "amp"),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Cache of {round(stime,3) -> rawpath} per profiles dir (built once per run).
|
|
44
|
+
_STIME_CACHE: dict[str, dict[float, str]] = {}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_pseudo_var(name: str) -> bool:
|
|
48
|
+
"""True when *name* is a diagnostic pseudo-variable (computed, not stored)."""
|
|
49
|
+
return name in _DIAG_INFO
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def pseudo_names() -> list[str]:
|
|
53
|
+
return list(_DIAG_INFO)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def pseudo_label(name: str) -> str:
|
|
57
|
+
return _DIAG_INFO[name][1]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def pseudo_cmap(name: str) -> str:
|
|
61
|
+
return _DIAG_INFO[name][2]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _raw_stime_map(profiles_dir: str) -> dict[float, str]:
|
|
65
|
+
"""Map ``round(stime, 3) -> rawpath`` over ``profiles_dir/*_prof*.nc``.
|
|
66
|
+
|
|
67
|
+
Built once per directory. Reading a scalar ``stime`` is cheap (metadata
|
|
68
|
+
only); matching is by value because the combo is stime-sorted across
|
|
69
|
+
instruments, so filename/glob order does not correspond to combo order.
|
|
70
|
+
"""
|
|
71
|
+
if profiles_dir in _STIME_CACHE:
|
|
72
|
+
return _STIME_CACHE[profiles_dir]
|
|
73
|
+
import xarray as xr
|
|
74
|
+
|
|
75
|
+
m: dict[float, str] = {}
|
|
76
|
+
for p in sorted(glob.glob(os.path.join(profiles_dir, "*prof*.nc"))):
|
|
77
|
+
try:
|
|
78
|
+
with xr.open_dataset(p, decode_times=False) as ds:
|
|
79
|
+
if "stime" in ds.variables:
|
|
80
|
+
m[round(float(ds["stime"].values), 3)] = p
|
|
81
|
+
except (OSError, ValueError):
|
|
82
|
+
continue
|
|
83
|
+
_STIME_CACHE[profiles_dir] = m
|
|
84
|
+
return m
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _variance_by_bin(sig: np.ndarray, pressure: np.ndarray, bin_edges: np.ndarray) -> np.ndarray:
|
|
88
|
+
"""Time-variance of *sig* within each pressure bin (NaN where < 3 samples)."""
|
|
89
|
+
n = len(bin_edges) - 1
|
|
90
|
+
out = np.full(n, np.nan)
|
|
91
|
+
good = np.isfinite(sig) & np.isfinite(pressure)
|
|
92
|
+
sig, pressure = sig[good], pressure[good]
|
|
93
|
+
if sig.size == 0:
|
|
94
|
+
return out
|
|
95
|
+
idx = np.digitize(pressure, bin_edges) - 1
|
|
96
|
+
idx[pressure == bin_edges[-1]] = n - 1 # keep the top edge in the last bin (cf. binning.py)
|
|
97
|
+
for i in range(n):
|
|
98
|
+
s = sig[idx == i]
|
|
99
|
+
if s.size > 2:
|
|
100
|
+
out[i] = float(np.var(s))
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _binned_channel_variance(
|
|
105
|
+
rawpath: str, channel: str, bin_edges: np.ndarray,
|
|
106
|
+
hp_cut: float, despike_thresh: float, despike_smooth: float,
|
|
107
|
+
) -> np.ndarray | None:
|
|
108
|
+
"""Per-depth-bin variance of one HP-filtered, despiked fast channel."""
|
|
109
|
+
import gsw
|
|
110
|
+
import xarray as xr
|
|
111
|
+
|
|
112
|
+
from odas_tpw.perturb.binning import _DEFAULT_BIN_LATITUDE
|
|
113
|
+
from odas_tpw.scor160.despike import despike
|
|
114
|
+
from odas_tpw.scor160.l2 import _hp_filter
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
with xr.open_dataset(rawpath, decode_times=False) as ds:
|
|
118
|
+
if not all(v in ds.variables for v in (channel, "P", "t_fast", "t_slow")):
|
|
119
|
+
return None
|
|
120
|
+
sig = np.asarray(ds[channel].values, dtype=float).ravel()
|
|
121
|
+
t_fast = np.asarray(ds["t_fast"].values, dtype=float)
|
|
122
|
+
t_slow = np.asarray(ds["t_slow"].values, dtype=float)
|
|
123
|
+
pres = np.asarray(ds["P"].values, dtype=float)
|
|
124
|
+
lat = float(ds["lat"].values) if "lat" in ds.variables else None
|
|
125
|
+
except (OSError, ValueError, RuntimeError): # one bad raw file shouldn't kill the figure
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
if sig.size < 16 or sig.size != t_fast.size:
|
|
129
|
+
return None
|
|
130
|
+
dt = float(np.median(np.diff(t_fast)))
|
|
131
|
+
if not np.isfinite(dt) or dt <= 0:
|
|
132
|
+
return None
|
|
133
|
+
fs = 1.0 / dt
|
|
134
|
+
|
|
135
|
+
clean = despike(_hp_filter(sig, fs, hp_cut), fs,
|
|
136
|
+
thresh=despike_thresh, smooth=despike_smooth).y
|
|
137
|
+
pressure_fast = np.interp(t_fast, t_slow, pres)
|
|
138
|
+
# The combo "bin" coordinate is depth in METERS (binning.py), and bin_edges
|
|
139
|
+
# are built from it, so convert the raw dbar pressure to depth on the same
|
|
140
|
+
# gsw convention before binning. Binning dbar straight onto meter edges puts
|
|
141
|
+
# every sample ~0.6-0.7 %/dbar too deep (~1 bin at depth). (#104 U6-F1.)
|
|
142
|
+
if lat is None or not np.isfinite(lat):
|
|
143
|
+
lat = _DEFAULT_BIN_LATITUDE
|
|
144
|
+
depth_fast = -np.asarray(gsw.z_from_p(pressure_fast, lat), dtype=float)
|
|
145
|
+
return _variance_by_bin(clean, depth_fast, bin_edges)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def compute_pseudo_grid(
|
|
149
|
+
pseudo: str,
|
|
150
|
+
combo_stime: np.ndarray,
|
|
151
|
+
bin_centers: np.ndarray,
|
|
152
|
+
profiles_dir: str,
|
|
153
|
+
*,
|
|
154
|
+
hp_cut: float = 1.0,
|
|
155
|
+
despike_thresh: float = 8.0,
|
|
156
|
+
despike_smooth: float = 0.5,
|
|
157
|
+
tol: float = 1.0,
|
|
158
|
+
) -> np.ndarray:
|
|
159
|
+
"""``(n_bin, n_profile)`` grid of a pseudo-variable for the given profiles.
|
|
160
|
+
|
|
161
|
+
Each combo profile (one per column of ``combo_stime``) is matched to its raw
|
|
162
|
+
file by ``stime`` (within ``tol`` seconds) and its channel variance is binned
|
|
163
|
+
on the combo's depth grid (``bin_centers``, m). Unmatched profiles are
|
|
164
|
+
left NaN and reported. Binning uses the combo's own bin edges so the panel
|
|
165
|
+
aligns with the stored-variable panels.
|
|
166
|
+
"""
|
|
167
|
+
from odas_tpw.perturb.plot import layout
|
|
168
|
+
|
|
169
|
+
channel = _DIAG_INFO[pseudo][0]
|
|
170
|
+
smap = _raw_stime_map(profiles_dir)
|
|
171
|
+
keys = np.array(sorted(smap), dtype=float)
|
|
172
|
+
bin_edges = layout.depth_edges(np.asarray(bin_centers, dtype=float))
|
|
173
|
+
|
|
174
|
+
n_bin = len(bin_centers)
|
|
175
|
+
combo_stime = np.asarray(combo_stime, dtype=float)
|
|
176
|
+
grid = np.full((n_bin, combo_stime.size), np.nan)
|
|
177
|
+
|
|
178
|
+
n_miss = 0
|
|
179
|
+
for j, st in enumerate(combo_stime):
|
|
180
|
+
path = None
|
|
181
|
+
if keys.size and np.isfinite(st):
|
|
182
|
+
i = int(np.argmin(np.abs(keys - st)))
|
|
183
|
+
if abs(keys[i] - st) <= tol:
|
|
184
|
+
path = smap[keys[i]]
|
|
185
|
+
if path is None:
|
|
186
|
+
n_miss += 1
|
|
187
|
+
continue
|
|
188
|
+
col = _binned_channel_variance(
|
|
189
|
+
path, channel, bin_edges, hp_cut, despike_thresh, despike_smooth
|
|
190
|
+
)
|
|
191
|
+
if col is not None:
|
|
192
|
+
grid[:, j] = col
|
|
193
|
+
if n_miss:
|
|
194
|
+
print(f"{pseudo}: {n_miss}/{combo_stime.size} profiles had no raw-file match")
|
|
195
|
+
return grid
|