oceanarray 0.1.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.
- oceanarray/__init__.py +20 -0
- oceanarray/_version.py +1 -0
- oceanarray/cli.py +1343 -0
- oceanarray/clock_offset.py +675 -0
- oceanarray/config/OS1_sensor_attrs.yaml +49 -0
- oceanarray/config/OS1_var_names.yaml +5 -0
- oceanarray/config/OS1_vocab_attrs.yaml +84 -0
- oceanarray/config/legacy/project_RAPID.yaml +69 -0
- oceanarray/config/legacy/rodb_keys.txt +206 -0
- oceanarray/config/legacy/rodb_keys.yaml +1012 -0
- oceanarray/config/logging.yaml +28 -0
- oceanarray/legacy/__init__.py +47 -0
- oceanarray/legacy/caldip.py +753 -0
- oceanarray/legacy/convertOS.py +527 -0
- oceanarray/legacy/find_deployment.py +489 -0
- oceanarray/legacy/mooring_rodb.py +429 -0
- oceanarray/legacy/process_rodb.py +369 -0
- oceanarray/legacy/rodb.py +330 -0
- oceanarray/logger.py +192 -0
- oceanarray/logsheet/__init__.py +60 -0
- oceanarray/logsheet/_columns.py +97 -0
- oceanarray/logsheet/_config.py +245 -0
- oceanarray/logsheet/_firmware.py +43 -0
- oceanarray/logsheet/_latex.py +128 -0
- oceanarray/logsheet/_render.py +126 -0
- oceanarray/logsheet/builders/__init__.py +13 -0
- oceanarray/logsheet/builders/caldip_download.py +195 -0
- oceanarray/logsheet/builders/caldip_setup.py +188 -0
- oceanarray/logsheet/builders/deployment_setup.py +218 -0
- oceanarray/logsheet/builders/mooring_download.py +243 -0
- oceanarray/logsheet/builders/recovery.py +157 -0
- oceanarray/logsheet/data/column_defs.yaml +686 -0
- oceanarray/logsheet/data/latex_templates/_base_instrument.tex.j2 +42 -0
- oceanarray/logsheet/data/latex_templates/caldip_download.tex.j2 +47 -0
- oceanarray/logsheet/data/latex_templates/caldip_setup.tex.j2 +70 -0
- oceanarray/logsheet/data/latex_templates/deployment_setup.tex.j2 +97 -0
- oceanarray/logsheet/data/latex_templates/mooring_download.tex.j2 +54 -0
- oceanarray/logsheet/data/latex_templates/recovery.tex.j2 +103 -0
- oceanarray/mooring_level.py +1368 -0
- oceanarray/oceanarray.mplstyle +18 -0
- oceanarray/parameters.py +270 -0
- oceanarray/plotter.py +942 -0
- oceanarray/plotters/__init__.py +97 -0
- oceanarray/plotters/_animation.py +261 -0
- oceanarray/plotters/_current.py +705 -0
- oceanarray/plotters/_diagnostic.py +667 -0
- oceanarray/plotters/_helpers.py +57 -0
- oceanarray/plotters/_primitives.py +101 -0
- oceanarray/plotters/_section.py +16 -0
- oceanarray/plotters/_timeseries.py +11 -0
- oceanarray/rapid_interp.py +739 -0
- oceanarray/read_rbr_hex.py +541 -0
- oceanarray/readers.py +303 -0
- oceanarray/report/__init__.py +5 -0
- oceanarray/report/_array.py +386 -0
- oceanarray/report/_grid.py +539 -0
- oceanarray/report/_html_helpers.py +1004 -0
- oceanarray/report/_instrument.py +853 -0
- oceanarray/report/_mooring.py +1385 -0
- oceanarray/report/_plots.py +4284 -0
- oceanarray/report/_stack.py +941 -0
- oceanarray/stage1.py +1959 -0
- oceanarray/stage2.py +1160 -0
- oceanarray/stage3.py +2494 -0
- oceanarray/time_gridding.py +958 -0
- oceanarray/tools.py +400 -0
- oceanarray/transports.py +1 -0
- oceanarray/utilities.py +538 -0
- oceanarray/validation.py +389 -0
- oceanarray/writers.py +99 -0
- oceanarray-0.1.0.dist-info/METADATA +503 -0
- oceanarray-0.1.0.dist-info/RECORD +76 -0
- oceanarray-0.1.0.dist-info/WHEEL +5 -0
- oceanarray-0.1.0.dist-info/entry_points.txt +2 -0
- oceanarray-0.1.0.dist-info/licenses/LICENSE +21 -0
- oceanarray-0.1.0.dist-info/top_level.txt +1 -0
oceanarray/cli.py
ADDED
|
@@ -0,0 +1,1343 @@
|
|
|
1
|
+
"""Command-line interface for oceanarray processing."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from .utilities import _status
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _resolve_basedir(basedir: str) -> str:
|
|
10
|
+
"""Strip trailing 'moor' component if user passed data/moor/ instead of data/."""
|
|
11
|
+
p = Path(basedir)
|
|
12
|
+
if p.name == "moor":
|
|
13
|
+
return str(p.parent)
|
|
14
|
+
return str(p)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_proc_root(basedir: str) -> Path:
|
|
18
|
+
"""Return basedir/proc, falling back to basedir/moor/proc for legacy layouts."""
|
|
19
|
+
base = Path(basedir)
|
|
20
|
+
proc = base / "proc"
|
|
21
|
+
if proc.is_dir():
|
|
22
|
+
return proc
|
|
23
|
+
legacy = base / "moor" / "proc"
|
|
24
|
+
return legacy if legacy.is_dir() else proc
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _parse_dirs(
|
|
28
|
+
args: "argparse.Namespace",
|
|
29
|
+
) -> "tuple[Path | None, Path, bool]":
|
|
30
|
+
"""Resolve raw and proc directories from CLI args.
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
raw_dir : Path or None
|
|
35
|
+
Cruise-level raw directory (``--raw-dir``). None in legacy mode.
|
|
36
|
+
proc_root : Path
|
|
37
|
+
Cruise-level processed output directory.
|
|
38
|
+
legacy : bool
|
|
39
|
+
True when ``--basedir`` was used (deprecated path).
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
import warnings
|
|
43
|
+
|
|
44
|
+
basedir = getattr(args, "basedir", None)
|
|
45
|
+
raw_dir = getattr(args, "raw_dir", None)
|
|
46
|
+
proc_dir = getattr(args, "proc_dir", None)
|
|
47
|
+
|
|
48
|
+
if raw_dir or proc_dir:
|
|
49
|
+
if not proc_dir:
|
|
50
|
+
raise SystemExit("ERROR: --proc-dir is required when --raw-dir is given.") # noqa: TRY003
|
|
51
|
+
return Path(raw_dir) if raw_dir else None, Path(proc_dir), False
|
|
52
|
+
|
|
53
|
+
if not basedir:
|
|
54
|
+
raise SystemExit( # noqa: TRY003
|
|
55
|
+
"ERROR: supply either --raw-dir + --proc-dir (recommended) "
|
|
56
|
+
"or --basedir (deprecated)."
|
|
57
|
+
)
|
|
58
|
+
basedir = _resolve_basedir(basedir)
|
|
59
|
+
warnings.warn(
|
|
60
|
+
"--basedir is deprecated; switch to --raw-dir and --proc-dir. "
|
|
61
|
+
"See MIGRATION-BASEDIR.md at the repository root.",
|
|
62
|
+
DeprecationWarning,
|
|
63
|
+
stacklevel=3,
|
|
64
|
+
)
|
|
65
|
+
return None, _get_proc_root(basedir), True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _print_report(basedir: "str | None", mooring: str, legacy: bool = True) -> None:
|
|
69
|
+
"""Print a per-instrument summary of stage1/stage2/stage3 NetCDF files for a mooring.
|
|
70
|
+
|
|
71
|
+
For each instrument, reports: serial number, record count (raw from stage1 vs
|
|
72
|
+
processed from stage2/stage3), time span, nominal depth, sampling interval, and
|
|
73
|
+
which geophysical variables are present (temperature, salinity, velocity, etc.).
|
|
74
|
+
"""
|
|
75
|
+
import datetime
|
|
76
|
+
import numpy as np
|
|
77
|
+
import xarray as xr
|
|
78
|
+
|
|
79
|
+
if legacy and basedir is not None:
|
|
80
|
+
proc_dir = _get_proc_root(basedir) / mooring
|
|
81
|
+
else:
|
|
82
|
+
proc_dir = Path(basedir) if basedir else Path(".")
|
|
83
|
+
|
|
84
|
+
# Collect all processed files; prefer _stage3 > _use as the "best" file per serial
|
|
85
|
+
use_files = sorted(proc_dir.rglob("*_stage2.nc"))
|
|
86
|
+
stage3_files = {
|
|
87
|
+
f.name.replace("_stage3.nc", ""): f for f in proc_dir.rglob("*_stage3.nc")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if not use_files and not stage3_files:
|
|
91
|
+
print(" No processed files found.")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
by_instrument: dict = {}
|
|
95
|
+
for nc in use_files:
|
|
96
|
+
instrument = nc.parent.name
|
|
97
|
+
by_instrument.setdefault(instrument, []).append(nc)
|
|
98
|
+
|
|
99
|
+
for instrument, files in sorted(by_instrument.items()):
|
|
100
|
+
print(f"\n {instrument}")
|
|
101
|
+
for nc in sorted(files):
|
|
102
|
+
# Use _stage3.nc for reporting if it exists
|
|
103
|
+
stem = nc.name.replace("_stage2.nc", "")
|
|
104
|
+
best_nc = stage3_files.get(stem, nc)
|
|
105
|
+
stage_label = "stage3" if best_nc != nc else "use"
|
|
106
|
+
try:
|
|
107
|
+
ds = xr.open_dataset(best_nc, decode_timedelta=False)
|
|
108
|
+
n_rec = len(ds.time)
|
|
109
|
+
t0 = str(ds.time.values[0])[:16].replace("T", " ")
|
|
110
|
+
t1 = str(ds.time.values[-1])[:16].replace("T", " ")
|
|
111
|
+
serial = ds["serial_number"].item() if "serial_number" in ds else "?"
|
|
112
|
+
depth = (
|
|
113
|
+
f"{ds['InstrDepth'].item():.0f} m" if "InstrDepth" in ds else "?"
|
|
114
|
+
)
|
|
115
|
+
vars_present = [
|
|
116
|
+
v
|
|
117
|
+
for v in (
|
|
118
|
+
"temperature",
|
|
119
|
+
"conductivity",
|
|
120
|
+
"pressure",
|
|
121
|
+
"east_velocity",
|
|
122
|
+
"north_velocity",
|
|
123
|
+
"up_velocity",
|
|
124
|
+
"velocity_beam1",
|
|
125
|
+
)
|
|
126
|
+
if v in ds.data_vars
|
|
127
|
+
]
|
|
128
|
+
if len(ds.time) > 1:
|
|
129
|
+
dt_s = np.median(
|
|
130
|
+
np.diff(ds.time.values).astype("timedelta64[s]").astype(float)
|
|
131
|
+
)
|
|
132
|
+
interval = f"{dt_s:.0f} s"
|
|
133
|
+
else:
|
|
134
|
+
interval = "? s"
|
|
135
|
+
ds.close()
|
|
136
|
+
|
|
137
|
+
raw_nc = nc.with_name(nc.name.replace("_stage2.nc", "_stage1.nc"))
|
|
138
|
+
try:
|
|
139
|
+
if raw_nc.exists():
|
|
140
|
+
ds_raw = xr.open_dataset(raw_nc, decode_timedelta=False)
|
|
141
|
+
n_raw = len(ds_raw.time)
|
|
142
|
+
ds_raw.close()
|
|
143
|
+
counts = f"{n_raw:>7} raw → {n_rec:>7} {stage_label}"
|
|
144
|
+
else:
|
|
145
|
+
counts = f"{'?':>7} raw → {n_rec:>7} {stage_label}"
|
|
146
|
+
except Exception: # noqa: BLE001 — intentional broad catch at I/O boundary
|
|
147
|
+
counts = f"{'HDF ERR':>7} raw → {n_rec:>7} {stage_label}"
|
|
148
|
+
|
|
149
|
+
mtime = datetime.datetime.fromtimestamp(
|
|
150
|
+
best_nc.stat().st_mtime
|
|
151
|
+
).strftime("%Y-%m-%d %H:%M")
|
|
152
|
+
print(
|
|
153
|
+
f" s/n {serial:<8} {counts} {t0} → {t1} depth: {depth} dt: {interval} processed: {mtime} vars: {', '.join(vars_present)}"
|
|
154
|
+
)
|
|
155
|
+
except Exception as e: # noqa: BLE001 — display helper; must not crash status output
|
|
156
|
+
print(f" {nc.name}: ERROR — {e}")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def cmd_process(args: argparse.Namespace) -> int:
|
|
160
|
+
"""Convert raw instrument files to processed NetCDF for one mooring.
|
|
161
|
+
|
|
162
|
+
By default runs stage 1 (raw → CF-NetCDF) and stage 2 (trim to deployment
|
|
163
|
+
window + clock-drift correction). Pass ``--stage 1 2 3`` to also run
|
|
164
|
+
stage 3 (QC flags, pressure interpolation, beam → ENU velocity rotation).
|
|
165
|
+
|
|
166
|
+
Optional side effects:
|
|
167
|
+
- ``--report``: after processing, print a per-instrument record summary to
|
|
168
|
+
the console (record counts, time spans, variables present).
|
|
169
|
+
- ``--plot``: save a PNG overview plot for each instrument alongside the
|
|
170
|
+
stage2 NetCDF file in the proc directory.
|
|
171
|
+
"""
|
|
172
|
+
from .stage1 import MooringProcessor
|
|
173
|
+
from .stage2 import Stage2Processor
|
|
174
|
+
from .stage3 import Stage3Processor
|
|
175
|
+
|
|
176
|
+
raw_dir, proc_root, legacy = _parse_dirs(args)
|
|
177
|
+
basedir = _resolve_basedir(args.basedir) if legacy else None
|
|
178
|
+
|
|
179
|
+
# If --stage was not explicitly set and --report is the only action, skip processing
|
|
180
|
+
stages = args.stage if (args.stage is not None or not args.report) else []
|
|
181
|
+
if stages is None:
|
|
182
|
+
stages = [1, 2]
|
|
183
|
+
|
|
184
|
+
overall_success = True
|
|
185
|
+
|
|
186
|
+
serials = args.serial or None
|
|
187
|
+
|
|
188
|
+
if 1 in stages:
|
|
189
|
+
_status("section", f"Stage 1: {args.mooring}")
|
|
190
|
+
if legacy:
|
|
191
|
+
proc = MooringProcessor(basedir)
|
|
192
|
+
else:
|
|
193
|
+
proc = MooringProcessor(raw_dir=str(raw_dir), proc_dir=str(proc_root))
|
|
194
|
+
ok = proc.process_mooring(args.mooring, serials=serials, force=args.force)
|
|
195
|
+
if not ok:
|
|
196
|
+
print("Stage 1 failed.")
|
|
197
|
+
overall_success = False
|
|
198
|
+
|
|
199
|
+
if 2 in stages:
|
|
200
|
+
_status("section", f"Stage 2: {args.mooring}")
|
|
201
|
+
if legacy:
|
|
202
|
+
s2 = Stage2Processor(basedir)
|
|
203
|
+
else:
|
|
204
|
+
s2 = Stage2Processor(proc_dir=str(proc_root))
|
|
205
|
+
ok = s2.process_mooring(args.mooring, serials=serials, force=args.force)
|
|
206
|
+
if not ok:
|
|
207
|
+
print("Stage 2 failed.")
|
|
208
|
+
overall_success = False
|
|
209
|
+
|
|
210
|
+
if 3 in stages:
|
|
211
|
+
dry = getattr(args, "dry_run", False)
|
|
212
|
+
print(
|
|
213
|
+
f"\n=== Stage 3: {args.mooring} (pressure interpolation)"
|
|
214
|
+
+ (" — DRY RUN" if dry else "")
|
|
215
|
+
+ " ==="
|
|
216
|
+
)
|
|
217
|
+
if legacy:
|
|
218
|
+
s3 = Stage3Processor(basedir)
|
|
219
|
+
else:
|
|
220
|
+
s3 = Stage3Processor(proc_dir=str(proc_root))
|
|
221
|
+
ok = s3.process_mooring(
|
|
222
|
+
args.mooring, serials=serials, force=args.force, dry_run=dry
|
|
223
|
+
)
|
|
224
|
+
if not ok:
|
|
225
|
+
print("Stage 3 failed.")
|
|
226
|
+
overall_success = False
|
|
227
|
+
|
|
228
|
+
if args.report:
|
|
229
|
+
_status("section", f"Record Summary: {args.mooring}")
|
|
230
|
+
_print_report(
|
|
231
|
+
basedir or str(proc_root / args.mooring), args.mooring, legacy=legacy
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if args.plot:
|
|
235
|
+
import matplotlib
|
|
236
|
+
|
|
237
|
+
matplotlib.use("Agg")
|
|
238
|
+
import xarray as xr
|
|
239
|
+
from .plotters import plot_microcat_raw
|
|
240
|
+
|
|
241
|
+
from .plotters import plot_aquadopp_raw
|
|
242
|
+
|
|
243
|
+
_status("section", f"Plotting: {args.mooring}")
|
|
244
|
+
proc_dir = (
|
|
245
|
+
_get_proc_root(basedir) / args.mooring
|
|
246
|
+
if legacy
|
|
247
|
+
else proc_root / args.mooring
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
for nc in sorted((proc_dir / "microcat").glob("*_stage2.nc")):
|
|
251
|
+
ds = xr.open_dataset(nc, decode_timedelta=False)
|
|
252
|
+
plot_microcat_raw(ds, save_path=nc.with_suffix(".png"))
|
|
253
|
+
print(f"Saved: {nc.with_suffix('.png')}")
|
|
254
|
+
|
|
255
|
+
for nc in sorted((proc_dir / "aquadopp").glob("*_stage2.nc")):
|
|
256
|
+
ds = xr.open_dataset(nc, decode_timedelta=False)
|
|
257
|
+
plot_aquadopp_raw(ds, save_path=nc.with_suffix(".png"))
|
|
258
|
+
print(f"Saved: {nc.with_suffix('.png')}")
|
|
259
|
+
|
|
260
|
+
return 0 if overall_success else 1
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def cmd_plot(args: argparse.Namespace) -> int:
|
|
264
|
+
"""Generate a multi-instrument mooring overview plot.
|
|
265
|
+
|
|
266
|
+
Reads the best available processed NetCDF (stage3 preferred, falling back to
|
|
267
|
+
stage2) for all instruments and produces a scatter/line plot with a user-chosen
|
|
268
|
+
y-axis variable (default: pressure) and colour variable (default: temperature).
|
|
269
|
+
|
|
270
|
+
When ``--output`` is given the plot is saved to that filename in the proc
|
|
271
|
+
directory (or ``--output-dir`` if specified). Without ``--output``, the plot
|
|
272
|
+
is displayed interactively.
|
|
273
|
+
"""
|
|
274
|
+
from pathlib import Path
|
|
275
|
+
from .plotters import plot_mooring_timeseries
|
|
276
|
+
from . import parameters as P
|
|
277
|
+
|
|
278
|
+
_, proc_root, _ = _parse_dirs(args)
|
|
279
|
+
|
|
280
|
+
if args.colormap:
|
|
281
|
+
P.DEFAULT_COLORMAP = args.colormap
|
|
282
|
+
if args.downsample:
|
|
283
|
+
P.DOWNSAMPLE_SECONDS = args.downsample
|
|
284
|
+
|
|
285
|
+
save_path = None
|
|
286
|
+
if args.output:
|
|
287
|
+
out_dir = Path(args.output_dir) if args.output_dir else proc_root / args.mooring
|
|
288
|
+
save_path = out_dir / args.output
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
plot_mooring_timeseries(
|
|
292
|
+
proc_dir=proc_root,
|
|
293
|
+
mooring=args.mooring,
|
|
294
|
+
var_y=args.var_y,
|
|
295
|
+
var_color=args.var_color,
|
|
296
|
+
markersize=args.markersize,
|
|
297
|
+
save_path=save_path,
|
|
298
|
+
show=args.show or save_path is None,
|
|
299
|
+
)
|
|
300
|
+
except (FileNotFoundError, ValueError) as e:
|
|
301
|
+
print(f"ERROR: {e}")
|
|
302
|
+
return 1
|
|
303
|
+
|
|
304
|
+
if save_path:
|
|
305
|
+
print(f"Saved: {save_path}")
|
|
306
|
+
return 0
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def cmd_report(args: argparse.Namespace) -> int:
|
|
310
|
+
"""Generate HTML quality-control reports for a mooring or an array.
|
|
311
|
+
|
|
312
|
+
Without ``--array``: generates the mooring summary page (and optionally
|
|
313
|
+
``--instruments``, ``--stack``, ``--grid`` pages) for a single mooring.
|
|
314
|
+
|
|
315
|
+
With ``--array``: the positional argument is treated as a path to an
|
|
316
|
+
``*.array.yaml`` file and an array-level HTML index is generated linking
|
|
317
|
+
all mooring reports. Other report flags are ignored in array mode.
|
|
318
|
+
"""
|
|
319
|
+
from pathlib import Path
|
|
320
|
+
|
|
321
|
+
_, proc_root, _ = _parse_dirs(args)
|
|
322
|
+
|
|
323
|
+
if getattr(args, "array", False):
|
|
324
|
+
from .report._array import generate_array_report
|
|
325
|
+
|
|
326
|
+
# Resolve the YAML path: try as-given first, then relative to proc_dir.
|
|
327
|
+
_yaml_path = Path(args.mooring)
|
|
328
|
+
if not _yaml_path.exists() and not _yaml_path.is_absolute():
|
|
329
|
+
_yaml_path = proc_root / args.mooring
|
|
330
|
+
_status("section", f"Array report: {_yaml_path.name}")
|
|
331
|
+
result = generate_array_report(
|
|
332
|
+
array_yaml_path=_yaml_path,
|
|
333
|
+
proc_dir=proc_root,
|
|
334
|
+
force=args.force,
|
|
335
|
+
)
|
|
336
|
+
return 0 if result else 1
|
|
337
|
+
|
|
338
|
+
from .report import MooringReport
|
|
339
|
+
|
|
340
|
+
raw_dir, proc_root, legacy = _parse_dirs(args)
|
|
341
|
+
basedir = _resolve_basedir(args.basedir) if legacy else None
|
|
342
|
+
_status("section", f"Report: {args.mooring}")
|
|
343
|
+
serials = getattr(args, "serial", None)
|
|
344
|
+
if legacy:
|
|
345
|
+
reporter = MooringReport(basedir)
|
|
346
|
+
else:
|
|
347
|
+
reporter = MooringReport(
|
|
348
|
+
proc_dir=str(proc_root),
|
|
349
|
+
raw_dir=str(raw_dir) if raw_dir else None,
|
|
350
|
+
)
|
|
351
|
+
all_reports = getattr(args, "all_reports", False)
|
|
352
|
+
result = reporter.generate(
|
|
353
|
+
args.mooring,
|
|
354
|
+
force=args.force,
|
|
355
|
+
outdir=getattr(args, "outdir", None),
|
|
356
|
+
serials=serials,
|
|
357
|
+
instruments=all_reports or args.instruments or bool(serials),
|
|
358
|
+
grid=all_reports or args.grid,
|
|
359
|
+
stack=all_reports or args.stack,
|
|
360
|
+
)
|
|
361
|
+
return 0 if result else 1
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def cmd_stack(args: argparse.Namespace) -> int:
|
|
365
|
+
"""Combine all instruments from a mooring onto a common time axis.
|
|
366
|
+
|
|
367
|
+
Reads ``{mooring}_{serial}_stage3.nc`` (falling back to ``_stage2.nc``) for
|
|
368
|
+
every instrument listed in the mooring YAML and resamples each time series to
|
|
369
|
+
a uniform sampling interval (default 60 s). Writes a single multi-instrument
|
|
370
|
+
file ``{mooring}_stack.nc`` in the proc directory.
|
|
371
|
+
|
|
372
|
+
Prerequisite: stage 1 and 2 (and ideally stage 3) must have completed for all
|
|
373
|
+
instruments before running this command.
|
|
374
|
+
"""
|
|
375
|
+
from .mooring_level import MooringStacker
|
|
376
|
+
|
|
377
|
+
_, proc_root, legacy = _parse_dirs(args)
|
|
378
|
+
basedir = _resolve_basedir(args.basedir) if legacy else None
|
|
379
|
+
_status("section", f"Stack: {args.mooring}")
|
|
380
|
+
if legacy:
|
|
381
|
+
stacker = MooringStacker(basedir)
|
|
382
|
+
else:
|
|
383
|
+
stacker = MooringStacker(proc_dir=str(proc_root))
|
|
384
|
+
ok = stacker.stack(
|
|
385
|
+
args.mooring,
|
|
386
|
+
dt_seconds=args.dt,
|
|
387
|
+
force=args.force,
|
|
388
|
+
)
|
|
389
|
+
return 0 if ok else 1
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def cmd_grid(args: argparse.Namespace) -> int:
|
|
393
|
+
"""Vertically interpolate a mooring stack onto a uniform pressure grid.
|
|
394
|
+
|
|
395
|
+
Reads ``{mooring}_stack.nc`` (produced by ``oceanarray stack``) and
|
|
396
|
+
interpolates scalar variables onto a regular pressure axis between
|
|
397
|
+
``--pmin`` and ``--pmax`` dbar at ``--dp`` dbar spacing. Writes
|
|
398
|
+
``{mooring}_grid.nc`` in the proc directory.
|
|
399
|
+
|
|
400
|
+
Prerequisite: ``oceanarray stack`` must have completed successfully.
|
|
401
|
+
"""
|
|
402
|
+
from .mooring_level import MooringGridder
|
|
403
|
+
|
|
404
|
+
_, proc_root, legacy = _parse_dirs(args)
|
|
405
|
+
basedir = _resolve_basedir(args.basedir) if legacy else None
|
|
406
|
+
_status("section", f"Grid: {args.mooring}")
|
|
407
|
+
gridder = (
|
|
408
|
+
MooringGridder(basedir) if legacy else MooringGridder(proc_dir=str(proc_root))
|
|
409
|
+
)
|
|
410
|
+
ok = gridder.grid(
|
|
411
|
+
args.mooring,
|
|
412
|
+
p_start=args.pmin,
|
|
413
|
+
p_end=args.pmax,
|
|
414
|
+
dp=args.dp,
|
|
415
|
+
force=args.force,
|
|
416
|
+
)
|
|
417
|
+
return 0 if ok else 1
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def cmd_run(args: argparse.Namespace) -> int:
|
|
421
|
+
"""Run the complete processing pipeline for one mooring.
|
|
422
|
+
|
|
423
|
+
Executes in order: stage 1 (raw → CF-NetCDF), stage 2 (trim + clock
|
|
424
|
+
correction), stage 3 (QC + velocity rotation), stack (common time axis),
|
|
425
|
+
grid (pressure interpolation), and all reports (summary, per-instrument,
|
|
426
|
+
stack, grid).
|
|
427
|
+
|
|
428
|
+
All steps run regardless of earlier failures. If any step fails the command
|
|
429
|
+
returns exit code 1 at the end, but subsequent steps are not skipped. Check
|
|
430
|
+
the log files in ``processing_logs/`` to diagnose partial failures.
|
|
431
|
+
"""
|
|
432
|
+
import argparse as _ap
|
|
433
|
+
|
|
434
|
+
overall_ok = True
|
|
435
|
+
_dirs = dict(
|
|
436
|
+
basedir=getattr(args, "basedir", None),
|
|
437
|
+
raw_dir=getattr(args, "raw_dir", None),
|
|
438
|
+
proc_dir=getattr(args, "proc_dir", None),
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
process_args = _ap.Namespace(
|
|
442
|
+
mooring=args.mooring,
|
|
443
|
+
stage=[1, 2, 3],
|
|
444
|
+
force=args.force,
|
|
445
|
+
serial=args.serial,
|
|
446
|
+
report=False,
|
|
447
|
+
plot=False,
|
|
448
|
+
dry_run=False,
|
|
449
|
+
**_dirs,
|
|
450
|
+
)
|
|
451
|
+
if cmd_process(process_args) != 0:
|
|
452
|
+
overall_ok = False
|
|
453
|
+
|
|
454
|
+
stack_args = _ap.Namespace(
|
|
455
|
+
mooring=args.mooring,
|
|
456
|
+
dt=args.dt,
|
|
457
|
+
force=args.force,
|
|
458
|
+
**_dirs,
|
|
459
|
+
)
|
|
460
|
+
if cmd_stack(stack_args) != 0:
|
|
461
|
+
overall_ok = False
|
|
462
|
+
|
|
463
|
+
grid_args = _ap.Namespace(
|
|
464
|
+
mooring=args.mooring,
|
|
465
|
+
pmin=args.pmin,
|
|
466
|
+
pmax=args.pmax,
|
|
467
|
+
dp=args.dp,
|
|
468
|
+
force=args.force,
|
|
469
|
+
**_dirs,
|
|
470
|
+
)
|
|
471
|
+
if cmd_grid(grid_args) != 0:
|
|
472
|
+
overall_ok = False
|
|
473
|
+
|
|
474
|
+
report_args = _ap.Namespace(
|
|
475
|
+
mooring=args.mooring,
|
|
476
|
+
force=args.force,
|
|
477
|
+
outdir=None,
|
|
478
|
+
serial=args.serial or None,
|
|
479
|
+
instruments=True,
|
|
480
|
+
grid=True,
|
|
481
|
+
stack=True,
|
|
482
|
+
**_dirs,
|
|
483
|
+
)
|
|
484
|
+
if cmd_report(report_args) != 0:
|
|
485
|
+
overall_ok = False
|
|
486
|
+
|
|
487
|
+
return 0 if overall_ok else 1
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def cmd_validate(args: argparse.Namespace) -> int:
|
|
491
|
+
"""Validate one or more mooring YAML files."""
|
|
492
|
+
from .validation import print_validation_report
|
|
493
|
+
|
|
494
|
+
all_ok = True
|
|
495
|
+
for yaml_path in args.yaml:
|
|
496
|
+
ok = print_validation_report(yaml_path)
|
|
497
|
+
if not ok:
|
|
498
|
+
all_ok = False
|
|
499
|
+
print()
|
|
500
|
+
|
|
501
|
+
return 0 if all_ok else 1
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def cmd_animate(args: argparse.Namespace) -> int:
|
|
505
|
+
"""Write animated hodograph GIF(s) for Aquadopp current-meter instruments.
|
|
506
|
+
|
|
507
|
+
A hodograph plots the tip of the horizontal velocity vector (east vs north
|
|
508
|
+
component) over time, showing how current speed and direction evolve. The
|
|
509
|
+
animation highlights rotary motion such as tidal ellipses or eddy passages.
|
|
510
|
+
|
|
511
|
+
Requires ``east_velocity`` and ``north_velocity`` in the NetCDF file, which
|
|
512
|
+
means stage 3 (BEAM → ENU coordinate rotation) must have completed for the
|
|
513
|
+
target instrument. A low-pass filter can be applied via ``--lp-days`` to
|
|
514
|
+
separate sub-inertial (eddy/mean) flow from tidal and near-inertial signals.
|
|
515
|
+
|
|
516
|
+
Requires the Pillow package (``pip install Pillow``); will fail at import time
|
|
517
|
+
if Pillow is not installed.
|
|
518
|
+
"""
|
|
519
|
+
import matplotlib
|
|
520
|
+
|
|
521
|
+
matplotlib.use("Agg")
|
|
522
|
+
import xarray as xr
|
|
523
|
+
|
|
524
|
+
from .plotters import animate_hodograph
|
|
525
|
+
|
|
526
|
+
_, proc_root, legacy = _parse_dirs(args)
|
|
527
|
+
basedir = _resolve_basedir(args.basedir) if legacy else None
|
|
528
|
+
proc_dir = (
|
|
529
|
+
_get_proc_root(basedir) / args.mooring if legacy else proc_root / args.mooring
|
|
530
|
+
)
|
|
531
|
+
serials = args.serial or []
|
|
532
|
+
|
|
533
|
+
# Build {stem: best_nc} — stage3 preferred over stage2
|
|
534
|
+
stage3_map = {
|
|
535
|
+
f.name.replace("_stage3.nc", ""): f
|
|
536
|
+
for f in sorted(proc_dir.rglob("*_stage3.nc"))
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if serials:
|
|
540
|
+
candidates: list[tuple[str, Path]] = []
|
|
541
|
+
for sn in serials:
|
|
542
|
+
matches = [stem for stem in stage3_map if str(sn) in stem]
|
|
543
|
+
if matches:
|
|
544
|
+
candidates.extend((m, stage3_map[m]) for m in matches)
|
|
545
|
+
else:
|
|
546
|
+
stage2 = sorted(proc_dir.rglob(f"*{sn}*_stage2.nc"))
|
|
547
|
+
if stage2:
|
|
548
|
+
stem = stage2[0].name.replace("_stage2.nc", "")
|
|
549
|
+
candidates.append((stem, stage2[0]))
|
|
550
|
+
else:
|
|
551
|
+
print(
|
|
552
|
+
f" WARNING: no stage2/3 NC found for serial {sn} under {proc_dir}"
|
|
553
|
+
)
|
|
554
|
+
else:
|
|
555
|
+
# All stage3 files; fill in with stage2 where stage3 is absent
|
|
556
|
+
seen: set[str] = set(stage3_map)
|
|
557
|
+
candidates = list(stage3_map.items())
|
|
558
|
+
for nc in sorted(proc_dir.rglob("*_stage2.nc")):
|
|
559
|
+
stem = nc.name.replace("_stage2.nc", "")
|
|
560
|
+
if stem not in seen:
|
|
561
|
+
seen.add(stem)
|
|
562
|
+
candidates.append((stem, nc))
|
|
563
|
+
|
|
564
|
+
if not candidates:
|
|
565
|
+
print(f" No processed files found under {proc_dir}")
|
|
566
|
+
return 1
|
|
567
|
+
|
|
568
|
+
n_ok = 0
|
|
569
|
+
for stem, nc in candidates:
|
|
570
|
+
if args.output and len(candidates) == 1:
|
|
571
|
+
out = Path(args.output)
|
|
572
|
+
else:
|
|
573
|
+
out = nc.parent / f"{stem}_hodograph.gif"
|
|
574
|
+
|
|
575
|
+
_status("section", f"Animate: {nc.name}")
|
|
576
|
+
try:
|
|
577
|
+
import numpy as np
|
|
578
|
+
import pandas as pd
|
|
579
|
+
|
|
580
|
+
ds = xr.open_dataset(nc, decode_timedelta=False)
|
|
581
|
+
t = ds["time"].values
|
|
582
|
+
t0_str = pd.Timestamp(t[0]).strftime("%Y-%m-%d %H:%M")
|
|
583
|
+
t1_str = pd.Timestamp(t[-1]).strftime("%Y-%m-%d %H:%M")
|
|
584
|
+
n_days = float((t[-1] - t[0]) / np.timedelta64(1, "D"))
|
|
585
|
+
if len(t) > 1:
|
|
586
|
+
dt_s = float(np.median(np.diff(t) / np.timedelta64(1, "s")))
|
|
587
|
+
step_n = max(1, int(round(args.frame_hours * 3600.0 / dt_s)))
|
|
588
|
+
n_exp = int(np.ceil(len(t) / step_n))
|
|
589
|
+
else:
|
|
590
|
+
dt_s = float("nan")
|
|
591
|
+
n_exp = 1
|
|
592
|
+
print(
|
|
593
|
+
f" Data: {t0_str} → {t1_str} ({n_days:.1f} days, "
|
|
594
|
+
f"{len(t)} records, dt={dt_s:.0f} s)"
|
|
595
|
+
)
|
|
596
|
+
print(f" Frames: ~{n_exp} ({args.frame_hours:.0f}-h steps)")
|
|
597
|
+
|
|
598
|
+
result = animate_hodograph(
|
|
599
|
+
ds,
|
|
600
|
+
out,
|
|
601
|
+
u_var=args.u_var,
|
|
602
|
+
v_var=args.v_var,
|
|
603
|
+
lp_days=args.lp_days,
|
|
604
|
+
smooth_hours=args.smooth_hours,
|
|
605
|
+
frame_hours=args.frame_hours,
|
|
606
|
+
fps=args.fps,
|
|
607
|
+
dpi=args.dpi,
|
|
608
|
+
)
|
|
609
|
+
ds.close()
|
|
610
|
+
except Exception as exc: # noqa: BLE001 — I/O boundary; report and continue
|
|
611
|
+
print(f" ERROR: {exc}")
|
|
612
|
+
continue
|
|
613
|
+
|
|
614
|
+
if result is None:
|
|
615
|
+
print(
|
|
616
|
+
f" Skipped: {nc.name} — no {args.u_var}/{args.v_var} variables "
|
|
617
|
+
"or pillow writer unavailable"
|
|
618
|
+
)
|
|
619
|
+
else:
|
|
620
|
+
print(f" Saved: {result}")
|
|
621
|
+
n_ok += 1
|
|
622
|
+
|
|
623
|
+
return 0 if n_ok > 0 else 1
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def cmd_list(args: argparse.Namespace) -> int:
|
|
627
|
+
"""Print allowed instrument types and file_type values for mooring YAML files."""
|
|
628
|
+
from . import parameters as P
|
|
629
|
+
|
|
630
|
+
topic = getattr(args, "topic", None)
|
|
631
|
+
|
|
632
|
+
instr_col = max(len(k) for k in P.INSTRUMENT_FILE_TYPES) + 2
|
|
633
|
+
file_col = max(len(", ".join(v)) for v in P.INSTRUMENT_FILE_TYPES.values()) + 2
|
|
634
|
+
|
|
635
|
+
if topic in (None, "instruments", "file-types"):
|
|
636
|
+
print()
|
|
637
|
+
print(" Mooring YAML — allowed values for instrument: and file_type:")
|
|
638
|
+
print()
|
|
639
|
+
print(f" {'instrument':<{instr_col}} {'file_type (seasenselib reader)'}")
|
|
640
|
+
print(f" {'-' * instr_col} {'-' * file_col}")
|
|
641
|
+
for name in sorted(P.INSTRUMENT_FILE_TYPES):
|
|
642
|
+
readers = ", ".join(P.INSTRUMENT_FILE_TYPES[name])
|
|
643
|
+
print(f" {name:<{instr_col}} {readers}")
|
|
644
|
+
|
|
645
|
+
if P.EXTRA_FILE_TYPES:
|
|
646
|
+
print()
|
|
647
|
+
print(" Additional file_type values (specialist / deprecated):")
|
|
648
|
+
print(f" {'-' * instr_col} {'-' * file_col}")
|
|
649
|
+
for ft, note in sorted(P.EXTRA_FILE_TYPES.items()):
|
|
650
|
+
print(f" {ft:<{instr_col}} {note}")
|
|
651
|
+
|
|
652
|
+
print()
|
|
653
|
+
print(" Mooring YAML fields for each instrument entry:")
|
|
654
|
+
print(" instrument: name from the table above; stored as instrument_type")
|
|
655
|
+
print(" in the NetCDF output. Drives Aquadopp-specific plots,")
|
|
656
|
+
print(" declination correction, and tilt QC.")
|
|
657
|
+
print(" file_type: selects the seasenselib reader for stage 1.")
|
|
658
|
+
print(" header: path to the Nortek .hdr file (aquadopp only); required")
|
|
659
|
+
print(" for beam→XYZ coordinate transformation.")
|
|
660
|
+
print()
|
|
661
|
+
|
|
662
|
+
return 0
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def cmd_logsheet(args: "argparse.Namespace") -> int:
|
|
666
|
+
"""Generate fieldwork logsheet PDFs for a mooring or cal-dip cast.
|
|
667
|
+
|
|
668
|
+
Reads ``cruise_config.yaml`` and ``instruments.yaml`` from ``--config-dir``,
|
|
669
|
+
or supply paths directly via ``--logsheet-config`` and ``--inventory``.
|
|
670
|
+
Mooring YAMLs are located via ``--proc-dir`` or ``mooring_yaml_map`` in
|
|
671
|
+
``cruise_config.yaml``.
|
|
672
|
+
"""
|
|
673
|
+
import sys
|
|
674
|
+
from pathlib import Path
|
|
675
|
+
from .logsheet import (
|
|
676
|
+
build_caldip_setup,
|
|
677
|
+
build_caldip_download,
|
|
678
|
+
build_recovery,
|
|
679
|
+
build_mooring_download,
|
|
680
|
+
build_deployment_setup,
|
|
681
|
+
)
|
|
682
|
+
from .logsheet._config import resolve_config, load_yaml
|
|
683
|
+
|
|
684
|
+
config_dir = Path(args.logsheet_config_dir) if args.logsheet_config_dir else None
|
|
685
|
+
inventory_path = Path(args.logsheet_inventory) if args.logsheet_inventory else None
|
|
686
|
+
logsheet_config_path = Path(args.logsheet_config) if args.logsheet_config else None
|
|
687
|
+
output_dir = Path(args.logsheet_output_dir)
|
|
688
|
+
# proc_dir is optional for logsheets — caldip sheets don't need it
|
|
689
|
+
_pd = getattr(args, "proc_dir", None)
|
|
690
|
+
proc_root = Path(_pd) if _pd else None
|
|
691
|
+
|
|
692
|
+
cfg = resolve_config(
|
|
693
|
+
config_dir=config_dir,
|
|
694
|
+
inventory_path=inventory_path,
|
|
695
|
+
logsheet_config_path=logsheet_config_path,
|
|
696
|
+
output_dir=output_dir,
|
|
697
|
+
proc_dir=proc_root,
|
|
698
|
+
)
|
|
699
|
+
fmt = args.logsheet_format
|
|
700
|
+
|
|
701
|
+
if args.logsheet_all:
|
|
702
|
+
if not cfg.cruise_config_path.exists():
|
|
703
|
+
print(
|
|
704
|
+
"Error: --all requires cruise_config.yaml; "
|
|
705
|
+
"provide --logsheet-config or --config-dir",
|
|
706
|
+
file=sys.stderr,
|
|
707
|
+
)
|
|
708
|
+
return 1
|
|
709
|
+
cruise_cfg = load_yaml(cfg.cruise_config_path)
|
|
710
|
+
for cast_id in cruise_cfg.get("casts", {}):
|
|
711
|
+
_status("section", f"caldip-setup {cast_id}")
|
|
712
|
+
build_caldip_setup(cast_id, cfg, fmt=fmt)
|
|
713
|
+
_status("section", f"caldip-download {cast_id}")
|
|
714
|
+
build_caldip_download(cast_id, cfg, fmt=fmt)
|
|
715
|
+
for mooring in cruise_cfg.get("moorings_to_recover", []):
|
|
716
|
+
_status("section", f"mooring-download {mooring}")
|
|
717
|
+
build_mooring_download(mooring, cfg, fmt=fmt)
|
|
718
|
+
_status("section", f"mooring-recovery {mooring}")
|
|
719
|
+
build_recovery(mooring, cfg, fmt=fmt)
|
|
720
|
+
for mooring in cruise_cfg.get("moorings_to_deploy", []):
|
|
721
|
+
_status("section", f"mooring-setup {mooring}")
|
|
722
|
+
build_deployment_setup(mooring, cfg, fmt=fmt)
|
|
723
|
+
return 0
|
|
724
|
+
|
|
725
|
+
ltypes: list[str] = args.logsheet_type or []
|
|
726
|
+
if not ltypes:
|
|
727
|
+
print("Error: --type is required (or use --all)", file=sys.stderr)
|
|
728
|
+
return 1
|
|
729
|
+
|
|
730
|
+
# Resolve caldip YAML once for all caldip sheet types in this call.
|
|
731
|
+
caldip_yaml = None
|
|
732
|
+
cast_val = args.logsheet_cast or ""
|
|
733
|
+
if any(t in ("caldip-setup", "caldip-download") for t in ltypes):
|
|
734
|
+
if not cast_val:
|
|
735
|
+
print("Error: --cast is required for caldip sheet types", file=sys.stderr)
|
|
736
|
+
return 1
|
|
737
|
+
cast_path = Path(cast_val)
|
|
738
|
+
if cast_path.suffix.lower() == ".yaml":
|
|
739
|
+
if not cast_path.exists():
|
|
740
|
+
print(f"Error: caldip YAML not found: {cast_val}", file=sys.stderr)
|
|
741
|
+
return 1
|
|
742
|
+
caldip_yaml = load_yaml(cast_path)
|
|
743
|
+
|
|
744
|
+
for ltype in ltypes:
|
|
745
|
+
if ltype in ("caldip-setup", "caldip-download"):
|
|
746
|
+
if ltype == "caldip-setup":
|
|
747
|
+
build_caldip_setup(cast_val, cfg, fmt=fmt, caldip_yaml=caldip_yaml)
|
|
748
|
+
else:
|
|
749
|
+
build_caldip_download(cast_val, cfg, fmt=fmt, caldip_yaml=caldip_yaml)
|
|
750
|
+
|
|
751
|
+
elif ltype in ("mooring-download", "mooring-recovery", "mooring-setup"):
|
|
752
|
+
if not args.logsheet_mooring:
|
|
753
|
+
print(
|
|
754
|
+
f"Error: --mooring is required for --type {ltype}",
|
|
755
|
+
file=sys.stderr,
|
|
756
|
+
)
|
|
757
|
+
return 1
|
|
758
|
+
if ltype == "mooring-download":
|
|
759
|
+
build_mooring_download(args.logsheet_mooring, cfg, fmt=fmt)
|
|
760
|
+
elif ltype == "mooring-recovery":
|
|
761
|
+
build_recovery(args.logsheet_mooring, cfg, fmt=fmt)
|
|
762
|
+
else:
|
|
763
|
+
build_deployment_setup(args.logsheet_mooring, cfg, fmt=fmt)
|
|
764
|
+
|
|
765
|
+
return 0
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def _add_dir_args(p: "argparse.ArgumentParser", raw_needed: bool = True) -> None:
|
|
769
|
+
"""Add directory arguments to a subparser.
|
|
770
|
+
|
|
771
|
+
Adds ``--raw-dir`` / ``--proc-dir`` (new layout) and the deprecated
|
|
772
|
+
``--basedir`` flag. When *raw_needed* is False (e.g. for ``stack``,
|
|
773
|
+
``grid``, ``report``, ``animate`` which operate on already-processed NetCDF
|
|
774
|
+
files) ``--raw-dir`` is silently omitted and the argument defaults to None.
|
|
775
|
+
Stage 1 processing requires raw instrument files and therefore sets
|
|
776
|
+
*raw_needed=True*.
|
|
777
|
+
|
|
778
|
+
Parameters
|
|
779
|
+
----------
|
|
780
|
+
p : argparse.ArgumentParser
|
|
781
|
+
The subparser to add arguments to.
|
|
782
|
+
raw_needed : bool
|
|
783
|
+
Whether to add ``--raw-dir``. Pass False for commands that do not
|
|
784
|
+
read raw instrument data files (stack, grid, report, animate).
|
|
785
|
+
|
|
786
|
+
"""
|
|
787
|
+
grp = p.add_mutually_exclusive_group()
|
|
788
|
+
grp.add_argument(
|
|
789
|
+
"--basedir",
|
|
790
|
+
default=None,
|
|
791
|
+
metavar="DIR",
|
|
792
|
+
help="[DEPRECATED] Root data directory (contains proc/). "
|
|
793
|
+
"Use --raw-dir + --proc-dir instead.",
|
|
794
|
+
)
|
|
795
|
+
if raw_needed:
|
|
796
|
+
p.add_argument(
|
|
797
|
+
"--raw-dir",
|
|
798
|
+
default=None,
|
|
799
|
+
dest="raw_dir",
|
|
800
|
+
metavar="DIR",
|
|
801
|
+
help="Cruise-level raw data directory (pipeline appends /{mooring}/). "
|
|
802
|
+
"Required for stage 1.",
|
|
803
|
+
)
|
|
804
|
+
else:
|
|
805
|
+
p.set_defaults(raw_dir=None)
|
|
806
|
+
p.add_argument(
|
|
807
|
+
"--proc-dir",
|
|
808
|
+
default=None,
|
|
809
|
+
dest="proc_dir",
|
|
810
|
+
metavar="DIR",
|
|
811
|
+
help="Cruise-level processed output directory (pipeline appends /{mooring}/).",
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
816
|
+
"""Build and return the top-level argument parser for the oceanarray CLI."""
|
|
817
|
+
epilog = (
|
|
818
|
+
"Typical per-mooring workflow (new canonical layout):\n"
|
|
819
|
+
" oceanarray process MOORING --raw-dir /data/raw --proc-dir /data/proc --stage 1 2 3\n"
|
|
820
|
+
" oceanarray stack MOORING --proc-dir /data/proc\n"
|
|
821
|
+
" oceanarray grid MOORING --proc-dir /data/proc --dp 20\n"
|
|
822
|
+
" oceanarray report MOORING --raw-dir /data/raw --proc-dir /data/proc --instruments --stack --grid\n"
|
|
823
|
+
"\n"
|
|
824
|
+
"Or run all steps in one command:\n"
|
|
825
|
+
" oceanarray run MOORING --raw-dir /data/raw --proc-dir /data/proc --dp 20 --force\n"
|
|
826
|
+
"\n"
|
|
827
|
+
"Legacy (deprecated):\n"
|
|
828
|
+
" oceanarray run MOORING --basedir /data --dp 20 --force\n"
|
|
829
|
+
" See MIGRATION-BASEDIR.md for migration instructions.\n"
|
|
830
|
+
)
|
|
831
|
+
parser = argparse.ArgumentParser(
|
|
832
|
+
prog="oceanarray",
|
|
833
|
+
description="Oceanographic mooring data processing.",
|
|
834
|
+
epilog=epilog,
|
|
835
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
836
|
+
)
|
|
837
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
838
|
+
|
|
839
|
+
p_process = sub.add_parser(
|
|
840
|
+
"process",
|
|
841
|
+
help="Run per-instrument processing stages 1-3 (raw→NetCDF, trim+clock, QC+ENU).",
|
|
842
|
+
description=(
|
|
843
|
+
"Run one or more per-instrument processing stages for a mooring.\n"
|
|
844
|
+
" Stage 1: read raw files → CF-NetCDF (_stage1.nc)\n"
|
|
845
|
+
" Stage 2: trim to deployment window + apply clock-drift correction (_stage2.nc)\n"
|
|
846
|
+
" Stage 3: QC flags, BEAM→ENU rotation, magnetic declination, derived vars (_stage3.nc)\n"
|
|
847
|
+
"Default when --stage is omitted: stages 1 and 2 only."
|
|
848
|
+
),
|
|
849
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
850
|
+
)
|
|
851
|
+
p_process.add_argument("mooring", help="Mooring name, e.g. dsG3_1_2026")
|
|
852
|
+
_add_dir_args(p_process, raw_needed=True)
|
|
853
|
+
p_process.add_argument(
|
|
854
|
+
"--stage",
|
|
855
|
+
type=int,
|
|
856
|
+
nargs="+",
|
|
857
|
+
choices=[1, 2, 3],
|
|
858
|
+
default=None,
|
|
859
|
+
metavar="{1,2,3}",
|
|
860
|
+
help="Stage(s) to run (default: 1 2). "
|
|
861
|
+
"Stage 3 applies QC, coordinate rotation, and derived variables.",
|
|
862
|
+
)
|
|
863
|
+
p_process.add_argument(
|
|
864
|
+
"--plot",
|
|
865
|
+
action="store_true",
|
|
866
|
+
help="Generate plots of processed microcat data after stage 2",
|
|
867
|
+
)
|
|
868
|
+
p_process.add_argument(
|
|
869
|
+
"--report",
|
|
870
|
+
action="store_true",
|
|
871
|
+
help="Print a summary of processed records per instrument and serial number",
|
|
872
|
+
)
|
|
873
|
+
p_process.add_argument(
|
|
874
|
+
"--serial",
|
|
875
|
+
nargs="+",
|
|
876
|
+
metavar="SERIAL",
|
|
877
|
+
default=[],
|
|
878
|
+
help="Process only instrument(s) with these serial number(s), e.g. --serial 14321 400118",
|
|
879
|
+
)
|
|
880
|
+
p_process.add_argument(
|
|
881
|
+
"--force",
|
|
882
|
+
action="store_true",
|
|
883
|
+
help="Overwrite existing output files (both stages skip existing files by default)",
|
|
884
|
+
)
|
|
885
|
+
p_process.add_argument(
|
|
886
|
+
"-n",
|
|
887
|
+
"--dry-run",
|
|
888
|
+
action="store_true",
|
|
889
|
+
dest="dry_run",
|
|
890
|
+
help="Do not actually modify any files; show what would be done (stage 3 only)",
|
|
891
|
+
)
|
|
892
|
+
p_process.set_defaults(func=cmd_process)
|
|
893
|
+
|
|
894
|
+
p_report = sub.add_parser(
|
|
895
|
+
"report",
|
|
896
|
+
help="Generate a mooring recovery HTML report.",
|
|
897
|
+
)
|
|
898
|
+
p_report.add_argument("mooring", help="Mooring name, e.g. dsG3_1_2026")
|
|
899
|
+
_add_dir_args(p_report, raw_needed=True)
|
|
900
|
+
p_report.add_argument(
|
|
901
|
+
"-o",
|
|
902
|
+
"--output-dir",
|
|
903
|
+
default=None,
|
|
904
|
+
dest="outdir",
|
|
905
|
+
metavar="DIR",
|
|
906
|
+
help="Directory for the HTML report (default: proc/{mooring}/ inside basedir)",
|
|
907
|
+
)
|
|
908
|
+
p_report.add_argument(
|
|
909
|
+
"--force", action="store_true", help="Overwrite existing report"
|
|
910
|
+
)
|
|
911
|
+
p_report.add_argument(
|
|
912
|
+
"--instruments",
|
|
913
|
+
"-i",
|
|
914
|
+
action="store_true",
|
|
915
|
+
default=False,
|
|
916
|
+
help="Also generate per-instrument HTML pages (slow; skipped by default)",
|
|
917
|
+
)
|
|
918
|
+
p_report.add_argument(
|
|
919
|
+
"--grid",
|
|
920
|
+
"-g",
|
|
921
|
+
action="store_true",
|
|
922
|
+
default=False,
|
|
923
|
+
help="Also generate the gridded-data report page (requires _grid.nc)",
|
|
924
|
+
)
|
|
925
|
+
p_report.add_argument(
|
|
926
|
+
"--stack",
|
|
927
|
+
"-s",
|
|
928
|
+
action="store_true",
|
|
929
|
+
default=False,
|
|
930
|
+
help="Also generate the stacked-data report page with pressure and T time series (requires _stack.nc)",
|
|
931
|
+
)
|
|
932
|
+
p_report.add_argument(
|
|
933
|
+
"--serial",
|
|
934
|
+
nargs="+",
|
|
935
|
+
metavar="SN",
|
|
936
|
+
default=None,
|
|
937
|
+
help="Generate per-instrument page(s) for these serial number(s) only "
|
|
938
|
+
"(implies --instruments for the listed serials)",
|
|
939
|
+
)
|
|
940
|
+
p_report.add_argument(
|
|
941
|
+
"--all",
|
|
942
|
+
"-A",
|
|
943
|
+
action="store_true",
|
|
944
|
+
default=False,
|
|
945
|
+
dest="all_reports",
|
|
946
|
+
help="Generate all report pages: equivalent to --stack --grid --instruments",
|
|
947
|
+
)
|
|
948
|
+
p_report.add_argument(
|
|
949
|
+
"--array",
|
|
950
|
+
action="store_true",
|
|
951
|
+
default=False,
|
|
952
|
+
help="Treat the positional argument as a *.array.yaml path and generate an "
|
|
953
|
+
"array-level HTML index linking all mooring reports.",
|
|
954
|
+
)
|
|
955
|
+
p_report.set_defaults(func=cmd_report)
|
|
956
|
+
|
|
957
|
+
p_stack = sub.add_parser(
|
|
958
|
+
"stack",
|
|
959
|
+
help="Mooring-level step 1: interpolate all instruments onto a common time axis → _stack.nc",
|
|
960
|
+
description=(
|
|
961
|
+
"Reads all _stage3.nc (or _stage2.nc) files for a mooring and interpolates\n"
|
|
962
|
+
"every instrument onto a uniform time grid, producing {mooring}_stack.nc.\n"
|
|
963
|
+
"Run after 'oceanarray process --stage 1 2 3'."
|
|
964
|
+
),
|
|
965
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
966
|
+
)
|
|
967
|
+
p_stack.add_argument("mooring", help="Mooring name, e.g. dsG3_1_2026")
|
|
968
|
+
_add_dir_args(p_stack, raw_needed=False)
|
|
969
|
+
p_stack.add_argument(
|
|
970
|
+
"--dt",
|
|
971
|
+
type=int,
|
|
972
|
+
default=60,
|
|
973
|
+
metavar="SECONDS",
|
|
974
|
+
help="Common time-grid interval in seconds (default: 60)",
|
|
975
|
+
)
|
|
976
|
+
p_stack.add_argument(
|
|
977
|
+
"--force", action="store_true", help="Overwrite existing output file"
|
|
978
|
+
)
|
|
979
|
+
p_stack.set_defaults(func=cmd_stack)
|
|
980
|
+
|
|
981
|
+
p_grid = sub.add_parser(
|
|
982
|
+
"grid",
|
|
983
|
+
help="Mooring-level step 2: vertically interpolate stack onto a pressure grid → _grid.nc",
|
|
984
|
+
description=(
|
|
985
|
+
"Reads {mooring}_stack.nc and interpolates temperature, salinity, and other\n"
|
|
986
|
+
"scalar fields onto a uniform pressure grid, producing {mooring}_grid.nc.\n"
|
|
987
|
+
"Run after 'oceanarray stack'.\n\n"
|
|
988
|
+
"Example:\n"
|
|
989
|
+
" oceanarray grid MOORING --proc-dir /data/proc --pmin 100 --pmax 2000 --dp 20"
|
|
990
|
+
),
|
|
991
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
992
|
+
)
|
|
993
|
+
p_grid.add_argument("mooring", help="Mooring name, e.g. dsG3_1_2026")
|
|
994
|
+
_add_dir_args(p_grid, raw_needed=False)
|
|
995
|
+
p_grid.add_argument(
|
|
996
|
+
"--pmin",
|
|
997
|
+
type=float,
|
|
998
|
+
default=200.0,
|
|
999
|
+
metavar="DBAR",
|
|
1000
|
+
help="Shallowest pressure level (default: 200)",
|
|
1001
|
+
)
|
|
1002
|
+
p_grid.add_argument(
|
|
1003
|
+
"--pmax",
|
|
1004
|
+
type=float,
|
|
1005
|
+
default=1000.0,
|
|
1006
|
+
metavar="DBAR",
|
|
1007
|
+
help="Deepest pressure level (default: 1000)",
|
|
1008
|
+
)
|
|
1009
|
+
p_grid.add_argument(
|
|
1010
|
+
"--dp",
|
|
1011
|
+
type=float,
|
|
1012
|
+
default=20.0,
|
|
1013
|
+
metavar="DBAR",
|
|
1014
|
+
help="Pressure grid spacing in dbar (default: 20)",
|
|
1015
|
+
)
|
|
1016
|
+
p_grid.add_argument(
|
|
1017
|
+
"--force", action="store_true", help="Overwrite existing output file"
|
|
1018
|
+
)
|
|
1019
|
+
p_grid.set_defaults(func=cmd_grid)
|
|
1020
|
+
|
|
1021
|
+
p_run = sub.add_parser(
|
|
1022
|
+
"run",
|
|
1023
|
+
help="Full pipeline: stages 1-3, stack, grid, and all reports in one command.",
|
|
1024
|
+
)
|
|
1025
|
+
p_run.add_argument("mooring", help="Mooring name, e.g. dsG3_1_2026")
|
|
1026
|
+
_add_dir_args(p_run, raw_needed=True)
|
|
1027
|
+
p_run.add_argument(
|
|
1028
|
+
"--force", action="store_true", help="Overwrite existing files at every stage"
|
|
1029
|
+
)
|
|
1030
|
+
p_run.add_argument(
|
|
1031
|
+
"--serial",
|
|
1032
|
+
nargs="+",
|
|
1033
|
+
metavar="SN",
|
|
1034
|
+
default=[],
|
|
1035
|
+
help="Restrict to these serial number(s) for processing and per-instrument reports",
|
|
1036
|
+
)
|
|
1037
|
+
p_run.add_argument(
|
|
1038
|
+
"--dt",
|
|
1039
|
+
type=int,
|
|
1040
|
+
default=60,
|
|
1041
|
+
metavar="SECONDS",
|
|
1042
|
+
help="Stack time-grid interval in seconds (default: 60)",
|
|
1043
|
+
)
|
|
1044
|
+
p_run.add_argument(
|
|
1045
|
+
"--dp",
|
|
1046
|
+
type=float,
|
|
1047
|
+
default=20.0,
|
|
1048
|
+
metavar="DBAR",
|
|
1049
|
+
help="Pressure grid spacing in dbar (default: 20)",
|
|
1050
|
+
)
|
|
1051
|
+
p_run.add_argument(
|
|
1052
|
+
"--pmin",
|
|
1053
|
+
type=float,
|
|
1054
|
+
default=200.0,
|
|
1055
|
+
metavar="DBAR",
|
|
1056
|
+
help="Shallowest pressure level for grid (default: 200)",
|
|
1057
|
+
)
|
|
1058
|
+
p_run.add_argument(
|
|
1059
|
+
"--pmax",
|
|
1060
|
+
type=float,
|
|
1061
|
+
default=1000.0,
|
|
1062
|
+
metavar="DBAR",
|
|
1063
|
+
help="Deepest pressure level for grid (default: 1000)",
|
|
1064
|
+
)
|
|
1065
|
+
p_run.set_defaults(func=cmd_run)
|
|
1066
|
+
|
|
1067
|
+
p_validate = sub.add_parser(
|
|
1068
|
+
"validate",
|
|
1069
|
+
help="Validate mooring YAML configuration file(s).",
|
|
1070
|
+
description=(
|
|
1071
|
+
"Parse and validate one or more .mooring.yaml files.\n"
|
|
1072
|
+
"Checks instrument types, serial numbers, file_type values, HAB fields,\n"
|
|
1073
|
+
"deployment dates, and inline instrument conventions.\n"
|
|
1074
|
+
"Exits 0 if all files are valid, 1 if any file has errors or warnings."
|
|
1075
|
+
),
|
|
1076
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1077
|
+
)
|
|
1078
|
+
p_validate.add_argument("yaml", nargs="+", help="Path(s) to .mooring.yaml file(s)")
|
|
1079
|
+
p_validate.set_defaults(func=cmd_validate)
|
|
1080
|
+
|
|
1081
|
+
p_plot = sub.add_parser("plot", help="Plot multi-instrument mooring overview.")
|
|
1082
|
+
p_plot.add_argument("mooring", help="Mooring name, e.g. dsG3_1_2026")
|
|
1083
|
+
_add_dir_args(p_plot, raw_needed=False)
|
|
1084
|
+
p_plot.add_argument(
|
|
1085
|
+
"--var_y",
|
|
1086
|
+
default="temperature",
|
|
1087
|
+
help="Variable on y-axis (default: temperature)",
|
|
1088
|
+
)
|
|
1089
|
+
p_plot.add_argument(
|
|
1090
|
+
"--var_color",
|
|
1091
|
+
default=None,
|
|
1092
|
+
help="Variable for scatter colour; omit for line plot",
|
|
1093
|
+
)
|
|
1094
|
+
p_plot.add_argument(
|
|
1095
|
+
"--colormap",
|
|
1096
|
+
default=None,
|
|
1097
|
+
help=f"Matplotlib colormap (default: {__import__('oceanarray.parameters', fromlist=['DEFAULT_COLORMAP']).DEFAULT_COLORMAP})",
|
|
1098
|
+
)
|
|
1099
|
+
p_plot.add_argument(
|
|
1100
|
+
"--markersize",
|
|
1101
|
+
type=float,
|
|
1102
|
+
default=None,
|
|
1103
|
+
metavar="PTS2",
|
|
1104
|
+
help="Scatter marker size in points² (default 4). Only used in scatter mode.",
|
|
1105
|
+
)
|
|
1106
|
+
p_plot.add_argument(
|
|
1107
|
+
"--downsample",
|
|
1108
|
+
type=int,
|
|
1109
|
+
default=None,
|
|
1110
|
+
metavar="SECONDS",
|
|
1111
|
+
help="Resample interval in seconds (default: 120)",
|
|
1112
|
+
)
|
|
1113
|
+
p_plot.add_argument(
|
|
1114
|
+
"--output",
|
|
1115
|
+
default=None,
|
|
1116
|
+
metavar="FILENAME",
|
|
1117
|
+
help="Base filename for saved figure (e.g. overview.png); "
|
|
1118
|
+
"combined with --output-dir if given",
|
|
1119
|
+
)
|
|
1120
|
+
p_plot.add_argument(
|
|
1121
|
+
"-o",
|
|
1122
|
+
"--output-dir",
|
|
1123
|
+
default=None,
|
|
1124
|
+
metavar="DIR",
|
|
1125
|
+
help="Directory for saved figure (default: mooring proc dir)",
|
|
1126
|
+
)
|
|
1127
|
+
p_plot.add_argument(
|
|
1128
|
+
"--show",
|
|
1129
|
+
action="store_true",
|
|
1130
|
+
help="Display the figure interactively (works alongside --output)",
|
|
1131
|
+
)
|
|
1132
|
+
p_plot.set_defaults(func=cmd_plot)
|
|
1133
|
+
|
|
1134
|
+
p_animate = sub.add_parser(
|
|
1135
|
+
"animate",
|
|
1136
|
+
help="Write an animated hodograph GIF for Aquadopp instrument(s).",
|
|
1137
|
+
)
|
|
1138
|
+
p_animate.add_argument("mooring", help="Mooring name, e.g. dsG2_1_2026")
|
|
1139
|
+
_add_dir_args(p_animate, raw_needed=False)
|
|
1140
|
+
p_animate.add_argument(
|
|
1141
|
+
"--serial",
|
|
1142
|
+
nargs="+",
|
|
1143
|
+
metavar="SN",
|
|
1144
|
+
default=[],
|
|
1145
|
+
help="Serial number(s) to animate (default: all instruments with velocity data)",
|
|
1146
|
+
)
|
|
1147
|
+
p_animate.add_argument(
|
|
1148
|
+
"-o",
|
|
1149
|
+
"--output",
|
|
1150
|
+
default=None,
|
|
1151
|
+
metavar="FILE",
|
|
1152
|
+
help="Output GIF path (only used when a single --serial is given; "
|
|
1153
|
+
"default: {stem}_hodograph.gif next to the NC file)",
|
|
1154
|
+
)
|
|
1155
|
+
p_animate.add_argument(
|
|
1156
|
+
"--u-var",
|
|
1157
|
+
default="east_velocity",
|
|
1158
|
+
dest="u_var",
|
|
1159
|
+
metavar="VAR",
|
|
1160
|
+
help="Eastward velocity variable name (default: east_velocity)",
|
|
1161
|
+
)
|
|
1162
|
+
p_animate.add_argument(
|
|
1163
|
+
"--v-var",
|
|
1164
|
+
default="north_velocity",
|
|
1165
|
+
dest="v_var",
|
|
1166
|
+
metavar="VAR",
|
|
1167
|
+
help="Northward velocity variable name (default: north_velocity)",
|
|
1168
|
+
)
|
|
1169
|
+
p_animate.add_argument(
|
|
1170
|
+
"--lp-days",
|
|
1171
|
+
type=float,
|
|
1172
|
+
default=4.0,
|
|
1173
|
+
dest="lp_days",
|
|
1174
|
+
metavar="DAYS",
|
|
1175
|
+
help="Low-pass window for eddy-component removal in days (default: 4.0)",
|
|
1176
|
+
)
|
|
1177
|
+
p_animate.add_argument(
|
|
1178
|
+
"--smooth-hours",
|
|
1179
|
+
type=float,
|
|
1180
|
+
default=3.0,
|
|
1181
|
+
dest="smooth_hours",
|
|
1182
|
+
metavar="HOURS",
|
|
1183
|
+
help="Tukey smoothing window in hours applied to both panels (default: 3.0)",
|
|
1184
|
+
)
|
|
1185
|
+
p_animate.add_argument(
|
|
1186
|
+
"--frame-hours",
|
|
1187
|
+
type=float,
|
|
1188
|
+
default=6.0,
|
|
1189
|
+
dest="frame_hours",
|
|
1190
|
+
metavar="HOURS",
|
|
1191
|
+
help="Time step between frames in hours (default: 6.0 → one frame per "
|
|
1192
|
+
"quarter-day of deployment)",
|
|
1193
|
+
)
|
|
1194
|
+
p_animate.add_argument(
|
|
1195
|
+
"--fps",
|
|
1196
|
+
type=int,
|
|
1197
|
+
default=20,
|
|
1198
|
+
metavar="FPS",
|
|
1199
|
+
help="Frames per second in the output GIF (default: 20)",
|
|
1200
|
+
)
|
|
1201
|
+
p_animate.add_argument(
|
|
1202
|
+
"--dpi",
|
|
1203
|
+
type=int,
|
|
1204
|
+
default=100,
|
|
1205
|
+
metavar="DPI",
|
|
1206
|
+
help="Resolution of each frame in dots per inch (default: 100)",
|
|
1207
|
+
)
|
|
1208
|
+
p_animate.set_defaults(func=cmd_animate)
|
|
1209
|
+
|
|
1210
|
+
p_list = sub.add_parser(
|
|
1211
|
+
"list",
|
|
1212
|
+
help="List allowed instrument types and file_type values for mooring YAML files.",
|
|
1213
|
+
)
|
|
1214
|
+
p_list.add_argument(
|
|
1215
|
+
"topic",
|
|
1216
|
+
nargs="?",
|
|
1217
|
+
choices=["instruments", "file-types"],
|
|
1218
|
+
default=None,
|
|
1219
|
+
help="Filter output: 'instruments' or 'file-types' (default: show both).",
|
|
1220
|
+
)
|
|
1221
|
+
p_list.set_defaults(func=cmd_list)
|
|
1222
|
+
|
|
1223
|
+
p_logsheet = sub.add_parser(
|
|
1224
|
+
"logsheet",
|
|
1225
|
+
help="Generate fieldwork logsheet PDFs (recovery, download, deployment, cal-dip).",
|
|
1226
|
+
description=(
|
|
1227
|
+
"Generate PDF or LaTeX logsheets for mooring fieldwork operations. "
|
|
1228
|
+
"Supply --config-dir (pointing to a directory with cruise_config.yaml "
|
|
1229
|
+
"and instruments.yaml), or pass --logsheet-config and --inventory directly. "
|
|
1230
|
+
"LaTeX templates and column definitions are bundled with the package."
|
|
1231
|
+
),
|
|
1232
|
+
)
|
|
1233
|
+
p_logsheet.add_argument(
|
|
1234
|
+
"--type",
|
|
1235
|
+
dest="logsheet_type",
|
|
1236
|
+
metavar="TYPE",
|
|
1237
|
+
nargs="+",
|
|
1238
|
+
choices=[
|
|
1239
|
+
"caldip-setup",
|
|
1240
|
+
"caldip-download",
|
|
1241
|
+
"mooring-download",
|
|
1242
|
+
"mooring-recovery",
|
|
1243
|
+
"mooring-setup",
|
|
1244
|
+
],
|
|
1245
|
+
help=(
|
|
1246
|
+
"Sheet type(s): caldip-setup, caldip-download, mooring-download, "
|
|
1247
|
+
"mooring-recovery, mooring-setup. Pass multiple values to generate "
|
|
1248
|
+
"several sheets in one call, e.g. --type caldip-setup caldip-download."
|
|
1249
|
+
),
|
|
1250
|
+
)
|
|
1251
|
+
p_logsheet.add_argument(
|
|
1252
|
+
"--mooring",
|
|
1253
|
+
dest="logsheet_mooring",
|
|
1254
|
+
metavar="MOORING",
|
|
1255
|
+
help="Mooring name, e.g. dsG3_1_2026 (required for mooring sheet types).",
|
|
1256
|
+
)
|
|
1257
|
+
p_logsheet.add_argument(
|
|
1258
|
+
"--cast",
|
|
1259
|
+
dest="logsheet_cast",
|
|
1260
|
+
metavar="CAST",
|
|
1261
|
+
help="Cast ID, e.g. B1 (required for caldip sheet types).",
|
|
1262
|
+
)
|
|
1263
|
+
p_logsheet.add_argument(
|
|
1264
|
+
"--config-dir",
|
|
1265
|
+
dest="logsheet_config_dir",
|
|
1266
|
+
metavar="DIR",
|
|
1267
|
+
help=(
|
|
1268
|
+
"Directory containing cruise_config.yaml and instruments.yaml. "
|
|
1269
|
+
"Shorthand for --logsheet-config DIR/cruise_config.yaml "
|
|
1270
|
+
"--inventory DIR/instruments.yaml."
|
|
1271
|
+
),
|
|
1272
|
+
)
|
|
1273
|
+
p_logsheet.add_argument(
|
|
1274
|
+
"--inventory",
|
|
1275
|
+
dest="logsheet_inventory",
|
|
1276
|
+
metavar="PATH",
|
|
1277
|
+
help="Path to instruments.yaml (or instrument_inventory.csv).",
|
|
1278
|
+
)
|
|
1279
|
+
p_logsheet.add_argument(
|
|
1280
|
+
"--logsheet-config",
|
|
1281
|
+
dest="logsheet_config",
|
|
1282
|
+
metavar="PATH",
|
|
1283
|
+
help=(
|
|
1284
|
+
"Path to cruise_config.yaml. Optional; paths/filenames left blank "
|
|
1285
|
+
"on the sheet if absent."
|
|
1286
|
+
),
|
|
1287
|
+
)
|
|
1288
|
+
p_logsheet.add_argument(
|
|
1289
|
+
"--output-dir",
|
|
1290
|
+
dest="logsheet_output_dir",
|
|
1291
|
+
metavar="DIR",
|
|
1292
|
+
default="logsheets",
|
|
1293
|
+
help="Directory where PDFs are written (default: ./logsheets/).",
|
|
1294
|
+
)
|
|
1295
|
+
p_logsheet.add_argument(
|
|
1296
|
+
"--format",
|
|
1297
|
+
dest="logsheet_format",
|
|
1298
|
+
choices=["pdf", "tex"],
|
|
1299
|
+
default="pdf",
|
|
1300
|
+
help="Output format: pdf (compile via pdflatex) or tex (LaTeX source only).",
|
|
1301
|
+
)
|
|
1302
|
+
p_logsheet.add_argument(
|
|
1303
|
+
"--all",
|
|
1304
|
+
action="store_true",
|
|
1305
|
+
dest="logsheet_all",
|
|
1306
|
+
help="Generate all sheet types for every cast and mooring in cruise_config.yaml.",
|
|
1307
|
+
)
|
|
1308
|
+
_add_dir_args(p_logsheet, raw_needed=False)
|
|
1309
|
+
p_logsheet.set_defaults(func=cmd_logsheet)
|
|
1310
|
+
|
|
1311
|
+
return parser
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
def main() -> None:
|
|
1315
|
+
"""Entry point for the ``oceanarray`` command-line tool."""
|
|
1316
|
+
parser = build_parser()
|
|
1317
|
+
args, _unknown = parser.parse_known_args()
|
|
1318
|
+
if _unknown:
|
|
1319
|
+
_raw_dir_unknowns = []
|
|
1320
|
+
_other_unknowns = []
|
|
1321
|
+
_skip_next = False
|
|
1322
|
+
for _i, _tok in enumerate(_unknown):
|
|
1323
|
+
if _skip_next:
|
|
1324
|
+
_skip_next = False
|
|
1325
|
+
continue
|
|
1326
|
+
if _tok == "--raw-dir":
|
|
1327
|
+
_raw_dir_unknowns.append(_tok)
|
|
1328
|
+
if _i + 1 < len(_unknown) and not _unknown[_i + 1].startswith("--"):
|
|
1329
|
+
_raw_dir_unknowns.append(_unknown[_i + 1])
|
|
1330
|
+
_skip_next = True
|
|
1331
|
+
elif _tok.startswith("--raw-dir="):
|
|
1332
|
+
_raw_dir_unknowns.append(_tok)
|
|
1333
|
+
else:
|
|
1334
|
+
_other_unknowns.append(_tok)
|
|
1335
|
+
if _raw_dir_unknowns:
|
|
1336
|
+
print(
|
|
1337
|
+
f"oceanarray: WARNING: --raw-dir is not used by this subcommand "
|
|
1338
|
+
f"and will be ignored ({' '.join(_raw_dir_unknowns)})",
|
|
1339
|
+
file=sys.stderr,
|
|
1340
|
+
)
|
|
1341
|
+
if _other_unknowns:
|
|
1342
|
+
parser.error(f"unrecognized arguments: {' '.join(_other_unknowns)}")
|
|
1343
|
+
sys.exit(args.func(args))
|