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.
Files changed (76) hide show
  1. oceanarray/__init__.py +20 -0
  2. oceanarray/_version.py +1 -0
  3. oceanarray/cli.py +1343 -0
  4. oceanarray/clock_offset.py +675 -0
  5. oceanarray/config/OS1_sensor_attrs.yaml +49 -0
  6. oceanarray/config/OS1_var_names.yaml +5 -0
  7. oceanarray/config/OS1_vocab_attrs.yaml +84 -0
  8. oceanarray/config/legacy/project_RAPID.yaml +69 -0
  9. oceanarray/config/legacy/rodb_keys.txt +206 -0
  10. oceanarray/config/legacy/rodb_keys.yaml +1012 -0
  11. oceanarray/config/logging.yaml +28 -0
  12. oceanarray/legacy/__init__.py +47 -0
  13. oceanarray/legacy/caldip.py +753 -0
  14. oceanarray/legacy/convertOS.py +527 -0
  15. oceanarray/legacy/find_deployment.py +489 -0
  16. oceanarray/legacy/mooring_rodb.py +429 -0
  17. oceanarray/legacy/process_rodb.py +369 -0
  18. oceanarray/legacy/rodb.py +330 -0
  19. oceanarray/logger.py +192 -0
  20. oceanarray/logsheet/__init__.py +60 -0
  21. oceanarray/logsheet/_columns.py +97 -0
  22. oceanarray/logsheet/_config.py +245 -0
  23. oceanarray/logsheet/_firmware.py +43 -0
  24. oceanarray/logsheet/_latex.py +128 -0
  25. oceanarray/logsheet/_render.py +126 -0
  26. oceanarray/logsheet/builders/__init__.py +13 -0
  27. oceanarray/logsheet/builders/caldip_download.py +195 -0
  28. oceanarray/logsheet/builders/caldip_setup.py +188 -0
  29. oceanarray/logsheet/builders/deployment_setup.py +218 -0
  30. oceanarray/logsheet/builders/mooring_download.py +243 -0
  31. oceanarray/logsheet/builders/recovery.py +157 -0
  32. oceanarray/logsheet/data/column_defs.yaml +686 -0
  33. oceanarray/logsheet/data/latex_templates/_base_instrument.tex.j2 +42 -0
  34. oceanarray/logsheet/data/latex_templates/caldip_download.tex.j2 +47 -0
  35. oceanarray/logsheet/data/latex_templates/caldip_setup.tex.j2 +70 -0
  36. oceanarray/logsheet/data/latex_templates/deployment_setup.tex.j2 +97 -0
  37. oceanarray/logsheet/data/latex_templates/mooring_download.tex.j2 +54 -0
  38. oceanarray/logsheet/data/latex_templates/recovery.tex.j2 +103 -0
  39. oceanarray/mooring_level.py +1368 -0
  40. oceanarray/oceanarray.mplstyle +18 -0
  41. oceanarray/parameters.py +270 -0
  42. oceanarray/plotter.py +942 -0
  43. oceanarray/plotters/__init__.py +97 -0
  44. oceanarray/plotters/_animation.py +261 -0
  45. oceanarray/plotters/_current.py +705 -0
  46. oceanarray/plotters/_diagnostic.py +667 -0
  47. oceanarray/plotters/_helpers.py +57 -0
  48. oceanarray/plotters/_primitives.py +101 -0
  49. oceanarray/plotters/_section.py +16 -0
  50. oceanarray/plotters/_timeseries.py +11 -0
  51. oceanarray/rapid_interp.py +739 -0
  52. oceanarray/read_rbr_hex.py +541 -0
  53. oceanarray/readers.py +303 -0
  54. oceanarray/report/__init__.py +5 -0
  55. oceanarray/report/_array.py +386 -0
  56. oceanarray/report/_grid.py +539 -0
  57. oceanarray/report/_html_helpers.py +1004 -0
  58. oceanarray/report/_instrument.py +853 -0
  59. oceanarray/report/_mooring.py +1385 -0
  60. oceanarray/report/_plots.py +4284 -0
  61. oceanarray/report/_stack.py +941 -0
  62. oceanarray/stage1.py +1959 -0
  63. oceanarray/stage2.py +1160 -0
  64. oceanarray/stage3.py +2494 -0
  65. oceanarray/time_gridding.py +958 -0
  66. oceanarray/tools.py +400 -0
  67. oceanarray/transports.py +1 -0
  68. oceanarray/utilities.py +538 -0
  69. oceanarray/validation.py +389 -0
  70. oceanarray/writers.py +99 -0
  71. oceanarray-0.1.0.dist-info/METADATA +503 -0
  72. oceanarray-0.1.0.dist-info/RECORD +76 -0
  73. oceanarray-0.1.0.dist-info/WHEEL +5 -0
  74. oceanarray-0.1.0.dist-info/entry_points.txt +2 -0
  75. oceanarray-0.1.0.dist-info/licenses/LICENSE +21 -0
  76. 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)