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.
Files changed (119) hide show
  1. microstructure_tpw-0.3.0.dist-info/METADATA +234 -0
  2. microstructure_tpw-0.3.0.dist-info/RECORD +119 -0
  3. microstructure_tpw-0.3.0.dist-info/WHEEL +5 -0
  4. microstructure_tpw-0.3.0.dist-info/entry_points.txt +7 -0
  5. microstructure_tpw-0.3.0.dist-info/licenses/LICENSE +674 -0
  6. microstructure_tpw-0.3.0.dist-info/top_level.txt +1 -0
  7. odas_tpw/__init__.py +18 -0
  8. odas_tpw/chi/__init__.py +48 -0
  9. odas_tpw/chi/batchelor.py +189 -0
  10. odas_tpw/chi/chi.py +857 -0
  11. odas_tpw/chi/fp07.py +649 -0
  12. odas_tpw/chi/l2_chi.py +153 -0
  13. odas_tpw/chi/l3_chi.py +388 -0
  14. odas_tpw/chi/l4_chi.py +391 -0
  15. odas_tpw/config_base.py +423 -0
  16. odas_tpw/perturb/__init__.py +18 -0
  17. odas_tpw/perturb/adcp.py +274 -0
  18. odas_tpw/perturb/atomic_io.py +47 -0
  19. odas_tpw/perturb/binning.py +761 -0
  20. odas_tpw/perturb/cli.py +591 -0
  21. odas_tpw/perturb/combo.py +346 -0
  22. odas_tpw/perturb/config.py +1046 -0
  23. odas_tpw/perturb/ctd.py +424 -0
  24. odas_tpw/perturb/diag/__init__.py +12 -0
  25. odas_tpw/perturb/diag/_common.py +140 -0
  26. odas_tpw/perturb/diag/chi.py +42 -0
  27. odas_tpw/perturb/diag/cli.py +67 -0
  28. odas_tpw/perturb/diag/data.py +662 -0
  29. odas_tpw/perturb/diag/epsilon.py +45 -0
  30. odas_tpw/perturb/diag/inspector.py +342 -0
  31. odas_tpw/perturb/diag/mixing.py +94 -0
  32. odas_tpw/perturb/diag/render.py +543 -0
  33. odas_tpw/perturb/discover.py +40 -0
  34. odas_tpw/perturb/fp07_cal.py +354 -0
  35. odas_tpw/perturb/gps.py +458 -0
  36. odas_tpw/perturb/hotel.py +572 -0
  37. odas_tpw/perturb/logging_setup.py +181 -0
  38. odas_tpw/perturb/merge.py +320 -0
  39. odas_tpw/perturb/mk_sections.py +310 -0
  40. odas_tpw/perturb/netcdf_schema.py +520 -0
  41. odas_tpw/perturb/pipeline.py +2654 -0
  42. odas_tpw/perturb/plot/__init__.py +15 -0
  43. odas_tpw/perturb/plot/cli.py +107 -0
  44. odas_tpw/perturb/plot/diagnostics.py +195 -0
  45. odas_tpw/perturb/plot/eps_chi.py +524 -0
  46. odas_tpw/perturb/plot/figure.py +679 -0
  47. odas_tpw/perturb/plot/gamma_scaling.py +1097 -0
  48. odas_tpw/perturb/plot/grid.py +112 -0
  49. odas_tpw/perturb/plot/layout.py +361 -0
  50. odas_tpw/perturb/plot/overview.py +547 -0
  51. odas_tpw/perturb/plot/presets/eps-chi.yaml +16 -0
  52. odas_tpw/perturb/plot/presets/profiles.yaml +32 -0
  53. odas_tpw/perturb/plot/presets/scalar.yaml +21 -0
  54. odas_tpw/perturb/plot/profiles.py +652 -0
  55. odas_tpw/perturb/plot/scalar.py +391 -0
  56. odas_tpw/perturb/plot/sections.py +506 -0
  57. odas_tpw/perturb/plot/xaxis.py +396 -0
  58. odas_tpw/perturb/qc_gate.py +177 -0
  59. odas_tpw/perturb/qc_rules.py +373 -0
  60. odas_tpw/perturb/resolve.py +306 -0
  61. odas_tpw/perturb/seawater.py +76 -0
  62. odas_tpw/perturb/speed.py +14 -0
  63. odas_tpw/perturb/trim.py +313 -0
  64. odas_tpw/processing/__init__.py +62 -0
  65. odas_tpw/processing/bottom.py +215 -0
  66. odas_tpw/processing/chi_combine.py +300 -0
  67. odas_tpw/processing/ct_align.py +172 -0
  68. odas_tpw/processing/epsilon_combine.py +210 -0
  69. odas_tpw/processing/mixing.py +630 -0
  70. odas_tpw/processing/thorpe.py +458 -0
  71. odas_tpw/processing/top_trim.py +274 -0
  72. odas_tpw/pyturb/__init__.py +3 -0
  73. odas_tpw/pyturb/__main__.py +5 -0
  74. odas_tpw/pyturb/_compat.py +204 -0
  75. odas_tpw/pyturb/_profind.py +124 -0
  76. odas_tpw/pyturb/bin.py +113 -0
  77. odas_tpw/pyturb/cli.py +175 -0
  78. odas_tpw/pyturb/eps.py +353 -0
  79. odas_tpw/pyturb/merge.py +101 -0
  80. odas_tpw/pyturb/p2nc.py +76 -0
  81. odas_tpw/rsi/__init__.py +31 -0
  82. odas_tpw/rsi/adapter.py +314 -0
  83. odas_tpw/rsi/bench.py +904 -0
  84. odas_tpw/rsi/binning.py +193 -0
  85. odas_tpw/rsi/channels.py +376 -0
  86. odas_tpw/rsi/chi_io.py +980 -0
  87. odas_tpw/rsi/cli.py +1411 -0
  88. odas_tpw/rsi/combine.py +120 -0
  89. odas_tpw/rsi/config.py +126 -0
  90. odas_tpw/rsi/config_patch.py +790 -0
  91. odas_tpw/rsi/convert.py +651 -0
  92. odas_tpw/rsi/deconvolve.py +119 -0
  93. odas_tpw/rsi/diss_look.py +343 -0
  94. odas_tpw/rsi/dissipation.py +669 -0
  95. odas_tpw/rsi/helpers.py +527 -0
  96. odas_tpw/rsi/mixing_look.py +509 -0
  97. odas_tpw/rsi/p_file.py +771 -0
  98. odas_tpw/rsi/pipeline.py +924 -0
  99. odas_tpw/rsi/profile.py +624 -0
  100. odas_tpw/rsi/quick_look.py +758 -0
  101. odas_tpw/rsi/sensor_inventory.py +684 -0
  102. odas_tpw/rsi/shear_noise.py +128 -0
  103. odas_tpw/rsi/speed.py +221 -0
  104. odas_tpw/rsi/vehicle.py +74 -0
  105. odas_tpw/rsi/viewer_base.py +1071 -0
  106. odas_tpw/rsi/window.py +459 -0
  107. odas_tpw/scor160/__init__.py +36 -0
  108. odas_tpw/scor160/cli.py +242 -0
  109. odas_tpw/scor160/compare.py +484 -0
  110. odas_tpw/scor160/despike.py +227 -0
  111. odas_tpw/scor160/goodman.py +288 -0
  112. odas_tpw/scor160/io.py +690 -0
  113. odas_tpw/scor160/l2.py +311 -0
  114. odas_tpw/scor160/l3.py +279 -0
  115. odas_tpw/scor160/l4.py +850 -0
  116. odas_tpw/scor160/nasmyth.py +208 -0
  117. odas_tpw/scor160/ocean.py +287 -0
  118. odas_tpw/scor160/profile.py +231 -0
  119. 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