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,853 @@
|
|
|
1
|
+
"""Per-instrument HTML report template and page generator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import socket
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from ._html_helpers import (
|
|
12
|
+
_duration_str,
|
|
13
|
+
_file_info,
|
|
14
|
+
_find_array_report_href,
|
|
15
|
+
_nav_buttons_html,
|
|
16
|
+
_parse_dt,
|
|
17
|
+
_parse_history,
|
|
18
|
+
_read_nc_metadata,
|
|
19
|
+
_read_qc_summary,
|
|
20
|
+
_read_qc_thresholds,
|
|
21
|
+
_safe_serial,
|
|
22
|
+
_stage_file_details,
|
|
23
|
+
)
|
|
24
|
+
from ._plots import (
|
|
25
|
+
_make_instrument_fig,
|
|
26
|
+
_make_windows_fig,
|
|
27
|
+
_make_ts_diagram,
|
|
28
|
+
_make_instrument_rose_b64,
|
|
29
|
+
_make_data_histogram,
|
|
30
|
+
_make_hodograph_b64,
|
|
31
|
+
_make_speed_boxplot,
|
|
32
|
+
_make_temperature_trajectory,
|
|
33
|
+
_make_analog_timeseries,
|
|
34
|
+
_make_adcp_velocity_b64,
|
|
35
|
+
_make_adcp_rose_b64,
|
|
36
|
+
_make_adcp_hodograph_b64,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Per-instrument HTML template
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
_INSTRUMENT_HTML_TEMPLATE = """\
|
|
45
|
+
<!DOCTYPE html>
|
|
46
|
+
<html lang="en">
|
|
47
|
+
<head>
|
|
48
|
+
<meta charset="UTF-8">
|
|
49
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
50
|
+
<title>{{ instr_type | title }} {{ serial }} — {{ mooring_name }}</title>
|
|
51
|
+
<style>
|
|
52
|
+
:root {
|
|
53
|
+
--ocean: #1a3a5c; --seafoam: #e8f4f8;
|
|
54
|
+
--good: #27ae60; --warn: #e67e22; --bad: #c0392b;
|
|
55
|
+
--interp: #2980b9; --muted: #95a5a6; --text: #2c3e50;
|
|
56
|
+
}
|
|
57
|
+
* { box-sizing: border-box; }
|
|
58
|
+
body { font-family: system-ui,-apple-system,"Segoe UI",sans-serif; font-size:14px;
|
|
59
|
+
color:var(--text); max-width:1150px; margin:0 auto; padding:1.5rem 2rem 4rem; line-height:1.5; }
|
|
60
|
+
.masthead { background:#1e8449; color:#fff; padding:1.6rem 2rem;
|
|
61
|
+
border-radius:8px; margin-bottom:2rem; }
|
|
62
|
+
.masthead h1 { margin:0 0 0.3rem; font-size:1.75rem; font-weight:700; letter-spacing:0.02em; }
|
|
63
|
+
.masthead .sub { font-size:0.9rem; opacity:0.88; margin:0 0 0.15rem; }
|
|
64
|
+
.masthead .sub a { color:#d5f5e3; font-weight:600; text-decoration:none; }
|
|
65
|
+
.masthead .sub a:hover { text-decoration:underline; }
|
|
66
|
+
.meta-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(190px,1fr));
|
|
67
|
+
gap:0.45rem 2rem; font-size:0.83rem; margin-top:0.9rem; }
|
|
68
|
+
.meta-grid dt { opacity:0.7; text-transform:uppercase; font-size:0.68rem;
|
|
69
|
+
letter-spacing:0.06em; margin-bottom:0.1rem; }
|
|
70
|
+
.meta-grid dd { margin:0; font-weight:600; }
|
|
71
|
+
.meta-miss dd { color:#e67e22; opacity:1; }
|
|
72
|
+
.file-table { width:100%; border-collapse:collapse; font-size:0.81rem; margin-bottom:1.2rem; }
|
|
73
|
+
.file-table th { background:var(--seafoam); text-align:left; padding:0.35rem 0.65rem;
|
|
74
|
+
border-bottom:2px solid #cde; font-weight:600; }
|
|
75
|
+
.file-table td { padding:0.3rem 0.65rem; border-bottom:1px solid #eef; vertical-align:middle; }
|
|
76
|
+
.file-table tr:nth-child(even) td { background:#f4f9fc; }
|
|
77
|
+
.file-table .ok { color:var(--good); font-weight:700; }
|
|
78
|
+
.file-table .miss { color:#bbb; }
|
|
79
|
+
h2 { color:var(--ocean); font-size:1rem; border-bottom:2px solid var(--seafoam);
|
|
80
|
+
padding-bottom:0.3rem; margin:2.2rem 0 0.9rem;
|
|
81
|
+
display:flex; justify-content:space-between; align-items:baseline; }
|
|
82
|
+
.top-link { font-size:0.72rem; font-weight:400; color:var(--muted);
|
|
83
|
+
text-decoration:none; margin-left:auto; white-space:nowrap; }
|
|
84
|
+
.top-link:hover { color:var(--ocean); text-decoration:underline; }
|
|
85
|
+
table { width:100%; border-collapse:collapse; font-size:0.82rem; }
|
|
86
|
+
th { background:var(--ocean); color:#fff; padding:0.4rem 0.65rem;
|
|
87
|
+
text-align:left; font-weight:600; white-space:nowrap; }
|
|
88
|
+
td { padding:0.35rem 0.65rem; border-bottom:1px solid #ecf0f1; vertical-align:middle; }
|
|
89
|
+
tr:nth-child(even) td { background:var(--seafoam); }
|
|
90
|
+
td.num { text-align:right; font-variant-numeric:tabular-nums; }
|
|
91
|
+
td.mono { font-family:monospace; font-size:0.8rem; }
|
|
92
|
+
.none-note { color:var(--muted); font-style:italic; }
|
|
93
|
+
.badge { display:inline-block; padding:0.12em 0.45em; border-radius:3px;
|
|
94
|
+
font-size:0.7rem; font-weight:700; white-space:nowrap; }
|
|
95
|
+
.b-ok { background:var(--good); color:#fff; }
|
|
96
|
+
.b-warn { background:var(--warn); color:#fff; }
|
|
97
|
+
.b-miss { background:#dfe6e9; color:#999; }
|
|
98
|
+
.history-list { list-style:none; padding:0; margin:0; }
|
|
99
|
+
.history-list li { display:flex; gap:1rem; padding:0.3rem 0;
|
|
100
|
+
border-bottom:1px solid #f0f0f0; font-size:0.83rem; }
|
|
101
|
+
.history-list li:last-child { border-bottom:none; }
|
|
102
|
+
.history-ts { color:var(--muted); white-space:nowrap; font-size:0.76rem;
|
|
103
|
+
min-width:11rem; padding-top:0.05rem; }
|
|
104
|
+
.history-text { flex:1; }
|
|
105
|
+
img.fig { width:100%; max-width:100%; border-radius:4px; margin-bottom:0.5rem; }
|
|
106
|
+
.qc-bar { display:flex; width:180px; height:13px; border-radius:3px;
|
|
107
|
+
overflow:hidden; gap:1px; background:#ecf0f1; }
|
|
108
|
+
.qc-bar div { height:100%; }
|
|
109
|
+
.var-qc { color:var(--good); font-size:0.78rem; }
|
|
110
|
+
.report-footer { margin-top:3rem; font-size:0.76rem; color:var(--muted);
|
|
111
|
+
border-top:1px solid #ecf0f1; padding-top:0.75rem; }
|
|
112
|
+
.jump-nav { background:var(--seafoam); padding:0.55rem 1rem; border-radius:6px;
|
|
113
|
+
margin-bottom:1.5rem; font-size:0.8rem; line-height:2.2; }
|
|
114
|
+
.jump-nav a { color:var(--ocean); text-decoration:none; font-weight:600;
|
|
115
|
+
margin:0 0.5rem 0 0; white-space:nowrap; }
|
|
116
|
+
.jump-nav a::before { content:"▸ "; font-size:0.7rem; }
|
|
117
|
+
.jump-nav a:hover { text-decoration:underline; }
|
|
118
|
+
@media print {
|
|
119
|
+
body { padding:0; max-width:100%; }
|
|
120
|
+
.masthead, th { -webkit-print-color-adjust:exact; print-color-adjust:exact; }
|
|
121
|
+
h2 { page-break-after:avoid; }
|
|
122
|
+
}
|
|
123
|
+
</style>
|
|
124
|
+
</head>
|
|
125
|
+
<body>
|
|
126
|
+
|
|
127
|
+
<div id="top" class="masthead">
|
|
128
|
+
<h1>{{ instr_type | title }} — s/n {{ serial }}</h1>
|
|
129
|
+
<p class="sub" style="display:flex;justify-content:space-between"><span>{{ mooring_name }}</span><span>generated {{ generated }}</span></p>
|
|
130
|
+
{{ nav_buttons | safe }}
|
|
131
|
+
<dl class="meta-grid">
|
|
132
|
+
<div{% if cruise == '—' %} class="meta-miss"{% endif %}><dt>Cruise</dt><dd>{{ cruise }}</dd></div>
|
|
133
|
+
<div><dt>HAB</dt><dd>{{ "%.1f"|format(hab) }} m</dd></div>
|
|
134
|
+
<div{% if depth is none %} class="meta-miss"{% endif %}><dt>Depth</dt><dd>{% if depth is not none %}~{{ "%.0f"|format(depth) }} m{% else %}—{% endif %}</dd></div>
|
|
135
|
+
<div{% if t_start is none or t_start == '—' %} class="meta-miss"{% endif %}><dt>Start</dt><dd>{{ t_start | default("—") }}</dd></div>
|
|
136
|
+
<div{% if t_end is none or t_end == '—' %} class="meta-miss"{% endif %}><dt>End</dt><dd>{{ t_end | default("—") }}</dd></div>
|
|
137
|
+
<div{% if duration == '—' %} class="meta-miss"{% endif %}><dt>Duration</dt><dd>{{ duration }}</dd></div>
|
|
138
|
+
<div{% if median_dt is none or median_dt == '—' %} class="meta-miss"{% endif %}><dt>Samp. Δt</dt><dd>{{ median_dt | default("—") }}</dd></div>
|
|
139
|
+
<div{% if n_records is none or n_records == '—' %} class="meta-miss"{% endif %}><dt>Records</dt><dd>{{ n_records | default("—") }}</dd></div>
|
|
140
|
+
<div><dt>Source file</dt><dd>{{ nc_file }}{% if data_stage and data_stage != 'stage3' %} <span style="color:#e67e22;font-weight:bold">({{ data_stage }} — QC not applied)</span>{% endif %}</dd></div>
|
|
141
|
+
</dl>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<nav class="jump-nav">
|
|
145
|
+
Jump to:
|
|
146
|
+
<a href="#files">Files</a>
|
|
147
|
+
<a href="#history">History</a>
|
|
148
|
+
{% if fig_adcp_velocity_b64 %}<a href="#adcp-velocity">Velocity</a>{% endif %}
|
|
149
|
+
<a href="#timeseries">Time series</a>
|
|
150
|
+
<a href="#start">Start/end windows</a>
|
|
151
|
+
{% if fig_tsd_b64 %}<a href="#ts">T-S diagram</a>{% endif %}
|
|
152
|
+
{% if fig_adcp_rose_b64 %}<a href="#adcp-rose">Current roses</a>{% endif %}
|
|
153
|
+
{% if fig_rose_b64 %}<a href="#rose">Current roses</a>{% endif %}
|
|
154
|
+
{% if fig_trajectory_b64 %}<a href="#trajectory">Trajectory</a>{% endif %}
|
|
155
|
+
{% if fig_hodograph_b64 %}<a href="#hodograph">Hodograph</a>{% endif %}
|
|
156
|
+
{% if fig_speed_boxplot_b64 %}<a href="#speed">Speed distribution</a>{% endif %}
|
|
157
|
+
{% if fig_analog_b64 %}<a href="#analog">Analog channels</a>{% endif %}
|
|
158
|
+
<a href="#dist">Distributions</a>
|
|
159
|
+
{% if qc_summary %}<a href="#qc">QC thresholds & flags</a>{% endif %}
|
|
160
|
+
{% if nc_meta.dims %}<a href="#dims">Dimensions</a>{% endif %}
|
|
161
|
+
<a href="#vars">Variables</a>
|
|
162
|
+
</nav>
|
|
163
|
+
|
|
164
|
+
<!-- ══ Files ══ -->
|
|
165
|
+
<h2 id="files">Files</h2>
|
|
166
|
+
<table class="file-table">
|
|
167
|
+
<thead><tr><th>Stage</th><th>Filename</th><th>Size</th><th>Last modified</th></tr></thead>
|
|
168
|
+
<tbody>
|
|
169
|
+
{% if raw_file.exists %}
|
|
170
|
+
<tr>
|
|
171
|
+
<td>Raw</td>
|
|
172
|
+
<td class="mono">{{ raw_file.name }}</td>
|
|
173
|
+
<td class="num">{{ raw_file.size }}</td>
|
|
174
|
+
<td>{{ raw_file.mtime }}</td>
|
|
175
|
+
</tr>
|
|
176
|
+
{% else %}
|
|
177
|
+
<tr>
|
|
178
|
+
<td>Raw</td>
|
|
179
|
+
<td class="mono miss">{{ raw_file.name }}</td>
|
|
180
|
+
<td colspan="2" class="miss">— not found</td>
|
|
181
|
+
</tr>
|
|
182
|
+
{% endif %}
|
|
183
|
+
{% for f in stage_files %}
|
|
184
|
+
<tr>
|
|
185
|
+
<td>{{ f.label }}</td>
|
|
186
|
+
{% if f.exists %}
|
|
187
|
+
<td class="mono">{{ f.name }}</td>
|
|
188
|
+
<td class="num">{{ f.size }}</td>
|
|
189
|
+
<td>{{ f.mtime }}</td>
|
|
190
|
+
{% else %}
|
|
191
|
+
<td class="mono miss">{{ f.name }}</td>
|
|
192
|
+
<td colspan="2" class="miss">— not generated</td>
|
|
193
|
+
{% endif %}
|
|
194
|
+
</tr>
|
|
195
|
+
{% endfor %}
|
|
196
|
+
</tbody>
|
|
197
|
+
</table>
|
|
198
|
+
|
|
199
|
+
<!-- ══ Processing history ══ -->
|
|
200
|
+
<h2 id="history">Processing history</h2>
|
|
201
|
+
{% if history_entries %}
|
|
202
|
+
<ul class="history-list">
|
|
203
|
+
{% for e in history_entries %}
|
|
204
|
+
<li>
|
|
205
|
+
<span class="history-ts">{{ e.timestamp }}</span>
|
|
206
|
+
<span class="history-text">{{ e.text }}</span>
|
|
207
|
+
</li>
|
|
208
|
+
{% endfor %}
|
|
209
|
+
</ul>
|
|
210
|
+
{% else %}
|
|
211
|
+
<p class="none-note">No history attribute found.</p>
|
|
212
|
+
{% endif %}
|
|
213
|
+
|
|
214
|
+
<!-- ══ ADCP velocity colour plots ══ -->
|
|
215
|
+
{% if fig_adcp_velocity_b64 %}
|
|
216
|
+
<h2 id="adcp-velocity">Velocity</h2>
|
|
217
|
+
<p style="font-size:0.8rem;color:#555;margin-top:-0.5rem;">
|
|
218
|
+
Time–range colour plots of the four velocity components (east, north, up, error).
|
|
219
|
+
Y-axis: range from transducer (m).
|
|
220
|
+
Colour scale: symmetric about zero; percentile 2/98 of all components combined.
|
|
221
|
+
{% if data_stage and data_stage != 'stage3' %}Magnetic declination has <b>not</b> been applied ({{ data_stage }} data).{% endif %}
|
|
222
|
+
</p>
|
|
223
|
+
<img class="fig" src="data:image/png;base64,{{ fig_adcp_velocity_b64 }}">
|
|
224
|
+
{% endif %}
|
|
225
|
+
|
|
226
|
+
<!-- ══ Full time series ══ -->
|
|
227
|
+
<h2 id="timeseries">Time series (full deployment)</h2>
|
|
228
|
+
{% if fig_ts_b64 %}
|
|
229
|
+
<img class="fig" src="data:image/png;base64,{{ fig_ts_b64 }}"
|
|
230
|
+
title="line = data; × = suspect; × = bad; + = interpolated">
|
|
231
|
+
{% else %}
|
|
232
|
+
<p class="none-note">No plottable variables found.</p>
|
|
233
|
+
{% endif %}
|
|
234
|
+
|
|
235
|
+
<!-- ══ Start / end windows ══ -->
|
|
236
|
+
<h2 id="start">Start & end windows — first / last 6 h</h2>
|
|
237
|
+
{% if fig_windows_b64 %}
|
|
238
|
+
<p style="font-size:0.8rem;color:#555;margin-top:-0.5rem;">
|
|
239
|
+
Left panel = first 7 h (1 h lead-in + 6 h of record) |
|
|
240
|
+
Right panel = last 7 h (6 h of record + 1 h tail).
|
|
241
|
+
<span style="color:#e67e22">││ orange dashed</span> = auto-detected
|
|
242
|
+
(pressure-based) suggested deploy/recover time •
|
|
243
|
+
<span style="color:#27ae60">││ green dashed</span> = YAML
|
|
244
|
+
<code>deployment_time</code> / <code>recovery_time</code> •
|
|
245
|
+
<span style="color:#cccccc">││ grey</span> = stage 1 raw record
|
|
246
|
+
(same variable, same units).
|
|
247
|
+
</p>
|
|
248
|
+
<p style="font-size:0.78rem;color:#777;margin-top:0.3rem;">
|
|
249
|
+
<strong>Reading the grey background:</strong>
|
|
250
|
+
where the raw stage 1 record is available and uses the same physical
|
|
251
|
+
units as the stage 2/3 data, it is shown in light grey so you can
|
|
252
|
+
visually verify that the chosen deployment window captures the in-water
|
|
253
|
+
period. Cases where grey may be absent:
|
|
254
|
+
<ul style="margin:0.3em 0 0.3em 1.2em;padding:0">
|
|
255
|
+
<li><em>Sensors without their own pressure</em> (e.g. a microCAT that
|
|
256
|
+
relies on interpolated pressure from a nearby instrument) — interpolated
|
|
257
|
+
pressure is only added at stage 3, so stage 1 has no pressure
|
|
258
|
+
variable and no grey trace appears.</li>
|
|
259
|
+
<li><em>Velocity in beam or XYZ coordinates</em> — if an Aquadopp or
|
|
260
|
+
ADCP was configured to record in BEAM or XYZ mode, the stage 1 file
|
|
261
|
+
contains beam/XYZ velocity variables while stage 3 stores
|
|
262
|
+
ENU velocities; the variable names differ so no grey velocity trace is shown.</li>
|
|
263
|
+
<li><em>Conductivity</em> — raw conductivity units sometimes differ
|
|
264
|
+
between stage 1 and stage 3 (e.g. mS/cm vs S/m) and the
|
|
265
|
+
grey trace is suppressed automatically to avoid a misleading scale mismatch.</li>
|
|
266
|
+
</ul>
|
|
267
|
+
If the <span style="color:#e67e22">orange vline</span> is absent from the right-hand panel,
|
|
268
|
+
no pressure-based recovery transition was detected in the final 25 % of the
|
|
269
|
+
stage 1 record — the suggested recovery time equals the last raw sample.
|
|
270
|
+
Check the timing table in the mooring summary report for the suggested UTC time.
|
|
271
|
+
</p>
|
|
272
|
+
<img class="fig" src="data:image/png;base64,{{ fig_windows_b64 }}">
|
|
273
|
+
{% else %}
|
|
274
|
+
<p class="none-note">Insufficient data for start/end windows.</p>
|
|
275
|
+
{% endif %}
|
|
276
|
+
|
|
277
|
+
<!-- ══ T-S diagram ══ -->
|
|
278
|
+
{% if fig_tsd_b64 %}
|
|
279
|
+
<h2 id="ts">T-S diagram</h2>
|
|
280
|
+
<p style="font-size:0.8rem;color:#555;margin-top:-0.5rem;">
|
|
281
|
+
Coloured by pressure (or sample index). × = suspect | × = bad (QC flags).
|
|
282
|
+
</p>
|
|
283
|
+
<img class="fig" src="data:image/png;base64,{{ fig_tsd_b64 }}">
|
|
284
|
+
{% endif %}
|
|
285
|
+
|
|
286
|
+
<!-- ══ ADCP current roses ══ -->
|
|
287
|
+
{% if fig_adcp_rose_b64 %}
|
|
288
|
+
<h2 id="adcp-rose">Current roses</h2>
|
|
289
|
+
<p style="font-size:0.8rem;color:#555;margin-top:-0.5rem;">
|
|
290
|
+
Depth-average followed by bins nearest 100, 200, 300 and 400 m from the transducer.
|
|
291
|
+
Bins with fewer than two valid samples are omitted.
|
|
292
|
+
Direction toward which the current flows; 0° = N, clockwise.
|
|
293
|
+
{% if data_stage and data_stage != 'stage3' %}Magnetic declination not yet applied ({{ data_stage }} data).{% endif %}
|
|
294
|
+
</p>
|
|
295
|
+
<img class="fig" src="data:image/png;base64,{{ fig_adcp_rose_b64 }}">
|
|
296
|
+
{% endif %}
|
|
297
|
+
|
|
298
|
+
<!-- ══ Current roses ══ -->
|
|
299
|
+
{% if fig_rose_b64 %}
|
|
300
|
+
<h2 id="rose">Current rose diagrams</h2>
|
|
301
|
+
{% if declination_warn %}
|
|
302
|
+
<p style="font-size:0.82rem;background:#fff8e1;border-left:4px solid #f9a825;padding:0.5rem 0.8rem;margin-bottom:0.6rem;">
|
|
303
|
+
⚠ Magnetic declination could not be applied — latitude/longitude are missing or all-zero in the mooring YAML (check <code>seabed_latitude</code>, <code>deployment_latitude</code>, or <code>latitude</code>/<code>longitude</code>).
|
|
304
|
+
ENU velocities use 0° declination (magnetic north, not true north).
|
|
305
|
+
</p>
|
|
306
|
+
{% endif %}
|
|
307
|
+
<p style="font-size:0.8rem;color:#555;margin-top:-0.5rem;">
|
|
308
|
+
{% if rose_has_xyz %}XYZ: instrument-frame velocities (before geographic rotation). {% endif %}ENU panels split by QARTOD flag: good (flag ≤ 2, Blues), suspect (flag 3, Oranges), fail (flag 4, Reds).
|
|
309
|
+
Direction toward which the current flows; 0° = N, clockwise.
|
|
310
|
+
</p>
|
|
311
|
+
<img class="fig" style="max-width:66%" src="data:image/png;base64,{{ fig_rose_b64 }}">
|
|
312
|
+
{% endif %}
|
|
313
|
+
|
|
314
|
+
<!-- ══ Lagrangian trajectory ══ -->
|
|
315
|
+
{% if fig_trajectory_b64 %}
|
|
316
|
+
<h2 id="trajectory">Particle trajectory</h2>
|
|
317
|
+
<p style="font-size:0.8rem;color:#555;margin-top:-0.5rem;">
|
|
318
|
+
Pseudo-Lagrangian displacement obtained by integrating east/north velocity over time
|
|
319
|
+
(Euler forward; NaN velocities set to zero). Coloured by temperature.
|
|
320
|
+
Origin (0, 0) = deployment position; axes in metres.
|
|
321
|
+
</p>
|
|
322
|
+
<img class="fig" src="data:image/png;base64,{{ fig_trajectory_b64 }}" style="max-width:600px;">
|
|
323
|
+
{% endif %}
|
|
324
|
+
|
|
325
|
+
<!-- ══ Hodograph ══ -->
|
|
326
|
+
{% if fig_hodograph_b64 %}
|
|
327
|
+
<h2 id="hodograph">Hodograph</h2>
|
|
328
|
+
<p style="font-size:0.8rem;color:#555;margin-top:-0.5rem;">
|
|
329
|
+
East vs north velocity (m s<sup>-1</sup>).
|
|
330
|
+
<b>Left</b>: full record.
|
|
331
|
+
<b>Right</b>: eddy component — raw minus 4-day low-pass (rolling mean).
|
|
332
|
+
</p>
|
|
333
|
+
<img class="fig" src="data:image/png;base64,{{ fig_hodograph_b64 }}">
|
|
334
|
+
{% endif %}
|
|
335
|
+
|
|
336
|
+
<!-- ══ Speed distribution ══ -->
|
|
337
|
+
{% if fig_speed_boxplot_b64 %}
|
|
338
|
+
<h2 id="speed">Current speed distribution</h2>
|
|
339
|
+
<img class="fig" src="data:image/png;base64,{{ fig_speed_boxplot_b64 }}" style="max-width:300px;">
|
|
340
|
+
{% endif %}
|
|
341
|
+
|
|
342
|
+
<!-- ══ Analog channels ══ -->
|
|
343
|
+
{% if fig_analog_b64 %}
|
|
344
|
+
<h2 id="analog">Analog channels</h2>
|
|
345
|
+
<p class="note">
|
|
346
|
+
Full-record time series of analog channel variables containing non-zero, non-NaN data.
|
|
347
|
+
One panel per channel.
|
|
348
|
+
Labels and units are read from the mooring YAML — update them there to change what appears here.
|
|
349
|
+
</p>
|
|
350
|
+
{% if analog_yaml_info %}
|
|
351
|
+
<ul style="font-size:0.8rem;margin:0.3rem 0 0.8rem 1.5rem;color:#555;">
|
|
352
|
+
{% for ch in analog_yaml_info %}
|
|
353
|
+
<li>
|
|
354
|
+
<code>{{ ch.varname }}</code> — from YAML:
|
|
355
|
+
<code>{{ ch.yaml_key }}: {{ ch.label if ch.label else "(not set)" }}</code>{% if ch.units %},
|
|
356
|
+
<code>{{ ch.yaml_key }}_units: "{{ ch.units }}"</code>{% endif %}{% if ch.serial %},
|
|
357
|
+
<code>{{ ch.yaml_key }}_serial_number: {{ ch.serial }}</code>{% endif %}
|
|
358
|
+
</li>
|
|
359
|
+
{% endfor %}
|
|
360
|
+
</ul>
|
|
361
|
+
{% endif %}
|
|
362
|
+
<img class="fig" src="data:image/png;base64,{{ fig_analog_b64 }}">
|
|
363
|
+
{% endif %}
|
|
364
|
+
|
|
365
|
+
<!-- ══ Data distributions ══ -->
|
|
366
|
+
<h2 id="dist">Data value distributions</h2>
|
|
367
|
+
<p style="font-size:0.8rem;color:#555;margin-top:-0.5rem;">
|
|
368
|
+
Orange dashed = suspect threshold | Red dotted = fail threshold (gross-range QC).
|
|
369
|
+
Histogram shows non-bad data only; bad-flagged count noted in red.
|
|
370
|
+
</p>
|
|
371
|
+
{% if fig_dt_b64 %}
|
|
372
|
+
<img class="fig" src="data:image/png;base64,{{ fig_dt_b64 }}">
|
|
373
|
+
{% else %}
|
|
374
|
+
<p class="none-note">Not enough samples to compute.</p>
|
|
375
|
+
{% endif %}
|
|
376
|
+
|
|
377
|
+
<!-- ══ QC flag breakdown ══ -->
|
|
378
|
+
{% if qc_summary %}
|
|
379
|
+
<h2 id="qc">QC flag breakdown</h2>
|
|
380
|
+
|
|
381
|
+
{% if qc_thresholds %}
|
|
382
|
+
<h3 style="font-size:0.85rem;color:var(--ocean);margin:0.8rem 0 0.3rem;">Thresholds applied (as stored in stage 3 file)</h3>
|
|
383
|
+
<table style="font-size:0.82rem;margin-bottom:1rem;">
|
|
384
|
+
<thead>
|
|
385
|
+
<tr>
|
|
386
|
+
<th>Variable</th>
|
|
387
|
+
<th>Test</th>
|
|
388
|
+
<th>Suspect range / threshold</th>
|
|
389
|
+
<th>Fail range / threshold</th>
|
|
390
|
+
</tr>
|
|
391
|
+
</thead>
|
|
392
|
+
<tbody>
|
|
393
|
+
{% for row in qc_thresholds %}
|
|
394
|
+
<tr>
|
|
395
|
+
<td class="mono">{{ row.var }}</td>
|
|
396
|
+
<td>{{ row.test }}</td>
|
|
397
|
+
<td class="num" style="color:var(--warn);font-family:monospace">{{ row.suspect }}</td>
|
|
398
|
+
<td class="num" style="color:var(--bad);font-family:monospace">{{ row.fail }}</td>
|
|
399
|
+
</tr>
|
|
400
|
+
{% endfor %}
|
|
401
|
+
</tbody>
|
|
402
|
+
</table>
|
|
403
|
+
{% endif %}
|
|
404
|
+
|
|
405
|
+
<h3 style="font-size:0.85rem;color:var(--ocean);margin:0.8rem 0 0.3rem;">Flag counts</h3>
|
|
406
|
+
<table>
|
|
407
|
+
<thead>
|
|
408
|
+
<tr>
|
|
409
|
+
<th>Variable</th>
|
|
410
|
+
<th class="num">N</th>
|
|
411
|
+
<th class="num">Good %</th>
|
|
412
|
+
<th class="num">Suspect %</th>
|
|
413
|
+
<th class="num">Bad %</th>
|
|
414
|
+
<th class="num">Interp. %</th>
|
|
415
|
+
<th class="num">Missing %</th>
|
|
416
|
+
<th>Distribution</th>
|
|
417
|
+
</tr>
|
|
418
|
+
</thead>
|
|
419
|
+
<tbody>
|
|
420
|
+
{% for row in qc_summary %}
|
|
421
|
+
{% set good = row.flags | selectattr("flag", "eq", 1) | first %}
|
|
422
|
+
{% set susp = row.flags | selectattr("flag", "eq", 3) | first %}
|
|
423
|
+
{% set bad = row.flags | selectattr("flag", "eq", 4) | first %}
|
|
424
|
+
{% set interp = row.flags | selectattr("flag", "eq", 8) | first %}
|
|
425
|
+
{% set miss = row.flags | selectattr("flag", "eq", 9) | first %}
|
|
426
|
+
<tr>
|
|
427
|
+
<td class="mono">{{ row.var }}</td>
|
|
428
|
+
<td class="num">{{ "{:,}".format(row.total) }}</td>
|
|
429
|
+
<td class="num" style="color:{% if good.pct >= 95 %}var(--good){% elif good.pct >= 80 %}var(--warn){% else %}var(--bad){% endif %}">{{ good.pct }}</td>
|
|
430
|
+
<td class="num">{% if susp.n > 0 %}<span style="color:var(--warn)" title="{{ susp.n }} records">{% if susp.pct > 0 %}{{ susp.pct }}{% else %}<0.1% ({{ susp.n }}){% endif %}</span>{% else %}–{% endif %}</td>
|
|
431
|
+
<td class="num">{% if bad.n > 0 %}<span style="color:var(--bad)" title="{{ bad.n }} records">{% if bad.pct > 0 %}{{ bad.pct }}{% else %}<0.1% ({{ bad.n }}){% endif %}</span>{% else %}–{% endif %}</td>
|
|
432
|
+
<td class="num">{% if interp.n > 0 %}<span style="color:var(--interp)" title="{{ interp.n }} records">{% if interp.pct > 0 %}{{ interp.pct }}{% else %}<0.1% ({{ interp.n }}){% endif %}</span>{% else %}–{% endif %}</td>
|
|
433
|
+
<td class="num">{% if miss.n > 0 %}<span title="{{ miss.n }} records">{% if miss.pct > 0 %}{{ miss.pct }}{% else %}<0.1% ({{ miss.n }}){% endif %}</span>{% else %}–{% endif %}</td>
|
|
434
|
+
<td>
|
|
435
|
+
<div class="qc-bar">
|
|
436
|
+
{% for f in row.flags %}{% if f.pct > 0 %}
|
|
437
|
+
<div style="width:{{ f.pct }}%;background:{{ f.color }};" title="{{ f.label }}: {{ f.pct }}%"></div>
|
|
438
|
+
{% endif %}{% endfor %}
|
|
439
|
+
</div>
|
|
440
|
+
</td>
|
|
441
|
+
</tr>
|
|
442
|
+
{% endfor %}
|
|
443
|
+
</tbody>
|
|
444
|
+
</table>
|
|
445
|
+
{% endif %}
|
|
446
|
+
|
|
447
|
+
<!-- ══ NetCDF dimensions ══ -->
|
|
448
|
+
{% if nc_meta.dims %}
|
|
449
|
+
<h2 id="dims">NetCDF dimensions — {{ nc_file }}</h2>
|
|
450
|
+
<table class="var-table" style="max-width:28rem">
|
|
451
|
+
<thead><tr><th>Dimension</th><th class="num">Size</th></tr></thead>
|
|
452
|
+
<tbody>
|
|
453
|
+
{% for dim, size in nc_meta.dims.items() %}
|
|
454
|
+
<tr><td class="mono">{{ dim }}</td><td class="num">{{ "{:,}".format(size) }}</td></tr>
|
|
455
|
+
{% endfor %}
|
|
456
|
+
</tbody>
|
|
457
|
+
</table>
|
|
458
|
+
{% endif %}
|
|
459
|
+
|
|
460
|
+
<!-- ══ NetCDF variables ══ -->
|
|
461
|
+
<h2 id="vars">NetCDF variables — {{ nc_file }}</h2>
|
|
462
|
+
{% if nc_meta.get("error") %}
|
|
463
|
+
<p class="none-note">Could not read file: {{ nc_meta.error }}</p>
|
|
464
|
+
{% else %}
|
|
465
|
+
|
|
466
|
+
<h3 style="font-size:0.88rem;color:var(--ocean);margin:1rem 0 0.4rem;">Time-series variables</h3>
|
|
467
|
+
<table>
|
|
468
|
+
<thead>
|
|
469
|
+
<tr><th>Variable</th><th>Type</th><th>Dims</th><th class="num">N</th><th class="num">Valid</th><th class="num">Min / Max</th><th>Units</th><th>Long name</th><th>Standard name</th><th>QC flag</th></tr>
|
|
470
|
+
</thead>
|
|
471
|
+
<tbody>
|
|
472
|
+
{% for v in nc_meta.time_vars %}
|
|
473
|
+
{% if not v.is_qc %}
|
|
474
|
+
<tr>
|
|
475
|
+
<td class="mono">{{ v.name }}</td>
|
|
476
|
+
<td class="mono" style="font-size:0.75rem">{{ v.dtype }}</td>
|
|
477
|
+
<td class="mono" style="font-size:0.75rem">{{ v.dims }}</td>
|
|
478
|
+
<td class="num">{{ "{:,}".format(v.n) }}</td>
|
|
479
|
+
<td class="num" {% if v.n_valid is defined and v.n_valid < v.n %}style="color:#c0392b;font-weight:600"{% endif %}>{{ "{:,}".format(v.n_valid) if v.n_valid is defined else "—" }}</td>
|
|
480
|
+
<td class="num" style="font-size:0.76rem;white-space:nowrap">{% if v.v_min is not none %}{{ v.v_min }} / {{ v.v_max }}{% else %}—{% endif %}</td>
|
|
481
|
+
<td>{{ v.units }}</td>
|
|
482
|
+
<td>{{ v.long_name }}</td>
|
|
483
|
+
<td style="font-size:0.78rem;color:var(--muted);max-width:18rem;word-break:break-all">{{ v.standard_name }}</td>
|
|
484
|
+
<td style="text-align:center">{% if v.has_qc %}<span class="var-qc">✓</span>{% else %}–{% endif %}</td>
|
|
485
|
+
</tr>
|
|
486
|
+
{% endif %}
|
|
487
|
+
{% endfor %}
|
|
488
|
+
</tbody>
|
|
489
|
+
</table>
|
|
490
|
+
|
|
491
|
+
{% if nc_meta.scalar_vars %}
|
|
492
|
+
<h3 style="font-size:0.88rem;color:var(--ocean);margin:1.4rem 0 0.4rem;">Scalar metadata variables</h3>
|
|
493
|
+
<table>
|
|
494
|
+
<thead>
|
|
495
|
+
<tr><th>Variable</th><th>Type</th><th>Value</th><th>Units</th><th>Long name</th></tr>
|
|
496
|
+
</thead>
|
|
497
|
+
<tbody>
|
|
498
|
+
{% for v in nc_meta.scalar_vars %}
|
|
499
|
+
<tr>
|
|
500
|
+
<td class="mono">{{ v.name }}</td>
|
|
501
|
+
<td class="mono" style="font-size:0.75rem">{{ v.dtype }}</td>
|
|
502
|
+
<td class="mono" style="font-size:0.78rem;word-break:break-all">{{ v.value }}</td>
|
|
503
|
+
<td>{{ v.units }}</td>
|
|
504
|
+
<td>{{ v.long_name }}</td>
|
|
505
|
+
</tr>
|
|
506
|
+
{% endfor %}
|
|
507
|
+
</tbody>
|
|
508
|
+
</table>
|
|
509
|
+
{% endif %}
|
|
510
|
+
|
|
511
|
+
{% if nc_meta.global_attrs %}
|
|
512
|
+
<h3 style="font-size:0.88rem;color:var(--ocean);margin:1.4rem 0 0.4rem;">Global attributes</h3>
|
|
513
|
+
<table>
|
|
514
|
+
<thead><tr><th>Attribute</th><th>Value</th></tr></thead>
|
|
515
|
+
<tbody>
|
|
516
|
+
{% for k, v in nc_meta.global_attrs.items() %}
|
|
517
|
+
<tr>
|
|
518
|
+
<td class="mono">{{ k }}</td>
|
|
519
|
+
<td style="font-size:0.8rem;word-break:break-all">{{ v }}</td>
|
|
520
|
+
</tr>
|
|
521
|
+
{% endfor %}
|
|
522
|
+
</tbody>
|
|
523
|
+
</table>
|
|
524
|
+
{% endif %}
|
|
525
|
+
|
|
526
|
+
{% endif %}
|
|
527
|
+
|
|
528
|
+
<div class="report-footer">
|
|
529
|
+
Generated by <strong>oceanarray</strong> on {{ generated }}{% if proc_machine %} • {{ proc_machine }}{% endif %}
|
|
530
|
+
</div>
|
|
531
|
+
<script>
|
|
532
|
+
document.querySelectorAll('h2').forEach(h => {
|
|
533
|
+
const a = document.createElement('a'); a.href = '#top'; a.className = 'top-link'; a.textContent = '↑ top'; h.appendChild(a);
|
|
534
|
+
});
|
|
535
|
+
</script>
|
|
536
|
+
</body>
|
|
537
|
+
</html>
|
|
538
|
+
"""
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
# ---------------------------------------------------------------------------
|
|
542
|
+
# Page generator
|
|
543
|
+
# ---------------------------------------------------------------------------
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def generate_instrument_pages(
|
|
547
|
+
mooring_name: str,
|
|
548
|
+
instruments: List[Dict[str, Any]],
|
|
549
|
+
cfg: Dict[str, Any],
|
|
550
|
+
proc_dir: Path,
|
|
551
|
+
out_dir: Path,
|
|
552
|
+
force: bool,
|
|
553
|
+
base_dir: Path,
|
|
554
|
+
serials: Optional[List[str]] = None,
|
|
555
|
+
raw_dir: Optional[Path] = None,
|
|
556
|
+
) -> None:
|
|
557
|
+
"""Generate one HTML quality-control report page per instrument.
|
|
558
|
+
|
|
559
|
+
For each instrument in *instruments*, reads the best available processed
|
|
560
|
+
NetCDF (stage3 preferred, falling back to stage2 or stage1) and renders an
|
|
561
|
+
HTML page with time-series plots, data-quality flags, a histogram panel,
|
|
562
|
+
hodograph (if velocity data are present), and provenance metadata. Pages are
|
|
563
|
+
written to ``{out_dir}/instrument/{mooring_name}_{serial}_report.html``.
|
|
564
|
+
|
|
565
|
+
Parameters
|
|
566
|
+
----------
|
|
567
|
+
mooring_name : str
|
|
568
|
+
Mooring identifier (e.g. ``"dsG3_1_2026"``).
|
|
569
|
+
instruments : list of dict
|
|
570
|
+
Instrument records extracted from the mooring YAML, each containing at
|
|
571
|
+
minimum ``serial``, ``instr_type``, and ``stages`` keys.
|
|
572
|
+
cfg : dict
|
|
573
|
+
Full mooring configuration dictionary (from the mooring YAML).
|
|
574
|
+
proc_dir : Path
|
|
575
|
+
Mooring-level processed output directory (e.g. ``proc/dsG3_1_2026/``).
|
|
576
|
+
out_dir : Path
|
|
577
|
+
Report output directory for this mooring (e.g. ``proc/dsG3_1_2026/report/``).
|
|
578
|
+
Per-instrument pages are placed in ``out_dir/instrument/``.
|
|
579
|
+
force : bool
|
|
580
|
+
If True, regenerate pages that already exist.
|
|
581
|
+
base_dir : Path
|
|
582
|
+
Legacy cruise-level base directory. Used to resolve raw file paths in
|
|
583
|
+
legacy mode (when *raw_dir* is None).
|
|
584
|
+
serials : list of str, optional
|
|
585
|
+
If provided, only generate pages for instruments whose serial number is in
|
|
586
|
+
this list. Useful for regenerating a single instrument without reprocessing
|
|
587
|
+
the full mooring.
|
|
588
|
+
raw_dir : Path, optional
|
|
589
|
+
Cruise-level raw data directory for the new directory layout. When given,
|
|
590
|
+
raw file existence is checked at ``{raw_dir}/{mooring}/{instr_type}/filename``.
|
|
591
|
+
When None, *base_dir* is used instead (legacy layout).
|
|
592
|
+
|
|
593
|
+
"""
|
|
594
|
+
mooring_report_link = f"../{mooring_name}_report.html"
|
|
595
|
+
generated = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
596
|
+
proc_machine = socket.gethostname().split(".")[0]
|
|
597
|
+
stack_exists = (proc_dir / f"{mooring_name}_stack.nc").exists()
|
|
598
|
+
grid_exists = (proc_dir / f"{mooring_name}_grid.nc").exists()
|
|
599
|
+
_d = cfg.get("deployment_cruise") or cfg.get("cruise", "—")
|
|
600
|
+
_r = cfg.get("recovery_cruise") or cfg.get("cruise") or _d
|
|
601
|
+
cruise = _d if _d == _r else f"{_d} / {_r}"
|
|
602
|
+
serial_filter = {_safe_serial(s) for s in serials} if serials else None
|
|
603
|
+
|
|
604
|
+
idx = 0
|
|
605
|
+
for instr in instruments:
|
|
606
|
+
serial = instr["serial"]
|
|
607
|
+
if serial_filter and serial not in serial_filter:
|
|
608
|
+
continue
|
|
609
|
+
instr_type = instr["instr_type"]
|
|
610
|
+
out_path = out_dir / "instrument" / f"{mooring_name}_{serial}_report.html"
|
|
611
|
+
prefix = f" [{idx:2d}] {instr_type:<12} s/n {serial:<12}"
|
|
612
|
+
idx += 1
|
|
613
|
+
|
|
614
|
+
stages = instr.get("stages", {})
|
|
615
|
+
if not any(stages.get(s) for s in ("stage1", "stage2", "stage3")):
|
|
616
|
+
print(f"{prefix} [skipped — no processed files]")
|
|
617
|
+
continue
|
|
618
|
+
|
|
619
|
+
if out_path.exists() and not force:
|
|
620
|
+
print(f"{prefix} {out_path.name} [exists]")
|
|
621
|
+
continue
|
|
622
|
+
|
|
623
|
+
_base = proc_dir / instr_type / f"{mooring_name}_{serial}"
|
|
624
|
+
stage3_nc = Path(str(_base) + "_stage3.nc")
|
|
625
|
+
stage3_nc = stage3_nc if stage3_nc.exists() else None
|
|
626
|
+
_stage2_nc = Path(str(_base) + "_stage2.nc")
|
|
627
|
+
_stage1_nc = Path(str(_base) + "_stage1.nc")
|
|
628
|
+
best_nc = (
|
|
629
|
+
stage3_nc
|
|
630
|
+
or (_stage2_nc if _stage2_nc.exists() else None)
|
|
631
|
+
or (_stage1_nc if _stage1_nc.exists() else None)
|
|
632
|
+
)
|
|
633
|
+
data_stage = (
|
|
634
|
+
"stage3"
|
|
635
|
+
if stage3_nc
|
|
636
|
+
else (
|
|
637
|
+
"stage2"
|
|
638
|
+
if _stage2_nc.exists()
|
|
639
|
+
else ("stage1" if _stage1_nc.exists() else None)
|
|
640
|
+
)
|
|
641
|
+
)
|
|
642
|
+
nc_file = best_nc.name if best_nc else "—"
|
|
643
|
+
|
|
644
|
+
nc_info = instr.get("nc", {}) or {}
|
|
645
|
+
n_records = nc_info.get("n_records", "—")
|
|
646
|
+
t_start = nc_info.get("t_start", "—")
|
|
647
|
+
t_end = nc_info.get("t_end", "—")
|
|
648
|
+
dt_s = nc_info.get("dt_s")
|
|
649
|
+
median_dt = f"{dt_s:.0f} s" if dt_s and dt_s == dt_s else "—"
|
|
650
|
+
duration = _duration_str(_parse_dt(t_start), _parse_dt(t_end))
|
|
651
|
+
|
|
652
|
+
history_entries: List[Dict[str, str]] = []
|
|
653
|
+
_sugg_deploy_utc: Optional[str] = None
|
|
654
|
+
_sugg_recover_utc: Optional[str] = None
|
|
655
|
+
if best_nc:
|
|
656
|
+
try:
|
|
657
|
+
import xarray as xr
|
|
658
|
+
|
|
659
|
+
with xr.open_dataset(best_nc, decode_timedelta=False) as _ds:
|
|
660
|
+
history_entries = _parse_history(_ds.attrs.get("history", ""))
|
|
661
|
+
_sugg_deploy_utc = _ds.attrs.get("suggested_deployment_time_utc")
|
|
662
|
+
_sugg_recover_utc = _ds.attrs.get("suggested_recovery_time_utc")
|
|
663
|
+
except Exception:
|
|
664
|
+
pass
|
|
665
|
+
|
|
666
|
+
# Build vertical-line markers for the start/end window plots.
|
|
667
|
+
# Orange = auto-detected from pressure; green = set in the YAML.
|
|
668
|
+
_yaml_deploy_str = cfg.get("deployment_time")
|
|
669
|
+
_yaml_recover_str = cfg.get("recovery_time")
|
|
670
|
+
# PyYAML may parse bare ISO datetime values as datetime objects; normalise to
|
|
671
|
+
# ISO strings so the vlines normalizer in _plots.py can handle them uniformly.
|
|
672
|
+
if hasattr(_yaml_deploy_str, "isoformat"):
|
|
673
|
+
_yaml_deploy_str = _yaml_deploy_str.isoformat()
|
|
674
|
+
if hasattr(_yaml_recover_str, "isoformat"):
|
|
675
|
+
_yaml_recover_str = _yaml_recover_str.isoformat()
|
|
676
|
+
_window_vlines: List[tuple] = []
|
|
677
|
+
if _sugg_deploy_utc:
|
|
678
|
+
_window_vlines.append((_sugg_deploy_utc, "#e67e22", "Sugg. deploy"))
|
|
679
|
+
if _sugg_recover_utc:
|
|
680
|
+
_window_vlines.append((_sugg_recover_utc, "#e67e22", "Sugg. recover"))
|
|
681
|
+
if _yaml_deploy_str:
|
|
682
|
+
_window_vlines.append((_yaml_deploy_str, "#27ae60", "YAML deploy"))
|
|
683
|
+
if _yaml_recover_str:
|
|
684
|
+
_window_vlines.append((_yaml_recover_str, "#27ae60", "YAML recover"))
|
|
685
|
+
|
|
686
|
+
# File listing — raw source and stage1/2/3 NC files
|
|
687
|
+
raw_filename = instr.get("filename", "")
|
|
688
|
+
if raw_filename:
|
|
689
|
+
if raw_dir is not None:
|
|
690
|
+
# New layout: {raw_dir}/{mooring}/{instrument}/filename
|
|
691
|
+
raw_file_path = raw_dir / mooring_name / instr_type / raw_filename
|
|
692
|
+
else:
|
|
693
|
+
# Legacy layout: {base_dir}/{directory}/{instrument}/filename
|
|
694
|
+
raw_file_path = (
|
|
695
|
+
base_dir
|
|
696
|
+
/ str(cfg.get("directory", "raw")).rstrip("/")
|
|
697
|
+
/ instr_type
|
|
698
|
+
/ raw_filename
|
|
699
|
+
)
|
|
700
|
+
else:
|
|
701
|
+
raw_file_path = Path("no_raw_file")
|
|
702
|
+
raw_file = (
|
|
703
|
+
_file_info(raw_file_path)
|
|
704
|
+
if raw_filename
|
|
705
|
+
else {"exists": False, "name": raw_filename or "—", "size": "", "mtime": ""}
|
|
706
|
+
)
|
|
707
|
+
stage_files = _stage_file_details(proc_dir, instr_type, mooring_name, serial)
|
|
708
|
+
|
|
709
|
+
ctx = {
|
|
710
|
+
"mooring_name": mooring_name,
|
|
711
|
+
"cruise": cruise,
|
|
712
|
+
"serial": serial,
|
|
713
|
+
"instr_type": instr_type,
|
|
714
|
+
"hab": instr["hab"],
|
|
715
|
+
"depth": instr["depth"],
|
|
716
|
+
"n_records": (
|
|
717
|
+
f"{n_records:,}" if isinstance(n_records, int) else n_records
|
|
718
|
+
),
|
|
719
|
+
"t_start": t_start,
|
|
720
|
+
"t_end": t_end,
|
|
721
|
+
"duration": duration,
|
|
722
|
+
"median_dt": median_dt,
|
|
723
|
+
"nc_file": nc_file,
|
|
724
|
+
"raw_file": raw_file,
|
|
725
|
+
"stage_files": stage_files,
|
|
726
|
+
"mooring_report_link": mooring_report_link,
|
|
727
|
+
"nav_buttons": _nav_buttons_html(
|
|
728
|
+
mooring_name,
|
|
729
|
+
instruments,
|
|
730
|
+
stack_exists=stack_exists,
|
|
731
|
+
grid_exists=grid_exists,
|
|
732
|
+
current_report=serial,
|
|
733
|
+
in_instrument_subdir=True,
|
|
734
|
+
array_report_href=_find_array_report_href(
|
|
735
|
+
out_dir, in_instrument_subdir=True
|
|
736
|
+
),
|
|
737
|
+
),
|
|
738
|
+
"generated": generated,
|
|
739
|
+
"proc_machine": proc_machine,
|
|
740
|
+
"history_entries": history_entries,
|
|
741
|
+
"fig_ts_b64": (
|
|
742
|
+
_make_instrument_fig(best_nc, instr_type) if best_nc else None
|
|
743
|
+
),
|
|
744
|
+
"fig_windows_b64": (
|
|
745
|
+
_make_windows_fig(
|
|
746
|
+
best_nc,
|
|
747
|
+
instr_type,
|
|
748
|
+
vlines=_window_vlines,
|
|
749
|
+
stage1_nc=_stage1_nc if _stage1_nc.exists() else None,
|
|
750
|
+
)
|
|
751
|
+
if best_nc
|
|
752
|
+
else None
|
|
753
|
+
),
|
|
754
|
+
"fig_tsd_b64": _make_ts_diagram(best_nc) if best_nc else None,
|
|
755
|
+
"fig_rose_b64": (
|
|
756
|
+
_make_instrument_rose_b64(best_nc)
|
|
757
|
+
if best_nc and instr_type.lower() != "adcp"
|
|
758
|
+
else None
|
|
759
|
+
),
|
|
760
|
+
"fig_adcp_velocity_b64": (
|
|
761
|
+
_make_adcp_velocity_b64(best_nc)
|
|
762
|
+
if best_nc and instr_type.lower() == "adcp"
|
|
763
|
+
else None
|
|
764
|
+
),
|
|
765
|
+
"fig_adcp_rose_b64": (
|
|
766
|
+
_make_adcp_rose_b64(best_nc)
|
|
767
|
+
if best_nc and instr_type.lower() in ("adcp", "rdi")
|
|
768
|
+
else None
|
|
769
|
+
),
|
|
770
|
+
"fig_trajectory_b64": (
|
|
771
|
+
_make_temperature_trajectory(best_nc)
|
|
772
|
+
if best_nc and instr_type.lower() == "aquadopp"
|
|
773
|
+
else None
|
|
774
|
+
),
|
|
775
|
+
"fig_speed_boxplot_b64": (
|
|
776
|
+
_make_speed_boxplot(best_nc)
|
|
777
|
+
if best_nc and instr_type.lower() == "aquadopp"
|
|
778
|
+
else None
|
|
779
|
+
),
|
|
780
|
+
"fig_hodograph_b64": (
|
|
781
|
+
_make_hodograph_b64(best_nc)
|
|
782
|
+
if best_nc and instr_type.lower() == "aquadopp"
|
|
783
|
+
else (
|
|
784
|
+
_make_adcp_hodograph_b64(best_nc)
|
|
785
|
+
if best_nc and instr_type.lower() in ("adcp", "rdi")
|
|
786
|
+
else None
|
|
787
|
+
)
|
|
788
|
+
),
|
|
789
|
+
"fig_dt_b64": _make_data_histogram(best_nc) if best_nc else None,
|
|
790
|
+
"qc_summary": _read_qc_summary(stage3_nc) if stage3_nc else [],
|
|
791
|
+
"qc_thresholds": _read_qc_thresholds(stage3_nc) if stage3_nc else [],
|
|
792
|
+
"nc_meta": _read_nc_metadata(best_nc) if best_nc else {},
|
|
793
|
+
"data_stage": data_stage, # "stage1", "stage2", "stage3", or None
|
|
794
|
+
}
|
|
795
|
+
analog_vars = ctx["nc_meta"].get("analog_vars", [])
|
|
796
|
+
ctx["fig_analog_b64"] = (
|
|
797
|
+
_make_analog_timeseries(best_nc, analog_vars)
|
|
798
|
+
if best_nc and analog_vars
|
|
799
|
+
else None
|
|
800
|
+
)
|
|
801
|
+
# Look up the per-instrument YAML entry for analog metadata.
|
|
802
|
+
# Search both 'clamp' and 'inline' lists (the serial in inline entries
|
|
803
|
+
# may contain a comma; _safe_serial strips it to the first token).
|
|
804
|
+
_instr_entries = list(cfg.get("clamp", cfg.get("instruments", []))) + [
|
|
805
|
+
e
|
|
806
|
+
for e in cfg.get("inline", [])
|
|
807
|
+
if isinstance(e, dict) and "instrument" in e
|
|
808
|
+
]
|
|
809
|
+
_yaml_entry: Dict[str, Any] = next(
|
|
810
|
+
(
|
|
811
|
+
e
|
|
812
|
+
for e in _instr_entries
|
|
813
|
+
if _safe_serial(str(e.get("serial", "")).split(",")[0].strip())
|
|
814
|
+
== serial
|
|
815
|
+
),
|
|
816
|
+
{},
|
|
817
|
+
)
|
|
818
|
+
# Build per-channel YAML-source info for display in the template.
|
|
819
|
+
# Variable name in NC is always "analog_input_{n}" (seasenselib native).
|
|
820
|
+
# YAML key matches: analog_input_{n}, analog_input_{n}_units, etc.
|
|
821
|
+
analog_yaml_info: List[Dict[str, str]] = []
|
|
822
|
+
for _av in analog_vars:
|
|
823
|
+
_n = _av.replace("analog_input_", "").replace("analog_", "")
|
|
824
|
+
_yaml_key = f"analog_input_{_n}"
|
|
825
|
+
analog_yaml_info.append(
|
|
826
|
+
{
|
|
827
|
+
"varname": _av,
|
|
828
|
+
"yaml_key": _yaml_key,
|
|
829
|
+
"label": str(_yaml_entry.get(_yaml_key, "")),
|
|
830
|
+
"units": str(_yaml_entry.get(f"{_yaml_key}_units", "")),
|
|
831
|
+
"serial": str(_yaml_entry.get(f"{_yaml_key}_serial_number", "")),
|
|
832
|
+
}
|
|
833
|
+
)
|
|
834
|
+
ctx["analog_yaml_info"] = analog_yaml_info
|
|
835
|
+
# Warn if magnetic declination was not applied (lat/lon missing from YAML)
|
|
836
|
+
ctx["declination_warn"] = (
|
|
837
|
+
instr_type.lower() == "aquadopp"
|
|
838
|
+
and "magnetic_declination" not in ctx["nc_meta"].get("global_attrs", {})
|
|
839
|
+
)
|
|
840
|
+
# True when the NC contains instrument-frame XYZ velocities (rose has XYZ panel)
|
|
841
|
+
_tvar_names = {v["name"] for v in ctx["nc_meta"].get("time_vars", [])}
|
|
842
|
+
ctx["rose_has_xyz"] = "velocity_x" in _tvar_names
|
|
843
|
+
|
|
844
|
+
try:
|
|
845
|
+
from jinja2 import Environment
|
|
846
|
+
|
|
847
|
+
env = Environment(autoescape=True)
|
|
848
|
+
html = env.from_string(_INSTRUMENT_HTML_TEMPLATE).render(**ctx)
|
|
849
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
850
|
+
out_path.write_text(html, encoding="utf-8")
|
|
851
|
+
print(f"{prefix} {out_path.name}")
|
|
852
|
+
except Exception as exc:
|
|
853
|
+
print(f"{prefix} ERROR: {exc}")
|