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
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""oceanarray.logsheet.builders.deployment_setup
|
|
2
|
+
================================================
|
|
3
|
+
Build setup-deployment PDF logsheets.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .._config import (
|
|
11
|
+
LogsheetsConfig,
|
|
12
|
+
load_yaml,
|
|
13
|
+
load_instruments,
|
|
14
|
+
sn_to_entry,
|
|
15
|
+
resolve_mooring_yaml,
|
|
16
|
+
)
|
|
17
|
+
from .._firmware import fw_group, FW_GROUP_LABELS
|
|
18
|
+
from .._latex import colspec_from_cols, header_row, data_row, example_data_row
|
|
19
|
+
from .._columns import resolve_columns, format_data_path_note
|
|
20
|
+
from .._render import make_jinja_env, render_sheet
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_deployment_setup(
|
|
24
|
+
mooring: str, cfg: LogsheetsConfig, fmt: str = "pdf"
|
|
25
|
+
) -> Path:
|
|
26
|
+
"""Build the setup-deployment PDF for a given mooring.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
mooring:
|
|
31
|
+
Mooring name, e.g. ``"dsG3_1_2026"``.
|
|
32
|
+
cfg:
|
|
33
|
+
Resolved logsheets configuration from :func:`~._config.resolve_config`.
|
|
34
|
+
fmt:
|
|
35
|
+
``"pdf"`` or ``"tex"``.
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
cruise_cfg = load_yaml(cfg.cruise_config_path)
|
|
39
|
+
col_defs = load_yaml(cfg.column_defs_path)
|
|
40
|
+
col_library = col_defs.get("column_library", {})
|
|
41
|
+
instruments = load_instruments(cfg)
|
|
42
|
+
|
|
43
|
+
sentinel = cruise_cfg.get("microcat_firmware_sentinel", "3.0d")
|
|
44
|
+
cruise = cruise_cfg["cruise"]
|
|
45
|
+
raw_data = cruise_cfg.get("paths", {}).get("raw_data", "")
|
|
46
|
+
mooring_yaml_map = cruise_cfg.get("mooring_yaml_map", {})
|
|
47
|
+
|
|
48
|
+
yaml_path = resolve_mooring_yaml(mooring, cfg, mooring_yaml_map)
|
|
49
|
+
if yaml_path is None:
|
|
50
|
+
sys.exit(f"Mooring {mooring} not found — checked mooring_yaml_map and proc_dir")
|
|
51
|
+
moor_data = load_yaml(yaml_path)
|
|
52
|
+
|
|
53
|
+
mcats_by_group: dict[str, list[dict]] = {
|
|
54
|
+
"old_seaterm": [],
|
|
55
|
+
"seaterm_v2": [],
|
|
56
|
+
"seaterm_v2_odo": [],
|
|
57
|
+
}
|
|
58
|
+
aquadopps: list[dict] = []
|
|
59
|
+
rbr_solot: list[dict] = []
|
|
60
|
+
sheet_warnings: list[str] = []
|
|
61
|
+
|
|
62
|
+
for clamp in moor_data.get("clamp", []):
|
|
63
|
+
sn = int(str(clamp["serial"]).rstrip("*"))
|
|
64
|
+
sint = clamp.get("sample_interval_seconds")
|
|
65
|
+
if sn == 0:
|
|
66
|
+
instr = str(clamp.get("instrument", "")).lower()
|
|
67
|
+
label = str(clamp.get("label", "")).lower()
|
|
68
|
+
if "sbe37" in instr or "microcat" in instr or "microcat" in label:
|
|
69
|
+
placeholder = {"serial": 0, "model": "SBE37", "firmware": None}
|
|
70
|
+
mcats_by_group["old_seaterm"].append(
|
|
71
|
+
{"entry": placeholder, "sint": sint}
|
|
72
|
+
)
|
|
73
|
+
mcats_by_group["seaterm_v2"].append(
|
|
74
|
+
{"entry": placeholder, "sint": sint}
|
|
75
|
+
)
|
|
76
|
+
continue
|
|
77
|
+
elif "aquadopp" in instr or "aquadopp" in label:
|
|
78
|
+
itype, entry = "aquadopp", {"serial": 0, "model": "Aquadopp"}
|
|
79
|
+
elif "solot" in instr or "solot" in label:
|
|
80
|
+
itype, entry = "rbr_solot", {"serial": 0, "model": "RBR soloT"}
|
|
81
|
+
else:
|
|
82
|
+
print(
|
|
83
|
+
f" Warning: SN 0 with unknown type (label={clamp.get('label', '')}) -- skipped"
|
|
84
|
+
)
|
|
85
|
+
continue
|
|
86
|
+
else:
|
|
87
|
+
try:
|
|
88
|
+
itype, entry = sn_to_entry(instruments, sn)
|
|
89
|
+
except KeyError:
|
|
90
|
+
sheet_warnings.append(f"SN {sn} not found in instruments.yaml")
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
item = {"entry": entry, "sint": sint}
|
|
94
|
+
if itype == "microcat":
|
|
95
|
+
mcats_by_group[fw_group(entry, sentinel)].append(item)
|
|
96
|
+
elif itype == "aquadopp":
|
|
97
|
+
aquadopps.append(item)
|
|
98
|
+
elif itype == "rbr_solot":
|
|
99
|
+
rbr_solot.append(item)
|
|
100
|
+
|
|
101
|
+
sections = []
|
|
102
|
+
|
|
103
|
+
def _note(defn, instrument):
|
|
104
|
+
return format_data_path_note(
|
|
105
|
+
defn.get("data_path_note", ""),
|
|
106
|
+
raw_data=raw_data,
|
|
107
|
+
mooring=mooring,
|
|
108
|
+
instrument=instrument,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def _sint_prefill(cols, sint):
|
|
112
|
+
if sint is None:
|
|
113
|
+
return {}
|
|
114
|
+
for i, c in enumerate(cols):
|
|
115
|
+
h = c.get("header", "").lower()
|
|
116
|
+
if "interval" in h and "average" not in h:
|
|
117
|
+
return {i: str(sint)}
|
|
118
|
+
return {}
|
|
119
|
+
|
|
120
|
+
mcat_def = col_defs["microcat_setup_deployment"]
|
|
121
|
+
for grp, items in mcats_by_group.items():
|
|
122
|
+
if not items:
|
|
123
|
+
continue
|
|
124
|
+
cols = resolve_columns(mcat_def[f"columns_{grp}"], col_library)
|
|
125
|
+
sections.append(
|
|
126
|
+
{
|
|
127
|
+
"title": FW_GROUP_LABELS[grp],
|
|
128
|
+
"colspec": colspec_from_cols(cols),
|
|
129
|
+
"header_row": header_row(cols),
|
|
130
|
+
"example_row": example_data_row(cols),
|
|
131
|
+
"note_lines": _note(mcat_def, "microcat"),
|
|
132
|
+
"data_rows": [
|
|
133
|
+
data_row(
|
|
134
|
+
cols,
|
|
135
|
+
sn=it["entry"]["serial"],
|
|
136
|
+
prefilled_extra=_sint_prefill(cols, it["sint"]),
|
|
137
|
+
)
|
|
138
|
+
for it in items
|
|
139
|
+
],
|
|
140
|
+
"ncols": len(cols),
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if aquadopps:
|
|
145
|
+
aq_def = col_defs["aquadopp_setup_deployment"]
|
|
146
|
+
cols = resolve_columns(aq_def["columns"], col_library)
|
|
147
|
+
sections.append(
|
|
148
|
+
{
|
|
149
|
+
"title": aq_def.get("section_title", "Aquadopp Deployment Setup"),
|
|
150
|
+
"colspec": colspec_from_cols(cols),
|
|
151
|
+
"header_row": header_row(cols),
|
|
152
|
+
"example_row": example_data_row(cols),
|
|
153
|
+
"note_lines": _note(aq_def, "aquadopp"),
|
|
154
|
+
"data_rows": [
|
|
155
|
+
data_row(
|
|
156
|
+
cols,
|
|
157
|
+
sn=it["entry"]["serial"],
|
|
158
|
+
prefilled_extra=_sint_prefill(cols, it["sint"]),
|
|
159
|
+
)
|
|
160
|
+
for it in aquadopps
|
|
161
|
+
],
|
|
162
|
+
"ncols": len(cols),
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if rbr_solot:
|
|
167
|
+
rbr_def = col_defs["rbr_solot_setup_deployment"]
|
|
168
|
+
cols = resolve_columns(rbr_def["columns"], col_library)
|
|
169
|
+
model_idx = next(
|
|
170
|
+
(j for j, c in enumerate(cols) if c["header"].strip() == "Model"), None
|
|
171
|
+
)
|
|
172
|
+
drows = []
|
|
173
|
+
for it in rbr_solot:
|
|
174
|
+
entry = it["entry"]
|
|
175
|
+
extra = (
|
|
176
|
+
{model_idx: entry.get("model", "RBR soloT")}
|
|
177
|
+
if model_idx is not None
|
|
178
|
+
else {}
|
|
179
|
+
)
|
|
180
|
+
drows.append(
|
|
181
|
+
data_row(
|
|
182
|
+
cols,
|
|
183
|
+
sn=entry["serial"],
|
|
184
|
+
extra_readonly=extra,
|
|
185
|
+
prefilled_extra=_sint_prefill(cols, it["sint"]),
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
sections.append(
|
|
189
|
+
{
|
|
190
|
+
"title": rbr_def.get("section_title", "RBR soloT Deployment Setup"),
|
|
191
|
+
"colspec": colspec_from_cols(cols),
|
|
192
|
+
"header_row": header_row(cols),
|
|
193
|
+
"example_row": example_data_row(cols),
|
|
194
|
+
"note_lines": _note(rbr_def, "rbr"),
|
|
195
|
+
"data_rows": drows,
|
|
196
|
+
"ncols": len(cols),
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
tex_source = (
|
|
201
|
+
make_jinja_env(cfg.templates_dir)
|
|
202
|
+
.get_template("deployment_setup.tex.j2")
|
|
203
|
+
.render(
|
|
204
|
+
cruise=cruise,
|
|
205
|
+
mooring_tex=mooring.replace("_", r"\_"),
|
|
206
|
+
sections=sections,
|
|
207
|
+
sheet_warnings=sheet_warnings,
|
|
208
|
+
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M"),
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
out_dir = cfg.output_dir / "setup-deployment"
|
|
213
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
214
|
+
stem = f"{mooring}_setup"
|
|
215
|
+
try:
|
|
216
|
+
return render_sheet(tex_source, out_dir, stem, fmt)
|
|
217
|
+
except RuntimeError:
|
|
218
|
+
sys.exit(1)
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""oceanarray.logsheet.builders.mooring_download
|
|
2
|
+
================================================
|
|
3
|
+
Build mooring-download PDF logsheets (all instrument types).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .._config import (
|
|
11
|
+
LogsheetsConfig,
|
|
12
|
+
load_yaml,
|
|
13
|
+
load_instruments,
|
|
14
|
+
sn_to_entry,
|
|
15
|
+
resolve_mooring_yaml,
|
|
16
|
+
)
|
|
17
|
+
from .._firmware import fw_group, FW_GROUP_LABELS
|
|
18
|
+
from .._latex import colspec_from_cols, header_row, data_row, example_data_row
|
|
19
|
+
from .._columns import resolve_columns, apply_download_convention, format_data_path_note
|
|
20
|
+
from .._render import make_jinja_env, render_sheet
|
|
21
|
+
|
|
22
|
+
_FW_CONV_KEY: dict[str, str] = {
|
|
23
|
+
"old_seaterm": "download_old_seaterm",
|
|
24
|
+
"seaterm_v2": "download_seaterm_v2",
|
|
25
|
+
"seaterm_v2_odo": "download_seaterm_v2",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _collect_instruments(
|
|
30
|
+
moor_data: dict, instruments: dict, sentinel: str, sheet_warnings: list
|
|
31
|
+
) -> dict:
|
|
32
|
+
"""Walk the mooring YAML and separate instruments into typed buckets."""
|
|
33
|
+
mcats_by_group: dict[str, list[dict]] = {
|
|
34
|
+
"old_seaterm": [],
|
|
35
|
+
"seaterm_v2": [],
|
|
36
|
+
"seaterm_v2_odo": [],
|
|
37
|
+
}
|
|
38
|
+
aquadopps: list[dict] = []
|
|
39
|
+
rbr_tr1050: list[dict] = []
|
|
40
|
+
rbr_solot: list[dict] = []
|
|
41
|
+
adcp_items: list[dict] = []
|
|
42
|
+
|
|
43
|
+
for clamp in moor_data.get("clamp", []):
|
|
44
|
+
sn = int(str(clamp["serial"]).rstrip("*"))
|
|
45
|
+
hab = clamp.get("hab", "")
|
|
46
|
+
|
|
47
|
+
if sn == 0:
|
|
48
|
+
instr = str(clamp.get("instrument", "")).lower()
|
|
49
|
+
label = str(clamp.get("label", "")).lower()
|
|
50
|
+
if "microcat" in instr or "microcat" in label or "sbe37" in instr:
|
|
51
|
+
placeholder = {"serial": 0, "model": "SBE37", "hab": hab}
|
|
52
|
+
mcats_by_group["old_seaterm"].append(placeholder)
|
|
53
|
+
mcats_by_group["seaterm_v2"].append(placeholder)
|
|
54
|
+
elif "aquadopp" in instr or "aquadopp" in label:
|
|
55
|
+
aquadopps.append({"serial": 0, "model": "Aquadopp", "hab": hab})
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
itype, entry = sn_to_entry(instruments, sn)
|
|
60
|
+
except KeyError:
|
|
61
|
+
sheet_warnings.append(f"SN {sn} not found in instruments.yaml")
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
entry = dict(entry, hab=hab)
|
|
65
|
+
if itype == "microcat":
|
|
66
|
+
mcats_by_group[fw_group(entry, sentinel)].append(entry)
|
|
67
|
+
elif itype == "aquadopp":
|
|
68
|
+
aquadopps.append(entry)
|
|
69
|
+
elif itype == "rbr_tr1050":
|
|
70
|
+
rbr_tr1050.append(entry)
|
|
71
|
+
elif itype == "rbr_solot":
|
|
72
|
+
rbr_solot.append(entry)
|
|
73
|
+
|
|
74
|
+
for item in moor_data.get("inline", []):
|
|
75
|
+
raw_serial = str(item.get("serial", "0"))
|
|
76
|
+
sn = int(raw_serial.split(",")[0].strip())
|
|
77
|
+
if item.get("instrument", "").lower() in ("adcp",) or item.get(
|
|
78
|
+
"file_type", ""
|
|
79
|
+
).startswith("rdi"):
|
|
80
|
+
try:
|
|
81
|
+
itype, entry = sn_to_entry(instruments, sn)
|
|
82
|
+
except KeyError:
|
|
83
|
+
entry = {"serial": sn, "model": "ADCP"}
|
|
84
|
+
adcp_items.append(
|
|
85
|
+
dict(entry, hab=item.get("hab_bottom") or item.get("hab_top") or "")
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"mcats_by_group": mcats_by_group,
|
|
90
|
+
"aquadopps": aquadopps,
|
|
91
|
+
"rbr_tr1050": rbr_tr1050,
|
|
92
|
+
"rbr_solot": rbr_solot,
|
|
93
|
+
"adcp": adcp_items,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def build_mooring_download(
|
|
98
|
+
mooring: str, cfg: LogsheetsConfig, fmt: str = "pdf"
|
|
99
|
+
) -> Path:
|
|
100
|
+
"""Build mooring-download PDF logsheets for all instrument types.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
mooring:
|
|
105
|
+
Mooring name, e.g. ``"dsG3_1_2026"``.
|
|
106
|
+
cfg:
|
|
107
|
+
Resolved logsheets configuration from :func:`~._config.resolve_config`.
|
|
108
|
+
fmt:
|
|
109
|
+
``"pdf"`` or ``"tex"``.
|
|
110
|
+
|
|
111
|
+
"""
|
|
112
|
+
cruise_cfg = load_yaml(cfg.cruise_config_path)
|
|
113
|
+
col_defs = load_yaml(cfg.column_defs_path)
|
|
114
|
+
col_library = col_defs.get("column_library", {})
|
|
115
|
+
instruments = load_instruments(cfg)
|
|
116
|
+
|
|
117
|
+
sentinel = cruise_cfg.get("microcat_firmware_sentinel", "3.0d")
|
|
118
|
+
cruise = cruise_cfg["cruise"]
|
|
119
|
+
raw_data = cruise_cfg.get("paths", {}).get("raw_data", "")
|
|
120
|
+
fn_conventions = cruise_cfg.get("filename_conventions", {})
|
|
121
|
+
|
|
122
|
+
mooring_yaml_map = cruise_cfg.get("mooring_yaml_map", {})
|
|
123
|
+
yaml_path = resolve_mooring_yaml(mooring, cfg, mooring_yaml_map)
|
|
124
|
+
if yaml_path:
|
|
125
|
+
moor_data = load_yaml(yaml_path)
|
|
126
|
+
else:
|
|
127
|
+
print(
|
|
128
|
+
f" Warning: no mooring YAML found for {mooring} — using empty instrument list"
|
|
129
|
+
)
|
|
130
|
+
moor_data = {"clamp": [], "inline": []}
|
|
131
|
+
|
|
132
|
+
sheet_warnings: list[str] = []
|
|
133
|
+
buckets = _collect_instruments(moor_data, instruments, sentinel, sheet_warnings)
|
|
134
|
+
sections = []
|
|
135
|
+
|
|
136
|
+
# ── MicroCAT sections ─────────────────────────────────────────────────────
|
|
137
|
+
mcat_def = col_defs.get(
|
|
138
|
+
"microcat_download_mooring", col_defs.get("microcat_download", {})
|
|
139
|
+
)
|
|
140
|
+
mcat_note_raw = mcat_def.get("data_path_note", "")
|
|
141
|
+
|
|
142
|
+
for grp, entries in buckets["mcats_by_group"].items():
|
|
143
|
+
if not entries:
|
|
144
|
+
continue
|
|
145
|
+
conv_key = _FW_CONV_KEY.get(grp, "download_seaterm_v2")
|
|
146
|
+
convention = fn_conventions.get("microcat", {}).get(conv_key, "")
|
|
147
|
+
col_key = f"columns_{grp}"
|
|
148
|
+
cols_raw = mcat_def.get(col_key, mcat_def.get("columns", []))
|
|
149
|
+
cols = resolve_columns(cols_raw, col_library)
|
|
150
|
+
cols = apply_download_convention(cols, convention)
|
|
151
|
+
note_lines = format_data_path_note(
|
|
152
|
+
mcat_note_raw,
|
|
153
|
+
raw_data=raw_data,
|
|
154
|
+
mooring=mooring,
|
|
155
|
+
download_convention=convention,
|
|
156
|
+
)
|
|
157
|
+
sections.append(
|
|
158
|
+
{
|
|
159
|
+
"title": FW_GROUP_LABELS[grp],
|
|
160
|
+
"colspec": colspec_from_cols(cols),
|
|
161
|
+
"header_row": header_row(cols),
|
|
162
|
+
"example_row": example_data_row(cols),
|
|
163
|
+
"note_lines": note_lines,
|
|
164
|
+
"data_rows": [data_row(cols, sn=e["serial"]) for e in entries],
|
|
165
|
+
"ncols": len(cols),
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# ── Aquadopp section ──────────────────────────────────────────────────────
|
|
170
|
+
if buckets["aquadopps"]:
|
|
171
|
+
aq_def = col_defs.get(
|
|
172
|
+
"aquadopp_download_mooring", col_defs.get("aquadopp_download", {})
|
|
173
|
+
)
|
|
174
|
+
cols = resolve_columns(aq_def.get("columns", []), col_library)
|
|
175
|
+
convention = fn_conventions.get("aquadopp", {}).get("download", "")
|
|
176
|
+
cols = apply_download_convention(cols, convention)
|
|
177
|
+
note_lines = format_data_path_note(
|
|
178
|
+
aq_def.get("data_path_note", ""),
|
|
179
|
+
raw_data=raw_data,
|
|
180
|
+
mooring=mooring,
|
|
181
|
+
instrument="aquadopp",
|
|
182
|
+
)
|
|
183
|
+
sections.append(
|
|
184
|
+
{
|
|
185
|
+
"title": aq_def.get("section_title", "Aquadopp Download"),
|
|
186
|
+
"colspec": colspec_from_cols(cols),
|
|
187
|
+
"header_row": header_row(cols),
|
|
188
|
+
"example_row": example_data_row(cols),
|
|
189
|
+
"note_lines": note_lines,
|
|
190
|
+
"data_rows": [
|
|
191
|
+
data_row(cols, sn=e["serial"]) for e in buckets["aquadopps"]
|
|
192
|
+
],
|
|
193
|
+
"ncols": len(cols),
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# ── RBR sections ──────────────────────────────────────────────────────────
|
|
198
|
+
for bucket_key, def_key, title_default in [
|
|
199
|
+
("rbr_tr1050", "rbr_tr1050_download_mooring", "RBR TR-1050 Download"),
|
|
200
|
+
("rbr_solot", "rbr_solot_download_mooring", "RBR soloT Download"),
|
|
201
|
+
]:
|
|
202
|
+
entries = buckets[bucket_key]
|
|
203
|
+
if not entries:
|
|
204
|
+
continue
|
|
205
|
+
defn = col_defs.get(def_key, {})
|
|
206
|
+
cols = resolve_columns(defn.get("columns", []), col_library)
|
|
207
|
+
note_lines = format_data_path_note(
|
|
208
|
+
defn.get("data_path_note", ""),
|
|
209
|
+
raw_data=raw_data,
|
|
210
|
+
mooring=mooring,
|
|
211
|
+
)
|
|
212
|
+
sections.append(
|
|
213
|
+
{
|
|
214
|
+
"title": defn.get("section_title", title_default),
|
|
215
|
+
"colspec": colspec_from_cols(cols),
|
|
216
|
+
"header_row": header_row(cols),
|
|
217
|
+
"example_row": example_data_row(cols),
|
|
218
|
+
"note_lines": note_lines,
|
|
219
|
+
"data_rows": [data_row(cols, sn=e["serial"]) for e in entries],
|
|
220
|
+
"ncols": len(cols),
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
tex_source = (
|
|
225
|
+
make_jinja_env(cfg.templates_dir)
|
|
226
|
+
.get_template("mooring_download.tex.j2")
|
|
227
|
+
.render(
|
|
228
|
+
cruise=cruise,
|
|
229
|
+
mooring=mooring,
|
|
230
|
+
mooring_tex=mooring.replace("_", r"\_"),
|
|
231
|
+
sections=sections,
|
|
232
|
+
sheet_warnings=sheet_warnings,
|
|
233
|
+
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M"),
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
out_dir = cfg.output_dir / "mooring-download"
|
|
238
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
239
|
+
stem = f"{mooring}_download"
|
|
240
|
+
try:
|
|
241
|
+
return render_sheet(tex_source, out_dir, stem, fmt)
|
|
242
|
+
except RuntimeError:
|
|
243
|
+
sys.exit(1)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""oceanarray.logsheet.builders.recovery
|
|
2
|
+
=========================================
|
|
3
|
+
Build mooring-recovery logsheet PDFs.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .._config import (
|
|
12
|
+
LogsheetsConfig,
|
|
13
|
+
load_yaml,
|
|
14
|
+
load_instruments,
|
|
15
|
+
sn_to_entry,
|
|
16
|
+
resolve_mooring_yaml,
|
|
17
|
+
)
|
|
18
|
+
from .._latex import colspec_from_cols, header_row, tex_safe
|
|
19
|
+
from .._render import make_jinja_env, render_sheet
|
|
20
|
+
|
|
21
|
+
_RECOVERY_HW_TYPES = {"float", "release"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _group_inline_floats(inline_items: list[dict]) -> list[dict]:
|
|
25
|
+
"""Collapse consecutive float entries sharing the same component_id into one."""
|
|
26
|
+
result = []
|
|
27
|
+
i = 0
|
|
28
|
+
while i < len(inline_items):
|
|
29
|
+
item = inline_items[i]
|
|
30
|
+
cid = item.get("component_id")
|
|
31
|
+
if item.get("hardware_type") != "float" or not cid:
|
|
32
|
+
result.append(item)
|
|
33
|
+
i += 1
|
|
34
|
+
continue
|
|
35
|
+
run = [item]
|
|
36
|
+
j = i + 1
|
|
37
|
+
while j < len(inline_items):
|
|
38
|
+
nxt = inline_items[j]
|
|
39
|
+
if nxt.get("hardware_type") == "float" and nxt.get("component_id") == cid:
|
|
40
|
+
run.append(nxt)
|
|
41
|
+
j += 1
|
|
42
|
+
else:
|
|
43
|
+
break
|
|
44
|
+
total = sum(g.get("repeat", 1) for g in run)
|
|
45
|
+
raw_label = next(
|
|
46
|
+
(g["label"] for g in run if g.get("label") and str(g["label"]) != "null"),
|
|
47
|
+
None,
|
|
48
|
+
)
|
|
49
|
+
base = re.sub(r"^\d+\s+", "", raw_label) if raw_label else "float"
|
|
50
|
+
combined = dict(run[0])
|
|
51
|
+
combined["label"] = f"{total} {base}" if total > 1 else base
|
|
52
|
+
combined["repeat"] = total
|
|
53
|
+
result.append(combined)
|
|
54
|
+
i = j
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def build_recovery(mooring: str, cfg: LogsheetsConfig, fmt: str = "pdf") -> Path:
|
|
59
|
+
"""Build the mooring-recovery PDF for a given mooring.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
mooring:
|
|
64
|
+
Mooring name, e.g. ``"dsG3_1_2026"``.
|
|
65
|
+
cfg:
|
|
66
|
+
Resolved logsheets configuration from :func:`~._config.resolve_config`.
|
|
67
|
+
fmt:
|
|
68
|
+
``"pdf"`` or ``"tex"``.
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
cruise_cfg = load_yaml(cfg.cruise_config_path)
|
|
72
|
+
col_defs = load_yaml(cfg.column_defs_path)
|
|
73
|
+
instruments = load_instruments(cfg)
|
|
74
|
+
cruise = cruise_cfg["cruise"]
|
|
75
|
+
|
|
76
|
+
rec_def = col_defs["recovery"]
|
|
77
|
+
comp_cols = rec_def["columns_component"]
|
|
78
|
+
rang_cols = rec_def["columns_ranging"]
|
|
79
|
+
|
|
80
|
+
rows = []
|
|
81
|
+
sheet_warnings: list[str] = []
|
|
82
|
+
waterdepth = ""
|
|
83
|
+
|
|
84
|
+
mooring_yaml_map = cruise_cfg.get("mooring_yaml_map", {})
|
|
85
|
+
yaml_path = resolve_mooring_yaml(mooring, cfg, mooring_yaml_map)
|
|
86
|
+
if yaml_path:
|
|
87
|
+
moor_data = load_yaml(yaml_path)
|
|
88
|
+
waterdepth = str(moor_data.get("waterdepth", ""))
|
|
89
|
+
|
|
90
|
+
for clamp in moor_data.get("clamp", []):
|
|
91
|
+
sn = int(str(clamp["serial"]).rstrip("*"))
|
|
92
|
+
hab = clamp.get("hab") or 0
|
|
93
|
+
try:
|
|
94
|
+
itype, entry = sn_to_entry(instruments, sn)
|
|
95
|
+
model = entry.get("model", "")
|
|
96
|
+
item_str = {
|
|
97
|
+
"microcat": "MicroCAT",
|
|
98
|
+
"aquadopp": "Aquadopp",
|
|
99
|
+
"rbr_tr1050": "RBR TR-1050",
|
|
100
|
+
"rbr_solot": "RBR soloT",
|
|
101
|
+
"adcp": f"ADCP {model}",
|
|
102
|
+
}.get(itype, itype)
|
|
103
|
+
except KeyError:
|
|
104
|
+
item_str = clamp.get("label", "?")
|
|
105
|
+
sheet_warnings.append(f"SN {sn} not found in instruments.yaml")
|
|
106
|
+
rows.append((hab, item_str, str(sn) if sn else ""))
|
|
107
|
+
|
|
108
|
+
for item in _group_inline_floats(moor_data.get("inline", [])):
|
|
109
|
+
if item.get("hardware_type") not in _RECOVERY_HW_TYPES:
|
|
110
|
+
continue
|
|
111
|
+
hab = item.get("hab_top") or item.get("hab_bottom") or 0
|
|
112
|
+
label = item.get("label") or item.get("hardware_type", "")
|
|
113
|
+
serial = item.get("serial", "")
|
|
114
|
+
sn_str = (
|
|
115
|
+
str(serial) if serial and str(serial) not in ("None", "null") else ""
|
|
116
|
+
)
|
|
117
|
+
rows.append((hab, label, sn_str))
|
|
118
|
+
else:
|
|
119
|
+
print(f" Warning: no mooring YAML found for {mooring}")
|
|
120
|
+
|
|
121
|
+
rows.sort(key=lambda r: r[0], reverse=True)
|
|
122
|
+
|
|
123
|
+
n_comp = len(comp_cols)
|
|
124
|
+
trail = (" & " + " & ".join([""] * (n_comp - 2))) if n_comp > 2 else ""
|
|
125
|
+
comp_rows = [tex_safe(item) + " & " + tex_safe(sn) + trail for _, item, sn in rows]
|
|
126
|
+
|
|
127
|
+
mooring_tex = mooring.replace("_", r"\_")
|
|
128
|
+
waterdepth_tex = (waterdepth + r"\,m") if waterdepth else "?"
|
|
129
|
+
n_rang = len(rang_cols)
|
|
130
|
+
rang_rows = [" & ".join([""] * n_rang)] * 18
|
|
131
|
+
|
|
132
|
+
tex_source = (
|
|
133
|
+
make_jinja_env(cfg.templates_dir)
|
|
134
|
+
.get_template("recovery.tex.j2")
|
|
135
|
+
.render(
|
|
136
|
+
cruise=cruise,
|
|
137
|
+
mooring=mooring,
|
|
138
|
+
mooring_tex=mooring_tex,
|
|
139
|
+
waterdepth_tex=waterdepth_tex,
|
|
140
|
+
comp_colspec=colspec_from_cols(comp_cols),
|
|
141
|
+
comp_header_row=header_row(comp_cols),
|
|
142
|
+
comp_rows=comp_rows,
|
|
143
|
+
rang_colspec=colspec_from_cols(rang_cols),
|
|
144
|
+
rang_header_row=header_row(rang_cols),
|
|
145
|
+
rang_rows=rang_rows,
|
|
146
|
+
sheet_warnings=sheet_warnings,
|
|
147
|
+
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M"),
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
out_dir = cfg.output_dir / "mooring-recovery"
|
|
152
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
stem = f"{mooring}_recovery"
|
|
154
|
+
try:
|
|
155
|
+
return render_sheet(tex_source, out_dir, stem, fmt)
|
|
156
|
+
except RuntimeError:
|
|
157
|
+
sys.exit(1)
|