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,539 @@
1
+ """Grid report HTML template and page generator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any, Dict
7
+
8
+ import numpy as np
9
+
10
+ from ._html_helpers import (
11
+ _find_array_report_href,
12
+ _nav_buttons_html,
13
+ _parse_history,
14
+ _read_nc_metadata,
15
+ _status,
16
+ )
17
+ from ._plots import (
18
+ _make_grid_hydro_b64,
19
+ _make_grid_hodograph_b64,
20
+ _make_grid_rose_b64,
21
+ _make_grid_rotary_spectrum_b64,
22
+ _make_grid_sigma_b64,
23
+ _make_grid_timeseries_b64,
24
+ _make_grid_trajectory_b64,
25
+ _make_grid_ts_diagram,
26
+ _make_grid_n2_b64,
27
+ _make_grid_velocity_stacked_b64,
28
+ _make_isopycnal_fig_b64,
29
+ _make_spectrum_fig_b64,
30
+ _make_velocity_iqr_profile_b64,
31
+ )
32
+ from .. import parameters as P
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Grid report HTML template
37
+ # ---------------------------------------------------------------------------
38
+
39
+ _GRID_HTML_TEMPLATE = """\
40
+ <!DOCTYPE html>
41
+ <html lang="en">
42
+ <head>
43
+ <meta charset="UTF-8">
44
+ <meta name="viewport" content="width=device-width, initial-scale=1">
45
+ <title>Grid report – {{ mooring_name }}</title>
46
+ <style>
47
+ :root { --ocean:#1a3a5c; --seafoam:#e8f4f8; --muted:#95a5a6; --text:#2c3e50; }
48
+ * { box-sizing:border-box; }
49
+ body { font-family:system-ui,-apple-system,"Segoe UI",sans-serif; font-size:14px;
50
+ color:var(--text); max-width:1200px; margin:0 auto; padding:1.5rem 2rem 4rem; }
51
+ .masthead { background:#8e44ad; color:#fff; padding:1.6rem 2rem;
52
+ border-radius:8px; margin-bottom:2rem; }
53
+ .masthead h1 { margin:0 0 0.3rem; font-size:1.75rem; font-weight:700; letter-spacing:0.02em; }
54
+ .masthead .sub { font-size:0.9rem; opacity:0.88; margin:0 0 0.15rem; }
55
+ .masthead .sub a { color:#e8d5ff; font-weight:600; text-decoration:none; }
56
+ .masthead .sub a:hover { text-decoration:underline; }
57
+ .meta-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(190px,1fr));
58
+ gap:0.5rem 2rem; font-size:0.84rem; margin-top:0.9rem; }
59
+ .meta-grid dt { opacity:0.7; text-transform:uppercase; font-size:0.7rem;
60
+ letter-spacing:0.06em; margin-bottom:0.1rem; }
61
+ .meta-grid dd { margin:0; font-weight:600; }
62
+ .meta-miss dd { color:#e67e22; opacity:1; }
63
+ h2 { color:var(--ocean); font-size:1rem; border-bottom:2px solid var(--seafoam);
64
+ padding-bottom:0.3rem; margin:2.5rem 0 1rem;
65
+ display:flex; justify-content:space-between; align-items:baseline; }
66
+ .top-link { font-size:0.72rem; font-weight:400; color:var(--muted);
67
+ text-decoration:none; margin-left:auto; white-space:nowrap; }
68
+ .top-link:hover { color:var(--ocean); text-decoration:underline; }
69
+ .fig { width:100%; border:1px solid #dce; border-radius:4px; margin-bottom:0.5rem; }
70
+ .note { color:var(--muted); font-size:0.82rem; margin-top:-0.5rem; }
71
+ .style-label { font-size:0.8rem; font-weight:600; color:var(--muted); margin:0.4rem 0 0.2rem; text-transform:uppercase; letter-spacing:0.05em; }
72
+ .var-table { width:100%; border-collapse:collapse; font-size:0.82rem; margin-bottom:1.5rem; }
73
+ .var-table th { background:var(--seafoam); text-align:left; padding:0.4rem 0.6rem; border-bottom:2px solid #cde; }
74
+ .var-table td { padding:0.3rem 0.6rem; border-bottom:1px solid #eef; vertical-align:top; }
75
+ .var-table tr:nth-child(even) td { background:#f4f9fc; }
76
+ .var-table tr:hover td { background:#e8f4f8; }
77
+ .report-footer { margin-top:3rem; font-size:0.76rem; color:var(--muted); border-top:1px solid #eee; padding-top:0.8rem; }
78
+ .jump-nav { background:var(--seafoam); padding:0.55rem 1rem; border-radius:6px;
79
+ margin-bottom:1.5rem; font-size:0.8rem; line-height:2.2; }
80
+ .jump-nav a { color:var(--ocean); text-decoration:none; font-weight:600;
81
+ margin:0 0.5rem 0 0; white-space:nowrap; }
82
+ .jump-nav a::before { content:"▸ "; font-size:0.7rem; }
83
+ .jump-nav a:hover { text-decoration:underline; }
84
+ .history-list { list-style:none; padding:0; margin:0; }
85
+ .history-list li { display:flex; gap:1rem; padding:0.3rem 0; border-bottom:1px solid #f0f0f0; font-size:0.83rem; }
86
+ .history-list li:last-child { border-bottom:none; }
87
+ .history-ts { color:var(--muted); white-space:nowrap; font-size:0.76rem; min-width:11rem; }
88
+ .history-text { flex:1; }
89
+ td.num { text-align:right; font-variant-numeric:tabular-nums; }
90
+ td.mono { font-family:monospace; font-size:0.8rem; }
91
+ .none-note { color:var(--muted); font-style:italic; }
92
+ .var-qc { color:var(--good); font-size:0.78rem; }
93
+ details summary.collapse-toggle { color:var(--muted); font-size:0.76rem; cursor:pointer;
94
+ list-style:none; padding:0.15rem 0 0.4rem; user-select:none; }
95
+ details summary.collapse-toggle::before { content:"▾ "; }
96
+ details[open] summary.collapse-toggle::before { content:"▴ "; }
97
+ @media print { .masthead { -webkit-print-color-adjust:exact; print-color-adjust:exact; } }
98
+ </style>
99
+ </head>
100
+ <body>
101
+
102
+ <div id="top" class="masthead">
103
+ <h1 style="display:flex;justify-content:space-between;align-items:baseline;gap:1rem"><span>{{ mooring_name }}</span><span style="font-size:1.2rem;opacity:0.8;white-space:nowrap">Gridded</span></h1>
104
+ <p class="sub" style="text-align:right">generated {{ generated }}</p>
105
+ {{ nav_buttons | safe }}
106
+ <dl class="meta-grid">
107
+ <div{% if cruise == '—' %} class="meta-miss"{% endif %}><dt>Cruise</dt><dd>{{ cruise }}</dd></div>
108
+ <div{% if ship == '—' %} class="meta-miss"{% endif %}><dt>Ship</dt><dd>{{ ship }}</dd></div>
109
+ <div{% if latitude == '—' %} class="meta-miss"{% endif %}><dt>Latitude</dt><dd>{{ latitude }}</dd></div>
110
+ <div{% if longitude == '—' %} class="meta-miss"{% endif %}><dt>Longitude</dt><dd>{{ longitude }}</dd></div>
111
+ <div{% if waterdepth == '—' %} class="meta-miss"{% endif %}><dt>Water depth</dt><dd>{{ waterdepth }}{% if waterdepth != '—' %}&thinsp;m{% endif %}</dd></div>
112
+ <div{% if deploy_time == '—' %} class="meta-miss"{% endif %}><dt>Deployment</dt><dd>{{ deploy_time }}</dd></div>
113
+ <div{% if recover_time == '—' %} class="meta-miss"{% endif %}><dt>Recovery</dt><dd>{{ recover_time }}</dd></div>
114
+ <div{% if duration == '—' %} class="meta-miss"{% endif %}><dt>Duration</dt><dd>{{ duration }}</dd></div>
115
+ <div{% if grid_dt_s == '—' %} class="meta-miss"{% endif %}><dt>Samp.&nbsp;&Delta;t</dt><dd>{{ grid_dt_s }}{% if grid_dt_s != '—' %}&thinsp;s{% endif %}</dd></div>
116
+ <div><dt>Records</dt><dd>{{ n_time }}</dd></div>
117
+ <div{% if grid_dp == '—' %} class="meta-miss"{% endif %}><dt>Grid&nbsp;&Delta;P</dt><dd>{{ grid_dp }}</dd></div>
118
+ <div><dt>Pressure&nbsp;levels</dt><dd>{{ n_levels }}</dd></div>
119
+ <div><dt>Pressure&nbsp;range</dt><dd>{{ p_range }}</dd></div>
120
+ <div><dt>Instruments</dt><dd>{{ n_instr }}</dd></div>
121
+ <div><dt>Source&nbsp;file</dt><dd>{{ nc_file }}</dd></div>
122
+ </dl>
123
+ </div>
124
+
125
+ <nav class="jump-nav">
126
+ Jump to:
127
+ {% if history_entries %}<a href="#history">History</a>{% endif %}
128
+ {% if fig_hydro_b64 %}<a href="#hydro">Hydrography</a>{% endif %}
129
+ {% if fig_vel_stacked_b64 %}<a href="#vel">Velocity</a>{% endif %}
130
+ {% if fig_ts_grid_b64 %}<a href="#ts">T-S diagram</a>{% endif %}
131
+ {% if fig_vel_iqr_b64 %}<a href="#vel-iqr">Velocity profiles</a>{% endif %}
132
+ {% if fig_grid_rose_b64 %}<a href="#grid-rose">Current roses</a>{% endif %}
133
+ {% if fig_grid_hodograph_b64 %}<a href="#grid-hodograph">Hodograph</a>{% endif %}
134
+ {% if fig_grid_traj_b64 %}<a href="#grid-traj">Particle trajectory</a>{% endif %}
135
+ {% if fig_grid_ts_b64 %}<a href="#grid-ts">Velocity time series</a>{% endif %}
136
+ {% if fig_sigma_b64 or fig_n2_b64 %}<a href="#strat">Stratification</a>{% endif %}
137
+ {% if fig_spectrum_b64 %}<a href="#spectrum">Power spectrum</a>{% endif %}
138
+ {% if fig_rotary_b64 %}<a href="#rotary">Rotary spectrum</a>{% endif %}
139
+ <a href="#vars">Variables</a>
140
+ </nav>
141
+
142
+ {% if history_entries %}
143
+ <h2 id="history">Processing history</h2>
144
+ <ul class="history-list">
145
+ {% for e in history_entries %}
146
+ <li>
147
+ <span class="history-ts">{{ e.timestamp }}</span>
148
+ <span class="history-text">{{ e.text }}</span>
149
+ </li>
150
+ {% endfor %}
151
+ </ul>
152
+ {% endif %}
153
+
154
+ <!-- ══ Hydrography: T and S ══ -->
155
+ {% if fig_hydro_b64 %}
156
+ <h2 id="hydro">Hydrography</h2>
157
+ <p class="note">Temperature, salinity, dissolved oxygen, and O₂ saturation (when present). Vertically interpolated to regular pressure grid &bull; 20 discrete colour levels.</p>
158
+ <details open><summary class="collapse-toggle">show / hide</summary>
159
+ <img class="fig" src="data:image/png;base64,{{ fig_hydro_b64 }}" alt="Temperature and salinity">
160
+ </details>
161
+ {% endif %}
162
+
163
+ <!-- ══ Velocity: E / N / Up ══ -->
164
+ {% if fig_vel_stacked_b64 %}
165
+ <h2 id="vel">Velocity</h2>
166
+ <p class="note">East, north, and up velocity. QC-flagged samples excluded before interpolation. Shared symmetric Spectral_r colormap for east/north; separate bounds for up. No temporal gap fill — NaN where no data.</p>
167
+ <details open><summary class="collapse-toggle">show / hide</summary>
168
+ <img class="fig" src="data:image/png;base64,{{ fig_vel_stacked_b64 }}" alt="Velocity pcolormesh">
169
+ </details>
170
+ {% endif %}
171
+
172
+ <!-- ══ T-S diagram (before derived quantities) ══ -->
173
+ {% if fig_ts_grid_b64 %}
174
+ <h2 id="ts">T-S diagram</h2>
175
+ <p class="note">Left: log₁₀(count+1) per T-S bin. Right (when O₂ present): median O₂ saturation per T-S bin — bins with &lt;5 samples masked. Colour scale: BrBG (brown=low, teal=high).</p>
176
+ <details open><summary class="collapse-toggle">show / hide</summary>
177
+ <img class="fig" src="data:image/png;base64,{{ fig_ts_grid_b64 }}" alt="T-S diagram">
178
+ </details>
179
+ {% endif %}
180
+
181
+ <!-- ══ Velocity IQR profiles ══ -->
182
+ {% if fig_vel_iqr_b64 %}
183
+ <h2 id="vel-iqr">Velocity IQR profiles</h2>
184
+ <p class="note">Median (solid line) and interquartile range (shaded, 25–75 %) across the full deployment at each pressure level. 2.5–97.5 % outer envelope for speed. Right panel: count of non-NaN values per depth.</p>
185
+ <details open><summary class="collapse-toggle">show / hide</summary>
186
+ <img class="fig" style="max-width:85%" src="data:image/png;base64,{{ fig_vel_iqr_b64 }}" alt="Velocity IQR profiles">
187
+ </details>
188
+ {% endif %}
189
+
190
+ <!-- ══ Current roses per pressure level ══ -->
191
+ {% if fig_grid_rose_b64 %}
192
+ <h2 id="grid-rose">Current roses</h2>
193
+ <p class="note">One rose per pressure level (up to 12, evenly subsampled). Suspect/bad QC excluded. Each petal shows the fraction of time current flowed in that direction; petal length encodes speed (m s⁻¹).</p>
194
+ <details open><summary class="collapse-toggle">show / hide</summary>
195
+ <img class="fig" src="data:image/png;base64,{{ fig_grid_rose_b64 }}" alt="Current roses by pressure level">
196
+ </details>
197
+ {% endif %}
198
+
199
+ <!-- ══ Hodograph ══ -->
200
+ {% if fig_grid_hodograph_b64 %}
201
+ <h2 id="grid-hodograph">Hodograph</h2>
202
+ <p class="note">Current hodographs at two pressure levels (25th and 75th percentile of the valid range). Top row = shallower level; bottom row = deeper level. Left = Tukey-smoothed raw; right = eddy component (LP mean removed). Colour indicates fractional time through the deployment (lime circle = start, red square = end). QC-bad data excluded.</p>
203
+ <details open><summary class="collapse-toggle">show / hide</summary>
204
+ <img class="fig" src="data:image/png;base64,{{ fig_grid_hodograph_b64 }}" alt="Hodograph at two pressure levels">
205
+ </details>
206
+ {% endif %}
207
+
208
+ <!-- ══ Particle trajectory ══ -->
209
+ {% if fig_grid_traj_b64 %}
210
+ <h2 id="grid-traj">Particle trajectory</h2>
211
+ <p class="note">Pseudo-Lagrangian trajectories at each gridded pressure level, integrated from east and north velocity (Euler forward). All start at the origin. Coloured by pressure (dbar).</p>
212
+ <details open><summary class="collapse-toggle">show / hide</summary>
213
+ <img class="fig" style="max-width:55%" src="data:image/png;base64,{{ fig_grid_traj_b64 }}" alt="Grid particle trajectory">
214
+ </details>
215
+ {% endif %}
216
+
217
+ <!-- ══ Velocity time series at depth of max speed ══ -->
218
+ {% if fig_grid_ts_b64 %}
219
+ <h2 id="grid-ts">Velocity time series at depth of maximum mean speed</h2>
220
+ <p class="note">Depth chosen as the pressure level with the highest time-mean current speed. Speed, east, and north velocity at that level.</p>
221
+ <details open><summary class="collapse-toggle">show / hide</summary>
222
+ <img class="fig" src="data:image/png;base64,{{ fig_grid_ts_b64 }}" alt="Velocity time series at max-speed depth">
223
+ </details>
224
+ {% endif %}
225
+
226
+ <!-- ══ Stratification: sigma0 + N² + isopycnal depths + spectrum ══ -->
227
+ {% if fig_sigma_b64 or fig_n2_b64 %}
228
+ <h2 id="strat">Stratification</h2>
229
+ {% endif %}
230
+
231
+ {% if fig_sigma_b64 %}
232
+ <p class="note">Potential density. 20 discrete colour levels.</p>
233
+ <details open><summary class="collapse-toggle">show / hide sigma0</summary>
234
+ <img class="fig" src="data:image/png;base64,{{ fig_sigma_b64 }}" alt="Potential density pcolormesh">
235
+ </details>
236
+ {% endif %}
237
+
238
+ {% if fig_n2_b64 %}
239
+ <h3 style="font-size:0.9rem;color:var(--ocean);margin:1.5rem 0 0.4rem;">Buoyancy frequency squared (N²)</h3>
240
+ <p class="note">log₁₀(N²) in s⁻². Purple = strongly stratified; yellow = weakly stratified. Computed from T and S via GSW.</p>
241
+ <details open><summary class="collapse-toggle">show / hide N²</summary>
242
+ <img class="fig" src="data:image/png;base64,{{ fig_n2_b64 }}" alt="N² pcolormesh">
243
+ </details>
244
+ {% endif %}
245
+
246
+ {% for sec in sigma_sections %}
247
+ {% if sec.isopycnal_zoom_b64 %}
248
+ <h3 style="font-size:0.9rem;color:var(--ocean);margin:1.5rem 0 0.4rem;">Isopycnal depths &mdash; {{ sec.name }} (3-day zoom, unfiltered)</h3>
249
+ <p class="note">3-day window centred on deployment midpoint &bull; raw gridded data.</p>
250
+ <details open><summary class="collapse-toggle">show / hide</summary>
251
+ <img class="fig" src="data:image/png;base64,{{ sec.isopycnal_zoom_b64 }}" alt="Isopycnal depths zoom">
252
+ </details>
253
+ {% endif %}
254
+ {% if sec.isopycnal_b64 %}
255
+ <h3 style="font-size:0.9rem;color:var(--ocean);margin:1.5rem 0 0.4rem;">Isopycnal depths &mdash; {{ sec.name }} (full record, 24 h Tukey filtered)</h3>
256
+ <p class="note">Full deployment &bull; 24 h Tukey (α=0.5) moving-average applied before contouring.</p>
257
+ <details open><summary class="collapse-toggle">show / hide</summary>
258
+ <img class="fig" src="data:image/png;base64,{{ sec.isopycnal_b64 }}" alt="Isopycnal depths">
259
+ </details>
260
+ {% endif %}
261
+ {% endfor %}
262
+
263
+ {% if fig_spectrum_b64 %}
264
+ <h3 style="font-size:0.9rem;color:var(--ocean);margin:1.5rem 0 0.4rem;" id="spectrum">Temperature power spectrum</h3>
265
+ <p class="note">Welch PSD (Hann window, 14-day segments, 50% overlap). One line per depth level; colour indicates pressure (shallow = light blue, deep = dark blue). Dashed vertical lines mark tidal and inertial frequencies. Dashed black line: &minus;2 spectral slope reference.</p>
266
+ <details open><summary class="collapse-toggle">show / hide</summary>
267
+ <img class="fig" src="data:image/png;base64,{{ fig_spectrum_b64 }}" alt="Temperature power spectrum">
268
+ </details>
269
+ {% endif %}
270
+
271
+ {% if fig_rotary_b64 %}
272
+ <h2 id="rotary">Rotary velocity spectrum</h2>
273
+ <p class="note">CW (clockwise, anticyclonic, solid red lines) and CCW (counter-clockwise, cyclonic, dashed blue lines) power spectra and rotary coefficient r&nbsp;=&nbsp;(CCW&minus;CW)/(CCW+CW). Welch PSD, Hann window, 14-day segments, 50&nbsp;% overlap. Up to 4 pressure levels (at most 1/5th of valid levels); colour encodes pressure depth. Vertical lines: M2, K1, 1.8&nbsp;d, 4&nbsp;d, and inertial period (f). r&nbsp;&gt;&nbsp;0&nbsp;=&nbsp;CCW dominant; r&nbsp;&lt;&nbsp;0&nbsp;=&nbsp;CW dominant. Physical interpretation (NH): inertial oscillations are inherently CW; for internal waves (f&nbsp;&lt;&nbsp;&omega;), CW dominance indicates upward energy propagation / downward phase propagation (Leaman &amp; Sanford 1975).</p>
274
+ <details open><summary class="collapse-toggle">show / hide</summary>
275
+ <img class="fig" src="data:image/png;base64,{{ fig_rotary_b64 }}" alt="Rotary velocity spectrum">
276
+ </details>
277
+ {% endif %}
278
+
279
+ <!-- ══ NetCDF variables ══ -->
280
+ <h2 id="vars">NetCDF variables &mdash; {{ nc_file }}</h2>
281
+ {% if nc_meta.get("error") %}
282
+ <p class="none-note">Could not read file: {{ nc_meta.error }}</p>
283
+ {% else %}
284
+
285
+ <h3 style="font-size:0.88rem;color:var(--ocean);margin:1rem 0 0.4rem;">Variables</h3>
286
+ <table class="var-table">
287
+ <thead>
288
+ <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&nbsp;flag</th></tr>
289
+ </thead>
290
+ <tbody>
291
+ {% for v in nc_meta.time_vars %}
292
+ {% if not v.is_qc %}
293
+ <tr>
294
+ <td class="mono">{{ v.name }}</td>
295
+ <td class="mono" style="font-size:0.75rem">{{ v.dtype }}</td>
296
+ <td class="mono" style="font-size:0.75rem">{{ v.dims }}</td>
297
+ <td class="num">{{ "{:,}".format(v.n) }}</td>
298
+ <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 "&mdash;" }}</td>
299
+ <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 %}&mdash;{% endif %}</td>
300
+ <td>{{ v.units }}</td>
301
+ <td>{{ v.long_name }}</td>
302
+ <td style="font-size:0.78rem;color:var(--muted)">{{ v.standard_name }}</td>
303
+ <td style="text-align:center">{% if v.has_qc %}<span class="var-qc">✓</span>{% else %}&ndash;{% endif %}</td>
304
+ </tr>
305
+ {% endif %}
306
+ {% endfor %}
307
+ </tbody>
308
+ </table>
309
+
310
+ {% if nc_meta.scalar_vars %}
311
+ <h3 style="font-size:0.88rem;color:var(--ocean);margin:1.4rem 0 0.4rem;">Scalar metadata variables</h3>
312
+ <table class="var-table">
313
+ <thead>
314
+ <tr><th>Variable</th><th>Value</th><th>Units</th><th>Long name</th></tr>
315
+ </thead>
316
+ <tbody>
317
+ {% for v in nc_meta.scalar_vars %}
318
+ <tr>
319
+ <td class="mono">{{ v.name }}</td>
320
+ <td class="mono" style="font-size:0.78rem;word-break:break-all">{{ v.value }}</td>
321
+ <td>{{ v.units }}</td>
322
+ <td>{{ v.long_name }}</td>
323
+ </tr>
324
+ {% endfor %}
325
+ </tbody>
326
+ </table>
327
+ {% endif %}
328
+
329
+ {% if nc_meta.global_attrs %}
330
+ <h3 style="font-size:0.88rem;color:var(--ocean);margin:1.4rem 0 0.4rem;">Global attributes</h3>
331
+ <table class="var-table">
332
+ <thead><tr><th>Attribute</th><th>Value</th></tr></thead>
333
+ <tbody>
334
+ {% for k, v in nc_meta.global_attrs.items() %}
335
+ <tr>
336
+ <td class="mono">{{ k }}</td>
337
+ <td style="font-size:0.8rem;word-break:break-all">{{ v }}</td>
338
+ </tr>
339
+ {% endfor %}
340
+ </tbody>
341
+ </table>
342
+ {% endif %}
343
+
344
+ {% endif %}
345
+
346
+ <div class="report-footer">
347
+ Generated by <strong>oceanarray</strong> on {{ generated }}{% if proc_machine %} &bull; {{ proc_machine }}{% endif %}
348
+ </div>
349
+ <script>
350
+ document.querySelectorAll('h2').forEach(h => {
351
+ const a = document.createElement('a'); a.href = '#top'; a.className = 'top-link'; a.textContent = '↑ top'; h.appendChild(a);
352
+ });
353
+ </script>
354
+ </body>
355
+ </html>
356
+ """
357
+
358
+
359
+ # ---------------------------------------------------------------------------
360
+ # Page generator
361
+ # ---------------------------------------------------------------------------
362
+
363
+
364
+ def generate_grid_page(
365
+ mooring_name: str,
366
+ grid_path: Path,
367
+ ctx: Dict[str, Any],
368
+ out_dir: Path,
369
+ force: bool,
370
+ base_dir: Path,
371
+ ) -> None:
372
+ """Generate a grid report HTML page with T/S pcolormesh figures."""
373
+ out_path = out_dir / f"{mooring_name}_grid_report.html"
374
+ if out_path.exists() and not force:
375
+ _status("skip", str(out_path.relative_to(base_dir)))
376
+ return
377
+
378
+ try:
379
+ import xarray as xr
380
+
381
+ ds = xr.open_dataset(grid_path).load()
382
+ pressure = ds["pressure"].values
383
+ n_levels = len(pressure)
384
+ n_time = ds.sizes["time"]
385
+ p_min, p_max = int(pressure.min()), int(pressure.max())
386
+ p_range = f"{p_min}–{p_max} dbar"
387
+ grid_dp = (
388
+ f"{int(round(float(np.median(np.diff(pressure)))))} dbar"
389
+ if len(pressure) > 1
390
+ else "—"
391
+ )
392
+ if n_time > 1:
393
+ dt_arr = np.diff(ds["time"].values) / np.timedelta64(1, "s")
394
+ grid_dt_s = str(int(np.median(dt_arr)))
395
+ else:
396
+ grid_dt_s = "—"
397
+ n_instr = ctx.get("n_instruments", "—")
398
+ grid_history = _parse_history(ds.attrs.get("history", ""))
399
+
400
+ # T-S diagram first so its axis limits can be passed to the hydro panels.
401
+ fig_ts_grid_b64, _ts_bounds = _make_grid_ts_diagram(ds)
402
+
403
+ # Hydrography: stacked T + S (+ O2 sat) — shared axis limits from T-S diagram
404
+ fig_hydro_b64 = _make_grid_hydro_b64(ds, var_bounds=_ts_bounds)
405
+
406
+ # Velocity: stacked E / N / Up
407
+ fig_vel_stacked_b64 = _make_grid_velocity_stacked_b64(ds)
408
+
409
+ fig_vel_iqr_b64 = _make_velocity_iqr_profile_b64(ds)
410
+ fig_grid_rose_b64 = _make_grid_rose_b64(ds)
411
+ fig_grid_hodograph_b64 = _make_grid_hodograph_b64(ds)
412
+ fig_grid_traj_b64 = _make_grid_trajectory_b64(ds)
413
+ fig_grid_ts_b64 = _make_grid_timeseries_b64(ds)
414
+
415
+ _lat_n2 = 0.0
416
+ for _lat_key in ("seabed_latitude", "deployment_latitude", "latitude"):
417
+ _lv = ds.attrs.get(_lat_key)
418
+ if _lv is not None:
419
+ try:
420
+ from ..mooring_level import _dms_to_deg
421
+
422
+ _lat_n2 = _dms_to_deg(str(_lv))
423
+ break
424
+ except Exception:
425
+ pass
426
+ fig_n2_b64 = _make_grid_n2_b64(ds, lat=_lat_n2)
427
+
428
+ # Stratification: sigma0 stacked panel + isopycnal time series
429
+ fig_sigma_b64 = _make_grid_sigma_b64(ds)
430
+ sigma_sections = []
431
+ for sv in [
432
+ v
433
+ for v in ds.data_vars
434
+ if v.startswith("sigma") and "pressure" in ds[v].dims
435
+ ]:
436
+ da = ds[sv]
437
+ label = da.attrs.get("long_name", sv)
438
+ _dt_s = float(ds.attrs.get("dt_seconds", 60))
439
+ _filter_s = max(3, int(24 * 3600 / _dt_s))
440
+ _n_t = da.sizes["time"]
441
+ _zoom_center = _n_t // 2
442
+ _zoom_n = max(3, int(3 * 24 * 3600 / _dt_s))
443
+ sigma_sections.append(
444
+ {
445
+ "name": sv,
446
+ "label": label,
447
+ "isopycnal_zoom_b64": _make_isopycnal_fig_b64(
448
+ da,
449
+ P.SIGMA_CONTOUR_LEVELS,
450
+ zoom_center_idx=_zoom_center,
451
+ zoom_n=_zoom_n,
452
+ )
453
+ if P.SIGMA_CONTOUR_LEVELS
454
+ else None,
455
+ "isopycnal_b64": _make_isopycnal_fig_b64(
456
+ da, P.SIGMA_CONTOUR_LEVELS, filter_samples=_filter_s
457
+ )
458
+ if P.SIGMA_CONTOUR_LEVELS
459
+ else None,
460
+ }
461
+ )
462
+
463
+ fig_spectrum_b64 = None
464
+ _lat = 0.0
465
+ for _lat_key in ("seabed_latitude", "deployment_latitude", "latitude"):
466
+ _lv = ds.attrs.get(_lat_key)
467
+ if _lv is not None:
468
+ try:
469
+ from ..mooring_level import _dms_to_deg
470
+
471
+ _lat = _dms_to_deg(str(_lv))
472
+ break
473
+ except Exception:
474
+ pass
475
+ if "temperature" in ds:
476
+ _dt_s = float(ds.attrs.get("dt_seconds", 3600))
477
+ fig_spectrum_b64 = _make_spectrum_fig_b64(
478
+ ds["temperature"], _dt_s, lat=_lat
479
+ )
480
+
481
+ fig_rotary_b64 = _make_grid_rotary_spectrum_b64(ds, lat=_lat)
482
+
483
+ ds.close()
484
+
485
+ nc_meta = _read_nc_metadata(grid_path)
486
+ stack_exists = (grid_path.parent / f"{mooring_name}_stack.nc").exists()
487
+
488
+ from jinja2 import Environment
489
+
490
+ env = Environment(autoescape=True)
491
+ html = env.from_string(_GRID_HTML_TEMPLATE).render(
492
+ mooring_name=mooring_name,
493
+ nav_buttons=_nav_buttons_html(
494
+ mooring_name,
495
+ ctx.get("instruments", []),
496
+ stack_exists=stack_exists,
497
+ grid_exists=True,
498
+ current_report="grid",
499
+ array_report_href=_find_array_report_href(out_dir),
500
+ ),
501
+ cruise=ctx.get("cruise", "—"),
502
+ ship=ctx.get("ship", "—"),
503
+ deploy_time=ctx["deploy_time"],
504
+ recover_time=ctx["recover_time"],
505
+ duration=ctx.get("duration", "—"),
506
+ waterdepth=ctx.get("waterdepth", "—"),
507
+ n_levels=n_levels,
508
+ n_time=n_time,
509
+ p_range=p_range,
510
+ mooring_report_link=f"{mooring_name}_report.html",
511
+ stack_exists=stack_exists,
512
+ history_entries=grid_history,
513
+ nc_meta=nc_meta,
514
+ nc_file=grid_path.name,
515
+ fig_hydro_b64=fig_hydro_b64,
516
+ fig_vel_stacked_b64=fig_vel_stacked_b64,
517
+ sigma_sections=sigma_sections,
518
+ fig_sigma_b64=fig_sigma_b64,
519
+ fig_vel_iqr_b64=fig_vel_iqr_b64,
520
+ fig_grid_rose_b64=fig_grid_rose_b64,
521
+ fig_grid_hodograph_b64=fig_grid_hodograph_b64,
522
+ fig_grid_traj_b64=fig_grid_traj_b64,
523
+ fig_grid_ts_b64=fig_grid_ts_b64,
524
+ fig_spectrum_b64=fig_spectrum_b64,
525
+ fig_rotary_b64=fig_rotary_b64,
526
+ fig_ts_grid_b64=fig_ts_grid_b64,
527
+ fig_n2_b64=fig_n2_b64,
528
+ latitude=ctx.get("latitude", "—"),
529
+ longitude=ctx.get("longitude", "—"),
530
+ n_instr=n_instr,
531
+ grid_dt_s=grid_dt_s,
532
+ grid_dp=grid_dp,
533
+ generated=ctx["generated"],
534
+ proc_machine=ctx.get("proc_machine", ""),
535
+ )
536
+ out_path.write_text(html, encoding="utf-8")
537
+ _status("file", str(out_path.relative_to(base_dir)))
538
+ except Exception as exc:
539
+ print(f" ERROR generating grid report: {exc}")