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/cli.py
ADDED
|
@@ -0,0 +1,1411 @@
|
|
|
1
|
+
# Mar-2026, Claude and Pat Welch, pat@mousebrains.com
|
|
2
|
+
"""
|
|
3
|
+
CLI for rsi-tpw.
|
|
4
|
+
|
|
5
|
+
Main command:
|
|
6
|
+
rsi-tpw <subcommand> [options]
|
|
7
|
+
|
|
8
|
+
Subcommands:
|
|
9
|
+
info — Print summary of .p file(s)
|
|
10
|
+
cutp — Copy a short record range from a .p file for debugging
|
|
11
|
+
nc — Convert .p files to NetCDF
|
|
12
|
+
patch-config — Edit config fields in .p file(s), writing new files
|
|
13
|
+
patch-template — Scaffold a patch-config edit spec from a .p file
|
|
14
|
+
prof — Extract profiles from .p or full-record .nc files
|
|
15
|
+
eps — Compute epsilon (TKE dissipation) from any pipeline stage
|
|
16
|
+
chi — Compute chi (thermal variance dissipation) from any pipeline stage
|
|
17
|
+
pipeline — Run full processing pipeline (.p → profiles → epsilon → chi)
|
|
18
|
+
sensors — Inventory shear/FP07 sensors across a .p file tree
|
|
19
|
+
ql — Interactive quick-look viewer
|
|
20
|
+
dl — Interactive dissipation quality viewer
|
|
21
|
+
ml — Interactive mixing viewer
|
|
22
|
+
bench — Bench-test diagnostic (quick_bench + auto checklist)
|
|
23
|
+
init — Generate a template configuration file
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import os
|
|
28
|
+
import sys
|
|
29
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _resolve_p_files(patterns: list[str]) -> list[Path]:
|
|
35
|
+
"""Expand glob patterns and return list of .p file Paths."""
|
|
36
|
+
import glob as globmod
|
|
37
|
+
|
|
38
|
+
files = []
|
|
39
|
+
for pattern in patterns:
|
|
40
|
+
expanded = sorted(globmod.glob(pattern))
|
|
41
|
+
if not expanded:
|
|
42
|
+
print(f"Warning: no files match '{pattern}'", file=sys.stderr)
|
|
43
|
+
for f in expanded:
|
|
44
|
+
p = Path(f)
|
|
45
|
+
if p.is_file() and p.suffix.lower() == ".p":
|
|
46
|
+
files.append(p)
|
|
47
|
+
if not files:
|
|
48
|
+
print("Error: no .p files found", file=sys.stderr)
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
return files
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _resolve_files(patterns: list[str], extensions: set[str] | None = None) -> list[Path]:
|
|
54
|
+
"""Expand glob patterns and return list of matching file Paths.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
patterns : list of str
|
|
59
|
+
File paths or glob patterns.
|
|
60
|
+
extensions : set of str or None
|
|
61
|
+
Allowed file extensions (e.g. {'.p', '.nc'}). If None, accept all.
|
|
62
|
+
"""
|
|
63
|
+
import glob as globmod
|
|
64
|
+
|
|
65
|
+
files = []
|
|
66
|
+
for pattern in patterns:
|
|
67
|
+
expanded = sorted(globmod.glob(pattern))
|
|
68
|
+
if not expanded:
|
|
69
|
+
print(f"Warning: no files match '{pattern}'", file=sys.stderr)
|
|
70
|
+
for f in expanded:
|
|
71
|
+
p = Path(f)
|
|
72
|
+
if p.is_file() and (extensions is None or p.suffix.lower() in extensions):
|
|
73
|
+
files.append(p)
|
|
74
|
+
if not files:
|
|
75
|
+
ext_str = ", ".join(extensions) if extensions else "any"
|
|
76
|
+
print(f"Error: no matching files found ({ext_str})", file=sys.stderr)
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
return files
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Config integration helpers
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _load_file_config(args: argparse.Namespace) -> dict[str, Any]:
|
|
87
|
+
"""Load config from the -c/--config flag, or return empty dict."""
|
|
88
|
+
if getattr(args, "config", None) is not None:
|
|
89
|
+
from odas_tpw.rsi.config import load_config
|
|
90
|
+
|
|
91
|
+
return load_config(args.config)
|
|
92
|
+
return {}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _extract_cli_overrides(args: argparse.Namespace, section: str) -> dict[str, Any]:
|
|
96
|
+
"""Extract CLI-provided overrides for a config section.
|
|
97
|
+
|
|
98
|
+
Only includes values that were explicitly specified on the command line
|
|
99
|
+
(i.e., not None). Handles arg-name -> config-key mapping and the
|
|
100
|
+
--no-goodman inversion.
|
|
101
|
+
"""
|
|
102
|
+
if section == "profiles":
|
|
103
|
+
mapping = {
|
|
104
|
+
"P_min": "P_min",
|
|
105
|
+
"W_min": "W_min",
|
|
106
|
+
"direction": "direction",
|
|
107
|
+
"vehicle": "vehicle",
|
|
108
|
+
"min_duration": "min_duration",
|
|
109
|
+
}
|
|
110
|
+
elif section == "epsilon":
|
|
111
|
+
mapping = {
|
|
112
|
+
"fft_length": "fft_length",
|
|
113
|
+
"diss_length": "diss_length",
|
|
114
|
+
"overlap": "overlap",
|
|
115
|
+
"speed": "speed",
|
|
116
|
+
"direction": "direction",
|
|
117
|
+
"vehicle": "vehicle",
|
|
118
|
+
"f_AA": "f_AA",
|
|
119
|
+
"salinity": "salinity",
|
|
120
|
+
}
|
|
121
|
+
elif section == "chi":
|
|
122
|
+
mapping = {
|
|
123
|
+
"fft_length": "fft_length",
|
|
124
|
+
"diss_length": "diss_length",
|
|
125
|
+
"overlap": "overlap",
|
|
126
|
+
"speed": "speed",
|
|
127
|
+
"direction": "direction",
|
|
128
|
+
"vehicle": "vehicle",
|
|
129
|
+
"fp07_model": "fp07_model",
|
|
130
|
+
"f_AA": "f_AA",
|
|
131
|
+
"fit_method": "fit_method",
|
|
132
|
+
"spectrum_model": "spectrum_model",
|
|
133
|
+
"salinity": "salinity",
|
|
134
|
+
}
|
|
135
|
+
elif section == "epsilon_pipeline":
|
|
136
|
+
mapping = {
|
|
137
|
+
"eps_fft_length": "fft_length",
|
|
138
|
+
"direction": "direction",
|
|
139
|
+
"vehicle": "vehicle",
|
|
140
|
+
"f_AA": "f_AA",
|
|
141
|
+
"speed": "speed",
|
|
142
|
+
"salinity": "salinity",
|
|
143
|
+
}
|
|
144
|
+
elif section == "chi_pipeline":
|
|
145
|
+
mapping = {
|
|
146
|
+
"chi_fft_length": "fft_length",
|
|
147
|
+
"direction": "direction",
|
|
148
|
+
"vehicle": "vehicle",
|
|
149
|
+
"f_AA": "f_AA",
|
|
150
|
+
"fp07_model": "fp07_model",
|
|
151
|
+
"spectrum_model": "spectrum_model",
|
|
152
|
+
"speed": "speed",
|
|
153
|
+
"salinity": "salinity",
|
|
154
|
+
}
|
|
155
|
+
else:
|
|
156
|
+
return {}
|
|
157
|
+
|
|
158
|
+
overrides = {}
|
|
159
|
+
for attr, key in mapping.items():
|
|
160
|
+
val = getattr(args, attr, None)
|
|
161
|
+
if val is not None:
|
|
162
|
+
overrides[key] = val
|
|
163
|
+
|
|
164
|
+
# Handle --no-goodman (store_const: None=not specified, True=specified)
|
|
165
|
+
if section in ("epsilon", "epsilon_pipeline", "chi", "chi_pipeline"):
|
|
166
|
+
no_goodman = getattr(args, "no_goodman", None)
|
|
167
|
+
if no_goodman is True:
|
|
168
|
+
overrides["goodman"] = False
|
|
169
|
+
|
|
170
|
+
return overrides
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _merge_for_section(args: argparse.Namespace, section: str) -> dict[str, Any]:
|
|
174
|
+
"""Load config file + CLI overrides and merge for the given section.
|
|
175
|
+
|
|
176
|
+
Returns the merged kwargs dict (None values stripped).
|
|
177
|
+
"""
|
|
178
|
+
from odas_tpw.rsi.config import merge_config
|
|
179
|
+
|
|
180
|
+
file_config = _load_file_config(args)
|
|
181
|
+
# For pipeline pseudo-sections, map to the real config section name
|
|
182
|
+
real_section = section.replace("_pipeline", "")
|
|
183
|
+
file_values = file_config.get(real_section, {})
|
|
184
|
+
cli_overrides = _extract_cli_overrides(args, section)
|
|
185
|
+
return merge_config(real_section, file_values, cli_overrides)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _setup_output_dir(
|
|
189
|
+
args: argparse.Namespace,
|
|
190
|
+
prefix: str,
|
|
191
|
+
section: str,
|
|
192
|
+
params: dict[str, Any],
|
|
193
|
+
upstream: list[tuple[str, dict[str, Any]]] | None = None,
|
|
194
|
+
) -> Path:
|
|
195
|
+
"""Resolve the sequential output directory, write signature file and config.yaml.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
upstream : list of (section, params) tuples, optional
|
|
200
|
+
Upstream sections to include in the hash for cumulative tracking.
|
|
201
|
+
"""
|
|
202
|
+
from odas_tpw.rsi.config import resolve_output_dir, write_resolved_config
|
|
203
|
+
|
|
204
|
+
real_section = section.replace("_pipeline", "")
|
|
205
|
+
output_dir = resolve_output_dir(args.output, prefix, real_section, params, upstream=upstream)
|
|
206
|
+
write_resolved_config(output_dir, real_section, params, upstream=upstream)
|
|
207
|
+
return output_dir
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
# Subcommand implementations
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _cmd_nc(args: argparse.Namespace) -> None:
|
|
216
|
+
"""Convert Rockland .p files to NetCDF4."""
|
|
217
|
+
from odas_tpw.rsi.convert import convert_all, p_to_L1
|
|
218
|
+
|
|
219
|
+
p_files = _resolve_p_files(args.files)
|
|
220
|
+
|
|
221
|
+
if len(p_files) == 1 and args.output and not Path(args.output).is_dir():
|
|
222
|
+
_, nc_path = p_to_L1(p_files[0], args.output)
|
|
223
|
+
print(f"Wrote {nc_path} ({nc_path.stat().st_size / 1e6:.1f} MB)")
|
|
224
|
+
else:
|
|
225
|
+
output_dir = Path(args.output) if args.output else None
|
|
226
|
+
convert_all(p_files, output_dir, jobs=args.jobs)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _cmd_info(args: argparse.Namespace) -> None:
|
|
230
|
+
"""Print summary information about .p file(s)."""
|
|
231
|
+
from odas_tpw.rsi.p_file import PFile
|
|
232
|
+
|
|
233
|
+
p_files = _resolve_p_files(args.files)
|
|
234
|
+
for i, pf_path in enumerate(p_files):
|
|
235
|
+
if i > 0:
|
|
236
|
+
print("\n" + "=" * 60 + "\n")
|
|
237
|
+
pf = PFile(pf_path)
|
|
238
|
+
pf.summary()
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _cmd_cutp(args: argparse.Namespace) -> None:
|
|
242
|
+
"""Copy a contiguous data-record range from a .p file."""
|
|
243
|
+
from odas_tpw.rsi.p_file import extract_pfile_segment
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
out = extract_pfile_segment(
|
|
247
|
+
args.file,
|
|
248
|
+
args.output,
|
|
249
|
+
start_record=args.start,
|
|
250
|
+
n_records=args.n_records,
|
|
251
|
+
overwrite=args.overwrite,
|
|
252
|
+
)
|
|
253
|
+
except FileExistsError:
|
|
254
|
+
print(f"Error: {args.output} exists; pass --force to replace it", file=sys.stderr)
|
|
255
|
+
sys.exit(1)
|
|
256
|
+
except (FileNotFoundError, OSError, ValueError) as e:
|
|
257
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
258
|
+
sys.exit(1)
|
|
259
|
+
|
|
260
|
+
print(f"Wrote {args.n_records} record(s) to {out}")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _cmd_init(args: argparse.Namespace) -> None:
|
|
264
|
+
"""Generate a template configuration file."""
|
|
265
|
+
from odas_tpw.rsi.config import generate_template
|
|
266
|
+
|
|
267
|
+
path = Path(args.path)
|
|
268
|
+
if path.exists() and not args.force:
|
|
269
|
+
print(f"Error: {path} already exists (use --force to overwrite)", file=sys.stderr)
|
|
270
|
+
sys.exit(1)
|
|
271
|
+
generate_template(path)
|
|
272
|
+
print(f"Wrote template config to {path}")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _cmd_patch_config(args: argparse.Namespace) -> None:
|
|
276
|
+
"""Patch config fields in .p file(s), writing new files."""
|
|
277
|
+
from datetime import datetime
|
|
278
|
+
|
|
279
|
+
from ruamel.yaml import YAMLError
|
|
280
|
+
|
|
281
|
+
from odas_tpw.rsi.config_patch import load_edit_spec, patch_files
|
|
282
|
+
|
|
283
|
+
files = _resolve_p_files(args.files)
|
|
284
|
+
when = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
285
|
+
try:
|
|
286
|
+
spec = load_edit_spec(args.edits)
|
|
287
|
+
patch_files(
|
|
288
|
+
files,
|
|
289
|
+
args.out,
|
|
290
|
+
spec,
|
|
291
|
+
dry_run=args.dry_run,
|
|
292
|
+
add_keys=args.add_keys,
|
|
293
|
+
batch_cal=args.batch_cal,
|
|
294
|
+
when=when,
|
|
295
|
+
)
|
|
296
|
+
# YAMLError: load_edit_spec parses with ruamel, whose ParserError/ScannerError
|
|
297
|
+
# subclass YAMLError (not ValueError/OSError) and otherwise escape as a traceback.
|
|
298
|
+
except (FileNotFoundError, FileExistsError, ValueError, OSError, YAMLError) as e:
|
|
299
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
300
|
+
sys.exit(1)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _cmd_patch_template(args: argparse.Namespace) -> None:
|
|
304
|
+
"""Scaffold a patch-config edit-spec template from a .p file."""
|
|
305
|
+
from odas_tpw.rsi.config_patch import scaffold_yaml
|
|
306
|
+
|
|
307
|
+
src = Path(args.file)
|
|
308
|
+
if not src.is_file():
|
|
309
|
+
print(f"Error: {src} not found", file=sys.stderr)
|
|
310
|
+
sys.exit(1)
|
|
311
|
+
try:
|
|
312
|
+
text = scaffold_yaml(src)
|
|
313
|
+
except (OSError, ValueError) as e:
|
|
314
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
315
|
+
sys.exit(1)
|
|
316
|
+
if args.output:
|
|
317
|
+
out = Path(args.output)
|
|
318
|
+
if out.exists() and not args.force:
|
|
319
|
+
print(f"Error: {out} already exists (use --force to overwrite)", file=sys.stderr)
|
|
320
|
+
sys.exit(1)
|
|
321
|
+
out.write_text(text)
|
|
322
|
+
print(f"Wrote template to {out}")
|
|
323
|
+
else:
|
|
324
|
+
print(text, end="")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _cmd_prof(args: argparse.Namespace) -> None:
|
|
328
|
+
"""Extract profiles from .p or full-record .nc files."""
|
|
329
|
+
from odas_tpw.rsi.profile import extract_profiles
|
|
330
|
+
|
|
331
|
+
files = _resolve_files(args.files, {".p", ".nc"})
|
|
332
|
+
|
|
333
|
+
merged = _merge_for_section(args, "profiles")
|
|
334
|
+
output_dir = _setup_output_dir(args, "prof", "profiles", merged)
|
|
335
|
+
print(f"Output directory: {output_dir}")
|
|
336
|
+
|
|
337
|
+
for f in files:
|
|
338
|
+
print(f"{f.name}:")
|
|
339
|
+
extract_profiles(f, output_dir, **merged)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _cmd_eps(args: argparse.Namespace) -> None:
|
|
343
|
+
"""Compute epsilon (TKE dissipation rate) from any pipeline stage."""
|
|
344
|
+
from odas_tpw.rsi.dissipation import _compute_diss_one, compute_diss_file
|
|
345
|
+
|
|
346
|
+
files = _resolve_files(args.files, {".p", ".nc"})
|
|
347
|
+
|
|
348
|
+
merged = _merge_for_section(args, "epsilon")
|
|
349
|
+
output_dir = _setup_output_dir(args, "eps", "epsilon", merged)
|
|
350
|
+
print(f"Output directory: {output_dir}")
|
|
351
|
+
|
|
352
|
+
jobs = args.jobs
|
|
353
|
+
if jobs == 0:
|
|
354
|
+
jobs = os.cpu_count() or 1
|
|
355
|
+
|
|
356
|
+
if jobs == 1:
|
|
357
|
+
for f in files:
|
|
358
|
+
print(f"{f.name}:")
|
|
359
|
+
try:
|
|
360
|
+
compute_diss_file(f, output_dir, **merged)
|
|
361
|
+
except (OSError, ValueError, RuntimeError) as e:
|
|
362
|
+
print(f" ERROR: {e}")
|
|
363
|
+
else:
|
|
364
|
+
work = []
|
|
365
|
+
for f in files:
|
|
366
|
+
work.append((f, output_dir, merged))
|
|
367
|
+
print(f"Processing {len(work)} files with {jobs} workers")
|
|
368
|
+
with ProcessPoolExecutor(max_workers=jobs) as pool:
|
|
369
|
+
futures = {pool.submit(_compute_diss_one, w): w for w in work}
|
|
370
|
+
for future in as_completed(futures):
|
|
371
|
+
src, _, _ = futures[future]
|
|
372
|
+
try:
|
|
373
|
+
name, n_profiles = future.result()
|
|
374
|
+
print(f" {Path(name).name}: {n_profiles} profile(s)")
|
|
375
|
+
except (OSError, ValueError, RuntimeError) as e:
|
|
376
|
+
print(f" {src.name}: ERROR: {e}")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _cmd_chi(args: argparse.Namespace) -> None:
|
|
380
|
+
"""Compute chi (thermal variance dissipation rate) from any pipeline stage."""
|
|
381
|
+
from odas_tpw.rsi.chi_io import _compute_chi_one, compute_chi_file
|
|
382
|
+
|
|
383
|
+
files = _resolve_files(args.files, {".p", ".nc"})
|
|
384
|
+
|
|
385
|
+
merged = _merge_for_section(args, "chi")
|
|
386
|
+
output_dir = _setup_output_dir(args, "chi", "chi", merged)
|
|
387
|
+
print(f"Output directory: {output_dir}")
|
|
388
|
+
|
|
389
|
+
# Load epsilon datasets if Method 1
|
|
390
|
+
epsilon_dir = getattr(args, "epsilon_dir", None)
|
|
391
|
+
|
|
392
|
+
jobs = args.jobs
|
|
393
|
+
if jobs == 0:
|
|
394
|
+
jobs = os.cpu_count() or 1
|
|
395
|
+
|
|
396
|
+
if jobs == 1:
|
|
397
|
+
for f in files:
|
|
398
|
+
print(f"{f.name}:")
|
|
399
|
+
|
|
400
|
+
kw = dict(merged)
|
|
401
|
+
eps_ds = None
|
|
402
|
+
if epsilon_dir is not None:
|
|
403
|
+
from odas_tpw.rsi.chi_io import load_epsilon_dataset
|
|
404
|
+
|
|
405
|
+
eps_ds = load_epsilon_dataset(f, epsilon_dir)
|
|
406
|
+
if eps_ds is not None:
|
|
407
|
+
kw["epsilon_ds"] = eps_ds
|
|
408
|
+
else:
|
|
409
|
+
print(
|
|
410
|
+
f" Warning: no epsilon files for {f.stem} under "
|
|
411
|
+
f"{epsilon_dir} (searched eps_* subdirectories too); "
|
|
412
|
+
"using Method 2"
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
try:
|
|
416
|
+
compute_chi_file(f, output_dir, **kw)
|
|
417
|
+
except (OSError, ValueError, RuntimeError) as e:
|
|
418
|
+
print(f" ERROR: {e}")
|
|
419
|
+
finally:
|
|
420
|
+
if eps_ds is not None:
|
|
421
|
+
eps_ds.close()
|
|
422
|
+
else:
|
|
423
|
+
work = []
|
|
424
|
+
for f in files:
|
|
425
|
+
work.append((f, output_dir, merged, epsilon_dir))
|
|
426
|
+
print(f"Processing {len(work)} files with {jobs} workers")
|
|
427
|
+
with ProcessPoolExecutor(max_workers=jobs) as pool:
|
|
428
|
+
futures = {pool.submit(_compute_chi_one, w): w for w in work}
|
|
429
|
+
for future in as_completed(futures):
|
|
430
|
+
src = futures[future][0]
|
|
431
|
+
try:
|
|
432
|
+
name, n_profiles = future.result()
|
|
433
|
+
print(f" {Path(name).name}: {n_profiles} profile(s)")
|
|
434
|
+
except (OSError, ValueError, RuntimeError) as e:
|
|
435
|
+
print(f" {Path(src).name}: ERROR: {e}")
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _cmd_pipeline(args: argparse.Namespace) -> None:
|
|
439
|
+
"""Run full processing pipeline: .p -> L2 -> L3 -> L4 -> chi -> binning."""
|
|
440
|
+
from odas_tpw.rsi.pipeline import run_pipeline
|
|
441
|
+
|
|
442
|
+
p_files = _resolve_p_files(args.files)
|
|
443
|
+
output_dir = Path(args.output)
|
|
444
|
+
|
|
445
|
+
# Gather parameters from CLI/config
|
|
446
|
+
eps_merged = _merge_for_section(args, "epsilon_pipeline")
|
|
447
|
+
chi_merged = _merge_for_section(args, "chi_pipeline")
|
|
448
|
+
|
|
449
|
+
kwargs = {
|
|
450
|
+
"direction": eps_merged.get("direction", "auto"),
|
|
451
|
+
"vehicle": eps_merged.get("vehicle"),
|
|
452
|
+
"speed": eps_merged.get("speed"),
|
|
453
|
+
"fft_length": eps_merged.get("fft_length", 1024),
|
|
454
|
+
"f_AA": eps_merged.get("f_AA", 98.0),
|
|
455
|
+
"salinity": eps_merged.get("salinity"),
|
|
456
|
+
"goodman": eps_merged.get("goodman", True),
|
|
457
|
+
"chi_fft_length": chi_merged.get("fft_length", 1024),
|
|
458
|
+
"fp07_model": chi_merged.get("fp07_model", "single_pole"),
|
|
459
|
+
"spectrum_model": chi_merged.get("spectrum_model", "kraichnan"),
|
|
460
|
+
}
|
|
461
|
+
# Remove None values
|
|
462
|
+
kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
|
463
|
+
|
|
464
|
+
# Speed model (pipeline-level; default pressure / |dP/dt|).
|
|
465
|
+
if getattr(args, "speed_method", None):
|
|
466
|
+
kwargs["speed_method"] = args.speed_method
|
|
467
|
+
if getattr(args, "aoa", None) is not None:
|
|
468
|
+
kwargs["aoa_deg"] = args.aoa
|
|
469
|
+
|
|
470
|
+
run_pipeline(p_files, output_dir, **kwargs)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _cmd_ql(args: argparse.Namespace) -> None:
|
|
474
|
+
"""Interactive quick-look viewer."""
|
|
475
|
+
from odas_tpw.rsi.quick_look import quick_look
|
|
476
|
+
|
|
477
|
+
spec_P_range = None
|
|
478
|
+
if args.spec_P_range is not None:
|
|
479
|
+
spec_P_range = tuple(args.spec_P_range)
|
|
480
|
+
|
|
481
|
+
# Merge config file + CLI overrides for epsilon section
|
|
482
|
+
merged = _merge_for_section(args, "epsilon")
|
|
483
|
+
fft_length = args.fft_length or merged.get("fft_length", 1024)
|
|
484
|
+
diss_length = args.diss_length or merged.get("diss_length")
|
|
485
|
+
f_AA = args.f_AA or merged.get("f_AA", 98.0)
|
|
486
|
+
goodman = merged.get("goodman", True) if not args.no_goodman else False
|
|
487
|
+
|
|
488
|
+
W_min = getattr(args, "W_min", None) or 0.3
|
|
489
|
+
|
|
490
|
+
p_files = _resolve_p_files(args.files)
|
|
491
|
+
for pf_path in p_files:
|
|
492
|
+
quick_look(
|
|
493
|
+
pf_path,
|
|
494
|
+
fft_length=fft_length,
|
|
495
|
+
diss_length=diss_length,
|
|
496
|
+
f_AA=f_AA,
|
|
497
|
+
goodman=goodman,
|
|
498
|
+
direction=args.direction or "auto",
|
|
499
|
+
vehicle=getattr(args, "vehicle", None),
|
|
500
|
+
W_min=W_min,
|
|
501
|
+
spec_P_range=spec_P_range,
|
|
502
|
+
chi_method=args.chi_method,
|
|
503
|
+
spectrum_model=args.spectrum_model,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _cmd_dl(args: argparse.Namespace) -> None:
|
|
508
|
+
"""Interactive dissipation quality viewer."""
|
|
509
|
+
from odas_tpw.rsi.diss_look import diss_look
|
|
510
|
+
|
|
511
|
+
spec_P_range = None
|
|
512
|
+
if args.spec_P_range is not None:
|
|
513
|
+
spec_P_range = tuple(args.spec_P_range)
|
|
514
|
+
|
|
515
|
+
# Merge config file + CLI overrides for epsilon section
|
|
516
|
+
merged = _merge_for_section(args, "epsilon")
|
|
517
|
+
fft_length = args.fft_length or merged.get("fft_length", 1024)
|
|
518
|
+
diss_length = args.diss_length or merged.get("diss_length")
|
|
519
|
+
f_AA = args.f_AA or merged.get("f_AA", 98.0)
|
|
520
|
+
goodman = merged.get("goodman", True) if not args.no_goodman else False
|
|
521
|
+
|
|
522
|
+
W_min = getattr(args, "W_min", None) or 0.3
|
|
523
|
+
|
|
524
|
+
p_files = _resolve_p_files(args.files)
|
|
525
|
+
for pf_path in p_files:
|
|
526
|
+
diss_look(
|
|
527
|
+
pf_path,
|
|
528
|
+
fft_length=fft_length,
|
|
529
|
+
diss_length=diss_length,
|
|
530
|
+
f_AA=f_AA,
|
|
531
|
+
goodman=goodman,
|
|
532
|
+
direction=args.direction or "auto",
|
|
533
|
+
vehicle=getattr(args, "vehicle", None),
|
|
534
|
+
W_min=W_min,
|
|
535
|
+
spec_P_range=spec_P_range,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _cmd_bench(args: argparse.Namespace) -> None:
|
|
540
|
+
"""Bench-test diagnostic: raw-count figures + auto-evaluated checklist."""
|
|
541
|
+
# Force a non-interactive backend unless the user wants windows, so batch /
|
|
542
|
+
# headless runs need no display. Must precede the bench import, which loads
|
|
543
|
+
# pyplot at module level.
|
|
544
|
+
if not args.show:
|
|
545
|
+
import matplotlib
|
|
546
|
+
|
|
547
|
+
matplotlib.use("Agg")
|
|
548
|
+
from odas_tpw.rsi.bench import run_bench
|
|
549
|
+
|
|
550
|
+
p_files = _resolve_p_files(args.files)
|
|
551
|
+
|
|
552
|
+
# Save by default (to ./bench/) unless the user only wants an interactive
|
|
553
|
+
# look (--show with no -o), in which case display without writing files.
|
|
554
|
+
out_dir = args.output
|
|
555
|
+
if out_dir is None and not args.show:
|
|
556
|
+
out_dir = "bench"
|
|
557
|
+
|
|
558
|
+
for i, pf_path in enumerate(p_files):
|
|
559
|
+
if i > 0:
|
|
560
|
+
print("\n" + "=" * 60 + "\n")
|
|
561
|
+
try:
|
|
562
|
+
run_bench(
|
|
563
|
+
pf_path,
|
|
564
|
+
out_dir=out_dir,
|
|
565
|
+
show=args.show,
|
|
566
|
+
sn=args.sn,
|
|
567
|
+
fft_sec=args.fft_sec,
|
|
568
|
+
dpi=args.dpi,
|
|
569
|
+
fmt=args.format,
|
|
570
|
+
)
|
|
571
|
+
except (OSError, ValueError) as e:
|
|
572
|
+
print(f" ERROR: {e}", file=sys.stderr)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _cmd_sensors(args: argparse.Namespace) -> None:
|
|
576
|
+
"""Inventory microstructure sensors across a tree of .p files."""
|
|
577
|
+
from odas_tpw.rsi.sensor_inventory import resolve_kinds, run
|
|
578
|
+
|
|
579
|
+
kinds = resolve_kinds(args.shear, args.fp07, args.want_all)
|
|
580
|
+
code = run(
|
|
581
|
+
[Path(p) for p in args.paths],
|
|
582
|
+
kinds,
|
|
583
|
+
csv_out=Path(args.csv) if args.csv else None,
|
|
584
|
+
verbose=args.verbose,
|
|
585
|
+
compact=args.compact,
|
|
586
|
+
)
|
|
587
|
+
if code != 0:
|
|
588
|
+
sys.exit(code)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _cmd_ml(args: argparse.Namespace) -> None:
|
|
592
|
+
"""Interactive mixing viewer."""
|
|
593
|
+
from odas_tpw.rsi.mixing_look import mixing_look
|
|
594
|
+
|
|
595
|
+
spec_P_range = None
|
|
596
|
+
if args.spec_P_range is not None:
|
|
597
|
+
spec_P_range = tuple(args.spec_P_range)
|
|
598
|
+
|
|
599
|
+
# Merge config file + CLI overrides for epsilon section
|
|
600
|
+
merged = _merge_for_section(args, "epsilon")
|
|
601
|
+
fft_length = args.fft_length or merged.get("fft_length", 1024)
|
|
602
|
+
diss_length = args.diss_length or merged.get("diss_length")
|
|
603
|
+
f_AA = args.f_AA or merged.get("f_AA", 98.0)
|
|
604
|
+
goodman = merged.get("goodman", True) if not args.no_goodman else False
|
|
605
|
+
|
|
606
|
+
W_min = getattr(args, "W_min", None) or 0.3
|
|
607
|
+
|
|
608
|
+
p_files = _resolve_p_files(args.files)
|
|
609
|
+
for pf_path in p_files:
|
|
610
|
+
mixing_look(
|
|
611
|
+
pf_path,
|
|
612
|
+
fft_length=fft_length,
|
|
613
|
+
diss_length=diss_length,
|
|
614
|
+
f_AA=f_AA,
|
|
615
|
+
goodman=goodman,
|
|
616
|
+
direction=args.direction or "auto",
|
|
617
|
+
vehicle=getattr(args, "vehicle", None),
|
|
618
|
+
W_min=W_min,
|
|
619
|
+
spec_P_range=spec_P_range,
|
|
620
|
+
salinity=args.salinity,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
# ---------------------------------------------------------------------------
|
|
625
|
+
# Subcommand parsers
|
|
626
|
+
# ---------------------------------------------------------------------------
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _add_nc_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
630
|
+
p = subparsers.add_parser(
|
|
631
|
+
"nc",
|
|
632
|
+
help="Convert .p files to NetCDF",
|
|
633
|
+
description="Convert Rockland Scientific .p data files to NetCDF4.",
|
|
634
|
+
)
|
|
635
|
+
p.add_argument("files", nargs="+", metavar="FILE", help=".p file(s) or glob pattern(s)")
|
|
636
|
+
p.add_argument(
|
|
637
|
+
"-o",
|
|
638
|
+
"--output",
|
|
639
|
+
metavar="PATH",
|
|
640
|
+
default=None,
|
|
641
|
+
help="Output file (single input) or directory (multiple inputs)",
|
|
642
|
+
)
|
|
643
|
+
p.add_argument(
|
|
644
|
+
"-j",
|
|
645
|
+
"--jobs",
|
|
646
|
+
type=int,
|
|
647
|
+
default=1,
|
|
648
|
+
metavar="N",
|
|
649
|
+
help="Parallel workers (0 = all cores, default: 1)",
|
|
650
|
+
)
|
|
651
|
+
p.set_defaults(func=_cmd_nc)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _add_info_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
655
|
+
p = subparsers.add_parser(
|
|
656
|
+
"info",
|
|
657
|
+
help="Print .p file summary",
|
|
658
|
+
description="Print summary of Rockland Scientific .p data files.",
|
|
659
|
+
)
|
|
660
|
+
p.add_argument("files", nargs="+", metavar="FILE", help=".p file(s) or glob pattern(s)")
|
|
661
|
+
p.set_defaults(func=_cmd_info)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _add_cutp_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
665
|
+
p = subparsers.add_parser(
|
|
666
|
+
"cutp",
|
|
667
|
+
help="Copy .p records for debugging",
|
|
668
|
+
description=(
|
|
669
|
+
"Create a valid .p file containing a contiguous range of complete "
|
|
670
|
+
"data records. This is a byte-level debugging utility, not a "
|
|
671
|
+
"pressure- or profile-aware scientific extraction. Absolute time "
|
|
672
|
+
"is correct only when --start is 0 because the header is copied "
|
|
673
|
+
"unchanged."
|
|
674
|
+
),
|
|
675
|
+
)
|
|
676
|
+
p.add_argument("file", metavar="FILE", help="Input .p file")
|
|
677
|
+
p.add_argument(
|
|
678
|
+
"-o",
|
|
679
|
+
"--output",
|
|
680
|
+
metavar="FILE",
|
|
681
|
+
required=True,
|
|
682
|
+
help="Output .p file",
|
|
683
|
+
)
|
|
684
|
+
p.add_argument(
|
|
685
|
+
"-s",
|
|
686
|
+
"--start",
|
|
687
|
+
type=int,
|
|
688
|
+
default=0,
|
|
689
|
+
metavar="N",
|
|
690
|
+
help="First data record to copy, 0-based after the config record (default: 0)",
|
|
691
|
+
)
|
|
692
|
+
p.add_argument(
|
|
693
|
+
"-n",
|
|
694
|
+
"--n-records",
|
|
695
|
+
type=int,
|
|
696
|
+
default=60,
|
|
697
|
+
metavar="N",
|
|
698
|
+
help="Number of complete data records to copy (default: 60)",
|
|
699
|
+
)
|
|
700
|
+
p.add_argument(
|
|
701
|
+
"-f",
|
|
702
|
+
"--force",
|
|
703
|
+
"--overwrite",
|
|
704
|
+
dest="overwrite",
|
|
705
|
+
action="store_true",
|
|
706
|
+
help="Overwrite output file if it exists",
|
|
707
|
+
)
|
|
708
|
+
p.set_defaults(func=_cmd_cutp)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def _add_init_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
712
|
+
p = subparsers.add_parser(
|
|
713
|
+
"init",
|
|
714
|
+
help="Generate a template configuration file",
|
|
715
|
+
description="Write a fully-commented template config.yaml with all default values.",
|
|
716
|
+
)
|
|
717
|
+
p.add_argument(
|
|
718
|
+
"path",
|
|
719
|
+
nargs="?",
|
|
720
|
+
default="config.yaml",
|
|
721
|
+
help="Output path (default: config.yaml)",
|
|
722
|
+
)
|
|
723
|
+
p.add_argument(
|
|
724
|
+
"--force",
|
|
725
|
+
action="store_true",
|
|
726
|
+
help="Overwrite existing file",
|
|
727
|
+
)
|
|
728
|
+
p.set_defaults(func=_cmd_init)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _add_prof_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
732
|
+
p = subparsers.add_parser(
|
|
733
|
+
"prof",
|
|
734
|
+
help="Extract profiles",
|
|
735
|
+
description="Extract profiles from .p or full-record .nc files.",
|
|
736
|
+
)
|
|
737
|
+
p.add_argument("files", nargs="+", metavar="FILE", help=".p or .nc file(s) or glob pattern(s)")
|
|
738
|
+
p.add_argument(
|
|
739
|
+
"-o",
|
|
740
|
+
"--output",
|
|
741
|
+
metavar="DIR",
|
|
742
|
+
required=True,
|
|
743
|
+
help="Output directory for per-profile .nc files",
|
|
744
|
+
)
|
|
745
|
+
p.add_argument(
|
|
746
|
+
"-j",
|
|
747
|
+
"--jobs",
|
|
748
|
+
type=int,
|
|
749
|
+
default=1,
|
|
750
|
+
metavar="N",
|
|
751
|
+
help="Parallel workers (0 = all cores, default: 1)",
|
|
752
|
+
)
|
|
753
|
+
p.add_argument(
|
|
754
|
+
"--P-min", type=float, default=None, help="Minimum pressure [dbar] (default: 0.5)"
|
|
755
|
+
)
|
|
756
|
+
p.add_argument(
|
|
757
|
+
"--W-min", type=float, default=None, help="Minimum fall rate [dbar/s] (default: 0.3)"
|
|
758
|
+
)
|
|
759
|
+
p.add_argument(
|
|
760
|
+
"--direction",
|
|
761
|
+
default=None,
|
|
762
|
+
choices=["auto", "up", "down", "glide", "horizontal"],
|
|
763
|
+
help="Profile direction (default: auto, from vehicle)",
|
|
764
|
+
)
|
|
765
|
+
p.add_argument(
|
|
766
|
+
"--vehicle",
|
|
767
|
+
default=None,
|
|
768
|
+
help="Vehicle type override (e.g. slocum_glider, vmp)",
|
|
769
|
+
)
|
|
770
|
+
p.add_argument(
|
|
771
|
+
"--min-duration", type=float, default=None, help="Minimum profile duration [s] (default: 7)"
|
|
772
|
+
)
|
|
773
|
+
p.set_defaults(func=_cmd_prof)
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def _add_eps_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
777
|
+
p = subparsers.add_parser(
|
|
778
|
+
"eps",
|
|
779
|
+
help="Compute epsilon (TKE dissipation)",
|
|
780
|
+
description="Compute TKE dissipation rate (epsilon) from VMP data.",
|
|
781
|
+
)
|
|
782
|
+
p.add_argument(
|
|
783
|
+
"files", nargs="+", metavar="FILE", help=".p, full-record .nc, or per-profile .nc file(s)"
|
|
784
|
+
)
|
|
785
|
+
p.add_argument(
|
|
786
|
+
"-o",
|
|
787
|
+
"--output",
|
|
788
|
+
metavar="DIR",
|
|
789
|
+
required=True,
|
|
790
|
+
help="Output directory for epsilon .nc files",
|
|
791
|
+
)
|
|
792
|
+
p.add_argument(
|
|
793
|
+
"-j",
|
|
794
|
+
"--jobs",
|
|
795
|
+
type=int,
|
|
796
|
+
default=1,
|
|
797
|
+
metavar="N",
|
|
798
|
+
help="Parallel workers (0 = all cores, default: 1)",
|
|
799
|
+
)
|
|
800
|
+
p.add_argument(
|
|
801
|
+
"--fft-length", type=int, default=None, help="FFT segment length [samples] (default: 1024)"
|
|
802
|
+
)
|
|
803
|
+
p.add_argument(
|
|
804
|
+
"--diss-length",
|
|
805
|
+
type=int,
|
|
806
|
+
default=None,
|
|
807
|
+
help="Dissipation window [samples] (default: 4*fft-length)",
|
|
808
|
+
)
|
|
809
|
+
p.add_argument(
|
|
810
|
+
"--overlap",
|
|
811
|
+
type=int,
|
|
812
|
+
default=None,
|
|
813
|
+
help="Window overlap [samples] (default: diss-length//2)",
|
|
814
|
+
)
|
|
815
|
+
p.add_argument(
|
|
816
|
+
"--speed",
|
|
817
|
+
type=float,
|
|
818
|
+
default=None,
|
|
819
|
+
help="Fixed profiling speed [m/s] (default: from dP/dt)",
|
|
820
|
+
)
|
|
821
|
+
p.add_argument(
|
|
822
|
+
"--direction",
|
|
823
|
+
default=None,
|
|
824
|
+
choices=["auto", "up", "down", "glide", "horizontal"],
|
|
825
|
+
help="Profile direction (default: auto, from vehicle)",
|
|
826
|
+
)
|
|
827
|
+
p.add_argument(
|
|
828
|
+
"--vehicle",
|
|
829
|
+
default=None,
|
|
830
|
+
help="Vehicle type override (e.g. slocum_glider, vmp)",
|
|
831
|
+
)
|
|
832
|
+
p.add_argument(
|
|
833
|
+
"--no-goodman",
|
|
834
|
+
action="store_const",
|
|
835
|
+
const=True,
|
|
836
|
+
default=None,
|
|
837
|
+
help="Disable Goodman coherent noise removal",
|
|
838
|
+
)
|
|
839
|
+
p.add_argument(
|
|
840
|
+
"--f-AA", type=float, default=None, help="Anti-aliasing filter cutoff [Hz] (default: 98)"
|
|
841
|
+
)
|
|
842
|
+
p.add_argument(
|
|
843
|
+
"--salinity",
|
|
844
|
+
type=float,
|
|
845
|
+
default=None,
|
|
846
|
+
help="Salinity [PSU] for viscosity (default: 35, fixed S)",
|
|
847
|
+
)
|
|
848
|
+
p.set_defaults(func=_cmd_eps)
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _add_chi_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
852
|
+
p = subparsers.add_parser(
|
|
853
|
+
"chi",
|
|
854
|
+
help="Compute chi (thermal dissipation)",
|
|
855
|
+
description="Compute thermal variance dissipation rate (chi) from VMP data.",
|
|
856
|
+
)
|
|
857
|
+
p.add_argument(
|
|
858
|
+
"files", nargs="+", metavar="FILE", help=".p, full-record .nc, or per-profile .nc file(s)"
|
|
859
|
+
)
|
|
860
|
+
p.add_argument(
|
|
861
|
+
"-o",
|
|
862
|
+
"--output",
|
|
863
|
+
metavar="DIR",
|
|
864
|
+
required=True,
|
|
865
|
+
help="Output directory for chi .nc files",
|
|
866
|
+
)
|
|
867
|
+
p.add_argument(
|
|
868
|
+
"-j",
|
|
869
|
+
"--jobs",
|
|
870
|
+
type=int,
|
|
871
|
+
default=1,
|
|
872
|
+
metavar="N",
|
|
873
|
+
help="Parallel workers (0 = all cores, default: 1)",
|
|
874
|
+
)
|
|
875
|
+
p.add_argument(
|
|
876
|
+
"--fft-length", type=int, default=None, help="FFT segment length [samples] (default: 1024)"
|
|
877
|
+
)
|
|
878
|
+
p.add_argument(
|
|
879
|
+
"--diss-length",
|
|
880
|
+
type=int,
|
|
881
|
+
default=None,
|
|
882
|
+
help="Dissipation window [samples] (default: 4*fft-length)",
|
|
883
|
+
)
|
|
884
|
+
p.add_argument(
|
|
885
|
+
"--overlap",
|
|
886
|
+
type=int,
|
|
887
|
+
default=None,
|
|
888
|
+
help="Window overlap [samples] (default: diss-length//2)",
|
|
889
|
+
)
|
|
890
|
+
p.add_argument(
|
|
891
|
+
"--speed",
|
|
892
|
+
type=float,
|
|
893
|
+
default=None,
|
|
894
|
+
help="Fixed profiling speed [m/s] (default: from dP/dt)",
|
|
895
|
+
)
|
|
896
|
+
p.add_argument(
|
|
897
|
+
"--direction",
|
|
898
|
+
default=None,
|
|
899
|
+
choices=["auto", "up", "down", "glide", "horizontal"],
|
|
900
|
+
help="Profile direction (default: auto, from vehicle)",
|
|
901
|
+
)
|
|
902
|
+
p.add_argument(
|
|
903
|
+
"--vehicle",
|
|
904
|
+
default=None,
|
|
905
|
+
help="Vehicle type override (e.g. slocum_glider, vmp)",
|
|
906
|
+
)
|
|
907
|
+
p.add_argument(
|
|
908
|
+
"--no-goodman",
|
|
909
|
+
action="store_const",
|
|
910
|
+
const=True,
|
|
911
|
+
default=None,
|
|
912
|
+
help="Disable Goodman coherent noise removal",
|
|
913
|
+
)
|
|
914
|
+
p.add_argument(
|
|
915
|
+
"--fp07-model",
|
|
916
|
+
default=None,
|
|
917
|
+
choices=["single_pole", "double_pole"],
|
|
918
|
+
help="FP07 transfer function model (default: single_pole)",
|
|
919
|
+
)
|
|
920
|
+
p.add_argument(
|
|
921
|
+
"--epsilon-dir",
|
|
922
|
+
metavar="DIR",
|
|
923
|
+
default=None,
|
|
924
|
+
help="Directory with epsilon .nc files from 'rsi-tpw eps' (Method 1). "
|
|
925
|
+
"If omitted, uses Method 2 (spectral fitting).",
|
|
926
|
+
)
|
|
927
|
+
p.add_argument(
|
|
928
|
+
"--fit-method",
|
|
929
|
+
default=None,
|
|
930
|
+
choices=["mle", "iterative"],
|
|
931
|
+
help="Method 2 fitting: mle or iterative (default: iterative)",
|
|
932
|
+
)
|
|
933
|
+
p.add_argument(
|
|
934
|
+
"--spectrum-model",
|
|
935
|
+
default=None,
|
|
936
|
+
choices=["batchelor", "kraichnan"],
|
|
937
|
+
help="Theoretical spectrum model (default: kraichnan)",
|
|
938
|
+
)
|
|
939
|
+
p.add_argument(
|
|
940
|
+
"--f-AA", type=float, default=None, help="Anti-aliasing filter cutoff [Hz] (default: 98)"
|
|
941
|
+
)
|
|
942
|
+
p.add_argument(
|
|
943
|
+
"--salinity",
|
|
944
|
+
type=float,
|
|
945
|
+
default=None,
|
|
946
|
+
help="Salinity [PSU] for viscosity (default: 35, fixed S)",
|
|
947
|
+
)
|
|
948
|
+
p.set_defaults(func=_cmd_chi)
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def _add_pipeline_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
952
|
+
p = subparsers.add_parser(
|
|
953
|
+
"pipeline",
|
|
954
|
+
help="Run full pipeline (.p -> epsilon -> chi)",
|
|
955
|
+
description="Run the full processing pipeline from raw .p files through "
|
|
956
|
+
"epsilon (TKE dissipation) and chi (thermal dissipation). "
|
|
957
|
+
"Profiles are detected automatically. Chi is computed using "
|
|
958
|
+
"Method 1 (from shear-probe epsilon).",
|
|
959
|
+
)
|
|
960
|
+
p.add_argument("files", nargs="+", metavar="FILE", help=".p file(s) or glob pattern(s)")
|
|
961
|
+
p.add_argument(
|
|
962
|
+
"-o",
|
|
963
|
+
"--output",
|
|
964
|
+
metavar="DIR",
|
|
965
|
+
required=True,
|
|
966
|
+
help="Base output directory",
|
|
967
|
+
)
|
|
968
|
+
p.add_argument(
|
|
969
|
+
"--direction",
|
|
970
|
+
default=None,
|
|
971
|
+
choices=["auto", "up", "down", "glide", "horizontal"],
|
|
972
|
+
help="Profile direction (default: auto, from vehicle)",
|
|
973
|
+
)
|
|
974
|
+
p.add_argument(
|
|
975
|
+
"--vehicle",
|
|
976
|
+
default=None,
|
|
977
|
+
help="Vehicle type override (e.g. slocum_glider, vmp)",
|
|
978
|
+
)
|
|
979
|
+
p.add_argument(
|
|
980
|
+
"--speed",
|
|
981
|
+
type=float,
|
|
982
|
+
default=None,
|
|
983
|
+
help="Fixed profiling speed [m/s] (default: from dP/dt)",
|
|
984
|
+
)
|
|
985
|
+
p.add_argument(
|
|
986
|
+
"--speed-method",
|
|
987
|
+
choices=["pressure", "em", "flight"],
|
|
988
|
+
default=None,
|
|
989
|
+
help=(
|
|
990
|
+
"Through-water speed model (default: pressure = |dP/dt|). "
|
|
991
|
+
"'em' uses the U_EM flowmeter channel; 'flight' uses the inviscid "
|
|
992
|
+
"glider flight model |W|/sin(|pitch|-aoa) from the inclinometers."
|
|
993
|
+
),
|
|
994
|
+
)
|
|
995
|
+
p.add_argument(
|
|
996
|
+
"--aoa",
|
|
997
|
+
type=float,
|
|
998
|
+
default=None,
|
|
999
|
+
help="Angle of attack [deg] for --speed-method flight (default: 3.0)",
|
|
1000
|
+
)
|
|
1001
|
+
p.add_argument(
|
|
1002
|
+
"--eps-fft-length",
|
|
1003
|
+
type=int,
|
|
1004
|
+
default=None,
|
|
1005
|
+
help="FFT length for epsilon [samples] (default: 1024)",
|
|
1006
|
+
)
|
|
1007
|
+
p.add_argument(
|
|
1008
|
+
"--chi-fft-length",
|
|
1009
|
+
type=int,
|
|
1010
|
+
default=None,
|
|
1011
|
+
help="FFT length for chi [samples] (default: 1024)",
|
|
1012
|
+
)
|
|
1013
|
+
p.add_argument(
|
|
1014
|
+
"--no-goodman",
|
|
1015
|
+
action="store_const",
|
|
1016
|
+
const=True,
|
|
1017
|
+
default=None,
|
|
1018
|
+
help="Disable Goodman coherent noise removal for epsilon and chi",
|
|
1019
|
+
)
|
|
1020
|
+
p.add_argument(
|
|
1021
|
+
"--fp07-model",
|
|
1022
|
+
default=None,
|
|
1023
|
+
choices=["single_pole", "double_pole"],
|
|
1024
|
+
help="FP07 transfer function model (default: single_pole)",
|
|
1025
|
+
)
|
|
1026
|
+
p.add_argument(
|
|
1027
|
+
"--spectrum-model",
|
|
1028
|
+
default=None,
|
|
1029
|
+
choices=["batchelor", "kraichnan"],
|
|
1030
|
+
help="Theoretical spectrum model for chi (default: kraichnan)",
|
|
1031
|
+
)
|
|
1032
|
+
p.add_argument(
|
|
1033
|
+
"--f-AA", type=float, default=None, help="Anti-aliasing filter cutoff [Hz] (default: 98)"
|
|
1034
|
+
)
|
|
1035
|
+
p.add_argument(
|
|
1036
|
+
"--salinity",
|
|
1037
|
+
type=float,
|
|
1038
|
+
default=None,
|
|
1039
|
+
help="Salinity [PSU] for viscosity (default: 35, fixed S)",
|
|
1040
|
+
)
|
|
1041
|
+
p.set_defaults(func=_cmd_pipeline)
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
def _add_ql_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
1045
|
+
p = subparsers.add_parser(
|
|
1046
|
+
"ql",
|
|
1047
|
+
help="Interactive quick-look viewer",
|
|
1048
|
+
description="Open an interactive multi-panel viewer with profile navigation.",
|
|
1049
|
+
)
|
|
1050
|
+
p.add_argument("files", nargs="+", metavar="FILE", help=".p file(s) or glob pattern(s)")
|
|
1051
|
+
p.add_argument(
|
|
1052
|
+
"--fft-length", type=int, default=None, help="FFT segment length [samples] (default: 1024)"
|
|
1053
|
+
)
|
|
1054
|
+
p.add_argument(
|
|
1055
|
+
"--diss-length",
|
|
1056
|
+
type=int,
|
|
1057
|
+
default=None,
|
|
1058
|
+
help="Dissipation window [samples] (default: 4*fft-length)",
|
|
1059
|
+
)
|
|
1060
|
+
p.add_argument(
|
|
1061
|
+
"--f-AA", type=float, default=None, help="Anti-aliasing filter cutoff [Hz] (default: 98)"
|
|
1062
|
+
)
|
|
1063
|
+
p.add_argument(
|
|
1064
|
+
"--no-goodman",
|
|
1065
|
+
action="store_true",
|
|
1066
|
+
default=False,
|
|
1067
|
+
help="Disable Goodman coherent noise removal",
|
|
1068
|
+
)
|
|
1069
|
+
p.add_argument(
|
|
1070
|
+
"--direction",
|
|
1071
|
+
default=None,
|
|
1072
|
+
choices=["auto", "up", "down", "glide", "horizontal"],
|
|
1073
|
+
help="Profile direction (default: auto, from vehicle)",
|
|
1074
|
+
)
|
|
1075
|
+
p.add_argument(
|
|
1076
|
+
"--vehicle",
|
|
1077
|
+
default=None,
|
|
1078
|
+
help="Vehicle type override (e.g. slocum_glider, vmp)",
|
|
1079
|
+
)
|
|
1080
|
+
p.add_argument(
|
|
1081
|
+
"--W-min", type=float, default=None, help="Minimum fall rate [dbar/s] (default: 0.3)"
|
|
1082
|
+
)
|
|
1083
|
+
p.add_argument(
|
|
1084
|
+
"--spec-P-range",
|
|
1085
|
+
type=float,
|
|
1086
|
+
nargs=2,
|
|
1087
|
+
metavar=("P_MIN", "P_MAX"),
|
|
1088
|
+
default=None,
|
|
1089
|
+
help="Pressure range [dbar] for spectral calculations (default: full profile)",
|
|
1090
|
+
)
|
|
1091
|
+
p.add_argument(
|
|
1092
|
+
"--chi-method",
|
|
1093
|
+
type=int,
|
|
1094
|
+
default=1,
|
|
1095
|
+
choices=[1, 2],
|
|
1096
|
+
help="Chi method for profile estimates: 1 = from epsilon, "
|
|
1097
|
+
"2 = iterative spectral fit (default: 1)",
|
|
1098
|
+
)
|
|
1099
|
+
p.add_argument(
|
|
1100
|
+
"--spectrum-model",
|
|
1101
|
+
default="kraichnan",
|
|
1102
|
+
choices=["batchelor", "kraichnan"],
|
|
1103
|
+
help="Theoretical spectrum model (default: kraichnan)",
|
|
1104
|
+
)
|
|
1105
|
+
p.set_defaults(func=_cmd_ql)
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def _add_dl_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
1109
|
+
p = subparsers.add_parser(
|
|
1110
|
+
"dl",
|
|
1111
|
+
help="Interactive dissipation quality viewer",
|
|
1112
|
+
description="Open an interactive viewer comparing epsilon, chi (Batchelor vs "
|
|
1113
|
+
"Kraichnan), and Lueck (2022) figure of merit (FM) with profile navigation.",
|
|
1114
|
+
)
|
|
1115
|
+
p.add_argument("files", nargs="+", metavar="FILE", help=".p file(s) or glob pattern(s)")
|
|
1116
|
+
p.add_argument(
|
|
1117
|
+
"--fft-length", type=int, default=None, help="FFT segment length [samples] (default: 1024)"
|
|
1118
|
+
)
|
|
1119
|
+
p.add_argument(
|
|
1120
|
+
"--diss-length",
|
|
1121
|
+
type=int,
|
|
1122
|
+
default=None,
|
|
1123
|
+
help="Dissipation window [samples] (default: 4*fft-length)",
|
|
1124
|
+
)
|
|
1125
|
+
p.add_argument(
|
|
1126
|
+
"--f-AA", type=float, default=None, help="Anti-aliasing filter cutoff [Hz] (default: 98)"
|
|
1127
|
+
)
|
|
1128
|
+
p.add_argument(
|
|
1129
|
+
"--no-goodman",
|
|
1130
|
+
action="store_true",
|
|
1131
|
+
default=False,
|
|
1132
|
+
help="Disable Goodman coherent noise removal",
|
|
1133
|
+
)
|
|
1134
|
+
p.add_argument(
|
|
1135
|
+
"--direction",
|
|
1136
|
+
default=None,
|
|
1137
|
+
choices=["auto", "up", "down", "glide", "horizontal"],
|
|
1138
|
+
help="Profile direction (default: auto, from vehicle)",
|
|
1139
|
+
)
|
|
1140
|
+
p.add_argument(
|
|
1141
|
+
"--vehicle",
|
|
1142
|
+
default=None,
|
|
1143
|
+
help="Vehicle type override (e.g. slocum_glider, vmp)",
|
|
1144
|
+
)
|
|
1145
|
+
p.add_argument(
|
|
1146
|
+
"--W-min", type=float, default=None, help="Minimum fall rate [dbar/s] (default: 0.3)"
|
|
1147
|
+
)
|
|
1148
|
+
p.add_argument(
|
|
1149
|
+
"--spec-P-range",
|
|
1150
|
+
type=float,
|
|
1151
|
+
nargs=2,
|
|
1152
|
+
metavar=("P_MIN", "P_MAX"),
|
|
1153
|
+
default=None,
|
|
1154
|
+
help="Pressure range [dbar] for spectral calculations (default: full profile)",
|
|
1155
|
+
)
|
|
1156
|
+
p.set_defaults(func=_cmd_dl)
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
def _add_sensors_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
1160
|
+
p = subparsers.add_parser(
|
|
1161
|
+
"sensors",
|
|
1162
|
+
help="Inventory shear/FP07 sensors across a .p file tree",
|
|
1163
|
+
description=(
|
|
1164
|
+
"Walk a directory tree of Rockland .p files and summarize, per sensor "
|
|
1165
|
+
"serial number, the date range of use, the number of files, the "
|
|
1166
|
+
"platform(s) it was mounted on, and whether its calibration parameters "
|
|
1167
|
+
"changed. Reads only each file's header + config block (not the data), "
|
|
1168
|
+
"so it is fast over large trees. Select sensor kinds with --shear / "
|
|
1169
|
+
"--fp07 / --all (default: all)."
|
|
1170
|
+
),
|
|
1171
|
+
)
|
|
1172
|
+
p.add_argument(
|
|
1173
|
+
"paths",
|
|
1174
|
+
nargs="+",
|
|
1175
|
+
metavar="PATH",
|
|
1176
|
+
help="Directories (scanned recursively), .p files, or glob patterns",
|
|
1177
|
+
)
|
|
1178
|
+
p.add_argument("--shear", action="store_true", help="Inventory shear probes")
|
|
1179
|
+
p.add_argument("--fp07", action="store_true", help="Inventory FP07 thermistors")
|
|
1180
|
+
p.add_argument(
|
|
1181
|
+
"--all",
|
|
1182
|
+
dest="want_all",
|
|
1183
|
+
action="store_true",
|
|
1184
|
+
help="Inventory every sensor kind (shear + fp07; the default if none is given)",
|
|
1185
|
+
)
|
|
1186
|
+
p.add_argument(
|
|
1187
|
+
"--csv",
|
|
1188
|
+
metavar="PATH",
|
|
1189
|
+
default=None,
|
|
1190
|
+
help="Write a per-(file,channel) CSV table (overwritten if it exists)",
|
|
1191
|
+
)
|
|
1192
|
+
p.add_argument(
|
|
1193
|
+
"-v",
|
|
1194
|
+
"--verbose",
|
|
1195
|
+
action="store_true",
|
|
1196
|
+
help="List the individual files behind each changed parameter value",
|
|
1197
|
+
)
|
|
1198
|
+
p.add_argument(
|
|
1199
|
+
"--compact",
|
|
1200
|
+
action="store_true",
|
|
1201
|
+
help="One line per probe: SN, file count, calibration, and date range",
|
|
1202
|
+
)
|
|
1203
|
+
p.set_defaults(func=_cmd_sensors)
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
def _add_ml_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
1207
|
+
p = subparsers.add_parser(
|
|
1208
|
+
"ml",
|
|
1209
|
+
help="Interactive mixing viewer",
|
|
1210
|
+
description="Open an interactive viewer of the background stratification "
|
|
1211
|
+
"(N², dT/dz) and derived diapycnal-mixing quantities (K_T, Γ, K_rho) with "
|
|
1212
|
+
"profile navigation.",
|
|
1213
|
+
)
|
|
1214
|
+
p.add_argument("files", nargs="+", metavar="FILE", help=".p file(s) or glob pattern(s)")
|
|
1215
|
+
p.add_argument(
|
|
1216
|
+
"--fft-length", type=int, default=None, help="FFT segment length [samples] (default: 1024)"
|
|
1217
|
+
)
|
|
1218
|
+
p.add_argument(
|
|
1219
|
+
"--diss-length",
|
|
1220
|
+
type=int,
|
|
1221
|
+
default=None,
|
|
1222
|
+
help="Dissipation window [samples] (default: 4*fft-length)",
|
|
1223
|
+
)
|
|
1224
|
+
p.add_argument(
|
|
1225
|
+
"--f-AA", type=float, default=None, help="Anti-aliasing filter cutoff [Hz] (default: 98)"
|
|
1226
|
+
)
|
|
1227
|
+
p.add_argument(
|
|
1228
|
+
"--no-goodman",
|
|
1229
|
+
action="store_true",
|
|
1230
|
+
default=False,
|
|
1231
|
+
help="Disable Goodman coherent noise removal",
|
|
1232
|
+
)
|
|
1233
|
+
p.add_argument(
|
|
1234
|
+
"--direction",
|
|
1235
|
+
default=None,
|
|
1236
|
+
choices=["auto", "up", "down", "glide", "horizontal"],
|
|
1237
|
+
help="Profile direction (default: auto, from vehicle)",
|
|
1238
|
+
)
|
|
1239
|
+
p.add_argument(
|
|
1240
|
+
"--vehicle",
|
|
1241
|
+
default=None,
|
|
1242
|
+
help="Vehicle type override (e.g. slocum_glider, vmp)",
|
|
1243
|
+
)
|
|
1244
|
+
p.add_argument(
|
|
1245
|
+
"--W-min", type=float, default=None, help="Minimum fall rate [dbar/s] (default: 0.3)"
|
|
1246
|
+
)
|
|
1247
|
+
p.add_argument(
|
|
1248
|
+
"--spec-P-range",
|
|
1249
|
+
type=float,
|
|
1250
|
+
nargs=2,
|
|
1251
|
+
metavar=("P_MIN", "P_MAX"),
|
|
1252
|
+
default=None,
|
|
1253
|
+
help="Pressure range [dbar] to highlight on the profiles (default: none)",
|
|
1254
|
+
)
|
|
1255
|
+
p.add_argument(
|
|
1256
|
+
"--salinity",
|
|
1257
|
+
type=float,
|
|
1258
|
+
default=None,
|
|
1259
|
+
help="Fixed practical salinity [PSU] for stratification "
|
|
1260
|
+
"(default: measured from JAC C/T, else 35)",
|
|
1261
|
+
)
|
|
1262
|
+
p.set_defaults(func=_cmd_ml)
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
def _add_bench_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
1266
|
+
p = subparsers.add_parser(
|
|
1267
|
+
"bench",
|
|
1268
|
+
help="Bench-test diagnostic (quick_bench + checklist)",
|
|
1269
|
+
description=(
|
|
1270
|
+
"Evaluate a bench-test recording (dummy probes, instrument at rest on "
|
|
1271
|
+
"foam). Produces the raw-count time-series and counts^2/Hz spectra "
|
|
1272
|
+
"figures of ODAS quick_bench.m, plus an automatic PASS/FAIL evaluation "
|
|
1273
|
+
"of the Rockland Bench Test Review Checklist (subjective items are "
|
|
1274
|
+
"flagged REVIEW). Figures and the checklist text are written to the "
|
|
1275
|
+
"output directory (default: ./bench/); --show also opens them "
|
|
1276
|
+
"interactively. Compare the spectra against the instrument's RSI "
|
|
1277
|
+
"calibration report."
|
|
1278
|
+
),
|
|
1279
|
+
)
|
|
1280
|
+
p.add_argument("files", nargs="+", metavar="FILE", help=".p file(s) or glob pattern(s)")
|
|
1281
|
+
p.add_argument(
|
|
1282
|
+
"-o",
|
|
1283
|
+
"--output",
|
|
1284
|
+
metavar="DIR",
|
|
1285
|
+
default=None,
|
|
1286
|
+
help="Output directory for figures + checklist (default: ./bench/, unless "
|
|
1287
|
+
"--show is given without -o, which only displays)",
|
|
1288
|
+
)
|
|
1289
|
+
p.add_argument(
|
|
1290
|
+
"--show", action="store_true", help="Open the figures in interactive windows"
|
|
1291
|
+
)
|
|
1292
|
+
p.add_argument(
|
|
1293
|
+
"--sn",
|
|
1294
|
+
default=None,
|
|
1295
|
+
metavar="SN",
|
|
1296
|
+
help="Serial number for figure titles/filenames (default: from config)",
|
|
1297
|
+
)
|
|
1298
|
+
p.add_argument(
|
|
1299
|
+
"--fft-sec",
|
|
1300
|
+
type=float,
|
|
1301
|
+
default=2.0,
|
|
1302
|
+
help="FFT segment length for spectra [seconds] (default: 2.0)",
|
|
1303
|
+
)
|
|
1304
|
+
p.add_argument(
|
|
1305
|
+
"--dpi", type=int, default=150, help="Figure resolution when saving (default: 150)"
|
|
1306
|
+
)
|
|
1307
|
+
p.add_argument(
|
|
1308
|
+
"--format",
|
|
1309
|
+
choices=["png", "pdf", "both", "pdf-bundle"],
|
|
1310
|
+
default="png",
|
|
1311
|
+
help="Saved figure format: one file per figure (png/pdf/both), or "
|
|
1312
|
+
"'pdf-bundle' for a single multi-page PDF (default: png)",
|
|
1313
|
+
)
|
|
1314
|
+
p.set_defaults(func=_cmd_bench)
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
def _add_patchconfig_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
1318
|
+
p = subparsers.add_parser(
|
|
1319
|
+
"patch-config",
|
|
1320
|
+
help="Patch config fields in .p file(s) (writes new files)",
|
|
1321
|
+
description=(
|
|
1322
|
+
"Edit selected configuration fields (instrument/cruise info, per-channel "
|
|
1323
|
+
"calibration) in Rockland .p files, driven by a YAML edit spec. Originals "
|
|
1324
|
+
"are never modified; patched copies are written to --out, with the change "
|
|
1325
|
+
"annotated and the full original configuration embedded for recovery. If a "
|
|
1326
|
+
"file's targeted values already match, that file is left unwritten. "
|
|
1327
|
+
"Scaffold the YAML with 'rsi-tpw patch-template'."
|
|
1328
|
+
),
|
|
1329
|
+
)
|
|
1330
|
+
p.add_argument("files", nargs="+", metavar="FILE", help=".p file(s) or glob pattern(s)")
|
|
1331
|
+
p.add_argument(
|
|
1332
|
+
"--edits", required=True, metavar="YAML", help="YAML edit spec (see 'patch-template')"
|
|
1333
|
+
)
|
|
1334
|
+
p.add_argument(
|
|
1335
|
+
"-o", "--out", required=True, metavar="DIR", help="Output directory for patched .p files"
|
|
1336
|
+
)
|
|
1337
|
+
p.add_argument(
|
|
1338
|
+
"--dry-run", action="store_true", help="Show the configuration diff; write nothing"
|
|
1339
|
+
)
|
|
1340
|
+
p.add_argument(
|
|
1341
|
+
"--add-keys", action="store_true", help="Allow adding keys that do not already exist"
|
|
1342
|
+
)
|
|
1343
|
+
p.add_argument(
|
|
1344
|
+
"--batch-cal",
|
|
1345
|
+
action="store_true",
|
|
1346
|
+
help="Allow per-channel calibration edits across multiple files",
|
|
1347
|
+
)
|
|
1348
|
+
p.set_defaults(func=_cmd_patch_config)
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
def _add_patchtemplate_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
1352
|
+
p = subparsers.add_parser(
|
|
1353
|
+
"patch-template",
|
|
1354
|
+
help="Scaffold a patch-config edit spec from a .p file",
|
|
1355
|
+
description=(
|
|
1356
|
+
"Write a commented YAML edit-spec template pre-filled with the file's "
|
|
1357
|
+
"current editable values and channel names. Edit it, then apply it with "
|
|
1358
|
+
"'rsi-tpw patch-config'."
|
|
1359
|
+
),
|
|
1360
|
+
)
|
|
1361
|
+
p.add_argument("file", metavar="FILE", help="Input .p file")
|
|
1362
|
+
p.add_argument("-o", "--output", metavar="YAML", help="Output path (default: print to stdout)")
|
|
1363
|
+
p.add_argument("--force", action="store_true", help="Overwrite existing output file")
|
|
1364
|
+
p.set_defaults(func=_cmd_patch_template)
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
# ---------------------------------------------------------------------------
|
|
1368
|
+
# Main entry point
|
|
1369
|
+
# ---------------------------------------------------------------------------
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
def main() -> None:
|
|
1373
|
+
"""Main CLI entry point for rsi-tpw."""
|
|
1374
|
+
import logging
|
|
1375
|
+
|
|
1376
|
+
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
1377
|
+
|
|
1378
|
+
from odas_tpw.rsi import __version__
|
|
1379
|
+
|
|
1380
|
+
parser = argparse.ArgumentParser(
|
|
1381
|
+
prog="rsi-tpw",
|
|
1382
|
+
description="microstructure-tpw: Read Rockland Scientific .P files and convert to NetCDF.",
|
|
1383
|
+
)
|
|
1384
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
1385
|
+
parser.add_argument(
|
|
1386
|
+
"-c",
|
|
1387
|
+
"--config",
|
|
1388
|
+
metavar="YAML",
|
|
1389
|
+
default=None,
|
|
1390
|
+
help="Configuration file (YAML). Generate a template with 'rsi-tpw init'.",
|
|
1391
|
+
)
|
|
1392
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
1393
|
+
|
|
1394
|
+
_add_info_parser(subparsers)
|
|
1395
|
+
_add_cutp_parser(subparsers)
|
|
1396
|
+
_add_nc_parser(subparsers)
|
|
1397
|
+
_add_init_parser(subparsers)
|
|
1398
|
+
_add_patchconfig_parser(subparsers)
|
|
1399
|
+
_add_patchtemplate_parser(subparsers)
|
|
1400
|
+
_add_prof_parser(subparsers)
|
|
1401
|
+
_add_eps_parser(subparsers)
|
|
1402
|
+
_add_chi_parser(subparsers)
|
|
1403
|
+
_add_pipeline_parser(subparsers)
|
|
1404
|
+
_add_ql_parser(subparsers)
|
|
1405
|
+
_add_dl_parser(subparsers)
|
|
1406
|
+
_add_ml_parser(subparsers)
|
|
1407
|
+
_add_bench_parser(subparsers)
|
|
1408
|
+
_add_sensors_parser(subparsers)
|
|
1409
|
+
|
|
1410
|
+
args = parser.parse_args()
|
|
1411
|
+
args.func(args)
|