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,128 @@
|
|
|
1
|
+
"""oceanarray.logsheet._latex
|
|
2
|
+
============================
|
|
3
|
+
LaTeX escaping, column-spec building, and row-rendering helpers.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
_TEX_SPECIAL: dict[str, str] = {
|
|
7
|
+
"&": r"\&",
|
|
8
|
+
"%": r"\%",
|
|
9
|
+
"$": r"\$",
|
|
10
|
+
"#": r"\#",
|
|
11
|
+
"_": r"\_",
|
|
12
|
+
"{": r"\{",
|
|
13
|
+
"}": r"\}",
|
|
14
|
+
"~": r"\textasciitilde{}",
|
|
15
|
+
"^": r"\^{}",
|
|
16
|
+
"\\": r"\textbackslash{}",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def tex_safe(s: str) -> str:
|
|
21
|
+
"""Escape a plain string that contains no LaTeX commands."""
|
|
22
|
+
if s is None:
|
|
23
|
+
return ""
|
|
24
|
+
return "".join(_TEX_SPECIAL.get(ch, ch) for ch in str(s))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def colspec_from_cols(cols: list[dict]) -> str:
|
|
28
|
+
"""Build a LaTeX tabular column-spec string from a resolved column list.
|
|
29
|
+
|
|
30
|
+
Column ``width`` values are relative weights normalised to fill
|
|
31
|
+
``\\linewidth``.
|
|
32
|
+
"""
|
|
33
|
+
total = sum(c["width"] for c in cols)
|
|
34
|
+
parts = ["|"]
|
|
35
|
+
for col in cols:
|
|
36
|
+
frac = col["width"] / total
|
|
37
|
+
col_type = "p" if col.get("align") == "left" else "C"
|
|
38
|
+
parts.append(
|
|
39
|
+
f"{col_type}{{\\dimexpr {frac:.5f}\\linewidth-2\\tabcolsep\\relax}}|"
|
|
40
|
+
)
|
|
41
|
+
return "".join(parts)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def header_row(cols: list[dict]) -> str:
|
|
45
|
+
"""Build the grey, bold LaTeX header row."""
|
|
46
|
+
cells = []
|
|
47
|
+
for col in cols:
|
|
48
|
+
text = " ".join(col["header"].split("\n"))
|
|
49
|
+
text = (
|
|
50
|
+
text.replace("_", r"\_")
|
|
51
|
+
.replace("{", r"\{")
|
|
52
|
+
.replace("}", r"\}")
|
|
53
|
+
.replace("&", r"\&")
|
|
54
|
+
.replace("#", r"\#")
|
|
55
|
+
)
|
|
56
|
+
cells.append(r"\cellcolor{hdrgray}{\bfseries\small " + text + r"}")
|
|
57
|
+
return " & ".join(cells)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def example_data_row(cols: list[dict]) -> str:
|
|
61
|
+
"""Build the shaded example row from the ``example`` field on each column."""
|
|
62
|
+
cells = []
|
|
63
|
+
for col in cols:
|
|
64
|
+
ex = str(col.get("example", ""))
|
|
65
|
+
if not ex:
|
|
66
|
+
cells.append("")
|
|
67
|
+
continue
|
|
68
|
+
ex_tex = ex if ex.startswith("\\") else tex_safe(ex)
|
|
69
|
+
cells.append(r"\textit{\small " + ex_tex + r"}")
|
|
70
|
+
return " & ".join(cells)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def data_row(
|
|
74
|
+
cols: list[dict],
|
|
75
|
+
sn: int | None = None,
|
|
76
|
+
prefills: dict | None = None,
|
|
77
|
+
extra_readonly: dict | None = None,
|
|
78
|
+
prefilled_extra: dict | None = None,
|
|
79
|
+
) -> str:
|
|
80
|
+
"""Build a single instrument data row.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
cols:
|
|
85
|
+
Resolved column dicts.
|
|
86
|
+
sn:
|
|
87
|
+
Serial number; rendered in ``readonly`` SN columns.
|
|
88
|
+
``0`` or ``None`` renders a blank SN cell.
|
|
89
|
+
prefills:
|
|
90
|
+
``{key: value}`` map for pre-filled dynamic values (e.g. ``"sint"``).
|
|
91
|
+
extra_readonly:
|
|
92
|
+
``{col_index: text}`` to force-fill specific columns as readonly text.
|
|
93
|
+
prefilled_extra:
|
|
94
|
+
``{col_index: text}`` to force-fill specific columns with ``\\pre{}``
|
|
95
|
+
styling (grey). Takes priority over all other logic for the matched column.
|
|
96
|
+
|
|
97
|
+
"""
|
|
98
|
+
cells = []
|
|
99
|
+
if prefills is None:
|
|
100
|
+
prefills = {}
|
|
101
|
+
if extra_readonly is None:
|
|
102
|
+
extra_readonly = {}
|
|
103
|
+
if prefilled_extra is None:
|
|
104
|
+
prefilled_extra = {}
|
|
105
|
+
|
|
106
|
+
for i, col in enumerate(cols):
|
|
107
|
+
inp = col.get("input", "free")
|
|
108
|
+
default = col.get("default")
|
|
109
|
+
|
|
110
|
+
if i in prefilled_extra:
|
|
111
|
+
cells.append(r"\pre{" + tex_safe(str(prefilled_extra[i])) + r"}")
|
|
112
|
+
elif i in extra_readonly:
|
|
113
|
+
cells.append(r"\small " + tex_safe(str(extra_readonly[i])))
|
|
114
|
+
elif inp == "readonly" and col["header"].strip().startswith("SN"):
|
|
115
|
+
cells.append(r"\small " + tex_safe(str(sn)) if sn else "")
|
|
116
|
+
elif inp == "readonly":
|
|
117
|
+
cells.append("")
|
|
118
|
+
elif inp == "tick":
|
|
119
|
+
cells.append("")
|
|
120
|
+
elif inp == "prefilled" and default:
|
|
121
|
+
cells.append(r"\pre{" + tex_safe(default) + r"}")
|
|
122
|
+
elif inp == "prefilled" and sn and "{sint}" in col["header"]:
|
|
123
|
+
val = prefills.get("sint", "")
|
|
124
|
+
cells.append(r"\pre{" + tex_safe(str(val)) + r"}")
|
|
125
|
+
else:
|
|
126
|
+
cells.append("")
|
|
127
|
+
|
|
128
|
+
return " & ".join(cells)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""oceanarray.logsheet._render
|
|
2
|
+
==============================
|
|
3
|
+
Jinja2 environment factory and PDF compilation helper.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from jinja2 import Environment, FileSystemLoader
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def make_jinja_env(templates_dir: Path) -> Environment:
|
|
15
|
+
"""Return a Jinja2 :class:`~jinja2.Environment` configured for LaTeX templates.
|
|
16
|
+
|
|
17
|
+
Uses custom delimiters to avoid clashing with LaTeX ``{}``:
|
|
18
|
+
|
|
19
|
+
- blocks: ``(% … %)``
|
|
20
|
+
- variables: ``(( … ))``
|
|
21
|
+
- comments: ``(# … #)``
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
templates_dir:
|
|
26
|
+
Directory containing the ``.tex.j2`` template files. Pass
|
|
27
|
+
``cfg.templates_dir`` (which defaults to the bundled package data).
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
return Environment(
|
|
31
|
+
loader=FileSystemLoader(str(templates_dir)),
|
|
32
|
+
block_start_string="(%",
|
|
33
|
+
block_end_string="%)",
|
|
34
|
+
variable_start_string="((",
|
|
35
|
+
variable_end_string="))",
|
|
36
|
+
comment_start_string="(#",
|
|
37
|
+
comment_end_string="#)",
|
|
38
|
+
keep_trailing_newline=True,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def compile_pdf(
|
|
43
|
+
tex_source: str,
|
|
44
|
+
out_pdf: Path,
|
|
45
|
+
out_tex: Path | None = None,
|
|
46
|
+
) -> Path:
|
|
47
|
+
"""Compile a LaTeX source string to PDF via ``pdflatex``.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
tex_source:
|
|
52
|
+
Complete LaTeX document source.
|
|
53
|
+
out_pdf:
|
|
54
|
+
Destination path for the compiled PDF.
|
|
55
|
+
out_tex:
|
|
56
|
+
If given, also write the ``.tex`` source alongside the PDF.
|
|
57
|
+
|
|
58
|
+
Raises
|
|
59
|
+
------
|
|
60
|
+
RuntimeError
|
|
61
|
+
If ``pdflatex`` fails to produce a PDF.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
if out_tex is not None:
|
|
65
|
+
out_tex.write_text(tex_source, encoding="utf-8")
|
|
66
|
+
print(f" Written: {out_tex}")
|
|
67
|
+
|
|
68
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
69
|
+
tex_file = Path(tmpdir) / "sheet.tex"
|
|
70
|
+
tex_file.write_text(tex_source, encoding="utf-8")
|
|
71
|
+
result = subprocess.run(
|
|
72
|
+
[
|
|
73
|
+
"pdflatex",
|
|
74
|
+
"-interaction=nonstopmode",
|
|
75
|
+
"-output-directory",
|
|
76
|
+
tmpdir,
|
|
77
|
+
str(tex_file),
|
|
78
|
+
],
|
|
79
|
+
capture_output=True,
|
|
80
|
+
text=True,
|
|
81
|
+
timeout=60,
|
|
82
|
+
)
|
|
83
|
+
pdf_tmp = Path(tmpdir) / "sheet.pdf"
|
|
84
|
+
if pdf_tmp.exists():
|
|
85
|
+
shutil.copy(pdf_tmp, out_pdf)
|
|
86
|
+
print(f" Written: {out_pdf}")
|
|
87
|
+
else:
|
|
88
|
+
print("LaTeX compilation failed. Log:")
|
|
89
|
+
print(result.stdout[-3000:])
|
|
90
|
+
raise RuntimeError(f"pdflatex failed for {out_pdf.name}")
|
|
91
|
+
|
|
92
|
+
return out_pdf
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def render_sheet(
|
|
96
|
+
tex_source: str,
|
|
97
|
+
out_dir: Path,
|
|
98
|
+
stem: str,
|
|
99
|
+
fmt: str = "pdf",
|
|
100
|
+
) -> Path:
|
|
101
|
+
"""Write a rendered LaTeX sheet to ``out_dir`` in the requested format.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
tex_source:
|
|
106
|
+
Complete LaTeX document source.
|
|
107
|
+
out_dir:
|
|
108
|
+
Output directory (created by caller).
|
|
109
|
+
stem:
|
|
110
|
+
Filename stem (no extension), e.g. ``"castN1_setup"``.
|
|
111
|
+
fmt:
|
|
112
|
+
``"pdf"`` — compile via pdflatex (also writes ``.tex`` alongside).
|
|
113
|
+
``"tex"`` — write ``.tex`` only; skip pdflatex.
|
|
114
|
+
|
|
115
|
+
Raises
|
|
116
|
+
------
|
|
117
|
+
RuntimeError
|
|
118
|
+
If ``fmt="pdf"`` and pdflatex fails.
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
if fmt == "tex":
|
|
122
|
+
out_path = out_dir / f"{stem}.tex"
|
|
123
|
+
out_path.write_text(tex_source, encoding="utf-8")
|
|
124
|
+
print(f" Written: {out_path}")
|
|
125
|
+
return out_path
|
|
126
|
+
return compile_pdf(tex_source, out_dir / f"{stem}.pdf", out_dir / f"{stem}.tex")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .caldip_setup import build_caldip_setup
|
|
2
|
+
from .caldip_download import build_caldip_download
|
|
3
|
+
from .recovery import build_recovery
|
|
4
|
+
from .mooring_download import build_mooring_download
|
|
5
|
+
from .deployment_setup import build_deployment_setup
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"build_caldip_setup",
|
|
9
|
+
"build_caldip_download",
|
|
10
|
+
"build_recovery",
|
|
11
|
+
"build_mooring_download",
|
|
12
|
+
"build_deployment_setup",
|
|
13
|
+
]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""oceanarray.logsheet.builders.caldip_download
|
|
2
|
+
===============================================
|
|
3
|
+
Build caldip-download 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
|
+
)
|
|
16
|
+
from .._firmware import fw_group, FW_GROUP_LABELS
|
|
17
|
+
from .._latex import colspec_from_cols, header_row, data_row, example_data_row
|
|
18
|
+
from .._columns import resolve_columns, format_data_path_note
|
|
19
|
+
from .._render import make_jinja_env, render_sheet
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_caldip_download(
|
|
23
|
+
cast_id: str,
|
|
24
|
+
cfg: LogsheetsConfig,
|
|
25
|
+
fmt: str = "pdf",
|
|
26
|
+
caldip_yaml: "dict | None" = None,
|
|
27
|
+
) -> Path:
|
|
28
|
+
"""Build the caldip-download PDF for a given cast.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
cast_id:
|
|
33
|
+
File path string (used for the output filename fallback when
|
|
34
|
+
*caldip_yaml* is provided) or a cast key in a legacy
|
|
35
|
+
``cruise_config.yaml → casts`` dict.
|
|
36
|
+
cfg:
|
|
37
|
+
Resolved logsheets configuration from :func:`~._config.resolve_config`.
|
|
38
|
+
fmt:
|
|
39
|
+
``"pdf"`` or ``"tex"``.
|
|
40
|
+
caldip_yaml:
|
|
41
|
+
Pre-loaded caldip YAML dict. The canonical format is a dict with a
|
|
42
|
+
top-level ``instruments:`` list; each entry has at minimum ``serial``
|
|
43
|
+
(int) and ``instrument`` (type string). When provided, cast info is
|
|
44
|
+
taken directly from this dict.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
col_defs = load_yaml(cfg.column_defs_path)
|
|
48
|
+
col_library = col_defs.get("column_library", {})
|
|
49
|
+
instruments = load_instruments(cfg)
|
|
50
|
+
|
|
51
|
+
# Global settings from logsheet_config.yaml (with safe fallback).
|
|
52
|
+
lsconf: dict = {}
|
|
53
|
+
if cfg.cruise_config_path.exists():
|
|
54
|
+
lsconf = load_yaml(cfg.cruise_config_path)
|
|
55
|
+
|
|
56
|
+
sentinel = lsconf.get("microcat_firmware_sentinel", "3.0d")
|
|
57
|
+
caldip_data = lsconf.get("paths", {}).get("caldip_data", "")
|
|
58
|
+
|
|
59
|
+
# Cast config: from explicit caldip_yaml, or looked up in legacy casts dict.
|
|
60
|
+
if caldip_yaml is not None:
|
|
61
|
+
cast_cfg = caldip_yaml
|
|
62
|
+
else:
|
|
63
|
+
cast_cfg = lsconf.get("casts", {}).get(cast_id)
|
|
64
|
+
if cast_cfg is None:
|
|
65
|
+
sys.exit(f"Cast {cast_id} not found in {cfg.cruise_config_path}")
|
|
66
|
+
|
|
67
|
+
# cruise may live in the caldip YAML itself or in logsheet_config.yaml.
|
|
68
|
+
cruise = cast_cfg.get("cruise") or lsconf.get("cruise", "")
|
|
69
|
+
|
|
70
|
+
# Label: try common key names, fall back to file stem.
|
|
71
|
+
cast_label = (
|
|
72
|
+
cast_cfg.get("label")
|
|
73
|
+
or cast_cfg.get("cast_label")
|
|
74
|
+
or cast_cfg.get("name")
|
|
75
|
+
or cast_cfg.get("cast")
|
|
76
|
+
or Path(cast_id).name.split(".")[0]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
all_mcats_by_group: dict[str, list[dict]] = {
|
|
80
|
+
"old_seaterm": [],
|
|
81
|
+
"seaterm_v2": [],
|
|
82
|
+
"seaterm_v2_odo": [],
|
|
83
|
+
}
|
|
84
|
+
all_aquadopps: list[dict] = []
|
|
85
|
+
rbr_tr1050_entries: list[dict] = []
|
|
86
|
+
rbr_solot_entries: list[dict] = []
|
|
87
|
+
seapoint_entries: list[dict] = []
|
|
88
|
+
sheet_warnings: list[str] = []
|
|
89
|
+
|
|
90
|
+
# Canonical caldip YAML format: top-level instruments: list.
|
|
91
|
+
for item in cast_cfg.get("instruments", []):
|
|
92
|
+
sn = int(item["serial"])
|
|
93
|
+
try:
|
|
94
|
+
itype, entry = sn_to_entry(instruments, sn)
|
|
95
|
+
except KeyError:
|
|
96
|
+
sheet_warnings.append(f"SN {sn} not found in inventory -- skipped")
|
|
97
|
+
continue
|
|
98
|
+
if itype == "microcat":
|
|
99
|
+
all_mcats_by_group[fw_group(entry, sentinel)].append(entry)
|
|
100
|
+
elif itype == "aquadopp":
|
|
101
|
+
all_aquadopps.append(entry)
|
|
102
|
+
elif itype in ("rbrsolo", "rbrduet"):
|
|
103
|
+
rbr_solot_entries.append(entry)
|
|
104
|
+
elif itype == "tr1050":
|
|
105
|
+
rbr_tr1050_entries.append(entry)
|
|
106
|
+
elif itype == "seapoint":
|
|
107
|
+
seapoint_entries.append(entry)
|
|
108
|
+
|
|
109
|
+
sections = []
|
|
110
|
+
|
|
111
|
+
mcat_def = col_defs["microcat_download_caldip"]
|
|
112
|
+
mcat_note_raw = mcat_def.get("data_path_note", "")
|
|
113
|
+
|
|
114
|
+
for grp, entries in all_mcats_by_group.items():
|
|
115
|
+
if not entries:
|
|
116
|
+
continue
|
|
117
|
+
cols = resolve_columns(mcat_def[f"columns_{grp}"], col_library)
|
|
118
|
+
note_lines = format_data_path_note(
|
|
119
|
+
mcat_note_raw, caldip_data=caldip_data, cast_label=cast_label
|
|
120
|
+
)
|
|
121
|
+
sections.append(
|
|
122
|
+
{
|
|
123
|
+
"title": FW_GROUP_LABELS[grp],
|
|
124
|
+
"colspec": colspec_from_cols(cols),
|
|
125
|
+
"header_row": header_row(cols),
|
|
126
|
+
"example_row": example_data_row(cols),
|
|
127
|
+
"note_lines": note_lines,
|
|
128
|
+
"data_rows": [data_row(cols, sn=e["serial"]) for e in entries],
|
|
129
|
+
"ncols": len(cols),
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if all_aquadopps:
|
|
134
|
+
aq_def = col_defs["aquadopp_download_caldip"]
|
|
135
|
+
cols = resolve_columns(aq_def["columns"], col_library)
|
|
136
|
+
sections.append(
|
|
137
|
+
{
|
|
138
|
+
"title": aq_def.get("section_title", "Aquadopp Cal Dip Download"),
|
|
139
|
+
"colspec": colspec_from_cols(cols),
|
|
140
|
+
"header_row": header_row(cols),
|
|
141
|
+
"example_row": example_data_row(cols),
|
|
142
|
+
"note_lines": format_data_path_note(
|
|
143
|
+
aq_def.get("data_path_note", ""),
|
|
144
|
+
caldip_data=caldip_data,
|
|
145
|
+
cast_label=cast_label,
|
|
146
|
+
),
|
|
147
|
+
"data_rows": [data_row(cols, sn=e["serial"]) for e in all_aquadopps],
|
|
148
|
+
"ncols": len(cols),
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def _rbr_section(def_key: str, entries: list[dict]) -> dict:
|
|
153
|
+
defn = col_defs[def_key]
|
|
154
|
+
cols = resolve_columns(defn["columns"], col_library)
|
|
155
|
+
note_lines = format_data_path_note(
|
|
156
|
+
defn.get("data_path_note", ""),
|
|
157
|
+
caldip_data=caldip_data,
|
|
158
|
+
cast_label=cast_label,
|
|
159
|
+
)
|
|
160
|
+
return {
|
|
161
|
+
"title": defn.get("section_title", def_key),
|
|
162
|
+
"colspec": colspec_from_cols(cols),
|
|
163
|
+
"header_row": header_row(cols),
|
|
164
|
+
"example_row": example_data_row(cols),
|
|
165
|
+
"note_lines": note_lines,
|
|
166
|
+
"data_rows": [data_row(cols, sn=e["serial"]) for e in entries],
|
|
167
|
+
"ncols": len(cols),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if rbr_tr1050_entries:
|
|
171
|
+
sections.append(_rbr_section("rbr_tr1050_download_caldip", rbr_tr1050_entries))
|
|
172
|
+
if rbr_solot_entries:
|
|
173
|
+
sections.append(_rbr_section("rbr_solot_download_caldip", rbr_solot_entries))
|
|
174
|
+
if seapoint_entries:
|
|
175
|
+
sections.append(_rbr_section("rbr_solot_download_caldip", seapoint_entries))
|
|
176
|
+
|
|
177
|
+
tex_source = (
|
|
178
|
+
make_jinja_env(cfg.templates_dir)
|
|
179
|
+
.get_template("caldip_download.tex.j2")
|
|
180
|
+
.render(
|
|
181
|
+
cruise=cruise,
|
|
182
|
+
cast_label=cast_label,
|
|
183
|
+
sections=sections,
|
|
184
|
+
sheet_warnings=sheet_warnings,
|
|
185
|
+
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M"),
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
out_dir = cfg.output_dir / "caldip-download"
|
|
190
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
stem = f"{cast_label}_download"
|
|
192
|
+
try:
|
|
193
|
+
return render_sheet(tex_source, out_dir, stem, fmt)
|
|
194
|
+
except RuntimeError:
|
|
195
|
+
sys.exit(1)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""oceanarray.logsheet.builders.caldip_setup
|
|
2
|
+
============================================
|
|
3
|
+
Build caldip-setup 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
|
+
)
|
|
16
|
+
from .._firmware import fw_group, FW_GROUP_LABELS
|
|
17
|
+
from .._latex import colspec_from_cols, header_row, data_row, example_data_row
|
|
18
|
+
from .._columns import resolve_columns
|
|
19
|
+
from .._render import make_jinja_env, render_sheet
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_caldip_setup(
|
|
23
|
+
cast_id: str,
|
|
24
|
+
cfg: LogsheetsConfig,
|
|
25
|
+
fmt: str = "pdf",
|
|
26
|
+
caldip_yaml: "dict | None" = None,
|
|
27
|
+
) -> Path:
|
|
28
|
+
"""Build the caldip-setup PDF for a given cast.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
cast_id:
|
|
33
|
+
File path string (used for the output filename fallback when
|
|
34
|
+
*caldip_yaml* is provided) or a cast key in a legacy
|
|
35
|
+
``cruise_config.yaml → casts`` dict.
|
|
36
|
+
cfg:
|
|
37
|
+
Resolved logsheets configuration from :func:`~._config.resolve_config`.
|
|
38
|
+
fmt:
|
|
39
|
+
``"pdf"`` or ``"tex"``.
|
|
40
|
+
caldip_yaml:
|
|
41
|
+
Pre-loaded caldip YAML dict. The canonical format is a dict with a
|
|
42
|
+
top-level ``instruments:`` list; each entry has at minimum ``serial``
|
|
43
|
+
(int) and ``instrument`` (type string). When provided, cast info is
|
|
44
|
+
taken directly from this dict.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
col_defs = load_yaml(cfg.column_defs_path)
|
|
48
|
+
col_library = col_defs.get("column_library", {})
|
|
49
|
+
instruments = load_instruments(cfg)
|
|
50
|
+
|
|
51
|
+
# Global settings from logsheet_config.yaml (with safe fallback).
|
|
52
|
+
lsconf: dict = {}
|
|
53
|
+
if cfg.cruise_config_path.exists():
|
|
54
|
+
lsconf = load_yaml(cfg.cruise_config_path)
|
|
55
|
+
|
|
56
|
+
sentinel = lsconf.get("microcat_firmware_sentinel", "3.0d")
|
|
57
|
+
caldip_data = lsconf.get("paths", {}).get("caldip_data", "")
|
|
58
|
+
|
|
59
|
+
# Cast config: from explicit caldip_yaml, or looked up in legacy casts dict.
|
|
60
|
+
if caldip_yaml is not None:
|
|
61
|
+
cast_cfg = caldip_yaml
|
|
62
|
+
else:
|
|
63
|
+
cast_cfg = lsconf.get("casts", {}).get(cast_id)
|
|
64
|
+
if cast_cfg is None:
|
|
65
|
+
sys.exit(f"Cast {cast_id} not found in {cfg.cruise_config_path}")
|
|
66
|
+
|
|
67
|
+
# cruise may live in the caldip YAML itself or in logsheet_config.yaml.
|
|
68
|
+
cruise = cast_cfg.get("cruise") or lsconf.get("cruise", "")
|
|
69
|
+
|
|
70
|
+
# Label: try common key names, fall back to file stem.
|
|
71
|
+
cast_label = (
|
|
72
|
+
cast_cfg.get("label")
|
|
73
|
+
or cast_cfg.get("cast_label")
|
|
74
|
+
or cast_cfg.get("name")
|
|
75
|
+
or cast_cfg.get("cast")
|
|
76
|
+
or Path(cast_id).name.split(".")[0]
|
|
77
|
+
)
|
|
78
|
+
post_dep = cast_cfg.get("post_deployment", False)
|
|
79
|
+
|
|
80
|
+
all_mcats_by_group: dict[str, list[dict]] = {
|
|
81
|
+
"old_seaterm": [],
|
|
82
|
+
"seaterm_v2": [],
|
|
83
|
+
"seaterm_v2_odo": [],
|
|
84
|
+
}
|
|
85
|
+
all_aquadopps: list[dict] = []
|
|
86
|
+
rbr_tr1050_entries: list[dict] = []
|
|
87
|
+
rbr_solot_entries: list[dict] = []
|
|
88
|
+
seapoint_entries: list[dict] = []
|
|
89
|
+
sheet_warnings: list[str] = []
|
|
90
|
+
|
|
91
|
+
# Canonical caldip YAML format: top-level instruments: list.
|
|
92
|
+
# Each entry has serial (int) and instrument (type string).
|
|
93
|
+
# Classify each entry by looking up the serial in the inventory.
|
|
94
|
+
for item in cast_cfg.get("instruments", []):
|
|
95
|
+
sn = int(item["serial"])
|
|
96
|
+
try:
|
|
97
|
+
itype, entry = sn_to_entry(instruments, sn)
|
|
98
|
+
except KeyError:
|
|
99
|
+
sheet_warnings.append(f"SN {sn} not found in inventory -- skipped")
|
|
100
|
+
continue
|
|
101
|
+
if itype == "microcat":
|
|
102
|
+
all_mcats_by_group[fw_group(entry, sentinel)].append(entry)
|
|
103
|
+
elif itype == "aquadopp":
|
|
104
|
+
all_aquadopps.append(entry)
|
|
105
|
+
elif itype in ("rbrsolo", "rbrduet"):
|
|
106
|
+
rbr_solot_entries.append(entry)
|
|
107
|
+
elif itype == "tr1050":
|
|
108
|
+
rbr_tr1050_entries.append(entry)
|
|
109
|
+
elif itype == "seapoint":
|
|
110
|
+
seapoint_entries.append(entry)
|
|
111
|
+
|
|
112
|
+
sections = []
|
|
113
|
+
|
|
114
|
+
mcat_def = col_defs["microcat_setup_caldip"]
|
|
115
|
+
for grp, entries in all_mcats_by_group.items():
|
|
116
|
+
if not entries:
|
|
117
|
+
continue
|
|
118
|
+
cols = resolve_columns(mcat_def[f"columns_{grp}"], col_library)
|
|
119
|
+
sections.append(
|
|
120
|
+
{
|
|
121
|
+
"title": FW_GROUP_LABELS[grp],
|
|
122
|
+
"colspec": colspec_from_cols(cols),
|
|
123
|
+
"header_row": header_row(cols),
|
|
124
|
+
"example_row": example_data_row(cols),
|
|
125
|
+
"data_rows": [data_row(cols, sn=e["serial"]) for e in entries],
|
|
126
|
+
"post_deployment": post_dep,
|
|
127
|
+
"ncols": len(cols),
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if all_aquadopps:
|
|
132
|
+
cols = resolve_columns(
|
|
133
|
+
col_defs["aquadopp_setup_caldip"]["columns"], col_library
|
|
134
|
+
)
|
|
135
|
+
sections.append(
|
|
136
|
+
{
|
|
137
|
+
"title": "Aquadopp",
|
|
138
|
+
"colspec": colspec_from_cols(cols),
|
|
139
|
+
"header_row": header_row(cols),
|
|
140
|
+
"example_row": example_data_row(cols),
|
|
141
|
+
"data_rows": [data_row(cols, sn=e["serial"]) for e in all_aquadopps],
|
|
142
|
+
"post_deployment": False,
|
|
143
|
+
"ncols": len(cols),
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def _rbr_section(entries: list[dict], title: str) -> dict:
|
|
148
|
+
cols = resolve_columns(col_defs["rbr_setup_caldip"]["columns"], col_library)
|
|
149
|
+
return {
|
|
150
|
+
"title": title,
|
|
151
|
+
"colspec": colspec_from_cols(cols),
|
|
152
|
+
"header_row": header_row(cols),
|
|
153
|
+
"example_row": example_data_row(cols),
|
|
154
|
+
"data_rows": [data_row(cols, sn=e["serial"]) for e in entries],
|
|
155
|
+
"post_deployment": False,
|
|
156
|
+
"ncols": len(cols),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if rbr_tr1050_entries:
|
|
160
|
+
sections.append(_rbr_section(rbr_tr1050_entries, "RBR TR-1050 Thermistors"))
|
|
161
|
+
if rbr_solot_entries:
|
|
162
|
+
sections.append(_rbr_section(rbr_solot_entries, "RBR soloT Thermistors"))
|
|
163
|
+
if seapoint_entries:
|
|
164
|
+
sections.append(_rbr_section(seapoint_entries, "Seapoint Turbidity"))
|
|
165
|
+
|
|
166
|
+
caldip_data_tex = caldip_data.replace("~", r"\textasciitilde{}").replace("_", r"\_")
|
|
167
|
+
|
|
168
|
+
tex_source = (
|
|
169
|
+
make_jinja_env(cfg.templates_dir)
|
|
170
|
+
.get_template("caldip_setup.tex.j2")
|
|
171
|
+
.render(
|
|
172
|
+
cruise=cruise,
|
|
173
|
+
cast_label=cast_label,
|
|
174
|
+
moorings_str="",
|
|
175
|
+
caldip_data=caldip_data_tex,
|
|
176
|
+
sections=sections,
|
|
177
|
+
sheet_warnings=sheet_warnings,
|
|
178
|
+
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M"),
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
out_dir = cfg.output_dir / "caldip-setup"
|
|
183
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
184
|
+
stem = f"{cast_label}_setup"
|
|
185
|
+
try:
|
|
186
|
+
return render_sheet(tex_source, out_dir, stem, fmt)
|
|
187
|
+
except RuntimeError:
|
|
188
|
+
sys.exit(1)
|