diff --git a/oceanarray/config/parameters.py b/oceanarray/config/parameters.py index 179bec0..ec035b7 100644 --- a/oceanarray/config/parameters.py +++ b/oceanarray/config/parameters.py @@ -22,6 +22,11 @@ import numpy as np +# --------------------------------------------------------------------------- +# Package identity (shown as the report masthead wordmark) +# --------------------------------------------------------------------------- +PACKAGE_NAME = "oceanarray" + # --------------------------------------------------------------------------- # Matplotlib style path (used by plotters via plt.style.use) # --------------------------------------------------------------------------- diff --git a/oceanarray/processors/stage1.py b/oceanarray/processors/stage1.py index 8088dd3..ee6eff9 100644 --- a/oceanarray/processors/stage1.py +++ b/oceanarray/processors/stage1.py @@ -845,8 +845,22 @@ def _iso_date(raw: str) -> str: return dataset + #: Unit strings (lower-cased) that denote conductivity in S/m. A conductivity + #: variable carrying any of these is converted to the project's canonical + #: mS/cm (1 S/m = 10 mS/cm). + _SM_CONDUCTIVITY_UNITS = frozenset( + {"s/m", "s m-1", "sm-1", "s.m-1", "siemens/meter", "siemens per metre"} + ) + def _normalize_conductivity(self, dataset: xr.Dataset) -> xr.Dataset: - """Convert conductivity to mS/cm and rename to 'conductivity' if needed.""" + """Normalise conductivity to the canonical mS/cm and rename it if needed. + + Handles the raw SBE CNV column names ``cond0S/m`` / ``cond0mS/cm`` and, + for inputs that already provide a ``conductivity`` variable, converts it + from S/m to mS/cm when its ``units`` attribute says so (1 S/m = 10 mS/cm). + The source unit is recorded in ``conductivity_normalised_from`` so the + treatment can be reconstructed from the output file alone. + """ if "cond0S/m" in dataset: # S/m → mS/cm: multiply by 10 data = dataset["cond0S/m"] * 10.0 @@ -856,6 +870,19 @@ def _normalize_conductivity(self, dataset: xr.Dataset) -> xr.Dataset: dataset["conductivity"] = data elif "cond0mS/cm" in dataset: dataset = dataset.rename({"cond0mS/cm": "conductivity"}) + + # An input already named 'conductivity' but reported in S/m (e.g. an + # older seasenselib release) → convert to the canonical mS/cm. + if "conductivity" in dataset: + src_units = str(dataset["conductivity"].attrs.get("units", "")).strip() + if src_units.lower() in self._SM_CONDUCTIVITY_UNITS: + data = dataset["conductivity"] * 10.0 + data.attrs = dict(dataset["conductivity"].attrs) + data.attrs["units"] = "mS cm-1" + data.attrs["conductivity_normalised_from"] = ( + f"{src_units} (x10 to mS cm-1)" + ) + dataset["conductivity"] = data return dataset # Known SBE CNV variable names containing '/' and their CF-compatible replacements. diff --git a/oceanarray/reports/_array.py b/oceanarray/reports/_array.py index 7a70727..73ef90b 100644 --- a/oceanarray/reports/_array.py +++ b/oceanarray/reports/_array.py @@ -17,6 +17,7 @@ import numpy as np +from ._env import render_template from ._html_helpers import ( _duration_str, _parse_dt, @@ -147,185 +148,6 @@ def _draw() -> "plt.Figure": return render_b64(_draw, optional=True) -# --------------------------------------------------------------------------- -# HTML template -# --------------------------------------------------------------------------- - -_ARRAY_HTML_TEMPLATE = """\ - - - - - -Array report – {{ array_name }} - - - - -
-

{{ array_name }}

-

Array summary • generated {{ generated }}

-
- {% if year %}
Year
{{ year }}
{% endif %} - {% if cruise %}
Cruise
{{ cruise }}
{% endif %} - {% if ship %}
Ship
{{ ship }}
{% endif %} - {% if project %}
Project
{{ project }}
{% endif %} - {% if deploy_time %}
Deployment
{{ deploy_time }}
{% endif %} - {% if recover_time %}
Recovery
{{ recover_time }}
{% endif %} -
Moorings
{{ moorings | length }}
-
-
- -

- Jump to: - Moorings • - Instrument summary • - Completeness -

- -{% if fig_map_b64 %} -

Mooring positions

-Array map -{% endif %} - -

Moorings

- - - - - - - - - - - - - - - - - {% for r in moorings %} - - - - - - - - - - - - - {% endfor %} - -
#MooringLatitudeLongitudeDepth (m)DeploymentRecoveryDurationInstrumentsReports
{% if r.color_hex %}{% endif %}{{ r.position }}{{ r.mooring }}{{ "%.4f"|format(r.lat) if r.lat is not none else "—" }}{{ "%.4f"|format(r.lon) if r.lon is not none else "—" }}{{ r.waterdepth if r.waterdepth else "—" }}{{ r.deploy_time }}{{ r.recover_time }}{{ r.duration }}{{ r.n_instruments }} - {% if r.report_exists %} - Summary - {% else %} - Summary - {% endif %} - {% if r.stack_exists %} - Stack - {% endif %} - {% if r.grid_exists %} - Grid - {% endif %} -
- -{% if type_summary %} -

Instrument type summary

- - - - - - - - - - - - - {% for row in type_summary %} - - - - - - - - - {% endfor %} - -
TypeDeployedComplete (Stage 3 ✓)Skipped / no rawStopped earlyNotes
{{ row.itype }}{{ row.deployed }}{{ row.complete }}{{ row.skipped }}{{ row.stopped_early }}{{ row.notes }}
-{% endif %} - -{% if mooring_completeness %} -

Data completeness by mooring

- - - - - - - - - - - - - - - - {% for row in mooring_completeness %} - - - - - - - - - - - - {% endfor %} - -
MooringDepth (m)DeployedCompleteSkippedStopped earlyDeployment (UTC)Recovery (UTC)Duration
{{ row.mooring }}{{ row.waterdepth if row.waterdepth else "—" }}{{ row.deployed }}{{ row.complete }}{{ row.skipped }}{{ row.stopped_early }}{{ row.deploy_time }}{{ row.recover_time }}{{ row.duration }}
-{% endif %} - - - -""" - - # --------------------------------------------------------------------------- # Instrument status helpers # --------------------------------------------------------------------------- @@ -522,8 +344,6 @@ def generate_array_report( """ from datetime import datetime, timezone - from jinja2 import Environment - array_yaml_path = Path(array_yaml_path) proc_dir = Path(proc_dir) @@ -655,8 +475,7 @@ def generate_array_report( "generated": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"), } - env = Environment(autoescape=True) - html = env.from_string(_ARRAY_HTML_TEMPLATE).render(**ctx) + html = render_template("array.html", **ctx) out_path.write_text(html, encoding="utf-8") _status("file", str(out_path)) return out_path diff --git a/oceanarray/reports/_env.py b/oceanarray/reports/_env.py new file mode 100644 index 0000000..049685a --- /dev/null +++ b/oceanarray/reports/_env.py @@ -0,0 +1,51 @@ +"""File-based Jinja environment for the report HTML templates. + +Report page templates live in ``reports/templates/`` and are loaded with a +:class:`~jinja2.FileSystemLoader` so they can use ``{% extends %}`` and +``{% include %}`` — which the previous inline template strings could not. + +The environment settings are byte-identical to the previous per-module +``Environment(autoescape=True)`` calls (autoescape on; ``trim_blocks`` and +``lstrip_blocks`` at Jinja's defaults of ``False``), so migrating a template +from an inline string to a file does not change the rendered output. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from jinja2 import Environment, FileSystemLoader + +from .. import parameters as params + +#: Directory holding the report page templates. +TEMPLATES_DIR: Path = Path(__file__).with_name("templates") + +_ENV = Environment( + loader=FileSystemLoader(str(TEMPLATES_DIR)), + autoescape=True, + auto_reload=False, +) +#: Package identity available to every template (masthead wordmark). +_ENV.globals["package_name"] = params.PACKAGE_NAME + + +def render_template(name: str, /, **context: Any) -> str: + """Render report template *name* with *context* and return the HTML string. + + Parameters + ---------- + name : str + Template filename relative to ``reports/templates/`` (e.g. + ``"instrument.html"``). + **context + Template variables, forwarded to :meth:`jinja2.Template.render`. + + Returns + ------- + str + The rendered HTML. + + """ + return _ENV.get_template(name).render(**context) diff --git a/oceanarray/reports/_grid.py b/oceanarray/reports/_grid.py index 9e8a5ce..839dfa9 100644 --- a/oceanarray/reports/_grid.py +++ b/oceanarray/reports/_grid.py @@ -9,6 +9,7 @@ import numpy as np from ..utilities import parse_latlon_with_source +from ._env import render_template from ._html_helpers import ( _find_array_report_href, _nav_buttons_html, @@ -39,353 +40,6 @@ from .. import parameters as params -# --------------------------------------------------------------------------- -# Grid report HTML template -# --------------------------------------------------------------------------- - -_GRID_HTML_TEMPLATE = """\ - - - - - -Grid report – {{ mooring_name }} - - - - -
-

{{ mooring_name }}Gridded

-

generated {{ generated }}

- {{ nav_buttons | safe }} -
-
Cruise
{{ cruise }}
-
Ship
{{ ship }}
-
Latitude
{{ latitude }}
-
Longitude
{{ longitude }}
-
Water depth
{{ waterdepth }}{% if waterdepth != '—' %} m{% endif %}
-
Deployment
{{ deploy_time }}
-
Recovery
{{ recover_time }}
-
Duration
{{ duration }}
-
Samp. Δt
{{ grid_dt_s }}{% if grid_dt_s != '—' %} s{% endif %}
-
Records
{{ n_time }}
-
Grid ΔP
{{ grid_dp }}
-
Pressure levels
{{ n_levels }}
-
Pressure range
{{ p_range }}
-
Instruments
{{ n_instr }}
-
Source file
{{ nc_file }}
- - - - - -{% if history_entries %} -

Processing history

-
    - {% for e in history_entries %} -
  • - {{ e.timestamp }} - {{ e.text }} -
  • - {% endfor %} -
-{% endif %} - - -{% if fig_hydro_b64 %} -

Hydrography

-

Temperature, salinity, dissolved oxygen, and O₂ saturation (when present). Vertically interpolated to regular pressure grid • 20 discrete colour levels.

-
show / hide -Temperature and salinity -
-{% endif %} - - -{% if fig_vel_stacked_b64 %} -

Velocity

-

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.

-
show / hide -Velocity pcolormesh -
-{% endif %} - - -{% if fig_ts_grid_b64 %} -

T-S diagram

-

Left: log₁₀(count+1) per T-S bin. Right (when O₂ present): median O₂ saturation per T-S bin — bins with <5 samples masked. Colour scale: BrBG (brown=low, teal=high).

-
show / hide -T-S diagram -
-{% endif %} - - -{% if fig_vel_iqr_b64 %} -

Velocity IQR profiles

-

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.

-
show / hide -Velocity IQR profiles -
-{% endif %} - - -{% if fig_grid_rose_b64 %} -

Current roses

-

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⁻¹).

-
show / hide -Current roses by pressure level -
-{% endif %} - - -{% if fig_grid_hodograph_b64 %} -

Hodograph

-

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.

-
show / hide -Hodograph at two pressure levels -
-{% endif %} - - -{% if fig_grid_traj_b64 %} -

Particle trajectory

-

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).

-
show / hide -Grid particle trajectory -
-{% endif %} - - -{% if fig_grid_ts_b64 %} -

Velocity time series at depth of maximum mean speed

-

Depth chosen as the pressure level with the highest time-mean current speed. Speed, east, and north velocity at that level.

-
show / hide -Velocity time series at max-speed depth -
-{% endif %} - - -{% if fig_sigma_b64 or fig_n2_b64 %} -

Stratification

-{% endif %} - -{% if fig_sigma_b64 %} -

Potential density. 20 discrete colour levels.

-
show / hide sigma0 -Potential density pcolormesh -
-{% endif %} - -{% if fig_n2_b64 %} -

Buoyancy frequency squared (N²)

-

log₁₀(N²) in s⁻². Purple = strongly stratified; yellow = weakly stratified. Computed from T and S via GSW.

-
show / hide N² -N² pcolormesh -
-{% endif %} - -{% if sigma_sections or fig_overflow_temp_b64 or fig_isopycnal_coverage_b64 %} -

Overflow ↑ top

-{% endif %} - -{% if fig_isopycnal_coverage_b64 %} -

Isopycnal coverage diagnostic

-

Percentage of time steps during which each σ₀ surface (0.1 kg m⁻³ spacing) lies within the measured column range. Green ≥ 80 %; amber 50–80 %; red < 50 %. Orange diamond = current --sig-level target. Dashed line = 80 % threshold. NaN gaps in the tracking figure above correspond to times when the isopycnal is outside the measured range (pycnocline above the shallowest grid level, or below the deepest).

-
show / hide -Isopycnal coverage diagnostic -
-{% endif %} - -{% for sec in sigma_sections %} -{% if sec.isopycnal_b64 %} -

Isopycnal height above seabed — {{ sec.name }}

-

Height above seabed (m) of each target σ₀ surface through time. Tracked by linear interpolation in density space from the gridded {{ sec.name }} field; 1-hour running median applied. NaN where the surface outcrops or is outside the observed range. Light blue = lighter water (shallower); dark blue = denser water (deeper). Targets set by --sig-level (default σ₀ = 27.70).

-
show / hide -Isopycnal height above seabed -
-{% endif %} -{% endfor %} - -{% if fig_overflow_temp_b64 %} -

Temperature ~100 m above seabed

-

1-hour running median temperature at the grid pressure level nearest to 100 m above the seabed. Level and height-above-seabed shown in figure title.

-
show / hide -Temperature near seabed -
-{% endif %} - -{% if fig_spectrum_b64 %} -

Temperature power spectrum

-

Left: low-frequency overview — 14-day Hann windows, 50 % overlap. Right: high-frequency zoom — 1-day windows (~14× more windows, smoother tidal/inertial estimate); x-axis in hours. LF markers: M2, 1.8 d, 4 d, f. HF markers: M2 (12.4 h), f (inertial). Dashed black line: −2 spectral slope. Colour encodes pressure (light blue = shallow, dark blue = deep). Window length controllable via hf_segment_days in _make_spectrum_fig_b64.

-
show / hide -Temperature power spectrum -
-{% endif %} - -{% if fig_wavelet_b64 %} -

Temperature wavelet scalogram

-

Continuous wavelet transform (Morlet, ω0 = 6; Torrence & Compo 1998). Three depth levels: 2nd from top, middle, 2nd from bottom of the 100-dbar-multiple valid levels. Colour shows log10(power) (°C² d); hatched grey region = cone of influence (edge-affected); hatched cross region = gap-filled data; black contour = 95 % significance against red noise.

-
show / hide -Temperature wavelet scalogram -
-{% endif %} - -{% if fig_rotary_b64 %} -

Rotary velocity spectrum

-

CW (clockwise, anticyclonic, solid red lines) and CCW (counter-clockwise, cyclonic, dashed blue lines) power spectra and rotary coefficient r = (CCW−CW)/(CCW+CW). Welch PSD, Hann window, 14-day segments, 50 % overlap. Up to 4 pressure levels (at most 1/5th of valid levels); colour encodes pressure depth. Vertical lines: M2, K1, 1.8 d, 4 d, and inertial period (f). r > 0 = CCW dominant; r < 0 = CW dominant. Physical interpretation (NH): inertial oscillations are inherently CW; for internal waves (f < ω), CW dominance indicates upward energy propagation / downward phase propagation (Leaman & Sanford 1975).

-
show / hide -Rotary velocity spectrum -
-{% endif %} - - -

NetCDF variables — {{ nc_file }}

-{% if nc_meta.get("error") %} -

Could not read file: {{ nc_meta.error }}

-{% else %} - -

Variables

- - - - - - {% for v in nc_meta.time_vars %} - {% if not v.is_qc %} - - - - - - - - - - - - - {% endif %} - {% endfor %} - -
VariableTypeDimsNValidMin / MaxUnitsLong nameStandard nameQC flag
{{ v.name }}{{ v.dtype }}{{ v.dims }}{{ "{:,}".format(v.n) }}{{ "{:,}".format(v.n_valid) if v.n_valid is defined else "—" }}{% if v.v_min is not none %}{{ v.v_min }} / {{ v.v_max }}{% else %}—{% endif %}{{ v.units }}{{ v.long_name }}{{ v.standard_name }}{% if v.has_qc %}{% else %}–{% endif %}
- -{% if nc_meta.scalar_vars %} -

Scalar metadata variables

- - - - - - {% for v in nc_meta.scalar_vars %} - - - - - - - {% endfor %} - -
VariableValueUnitsLong name
{{ v.name }}{{ v.value }}{{ v.units }}{{ v.long_name }}
-{% endif %} - -{% if nc_meta.global_attrs %} -

Global attributes

- - - - {% for k, v in nc_meta.global_attrs.items() %} - - - - - {% endfor %} - -
AttributeValue
{{ k }}{{ v }}
-{% endif %} - -{% endif %} - - - - - -""" - - # --------------------------------------------------------------------------- # Page generator # --------------------------------------------------------------------------- @@ -502,10 +156,8 @@ def generate_grid_page( nc_meta = _read_nc_metadata(grid_path) stack_exists = (grid_path.parent / f"{mooring_name}_stack.nc").exists() - from jinja2 import Environment - - env = Environment(autoescape=True) - html = env.from_string(_GRID_HTML_TEMPLATE).render( + html = render_template( + "grid.html", mooring_name=mooring_name, nav_buttons=_nav_buttons_html( mooring_name, diff --git a/oceanarray/reports/_instrument.py b/oceanarray/reports/_instrument.py index be7c0ec..a56bb16 100644 --- a/oceanarray/reports/_instrument.py +++ b/oceanarray/reports/_instrument.py @@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional +from ._env import render_template from ._html_helpers import ( _duration_str, _file_info, @@ -42,512 +43,6 @@ # Per-instrument HTML template # --------------------------------------------------------------------------- -_INSTRUMENT_HTML_TEMPLATE = """\ - - - - - -{{ instr_type | title }} {{ serial }} — {{ mooring_name }} - - - - -
-

{{ instr_type | title }} — s/n {{ serial }}

-

{{ mooring_name }}generated {{ generated }}

- {{ nav_buttons | safe }} -
-
Cruise
{{ cruise }}
-
HAB
{{ "%.1f"|format(hab) }} m
-
Depth
{% if depth is not none %}~{{ "%.0f"|format(depth) }} m{% else %}—{% endif %}
-
Start
{{ t_start | default("—") }}
-
End
{{ t_end | default("—") }}
-
Duration
{{ duration }}
-
Samp. Δt
{{ median_dt | default("—") }}
-
Records
{{ n_records | default("—") }}
-
Source file
{{ nc_file }}{% if data_stage and data_stage != 'stage3' %} ({{ data_stage }} — QC not applied){% endif %}
- - - - - - -

Files

- - - - {% if raw_file.exists %} - - - - - - - {% elif raw_file.unknown %} - - - - - - {% else %} - - - - - - {% endif %} - {% for f in stage_files %} - - - {% if f.exists %} - - - - {% else %} - - - {% endif %} - - {% endfor %} - -
StageFilenameSizeLast modified
Raw{{ raw_file.name }}{{ raw_file.size }}{{ raw_file.mtime }}
Raw{{ raw_file.name }}— not checked (no --raw-dir)
Raw{{ raw_file.name }}— not found
{{ f.label }}{{ f.name }}{{ f.size }}{{ f.mtime }}{{ f.name }}— not generated
- - -

Processing history

-{% if history_entries %} -
    - {% for e in history_entries %} -
  • - {{ e.timestamp }} - {{ e.text }} -
  • - {% endfor %} -
-{% else %} -

No history attribute found.

-{% endif %} - - -{% if fig_adcp_velocity_b64 %} -

Velocity

-

- Time–range colour plots of the four velocity components (east, north, up, error). - Y-axis: range from transducer (m). - Colour scale: symmetric about zero; percentile 2/98 of all components combined. - {% if data_stage and data_stage != 'stage3' %}Magnetic declination has not been applied ({{ data_stage }} data).{% endif %} -

- -{% endif %} - - -

Time series (full deployment)

-{% if fig_ts_b64 %} -{% for _ts_img in fig_ts_b64 %} - -{% endfor %} -{% else %} -

No plottable variables found.

-{% endif %} - - -

Start & end windows — first / last 6 h

-{% if fig_windows_b64 %} -

- Left panel = first 7 h (1 h lead-in + 6 h of record)  |  - Right panel = last 7 h (6 h of record + 1 h tail). - ││ orange dashed = auto-detected - (pressure-based) suggested deploy/recover time • - ││ green dashed = YAML - deployment_time / recovery_time • - ││ grey = stage 1 raw record - (same variable, same units). -

-

- Reading the grey background: - where the raw stage 1 record is available and uses the same physical - units as the stage 2/3 data, it is shown in light grey so you can - visually verify that the chosen deployment window captures the in-water - period. Cases where grey may be absent: -

    -
  • Sensors without their own pressure (e.g. a microCAT that - relies on interpolated pressure from a nearby instrument) — interpolated - pressure is only added at stage 3, so stage 1 has no pressure - variable and no grey trace appears.
  • -
  • Velocity in beam or XYZ coordinates — if an Aquadopp or - ADCP was configured to record in BEAM or XYZ mode, the stage 1 file - contains beam/XYZ velocity variables while stage 3 stores - ENU velocities; the variable names differ so no grey velocity trace is shown.
  • -
  • Conductivity — raw conductivity units sometimes differ - between stage 1 and stage 3 (e.g. mS/cm vs S/m) and the - grey trace is suppressed automatically to avoid a misleading scale mismatch.
  • -
- If the orange vline is absent from the right-hand panel, - no pressure-based recovery transition was detected in the final 25 % of the - stage 1 record — the suggested recovery time equals the last raw sample. - Check the timing table in the mooring summary report for the suggested UTC time. -

-{% for _win_img in fig_windows_b64 %} - -{% endfor %} -{% else %} -

Insufficient data for start/end windows.

-{% endif %} - - -{% if fig_tsd_b64 %} -

T-S diagram

-

- Coloured by pressure (or sample index). × = suspect  |  × = bad (QC flags). -

- -{% endif %} - - -{% if fig_adcp_rose_b64 %} -

Current roses

-

- Depth-average followed by bins nearest 100, 200, 300 and 400 m from the transducer. - Bins with fewer than two valid samples are omitted. - Direction toward which the current flows; 0° = N, clockwise. - {% if data_stage and data_stage != 'stage3' %}Magnetic declination not yet applied ({{ data_stage }} data).{% endif %} -

- -{% endif %} - - -{% if fig_rose_b64 %} -

Current rose diagrams

-{% if declination_warn %} -

- ⚠ Magnetic declination could not be applied — latitude/longitude are missing or all-zero in the mooring YAML (check seabed_latitude, deployment_latitude, or latitude/longitude). - ENU velocities use 0° declination (magnetic north, not true north). -

-{% endif %} -

- {% 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). - Direction toward which the current flows; 0° = N, clockwise. -

- -{% endif %} - - -{% if fig_trajectory_b64 %} -

Particle trajectory

-

- Pseudo-Lagrangian displacement obtained by integrating east/north velocity over time - (Euler forward; NaN velocities set to zero). Coloured by temperature. - Origin (0, 0) = deployment position; axes in metres. -

- -{% endif %} - - -{% if fig_hodograph_b64 %} -

Hodograph

-

- East vs north velocity (m s-1). - Left: full record. - Right: eddy component — raw minus 4-day low-pass (rolling mean). -

- -{% endif %} - - -{% if fig_speed_boxplot_b64 %} -

Current speed distribution

- -{% endif %} - - -{% if fig_analog_b64 %} -

Analog channels

-

- Full-record time series of analog channel variables containing non-zero, non-NaN data. - One panel per channel. - Labels and units are read from the mooring YAML — update them there to change what appears here. -

-{% if analog_yaml_info %} -
    - {% for ch in analog_yaml_info %} -
  • - {{ ch.varname }} — from YAML: - {{ ch.yaml_key }}: {{ ch.label if ch.label else "(not set)" }}{% if ch.units %}, - {{ ch.yaml_key }}_units: "{{ ch.units }}"{% endif %}{% if ch.serial %}, - {{ ch.yaml_key }}_serial_number: {{ ch.serial }}{% endif %} -
  • - {% endfor %} -
-{% endif %} - -{% endif %} - - -

Data value distributions

-

- Orange dashed = suspect threshold  |  Red dotted = fail threshold (gross-range QC). - Histogram shows non-bad data only; bad-flagged count noted in red. -

-{% if fig_dt_b64 %} - -{% else %} -

Not enough samples to compute.

-{% endif %} - - -{% if qc_summary %} -

QC flag breakdown

- -{% if qc_thresholds %} -

Thresholds applied (as stored in stage 3 file)

- - - - - - - - - - - {% for row in qc_thresholds %} - - - - - - - {% endfor %} - -
VariableTestSuspect range / thresholdFail range / threshold
{{ row.var }}{{ row.test }}{{ row.suspect }}{{ row.fail }}
-{% endif %} - -

Flag counts

- - - - - - - - - - - - - - - {% for row in qc_summary %} - {% set good = row.flags | selectattr("flag", "eq", 1) | first %} - {% set susp = row.flags | selectattr("flag", "eq", 3) | first %} - {% set bad = row.flags | selectattr("flag", "eq", 4) | first %} - {% set interp = row.flags | selectattr("flag", "eq", 8) | first %} - {% set miss = row.flags | selectattr("flag", "eq", 9) | first %} - - - - - - - - - - - {% endfor %} - -
VariableNGood %Suspect %Bad %Interp. %Missing %Distribution
{{ row.var }}{{ "{:,}".format(row.total) }}{{ good.pct }}{% if susp.n > 0 %}{% if susp.pct > 0 %}{{ susp.pct }}{% else %}<0.1% ({{ susp.n }}){% endif %}{% else %}–{% endif %}{% if bad.n > 0 %}{% if bad.pct > 0 %}{{ bad.pct }}{% else %}<0.1% ({{ bad.n }}){% endif %}{% else %}–{% endif %}{% if interp.n > 0 %}{% if interp.pct > 0 %}{{ interp.pct }}{% else %}<0.1% ({{ interp.n }}){% endif %}{% else %}–{% endif %}{% if miss.n > 0 %}{% if miss.pct > 0 %}{{ miss.pct }}{% else %}<0.1% ({{ miss.n }}){% endif %}{% else %}–{% endif %} -
- {% for f in row.flags %}{% if f.pct > 0 %} -
- {% endif %}{% endfor %} -
-
-{% endif %} - - -{% if nc_meta.dims %} -

NetCDF dimensions — {{ nc_file }}

- - - - {% for dim, size in nc_meta.dims.items() %} - - {% endfor %} - -
DimensionSize
{{ dim }}{{ "{:,}".format(size) }}
-{% endif %} - - -

NetCDF variables — {{ nc_file }}

-{% if nc_meta.get("error") %} -

Could not read file: {{ nc_meta.error }}

-{% else %} - -

Time-series variables

- - - - - - {% for v in nc_meta.time_vars %} - {% if not v.is_qc %} - - - - - - - - - - - - - {% endif %} - {% endfor %} - -
VariableTypeDimsNValidMin / MaxUnitsLong nameStandard nameQC flag
{{ v.name }}{{ v.dtype }}{{ v.dims }}{{ "{:,}".format(v.n) }}{{ "{:,}".format(v.n_valid) if v.n_valid is defined else "—" }}{% if v.v_min is not none %}{{ v.v_min }} / {{ v.v_max }}{% else %}—{% endif %}{{ v.units }}{{ v.long_name }}{{ v.standard_name }}{% if v.has_qc %}{% else %}–{% endif %}
- -{% if nc_meta.scalar_vars %} -

Scalar metadata variables

- - - - - - {% for v in nc_meta.scalar_vars %} - - - - - - - - {% endfor %} - -
VariableTypeValueUnitsLong name
{{ v.name }}{{ v.dtype }}{{ v.value }}{{ v.units }}{{ v.long_name }}
-{% endif %} - -{% if nc_meta.global_attrs %} -

Global attributes

- - - - {% for k, v in nc_meta.global_attrs.items() %} - - - - - {% endfor %} - -
AttributeValue
{{ k }}{{ v }}
-{% endif %} - -{% endif %} - - - - - -""" - # --------------------------------------------------------------------------- # Page generator @@ -859,10 +354,7 @@ def generate_instrument_pages( ctx["rose_has_xyz"] = "velocity_x" in _tvar_names try: - from jinja2 import Environment - - env = Environment(autoescape=True) - html = env.from_string(_INSTRUMENT_HTML_TEMPLATE).render(**ctx) + html = render_template("instrument.html", **ctx) out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(html, encoding="utf-8") print(f"{prefix} {out_path.name}") diff --git a/oceanarray/reports/_mooring.py b/oceanarray/reports/_mooring.py index 0a65925..daac1a4 100644 --- a/oceanarray/reports/_mooring.py +++ b/oceanarray/reports/_mooring.py @@ -31,6 +31,7 @@ _stage_files, _status, ) +from ._env import render_template from ._grid import generate_grid_page from ._instrument import generate_instrument_pages from ._plots import ( @@ -43,853 +44,6 @@ from ..utilities import extract_inline_instruments -# --------------------------------------------------------------------------- -# Mooring summary HTML template -# --------------------------------------------------------------------------- - -_HTML_TEMPLATE = """\ - - - - - -Mooring Recovery Report – {{ mooring_name }} - - - - - -
-

{{ mooring_name }}Summary

-

Mooring recovery reportgenerated {{ generated }}

- {{ nav_buttons | safe }} -
-
Cruise
{{ cruise }}
-
Ship
{{ ship }}
-
Latitude
{{ latitude }}
-
Longitude
{{ longitude }}
-
Water depth
{{ waterdepth }}{% if waterdepth != '—' %} m{% endif %}
-
Deployment
{{ deploy_time }}
-
Recovery
{{ recover_time }}
-
Duration
{{ duration }}
-
Instruments
{{ n_instruments }}
- - - - - - -

2 — Processing pipeline

-

- Raw = file present in raw directory • - Read = format check passed • - Stage 1–3 = processed NetCDF files exist • - Stack = mooring-level _stack.nc • - Grid = pressure-gridded _grid.nc -

-

- Instruments marked skip: true in the YAML are excluded from all - processing and are shown with a skipped note in the Note column. - Stack and Grid pills are grey for any instrument that did not reach Stage 3. - See Table 3 for skip reasons where given. -

- - - - - - - - - - - - - - {% for instr in instruments %} - - - - - - - - - - {% endfor %} - -
#TypeS/NHab (m)Depth (m)PipelineNote
{{ loop.index }}{{ instr.instr_type }}{% if instr.report_exists %}{{ instr.serial }}{% else %}{{ instr.serial }}{% endif %}{{ "%.1f"|format(instr.hab) }}{{ "%.0f"|format(instr.depth) if instr.depth is not none else "—" }} -
- {# raw file #} - {% if instr.raw_exists %} - Raw ✓ - {% elif instr.filename %} - Raw ✗ - {% else %} - Raw — - {% endif %} - - {# readability check #} - {% if instr.raw_exists %} - {% if instr.readable %} - Read ✓ - {% else %} - Read ✗ - {% endif %} - {% else %} - Read — - {% endif %} - - {# stage 1 #} - {% if instr.stages.stage1 %} - Stage 1 ✓ - {% else %} - Stage 1 ○ - {% endif %} - - {# stage 2 #} - {% if instr.stages.stage2 %} - Stage 2 ✓ - {% else %} - Stage 2 ○ - {% endif %} - - {# stage 3 #} - {% if instr.stages.stage3 %} - Stage 3 ✓ - {% else %} - Stage 3 ○ - {% endif %} - - {# stack — only coloured if this instrument is in the stack file #} - {% if instr.in_stack %} - Stack ✓ - {% else %} - Stack ○ - {% endif %} - - {# grid — only coloured if this instrument is in the grid file #} - {% if instr.in_grid %} - Grid ✓ - {% else %} - Grid ○ - {% endif %} -
-
- {% if instr.skipped %}skipped{% endif %} -
- - -

3 — Instrument summary

-

- Variables and record length are read from stage 3 files where available, - otherwise stage 2. The P badge is shown as present - whether pressure was directly measured or interpolated from neighbouring - instruments — see Table 6 (QC flag summary) to distinguish. Instruments - marked skip: true are listed with the skip reason where provided. -

- - - - - - - - - - - - - - - - - - - - - - {% for instr in instruments %} - - - - - - {% if instr.nc and instr.nc.get("error") %} - - {% elif instr.nc and instr.nc.get("t_start") %} - - - - - - {% for label, present in instr.nc.shorthands %} - - {% endfor %} - {% elif instr.skipped %} - - {% else %} - - {% endif %} - - {% endfor %} - -
#TypeS/NHab (m)First sampleLast sampleN recordsΔt (s)P range (dbar)TCPUV
{{ loop.index }}{{ instr.instr_type }}{% if instr.report_exists %}{{ instr.serial }}{% else %}{{ instr.serial }}{% endif %}{{ "%.1f"|format(instr.hab) }}Error: {{ instr.nc.error }}{{ instr.nc.t_start }}{{ instr.nc.t_end }}{{ "{:,}".format(instr.nc.n_records) }} - {% set dt = instr.nc.dt_s %} - {% if dt == dt %}{# NaN check: NaN != NaN #} - {{ "%.0f"|format(dt) }}{% if instr.dt_mismatch %} *{% endif %} - {% else %} - - {% endif %} - - {% if instr.nc.p_min is not none and instr.nc.p_max is not none %} - {% if instr.nc.get("pressure_interpolated") %} - ({{ "%.0f"|format(instr.nc.p_min) }}, {{ "%.0f"|format(instr.nc.p_max) }}) - {% else %} - ({{ "%.0f"|format(instr.nc.p_min) }}, {{ "%.0f"|format(instr.nc.p_max) }}) - {% endif %} - {% else %} - - {% endif %} - - {% if label == "P" and present and instr.nc.get("pressure_interpolated") %} - {{ label }} - {% else %} - {{ label }} - {% endif %} - skipped{% if instr.skip_reason %} — {{ instr.skip_reason }}{% endif %}no processed file
- -{% set dt_mismatches = instruments | selectattr("dt_mismatch") | list %} -{% if dt_mismatches %} -

- * Δt mismatch (YAML vs observed p90): - {% for instr in dt_mismatches %} -
  {{ instr.serial }}: - YAML gives Δt of {{ instr.yaml_interval_s }} s; - stage file shows Δt of {{ "%.0f"|format(instr.nc.dt_s) }} s. - {% endfor %} -

-{% endif %} - -{% if grid_p_start is not none and grid_p_end is not none %} -

- Recommended pressure range for oceanarray grid, derived from the - min/max pressure across all instruments (rounded outward to the nearest 20 dbar): -

-
--pmin {{ grid_p_start }} --pmax {{ grid_p_end }}
-{% endif %} - - -

3.5 — Deployment timing

-

- Stage 1 and Sugg. (raw) columns are in the raw - instrument clock (uncorrected). Sugg. UTC columns - apply the constant clock offset (and drift, for the end) and are safe to - paste into the YAML deployment_time / - recovery_time fields. Suggested UTC cells are highlighted - amber when the - pressure-derived suggested time differs from the YAML time by more than - 2 Δt (indicating a sinking/rising transient was detected). - The Stage 1 last cell is highlighted - orange when the - raw record ended more than 2 Δt before the YAML recovery time - (instrument may have stopped early). - Serial numbers link to the per-instrument report; the - 6 h link jumps directly to the start/end window plots. -

-

- Spot-check recommended: open the 6 h start/end - window plots for each instrument (click 6 h) and visually - confirm that the orange suggested line (or green YAML line if they match) - sits at a plausible transition in the pressure (or temperature) record - before updating the YAML times. -

-
- - - - - - - - - - - - - - - - - - - - {% for instr in instruments %} - {% set tm = instr.timing %} - - - - - {% if tm %} - {# Anchor dates for deduplication: show only time when on the same day #} - {% set d_start = (tm.get("stage1_start") or "")[:10] %} - {% set d_end = (tm.get("stage1_end") or "")[:10] %} - {# Stage1 first — always full date+time; anchors deduplication for start columns #} - - {# Sugg. start (raw) — only present for pressure instruments; time only when same date #} - {% set v = tm.get("sugg_start") or "" %} - - {{- v[11:] if (v and v[:10] == d_start) else (v or "—") -}} - - {# Sugg. start (UTC) #} - {% set v = tm.get("sugg_start_utc") or "" %} - - {{- v[11:] if (v and v[:10] == d_start) else (v or "—") -}} - - {# Stage1 last — orange-amber when instrument stopped early #} - - {{- tm.get("stage1_end") or "—" -}} - - {# Sugg. end (raw) #} - {% set v = tm.get("sugg_end") or "" %} - - {{- v[11:] if (v and v[:10] == d_end) else (v or "—") -}} - - {# Sugg. end (UTC) #} - {% set v = tm.get("sugg_end_utc") or "" %} - - {{- v[11:] if (v and v[:10] == d_end) else (v or "—") -}} - - {% elif instr.skipped %} - - {% else %} - - {% endif %} - - {% endfor %} - - {% if rec_deploy_sec or rec_recover_sec %} - - - - - - - - - - - - - {% endif %} -
#TypeS/NStartEnd
Stage1 firstSugg. start (raw)Sugg. start (UTC)Stage1 lastSugg. end (raw)Sugg. end (UTC)
{{ loop.index }}{{ instr.instr_type }} - {{ instr.serial }} - 6 h - {{ tm.get("stage1_start") or "—" }}skippedno stage 2 file
MooringMooring start{{ rec_deploy_sec or "—" }}Mooring end{{ rec_recover_sec or "—" }}
-
- -{% if yaml_deploy_time or yaml_recover_time or rec_deploy or rec_recover %} -
-{% if rec_differs %} -

- Deployment times — pressure-based instruments suggest times that differ - from the current YAML. Copy the suggested snippet (blue border) into your YAML file. - Times are given to the minute. -

-
-
-

- Current YAML

- -
-
-

- Suggested (copy → paste into YAML)

- -
-
-{% else %} -

- ✓ Current YAML times match the pressure-based suggestion from the instruments.

-

- Current YAML times

- -{% endif %} -
-{% endif %} - - -

4 — Clock corrections

-

- Positive drift/offset = instrument was slow (behind UTC); correction shifts times later. - Negative = instrument was fast (ahead of UTC); correction shifts times earlier. -

- - - - - - - - - - - - - - - - {% for instr in instruments %} - - - - - - - - - - - - {% endfor %} - -
#TypeS/NHab (m)Offset (s)Computer time at recoveryInstrument time at recoveryDrift (s)Source
{{ loop.index }}{{ instr.instr_type }}{{ instr.serial }}{{ "%.1f"|format(instr.hab) }} - {% if instr.clock.offset_s == 0 %} - - {% elif instr.clock.offset_s > 0 %} - +{{ "%.1f"|format(instr.clock.offset_s) }} - {% else %} - {{ "%.1f"|format(instr.clock.offset_s) }} - {% endif %} - {% if instr.clock.computer_time %}{{ instr.clock.computer_time }}{% else %}{% endif %}{% if instr.clock.instrument_time %}{{ instr.clock.instrument_time }}{% else %}{% endif %} - {% if instr.clock.drift_s is none or instr.clock.drift_s == 0 %} - - {% elif instr.clock.drift_s > 0 %} - +{{ "%.1f"|format(instr.clock.drift_s) }} - {% else %} - {{ "%.1f"|format(instr.clock.drift_s) }} - {% endif %} - - {% if instr.clock.method == "none" %} - none - {% else %} - {{ instr.clock.method }} - {% endif %} -
- -{% if fig_clock_check_b64 %} -

Clock alignment check

-

- Overlaid temperature records zoomed to the first and last 30 minutes of the deployment. - If instrument clocks are misaligned the temperature curves will appear shifted in time. - Data are from stage‑3 (or stage‑2 if stage‑3 is not yet available). - Instruments with no temperature data are omitted. -

-Clock alignment check -{% endif %} - - -

5 — Sensor calibration

- -{% set has_sensors = instruments | selectattr("sensors") | list %} -{% if has_sensors %} - - - - - - - - - - - - - - - {% set ns = namespace(idx=0) %} - {% for instr in instruments %} - {% if instr.sensors %} - {% set ns.idx = ns.idx + 1 %} - {% for sensor in instr.sensors %} - - - - - - - - - - - {% endfor %} - {% endif %} - {% endfor %} - -
#TypeInstr S/NSensorModelSensor S/NCal dateCoefficients
{% if loop.index0 == 0 %}{{ ns.idx }}{% endif %}{% if loop.index0 == 0 %}{{ instr.instr_type }}{% endif %}{% if loop.index0 == 0 %}{{ instr.serial }}{% endif %}{{ sensor.sensor_type | title }}{{ sensor.sensor_model }}{{ sensor.sensor_serial }}{{ sensor.cal_date }} - {% if sensor.coefficients %} -
- show -
{{ sensor.coefficients }}
-
- {% else %} - - {% endif %} -
-{% else %} -

No sensor calibration metadata found in processed files.

-{% endif %} - - -

6 — QC flag summary

- -
- good (1) - prob. good (2) - suspect (3) - bad (4) - interp. (8) - missing (9) -
-{% set has_qc = instruments | selectattr("qc_summary") | list %} -{% if has_qc %} - - - - - - - - - - - - - - - - - - {% set ns = namespace(idx=0) %} - {% for instr in instruments %} - {% if instr.qc_summary %} - {% set ns.idx = ns.idx + 1 %} - {% for row in instr.qc_summary %} - {% set good = row.flags | selectattr("flag", "eq", 1) | first %} - {% set susp = row.flags | selectattr("flag", "eq", 3) | first %} - {% set bad = row.flags | selectattr("flag", "eq", 4) | first %} - {% set interp = row.flags | selectattr("flag", "eq", 8) | first %} - {% set miss = row.flags | selectattr("flag", "eq", 9) | first %} - - - - - - - - - - - - - - {% endfor %} - {% endif %} - {% endfor %} - -
#TypeS/NVariableNGood %Suspect %Bad %Interp. %Missing %Distribution
{% if loop.index0 == 0 %}{{ ns.idx }}{% endif %}{% if loop.index0 == 0 %}{{ instr.instr_type }}{% endif %}{% if loop.index0 == 0 %}{{ instr.serial }}{% endif %}{{ row.var }}{{ "{:,}".format(row.total) }} - {{ good.pct }} - {% if susp.pct > 0 %}{{ susp.pct }}{% else %}–{% endif %}{% if bad.pct > 0 %}{{ bad.pct }}{% else %}–{% endif %}{% if interp.pct > 0 %}{{ interp.pct }}{% else %}–{% endif %}{% if miss.pct > 0 %}{{ miss.pct }}{% else %}–{% endif %} -
- {% for f in row.flags %} - {% if f.pct > 0 %} -
- {% endif %} - {% endfor %} -
-
-{% else %} -

No stage 3 QC files found — run oceanarray stage3 first.

-{% endif %} - -{% if fig_knockdown_hab_b64 or fig_knockdown_anomaly_b64 or fig_knockdown_displacement_b64 %} - -

Mooring knockdown

-
-{% if fig_knockdown_hab_b64 %} -
-

Nominal HAB (x) vs. measured pressure (y). The dashed line shows expected pressure - (water depth − HAB). Instruments below the line were knocked down. - Interpolated pressure (QC flag 8) excluded.

-Knockdown HAB vs pressure -
-{% endif %} -{% if fig_knockdown_anomaly_b64 %} -
-

IQR of pressure anomaly (measured − nominal) per instrument. - Positive = deeper than design depth. - Green < 100 dbar, - yellow 100–200, - amber 200–300, - red > 300 dbar.

-Knockdown pressure anomaly -
-{% endif %} -
-{% if fig_knockdown_displacement_b64 %} -

Estimated horizontal displacement (m) vs. measured pressure. - Left: scatter per instrument; right: normalised 2-D density across all instruments. - Displacement derived from the rigid-pendulum approximation: - x = √(habnom² − habmeas²).

-Knockdown horizontal displacement -{% endif %} -{% endif %} - -{% if diagram_b64 %} - -

Mooring diagram

- -{% endif %} - - -{% if issues.any %} -

Issues for cruise report

- -{% if issues.skipped %} -

Skipped / missing instruments

-
    -{% for i in issues.skipped %} -
  • SN {{ i.serial }} ({{ i.itype }}, {{ i.depth_str }}): {{ i.reason }}
  • -{% endfor %} -
-{% endif %} - -{% if issues.stopped_early %} -

Stopped early

-
    -{% for i in issues.stopped_early %} -
  • SN {{ i.serial }} ({{ i.itype }}, {{ i.depth_str }}): last sample {{ i.t_end }}, expected {{ i.expected }} ({{ i.delta_h }} h short)
  • -{% endfor %} -
-{% endif %} - -{% if issues.qc_flagged %} -

Data quality flags

-
    -{% for i in issues.qc_flagged %} -
  • SN {{ i.serial }} ({{ i.itype }}): {{ i.note }}
  • -{% endfor %} -
-{% endif %} - -{% endif %} - - - - - - - -""" - - # --------------------------------------------------------------------------- # Orchestrator class # --------------------------------------------------------------------------- @@ -1512,9 +666,4 @@ def _combined(deploy_key: str, recover_key: str, legacy_key: str) -> str: } def _render(self, ctx: Dict[str, Any]) -> str: - try: - from jinja2 import Environment - except ImportError as exc: - raise ImportError("pip install jinja2") from exc - env = Environment(autoescape=True) - return env.from_string(_HTML_TEMPLATE).render(**ctx) + return render_template("mooring.html", **ctx) diff --git a/oceanarray/reports/_recovery_table.py b/oceanarray/reports/_recovery_table.py index 4dacc14..232a6db 100644 --- a/oceanarray/reports/_recovery_table.py +++ b/oceanarray/reports/_recovery_table.py @@ -17,6 +17,7 @@ import yaml +from ._env import render_template from ._html_helpers import ( _parse_dt, _resolve_clock, @@ -43,149 +44,6 @@ _NON_LOGGING = {"beacon", "release", "float", "swivel", "shackle"} -# --------------------------------------------------------------------------- -# HTML template -# --------------------------------------------------------------------------- - -_RECOVERY_TABLE_TEMPLATE = """\ - - - - - -{{ mooring_name }} – Mooring Recovery Table - - - - -

{{ mooring_name }} ({{ year }}) – Mooring Recovery

-{% if location %}

{{ location }}

{% endif %} -

- Depth: {{ waterdepth }} m • - Start: {{ deploy_time }} • - End: {{ recover_time }} • - Duration: {{ duration }} -

- - - - - - - - - - - - - - - - - {% for row in rows %} - {% if row.nonlog %} - - - - - - {% else %} - - - - - - - - - - - - {% endif %} - {% endfor %} - -
Height\n(above\nbottom)\n(m)Depth\n(nominal)\n(m)InstrumentParam.Sample\nint.\n(s)Start/stop\ntime UTCClock\ndriftFirst good /\nLast good recordNotes
{{ row.hab if row.hab is not none else "" }}{{ row.depth if row.depth is not none else "" }}{{ row.description }}
{{ row.hab if row.hab is not none else "?" }}{{ row.depth if row.depth is not none else "?" }}{{ row.instrument }}{{ row.params }}{{ row.interval_s if row.interval_s else "" }}{{ row.start_stop }}{{ row.clock_drift }}{{ row.first_last }}{{ "*" if row.comment else "" }}
- -{% if comments %} -
-

Notes

-
- {% for c in comments %} -
SN {{ c.serial }} ({{ c.instrument }}, {{ c.depth_str }})
-
{{ c.comment }}
- {% endfor %} -
-
-{% endif %} - - - - - -""" - - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -437,8 +295,6 @@ def generate_recovery_table( """ from datetime import datetime, timezone - from jinja2 import Environment - from ._html_helpers import _duration_str proc_dir = Path(proc_dir) @@ -462,7 +318,7 @@ def generate_recovery_table( deploy_dt = _parse_dt(cfg.get("deployment_time")) recover_dt = _parse_dt(cfg.get("recovery_time")) - # Build a brief location string from lat/lon if available + # Latitude/longitude for the canonical meta fields, if available. lat = ( cfg.get("seabed_latitude") or cfg.get("deployment_latitude") @@ -473,7 +329,8 @@ def generate_recovery_table( or cfg.get("deployment_longitude") or cfg.get("longitude") ) - location = f"{lat} N, {lon} W" if (lat and lon) else "" + latitude = f"{lat} N" if lat else "" + longitude = f"{lon} W" if lon else "" rows, comments = _build_rows(proc_dir, mooring_name, cfg) @@ -481,7 +338,8 @@ def generate_recovery_table( "mooring_name": mooring_name, "year": cfg.get("year", ""), "waterdepth": cfg.get("waterdepth", "?"), - "location": location, + "latitude": latitude, + "longitude": longitude, "deploy_time": deploy_dt.strftime("%Y-%m-%d %H:%M UTC") if deploy_dt else "?", "recover_time": recover_dt.strftime("%Y-%m-%d %H:%M UTC") if recover_dt @@ -492,8 +350,7 @@ def generate_recovery_table( "generated": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"), } - env = Environment(autoescape=True) - html = env.from_string(_RECOVERY_TABLE_TEMPLATE).render(**ctx) + html = render_template("recovery_table.html", **ctx) out_path.write_text(html, encoding="utf-8") _status("file", str(out_path)) return out_path diff --git a/oceanarray/reports/_stack.py b/oceanarray/reports/_stack.py index 177fa7c..526ffba 100644 --- a/oceanarray/reports/_stack.py +++ b/oceanarray/reports/_stack.py @@ -8,6 +8,7 @@ import numpy as np +from ._env import render_template from ._html_helpers import ( _fig_to_base64, _find_array_report_href, @@ -34,381 +35,6 @@ from .. import parameters as params -# --------------------------------------------------------------------------- -# Stack report HTML template -# --------------------------------------------------------------------------- - -_STACK_HTML_TEMPLATE = """\ - - - - - -Stack report – {{ mooring_name }} - - - - -
-

{{ mooring_name }}Stacked

-

generated {{ generated }}

- {{ nav_buttons | safe }} -
-
Cruise
{{ cruise }}
-
Ship
{{ ship }}
-
Latitude
{{ latitude }}
-
Longitude
{{ longitude }}
-
Water depth
{{ waterdepth }}{% if waterdepth != '—' %} m{% endif %}
-
Deployment
{{ deploy_time }}
-
Recovery
{{ recover_time }}
-
Duration
{{ duration }}
-
Samp. Δt
{{ dt_seconds }}{% if dt_seconds != '—' %} s{% endif %}
-
Records
{{ n_time }}
-
Instruments
{{ n_instr }}
-
Source file
{{ nc_file }}
- - - - - - -{% if history_entries %} -

Processing history

-
    - {% for e in history_entries %} -
  • - {{ e.timestamp }} - {{ e.text }} -
  • - {% endfor %} -
-{% endif %} - - -

Instruments (deep-first)

- - - - {% for row in instr_rows %} - - - - - - - - {% endfor %} - -
#TypeSerialHAB (m)~Depth (m)
{{ loop.index0 }}{{ row.instr_type }}{% if row.report_exists %}{{ row.serial }}{% else %}{{ row.serial }}{% endif %}{{ row.hab }}{{ row.depth }}
- - -{% if fig_pressure_b64 %} -

Pressure records (all instruments)

-

Values with QC flag ≥ 3 (suspect/bad) masked to NaN before plotting. All data values are in {{ nc_file }} without masking.

-Pressure time series -{% endif %} - - -{% if fig_temp_b64 %} -

Temperature (all instruments)

-

Values with QC flag ≥ 3 (suspect/bad) masked to NaN before plotting. All data values are in {{ nc_file }} without masking.

-Temperature time series -{% endif %} - - -{% if fig_sal_b64 %} -

Salinity (all instruments)

-

Values with QC flag ≥ 3 (suspect/bad) masked to NaN before plotting. All data values are in {{ nc_file }} without masking.

-Salinity time series -{% endif %} - -{% if fig_dissolved_oxygen_b64 %} -

Dissolved oxygen (all instruments)

-

One line per instrument with dissolved oxygen data (SBE ODO sensor); QC flags ≥ 3 masked. Units: µmol L⁻¹. % saturation available in per-instrument reports.

-Dissolved oxygen time series -{% endif %} - - -{% if fig_east_vel_b64 %} -

East velocity (U)

-

ENU frame. Values with velocity_flag ≥ 3 masked to NaN before plotting. All data values are in {{ nc_file }} without masking. Instruments without velocity data omitted.

-East velocity time series -{% endif %} - -{% if fig_north_vel_b64 %} -

North velocity (V)

-

ENU frame. Values with velocity_flag ≥ 3 masked to NaN before plotting. All data values are in {{ nc_file }} without masking.

-North velocity time series -{% endif %} - -{% if fig_up_vel_b64 %} -

Vertical velocity (W)

-

ENU frame. Values with velocity_flag ≥ 3 masked to NaN before plotting. All data values are in {{ nc_file }} without masking.

-Vertical velocity time series -{% endif %} - -{% if fig_turbidity_b64 %} -

Turbidity

-

One line per instrument with turbidity data; QC flags ≥ 3 masked. Dots overlaid to reveal individual samples near zero. Units from file attrs (verify: NTU, FTU, or V depending on sensor).

-Turbidity time series -{% endif %} - -{% if fig_trajectories_b64 or fig_adcp_trajectories_b64 %} -

Particle trajectories

-

- Pseudo-Lagrangian displacement: east/north velocity integrated over time - (Euler forward; NaN velocities set to zero). All trajectories share a common origin (0, 0). - Aquadopp: colour shows temperature (shared scale); end points labelled with serial and HAB. - ADCP: per-bin trajectories coloured by height above bottom; bins entirely below the seabed are omitted. -

-
- {% if fig_trajectories_b64 %} -
-

Aquadopp

- Aquadopp trajectories -
- {% endif %} - {% if fig_adcp_trajectories_b64 %} -
-

ADCP

- ADCP trajectories -
- {% endif %} -
-{% endif %} - -{% if fig_speed_profile_b64 %} -

Aquadopp speed profile

-

- Horizontal boxplot per Aquadopp, positioned at its nominal height above bottom - (per-instrument design value; does not account for mooring knockdown). - Box = interquartile range; line = median; whiskers = 1.5×IQR; dots = outliers. - Computed from east/north velocity components if current_speed is not stored. -

-Aquadopp speed profile -{% endif %} - -{% if fig_analog_b64 %} -

Analog channels

-

- Full-record time series of analog channel variables containing non-zero, non-NaN data. - One panel per channel. -

-Analog channels -{% endif %} - -{% if fig_ts_stack_b64 %} -

T-S diagram

-

Left: scatter coloured by pressure. Middle: 2-D count heatmap. Right (when oxygen data present): scatter coloured by O₂ saturation (%). Bad (flag 4) and missing (flag 9) excluded; interpolated pressure (flag 8) retained.

-T-S diagram -{% endif %} - -{% if fig_rose_grid_b64 %} -

Current rose diagrams

-

Direction the current flows toward (oceanographic convention, 0°=N). Speed coloured light→dark blue (slow→fast). QC-flagged samples excluded. Title shows serial number and height above bottom (m).

-{% if rose_declination_warn %} -

- ⚠ Magnetic declination could not be applied to - {% if rose_declination_missing_serials %}Aquadopp(s) s/n {{ rose_declination_missing_serials | join(', ') }}{% else %}one or more Aquadopps{% endif %} - — latitude/longitude are missing or all-zero in the mooring YAML (check seabed_latitude, deployment_latitude, or latitude/longitude). - Re-run oceanarray process … --stage 3 for the affected instrument(s) after fixing the YAML. - Affected ENU velocities currently use 0° declination (magnetic north, not true north). -

-{% endif %} -{% if rose_declination_note %}

{{ rose_declination_note }}

{% endif %} -Current rose grid -{% endif %} - -{% if fig_aquadopp_tilt_b64 %} -

Aquadopp tilt (|pitch| / |roll| / pressure estimate)

-

- One panel per Aquadopp (deep-first). Blue = |pitch|, green = |roll|, orange dashed = tilt - estimated from pressure difference between the Aquadopp and the nearest instrument ≥10 m above - with valid pressure (arccos(ΔP / rope length)). All curves are non-negative. - Horizontal lines: orange dashed = suspect threshold, red dotted = fail threshold (read from file attrs). - Pitch and roll are stored unmasked in the stack file; use pitch_qc / - roll_qc to filter. Plots show all available values. -

-Aquadopp tilt panels -{% endif %} - -{% if fig_spacing_b64 %} -

Adjacent instrument spacing

-

Distribution of pressure differences between adjacent instrument pairs (pairs < 2 dbar apart excluded as co-located).

-Instrument spacing histogram -{% endif %} - -{% if fig_clock_check_b64 %} -

Clock alignment check

-

- Temperature records from all instruments overlaid, zoomed to the first and last - 10 minutes of the deployment. A horizontal shift between curves indicates - a clock offset between instruments. Data are from stage‑3 (or stage‑2 - if stage‑3 is not yet available). Instruments without temperature are omitted. -

-Clock alignment check -{% endif %} - - -

NetCDF dimensions — {{ nc_file }}

-{% if nc_meta.dims %} - - - - {% for dim, size in nc_meta.dims.items() %} - - {% endfor %} - -
DimensionSize
{{ dim }}{{ "{:,}".format(size) }}
-{% endif %} - - -

NetCDF variables — {{ nc_file }}

-{% if nc_meta.get("error") %} -

Could not read file: {{ nc_meta.error }}

-{% else %} - -

Variables

- - - - - - {% for v in nc_meta.time_vars %} - {% if not v.is_qc %} - - - - - - - - - - - - - {% endif %} - {% endfor %} - -
VariableTypeDimsNValidMin / MaxUnitsLong nameStandard nameQC flag
{{ v.name }}{{ v.dtype }}{{ v.dims }}{{ "{:,}".format(v.n) }}{{ "{:,}".format(v.n_valid) if v.n_valid is defined else "—" }}{% if v.v_min is not none %}{{ v.v_min }} / {{ v.v_max }}{% else %}—{% endif %}{{ v.units }}{{ v.long_name }}{{ v.standard_name }}{% if v.has_qc %}{% else %}–{% endif %}
- -{% if nc_meta.scalar_vars %} -

Scalar metadata variables

- - - - - - {% for v in nc_meta.scalar_vars %} - - - - - - - - {% endfor %} - -
VariableTypeValueUnitsLong name
{{ v.name }}{{ v.dtype }}{{ v.value }}{{ v.units }}{{ v.long_name }}
-{% endif %} - -{% if nc_meta.global_attrs %} -

Global attributes

- - - - {% for k, v in nc_meta.global_attrs.items() %} - - - - - {% endfor %} - -
AttributeValue
{{ k }}{{ v }}
-{% endif %} - -{% endif %} - - - - - -""" - - # --------------------------------------------------------------------------- # Aquadopp tilt helper (was @staticmethod on MooringReport) # --------------------------------------------------------------------------- @@ -888,10 +514,8 @@ def _ts_fig( ) grid_exists = (stack_path.parent / f"{mooring_name}_grid.nc").exists() - from jinja2 import Environment - - env = Environment(autoescape=True) - html = env.from_string(_STACK_HTML_TEMPLATE).render( + html = render_template( + "stack.html", mooring_name=mooring_name, nav_buttons=_nav_buttons_html( mooring_name, diff --git a/oceanarray/reports/templates/array.html b/oceanarray/reports/templates/array.html new file mode 100644 index 0000000..e425c00 --- /dev/null +++ b/oceanarray/reports/templates/array.html @@ -0,0 +1,153 @@ +{% extends "base.html" %} +{% block title %}{{ array_name }} – array summary{% endblock %} +{% block page_styles %} + .meta-grid div { font-size:0.82rem; } + table { border-collapse:collapse; width:100%; margin:0.6rem 0 1.2rem; font-size:0.84rem; } + th { background:var(--ocean); color:#fff; padding:0.4rem 0.6rem; text-align:left; } + td { padding:0.35rem 0.6rem; border-bottom:1px solid #e8ecef; } + tr:nth-child(even) td { background:#f7f9fb; } + .num { text-align:right; } + .btn { display:inline-block; padding:0.15em 0.55em; border-radius:4px; font-size:0.75rem; + font-weight:700; text-decoration:none; color:#fff; margin:0 0.15rem 0.2rem 0; } + .btn-sum { background:#2c3e50; } + .btn-stk { background:#2980b9; } + .btn-grd { background:#8e44ad; } + .btn-miss { background:#bbb; cursor:default; pointer-events:none; } + .note { font-size:0.78rem; color:#555; margin:0.2rem 0 0.6rem; } + .fig { max-width:60%; display:block; margin:0.5rem auto 1rem; border:1px solid #e0e4e8; border-radius:4px; } +{% endblock %} +{% block masthead_title %}{{ array_name }}{% endblock %} +{% block masthead_type %}Array{% endblock %} +{% block masthead_nav %}{% endblock %} +{% block masthead_meta %} + {% if year %}
Year
{{ year }}
{% endif %} + {% if cruise %}
Cruise
{{ cruise }}
{% endif %} + {% if ship %}
Ship
{{ ship }}
{% endif %} + {% if project %}
Project
{{ project }}
{% endif %} + {% if deploy_time %}
Deployment
{{ deploy_time }}
{% endif %} + {% if recover_time %}
Recovery
{{ recover_time }}
{% endif %} +
Moorings
{{ moorings | length }}
+ {% endblock %} +{% block content %} + +

+ Jump to: + Moorings • + Instrument summary • + Completeness +

+ +{% if fig_map_b64 %} +

Mooring positions

+Array map +{% endif %} + +

Moorings

+ + + + + + + + + + + + + + + + + {% for r in moorings %} + + + + + + + + + + + + + {% endfor %} + +
#MooringLatitudeLongitudeDepth (m)DeploymentRecoveryDurationInstrumentsReports
{% if r.color_hex %}{% endif %}{{ r.position }}{{ r.mooring }}{{ "%.4f"|format(r.lat) if r.lat is not none else "—" }}{{ "%.4f"|format(r.lon) if r.lon is not none else "—" }}{{ r.waterdepth if r.waterdepth else "—" }}{{ r.deploy_time }}{{ r.recover_time }}{{ r.duration }}{{ r.n_instruments }} + {% if r.report_exists %} + Summary + {% else %} + Summary + {% endif %} + {% if r.stack_exists %} + Stack + {% endif %} + {% if r.grid_exists %} + Grid + {% endif %} +
+ +{% if type_summary %} +

Instrument type summary

+ + + + + + + + + + + + + {% for row in type_summary %} + + + + + + + + + {% endfor %} + +
TypeDeployedComplete (Stage 3 ✓)Skipped / no rawStopped earlyNotes
{{ row.itype }}{{ row.deployed }}{{ row.complete }}{{ row.skipped }}{{ row.stopped_early }}{{ row.notes }}
+{% endif %} + +{% if mooring_completeness %} +

Data completeness by mooring

+ + + + + + + + + + + + + + + + {% for row in mooring_completeness %} + + + + + + + + + + + + {% endfor %} + +
MooringDepth (m)DeployedCompleteSkippedStopped earlyDeployment (UTC)Recovery (UTC)Duration
{{ row.mooring }}{{ row.waterdepth if row.waterdepth else "—" }}{{ row.deployed }}{{ row.complete }}{{ row.skipped }}{{ row.stopped_early }}{{ row.deploy_time }}{{ row.recover_time }}{{ row.duration }}
+{% endif %} + +{% endblock %} diff --git a/oceanarray/reports/templates/base.html b/oceanarray/reports/templates/base.html new file mode 100644 index 0000000..b76b4ae --- /dev/null +++ b/oceanarray/reports/templates/base.html @@ -0,0 +1,81 @@ + + + + + +{% block title %}{{ mooring_name }}{% endblock %} + +{% block head_extra %}{% endblock %} + + +{% block masthead %} +
+

{% block masthead_title %}{{ mooring_name }}{% endblock %}{% block masthead_type %}{% endblock %}

+

{% block masthead_sub %}{% endblock %}generated {{ generated }}

+ {% block masthead_nav %}{{ nav_buttons | safe }}{% endblock %} +
+ {% block masthead_meta %}{% endblock %} +
+ {{ package_name }} +
+{% endblock %} +{% block content %}{% endblock %} + + diff --git a/oceanarray/reports/templates/grid.html b/oceanarray/reports/templates/grid.html new file mode 100644 index 0000000..259d4b7 --- /dev/null +++ b/oceanarray/reports/templates/grid.html @@ -0,0 +1,290 @@ +{% extends "base.html" %} +{% block title %}Grid report – {{ mooring_name }}{% endblock %} +{% block page_styles %} + :root { --accent:#8e44ad; --accent-link:#e8d5ff; } + .fig { width:100%; border:1px solid #dce; border-radius:4px; margin-bottom:0.5rem; } + .note { color:var(--muted); font-size:0.82rem; margin-top:-0.5rem; } + .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; } + .var-table { width:100%; border-collapse:collapse; font-size:0.82rem; margin-bottom:1.5rem; } + .var-table th { background:var(--seafoam); text-align:left; padding:0.4rem 0.6rem; border-bottom:2px solid #cde; } + .var-table td { padding:0.3rem 0.6rem; border-bottom:1px solid #eef; vertical-align:top; } + .var-table tr:nth-child(even) td { background:#f4f9fc; } + .var-table tr:hover td { background:#e8f4f8; } + details summary.collapse-toggle { color:var(--muted); font-size:0.76rem; cursor:pointer; + list-style:none; padding:0.15rem 0 0.4rem; user-select:none; } + details summary.collapse-toggle::before { content:"▾ "; } + details[open] summary.collapse-toggle::before { content:"▴ "; } +{% endblock %} +{% block masthead_type %}Gridded{% endblock %} +{% block masthead_meta %} +
Cruise
{{ cruise }}
+
Ship
{{ ship }}
+
Latitude
{{ latitude }}
+
Longitude
{{ longitude }}
+
Water depth
{{ waterdepth }}{% if waterdepth != '—' %} m{% endif %}
+
Deployment
{{ deploy_time }}
+
Recovery
{{ recover_time }}
+
Duration
{{ duration }}
+
Samp. Δt
{{ grid_dt_s }}{% if grid_dt_s != '—' %} s{% endif %}
+
Records
{{ n_time }}
+
Grid ΔP
{{ grid_dp }}
+
Pressure levels
{{ n_levels }}
+
Pressure range
{{ p_range }}
+
Instruments
{{ n_instr }}
+
Source file
{{ nc_file }}
+ {% endblock %} +{% block content %} + + + +{% if history_entries %} +

Processing history

+
    + {% for e in history_entries %} +
  • + {{ e.timestamp }} + {{ e.text }} +
  • + {% endfor %} +
+{% endif %} + + +{% if fig_hydro_b64 %} +

Hydrography

+

Temperature, salinity, dissolved oxygen, and O₂ saturation (when present). Vertically interpolated to regular pressure grid • 20 discrete colour levels.

+
show / hide +Temperature and salinity +
+{% endif %} + + +{% if fig_vel_stacked_b64 %} +

Velocity

+

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.

+
show / hide +Velocity pcolormesh +
+{% endif %} + + +{% if fig_ts_grid_b64 %} +

T-S diagram

+

Left: log₁₀(count+1) per T-S bin. Right (when O₂ present): median O₂ saturation per T-S bin — bins with <5 samples masked. Colour scale: BrBG (brown=low, teal=high).

+
show / hide +T-S diagram +
+{% endif %} + + +{% if fig_vel_iqr_b64 %} +

Velocity IQR profiles

+

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.

+
show / hide +Velocity IQR profiles +
+{% endif %} + + +{% if fig_grid_rose_b64 %} +

Current roses

+

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⁻¹).

+
show / hide +Current roses by pressure level +
+{% endif %} + + +{% if fig_grid_hodograph_b64 %} +

Hodograph

+

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.

+
show / hide +Hodograph at two pressure levels +
+{% endif %} + + +{% if fig_grid_traj_b64 %} +

Particle trajectory

+

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).

+
show / hide +Grid particle trajectory +
+{% endif %} + + +{% if fig_grid_ts_b64 %} +

Velocity time series at depth of maximum mean speed

+

Depth chosen as the pressure level with the highest time-mean current speed. Speed, east, and north velocity at that level.

+
show / hide +Velocity time series at max-speed depth +
+{% endif %} + + +{% if fig_sigma_b64 or fig_n2_b64 %} +

Stratification

+{% endif %} + +{% if fig_sigma_b64 %} +

Potential density. 20 discrete colour levels.

+
show / hide sigma0 +Potential density pcolormesh +
+{% endif %} + +{% if fig_n2_b64 %} +

Buoyancy frequency squared (N²)

+

log₁₀(N²) in s⁻². Purple = strongly stratified; yellow = weakly stratified. Computed from T and S via GSW.

+
show / hide N² +N² pcolormesh +
+{% endif %} + +{% if sigma_sections or fig_overflow_temp_b64 or fig_isopycnal_coverage_b64 %} +

Overflow ↑ top

+{% endif %} + +{% if fig_isopycnal_coverage_b64 %} +

Isopycnal coverage diagnostic

+

Percentage of time steps during which each σ₀ surface (0.1 kg m⁻³ spacing) lies within the measured column range. Green ≥ 80 %; amber 50–80 %; red < 50 %. Orange diamond = current --sig-level target. Dashed line = 80 % threshold. NaN gaps in the tracking figure above correspond to times when the isopycnal is outside the measured range (pycnocline above the shallowest grid level, or below the deepest).

+
show / hide +Isopycnal coverage diagnostic +
+{% endif %} + +{% for sec in sigma_sections %} +{% if sec.isopycnal_b64 %} +

Isopycnal height above seabed — {{ sec.name }}

+

Height above seabed (m) of each target σ₀ surface through time. Tracked by linear interpolation in density space from the gridded {{ sec.name }} field; 1-hour running median applied. NaN where the surface outcrops or is outside the observed range. Light blue = lighter water (shallower); dark blue = denser water (deeper). Targets set by --sig-level (default σ₀ = 27.70).

+
show / hide +Isopycnal height above seabed +
+{% endif %} +{% endfor %} + +{% if fig_overflow_temp_b64 %} +

Temperature ~100 m above seabed

+

1-hour running median temperature at the grid pressure level nearest to 100 m above the seabed. Level and height-above-seabed shown in figure title.

+
show / hide +Temperature near seabed +
+{% endif %} + +{% if fig_spectrum_b64 %} +

Temperature power spectrum

+

Left: low-frequency overview — 14-day Hann windows, 50 % overlap. Right: high-frequency zoom — 1-day windows (~14× more windows, smoother tidal/inertial estimate); x-axis in hours. LF markers: M2, 1.8 d, 4 d, f. HF markers: M2 (12.4 h), f (inertial). Dashed black line: −2 spectral slope. Colour encodes pressure (light blue = shallow, dark blue = deep). Window length controllable via hf_segment_days in _make_spectrum_fig_b64.

+
show / hide +Temperature power spectrum +
+{% endif %} + +{% if fig_wavelet_b64 %} +

Temperature wavelet scalogram

+

Continuous wavelet transform (Morlet, ω0 = 6; Torrence & Compo 1998). Three depth levels: 2nd from top, middle, 2nd from bottom of the 100-dbar-multiple valid levels. Colour shows log10(power) (°C² d); hatched grey region = cone of influence (edge-affected); hatched cross region = gap-filled data; black contour = 95 % significance against red noise.

+
show / hide +Temperature wavelet scalogram +
+{% endif %} + +{% if fig_rotary_b64 %} +

Rotary velocity spectrum

+

CW (clockwise, anticyclonic, solid red lines) and CCW (counter-clockwise, cyclonic, dashed blue lines) power spectra and rotary coefficient r = (CCW−CW)/(CCW+CW). Welch PSD, Hann window, 14-day segments, 50 % overlap. Up to 4 pressure levels (at most 1/5th of valid levels); colour encodes pressure depth. Vertical lines: M2, K1, 1.8 d, 4 d, and inertial period (f). r > 0 = CCW dominant; r < 0 = CW dominant. Physical interpretation (NH): inertial oscillations are inherently CW; for internal waves (f < ω), CW dominance indicates upward energy propagation / downward phase propagation (Leaman & Sanford 1975).

+
show / hide +Rotary velocity spectrum +
+{% endif %} + + +

NetCDF variables — {{ nc_file }}

+{% if nc_meta.get("error") %} +

Could not read file: {{ nc_meta.error }}

+{% else %} + +

Variables

+ + + + + + {% for v in nc_meta.time_vars %} + {% if not v.is_qc %} + + + + + + + + + + + + + {% endif %} + {% endfor %} + +
VariableTypeDimsNValidMin / MaxUnitsLong nameStandard nameQC flag
{{ v.name }}{{ v.dtype }}{{ v.dims }}{{ "{:,}".format(v.n) }}{{ "{:,}".format(v.n_valid) if v.n_valid is defined else "—" }}{% if v.v_min is not none %}{{ v.v_min }} / {{ v.v_max }}{% else %}—{% endif %}{{ v.units }}{{ v.long_name }}{{ v.standard_name }}{% if v.has_qc %}{% else %}–{% endif %}
+ +{% if nc_meta.scalar_vars %} +

Scalar metadata variables

+ + + + + + {% for v in nc_meta.scalar_vars %} + + + + + + + {% endfor %} + +
VariableValueUnitsLong name
{{ v.name }}{{ v.value }}{{ v.units }}{{ v.long_name }}
+{% endif %} + +{% if nc_meta.global_attrs %} +

Global attributes

+ + + + {% for k, v in nc_meta.global_attrs.items() %} + + + + + {% endfor %} + +
AttributeValue
{{ k }}{{ v }}
+{% endif %} + +{% endif %} + + + +{% endblock %} diff --git a/oceanarray/reports/templates/instrument.html b/oceanarray/reports/templates/instrument.html new file mode 100644 index 0000000..3ff25f6 --- /dev/null +++ b/oceanarray/reports/templates/instrument.html @@ -0,0 +1,444 @@ +{% extends "base.html" %} +{% block title %}{{ instr_type | title }} – s/n {{ serial }}{% endblock %} +{% block page_styles %} + :root { --accent:#1e8449; --accent-link:#d5f5e3; } + .file-table { width:100%; border-collapse:collapse; font-size:0.81rem; margin-bottom:1.2rem; } + .file-table th { background:var(--seafoam); text-align:left; padding:0.35rem 0.65rem; + border-bottom:2px solid #cde; font-weight:600; } + .file-table td { padding:0.3rem 0.65rem; border-bottom:1px solid #eef; vertical-align:middle; } + .file-table tr:nth-child(even) td { background:#f4f9fc; } + .file-table .ok { color:var(--good); font-weight:700; } + .file-table .miss { color:#bbb; } + table { width:100%; border-collapse:collapse; font-size:0.82rem; } + th { background:var(--ocean); color:#fff; padding:0.4rem 0.65rem; + text-align:left; font-weight:600; white-space:nowrap; } + td { padding:0.35rem 0.65rem; border-bottom:1px solid #ecf0f1; vertical-align:middle; } + tr:nth-child(even) td { background:var(--seafoam); } + .badge { display:inline-block; padding:0.12em 0.45em; border-radius:3px; + font-size:0.7rem; font-weight:700; white-space:nowrap; } + .b-ok { background:var(--good); color:#fff; } + .b-warn { background:var(--warn); color:#fff; } + .b-miss { background:#dfe6e9; color:#999; } + img.fig { width:100%; max-width:100%; border-radius:4px; margin-bottom:0.5rem; } + .qc-bar { display:flex; width:180px; height:13px; border-radius:3px; + overflow:hidden; gap:1px; background:#ecf0f1; } + .qc-bar div { height:100%; } +{% endblock %} +{% block masthead_title %}{{ instr_type | title }} — s/n {{ serial }}{% endblock %} +{% block masthead_sub %}{{ mooring_name }}{% endblock %} +{% block masthead_meta %} +
Cruise
{{ cruise }}
+
HAB
{{ "%.1f"|format(hab) }} m
+
Depth
{% if depth is not none %}~{{ "%.0f"|format(depth) }} m{% else %}—{% endif %}
+
Start
{{ t_start | default("—") }}
+
End
{{ t_end | default("—") }}
+
Duration
{{ duration }}
+
Samp. Δt
{{ median_dt | default("—") }}
+
Records
{{ n_records | default("—") }}
+
Source file
{{ nc_file }}{% if data_stage and data_stage != 'stage3' %} ({{ data_stage }} — QC not applied){% endif %}
+ {% endblock %} +{% block content %} + + + + +

Files

+ + + + {% if raw_file.exists %} + + + + + + + {% elif raw_file.unknown %} + + + + + + {% else %} + + + + + + {% endif %} + {% for f in stage_files %} + + + {% if f.exists %} + + + + {% else %} + + + {% endif %} + + {% endfor %} + +
StageFilenameSizeLast modified
Raw{{ raw_file.name }}{{ raw_file.size }}{{ raw_file.mtime }}
Raw{{ raw_file.name }}— not checked (no --raw-dir)
Raw{{ raw_file.name }}— not found
{{ f.label }}{{ f.name }}{{ f.size }}{{ f.mtime }}{{ f.name }}— not generated
+ + +

Processing history

+{% if history_entries %} +
    + {% for e in history_entries %} +
  • + {{ e.timestamp }} + {{ e.text }} +
  • + {% endfor %} +
+{% else %} +

No history attribute found.

+{% endif %} + + +{% if fig_adcp_velocity_b64 %} +

Velocity

+

+ Time–range colour plots of the four velocity components (east, north, up, error). + Y-axis: range from transducer (m). + Colour scale: symmetric about zero; percentile 2/98 of all components combined. + {% if data_stage and data_stage != 'stage3' %}Magnetic declination has not been applied ({{ data_stage }} data).{% endif %} +

+ +{% endif %} + + +

Time series (full deployment)

+{% if fig_ts_b64 %} +{% for _ts_img in fig_ts_b64 %} + +{% endfor %} +{% else %} +

No plottable variables found.

+{% endif %} + + +

Start & end windows — first / last 6 h

+{% if fig_windows_b64 %} +

+ Left panel = first 7 h (1 h lead-in + 6 h of record)  |  + Right panel = last 7 h (6 h of record + 1 h tail). + ││ orange dashed = auto-detected + (pressure-based) suggested deploy/recover time • + ││ green dashed = YAML + deployment_time / recovery_time • + ││ grey = stage 1 raw record + (same variable, same units). +

+

+ Reading the grey background: + where the raw stage 1 record is available and uses the same physical + units as the stage 2/3 data, it is shown in light grey so you can + visually verify that the chosen deployment window captures the in-water + period. Cases where grey may be absent: +

    +
  • Sensors without their own pressure (e.g. a microCAT that + relies on interpolated pressure from a nearby instrument) — interpolated + pressure is only added at stage 3, so stage 1 has no pressure + variable and no grey trace appears.
  • +
  • Velocity in beam or XYZ coordinates — if an Aquadopp or + ADCP was configured to record in BEAM or XYZ mode, the stage 1 file + contains beam/XYZ velocity variables while stage 3 stores + ENU velocities; the variable names differ so no grey velocity trace is shown.
  • +
  • Conductivity — raw conductivity units sometimes differ + between stage 1 and stage 3 (e.g. mS/cm vs S/m) and the + grey trace is suppressed automatically to avoid a misleading scale mismatch.
  • +
+ If the orange vline is absent from the right-hand panel, + no pressure-based recovery transition was detected in the final 25 % of the + stage 1 record — the suggested recovery time equals the last raw sample. + Check the timing table in the mooring summary report for the suggested UTC time. +

+{% for _win_img in fig_windows_b64 %} + +{% endfor %} +{% else %} +

Insufficient data for start/end windows.

+{% endif %} + + +{% if fig_tsd_b64 %} +

T-S diagram

+

+ Coloured by pressure (or sample index). × = suspect  |  × = bad (QC flags). +

+ +{% endif %} + + +{% if fig_adcp_rose_b64 %} +

Current roses

+

+ Depth-average followed by bins nearest 100, 200, 300 and 400 m from the transducer. + Bins with fewer than two valid samples are omitted. + Direction toward which the current flows; 0° = N, clockwise. + {% if data_stage and data_stage != 'stage3' %}Magnetic declination not yet applied ({{ data_stage }} data).{% endif %} +

+ +{% endif %} + + +{% if fig_rose_b64 %} +

Current rose diagrams

+{% if declination_warn %} +

+ ⚠ Magnetic declination could not be applied — latitude/longitude are missing or all-zero in the mooring YAML (check seabed_latitude, deployment_latitude, or latitude/longitude). + ENU velocities use 0° declination (magnetic north, not true north). +

+{% endif %} +

+ {% 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). + Direction toward which the current flows; 0° = N, clockwise. +

+ +{% endif %} + + +{% if fig_trajectory_b64 %} +

Particle trajectory

+

+ Pseudo-Lagrangian displacement obtained by integrating east/north velocity over time + (Euler forward; NaN velocities set to zero). Coloured by temperature. + Origin (0, 0) = deployment position; axes in metres. +

+ +{% endif %} + + +{% if fig_hodograph_b64 %} +

Hodograph

+

+ East vs north velocity (m s-1). + Left: full record. + Right: eddy component — raw minus 4-day low-pass (rolling mean). +

+ +{% endif %} + + +{% if fig_speed_boxplot_b64 %} +

Current speed distribution

+ +{% endif %} + + +{% if fig_analog_b64 %} +

Analog channels

+

+ Full-record time series of analog channel variables containing non-zero, non-NaN data. + One panel per channel. + Labels and units are read from the mooring YAML — update them there to change what appears here. +

+{% if analog_yaml_info %} +
    + {% for ch in analog_yaml_info %} +
  • + {{ ch.varname }} — from YAML: + {{ ch.yaml_key }}: {{ ch.label if ch.label else "(not set)" }}{% if ch.units %}, + {{ ch.yaml_key }}_units: "{{ ch.units }}"{% endif %}{% if ch.serial %}, + {{ ch.yaml_key }}_serial_number: {{ ch.serial }}{% endif %} +
  • + {% endfor %} +
+{% endif %} + +{% endif %} + + +

Data value distributions

+

+ Orange dashed = suspect threshold  |  Red dotted = fail threshold (gross-range QC). + Histogram shows non-bad data only; bad-flagged count noted in red. +

+{% if fig_dt_b64 %} + +{% else %} +

Not enough samples to compute.

+{% endif %} + + +{% if qc_summary %} +

QC flag breakdown

+ +{% if qc_thresholds %} +

Thresholds applied (as stored in stage 3 file)

+ + + + + + + + + + + {% for row in qc_thresholds %} + + + + + + + {% endfor %} + +
VariableTestSuspect range / thresholdFail range / threshold
{{ row.var }}{{ row.test }}{{ row.suspect }}{{ row.fail }}
+{% endif %} + +

Flag counts

+ + + + + + + + + + + + + + + {% for row in qc_summary %} + {% set good = row.flags | selectattr("flag", "eq", 1) | first %} + {% set susp = row.flags | selectattr("flag", "eq", 3) | first %} + {% set bad = row.flags | selectattr("flag", "eq", 4) | first %} + {% set interp = row.flags | selectattr("flag", "eq", 8) | first %} + {% set miss = row.flags | selectattr("flag", "eq", 9) | first %} + + + + + + + + + + + {% endfor %} + +
VariableNGood %Suspect %Bad %Interp. %Missing %Distribution
{{ row.var }}{{ "{:,}".format(row.total) }}{{ good.pct }}{% if susp.n > 0 %}{% if susp.pct > 0 %}{{ susp.pct }}{% else %}<0.1% ({{ susp.n }}){% endif %}{% else %}–{% endif %}{% if bad.n > 0 %}{% if bad.pct > 0 %}{{ bad.pct }}{% else %}<0.1% ({{ bad.n }}){% endif %}{% else %}–{% endif %}{% if interp.n > 0 %}{% if interp.pct > 0 %}{{ interp.pct }}{% else %}<0.1% ({{ interp.n }}){% endif %}{% else %}–{% endif %}{% if miss.n > 0 %}{% if miss.pct > 0 %}{{ miss.pct }}{% else %}<0.1% ({{ miss.n }}){% endif %}{% else %}–{% endif %} +
+ {% for f in row.flags %}{% if f.pct > 0 %} +
+ {% endif %}{% endfor %} +
+
+{% endif %} + + +{% if nc_meta.dims %} +

NetCDF dimensions — {{ nc_file }}

+ + + + {% for dim, size in nc_meta.dims.items() %} + + {% endfor %} + +
DimensionSize
{{ dim }}{{ "{:,}".format(size) }}
+{% endif %} + + +

NetCDF variables — {{ nc_file }}

+{% if nc_meta.get("error") %} +

Could not read file: {{ nc_meta.error }}

+{% else %} + +

Time-series variables

+ + + + + + {% for v in nc_meta.time_vars %} + {% if not v.is_qc %} + + + + + + + + + + + + + {% endif %} + {% endfor %} + +
VariableTypeDimsNValidMin / MaxUnitsLong nameStandard nameQC flag
{{ v.name }}{{ v.dtype }}{{ v.dims }}{{ "{:,}".format(v.n) }}{{ "{:,}".format(v.n_valid) if v.n_valid is defined else "—" }}{% if v.v_min is not none %}{{ v.v_min }} / {{ v.v_max }}{% else %}—{% endif %}{{ v.units }}{{ v.long_name }}{{ v.standard_name }}{% if v.has_qc %}{% else %}–{% endif %}
+ +{% if nc_meta.scalar_vars %} +

Scalar metadata variables

+ + + + + + {% for v in nc_meta.scalar_vars %} + + + + + + + + {% endfor %} + +
VariableTypeValueUnitsLong name
{{ v.name }}{{ v.dtype }}{{ v.value }}{{ v.units }}{{ v.long_name }}
+{% endif %} + +{% if nc_meta.global_attrs %} +

Global attributes

+ + + + {% for k, v in nc_meta.global_attrs.items() %} + + + + + {% endfor %} + +
AttributeValue
{{ k }}{{ v }}
+{% endif %} + +{% endif %} + + + +{% endblock %} diff --git a/oceanarray/reports/templates/mooring.html b/oceanarray/reports/templates/mooring.html new file mode 100644 index 0000000..b9f09ed --- /dev/null +++ b/oceanarray/reports/templates/mooring.html @@ -0,0 +1,749 @@ +{% extends "base.html" %} +{% block title %}{{ mooring_name }} – recovery report{% endblock %} +{% block page_styles %} + table { width: 100%; border-collapse: collapse; font-size: 0.83rem; } + th { + background: var(--ocean); + color: #fff; + padding: 0.45rem 0.7rem; + text-align: left; + font-weight: 600; + white-space: nowrap; + } + td { padding: 0.38rem 0.7rem; border-bottom: 1px solid #ecf0f1; vertical-align: middle; } + tr:nth-child(even) td { background: var(--seafoam); } + tr:hover td { background: #d6eaf8; } + .pipeline { white-space: nowrap; display: flex; flex-wrap: wrap; gap: 0.2rem; align-items: center; } + .badge { + display: inline-block; + padding: 0.15em 0.5em; + border-radius: 3px; + font-size: 0.73rem; + font-weight: 700; + white-space: nowrap; + } + .b-ok { background: var(--good); color: #fff; } + .b-warn { background: var(--warn); color: #fff; } + .b-miss { background: #dfe6e9; color: #999; } + .b-stack { background: var(--interp); color: #fff; } + .b-grid { background: #8e44ad; color: #fff; } + .arrow { color: #ccc; font-size: 0.8rem; margin: 0 0.05rem; } + .pos { color: var(--warn); font-weight: 600; } + .neg { color: var(--interp); font-weight: 600; } + tr.row-warn td { background: #fef3cd !important; } +{% endblock %} +{% block masthead_type %}Summary{% endblock %} +{% block masthead_sub %}Mooring recovery report{% endblock %} +{% block masthead_meta %} +
Cruise
{{ cruise }}
+
Ship
{{ ship }}
+
Latitude
{{ latitude }}
+
Longitude
{{ longitude }}
+
Water depth
{{ waterdepth }}{% if waterdepth != '—' %} m{% endif %}
+
Deployment
{{ deploy_time }}
+
Recovery
{{ recover_time }}
+
Duration
{{ duration }}
+
Instruments
{{ n_instruments }}
+ {% endblock %} +{% block content %} + + + + +

2 — Processing pipeline

+

+ Raw = file present in raw directory • + Read = format check passed • + Stage 1–3 = processed NetCDF files exist • + Stack = mooring-level _stack.nc • + Grid = pressure-gridded _grid.nc +

+

+ Instruments marked skip: true in the YAML are excluded from all + processing and are shown with a skipped note in the Note column. + Stack and Grid pills are grey for any instrument that did not reach Stage 3. + See Table 3 for skip reasons where given. +

+ + + + + + + + + + + + + + {% for instr in instruments %} + + + + + + + + + + {% endfor %} + +
#TypeS/NHab (m)Depth (m)PipelineNote
{{ loop.index }}{{ instr.instr_type }}{% if instr.report_exists %}{{ instr.serial }}{% else %}{{ instr.serial }}{% endif %}{{ "%.1f"|format(instr.hab) }}{{ "%.0f"|format(instr.depth) if instr.depth is not none else "—" }} +
+ {# raw file #} + {% if instr.raw_exists %} + Raw ✓ + {% elif instr.filename %} + Raw ✗ + {% else %} + Raw — + {% endif %} + + {# readability check #} + {% if instr.raw_exists %} + {% if instr.readable %} + Read ✓ + {% else %} + Read ✗ + {% endif %} + {% else %} + Read — + {% endif %} + + {# stage 1 #} + {% if instr.stages.stage1 %} + Stage 1 ✓ + {% else %} + Stage 1 ○ + {% endif %} + + {# stage 2 #} + {% if instr.stages.stage2 %} + Stage 2 ✓ + {% else %} + Stage 2 ○ + {% endif %} + + {# stage 3 #} + {% if instr.stages.stage3 %} + Stage 3 ✓ + {% else %} + Stage 3 ○ + {% endif %} + + {# stack — only coloured if this instrument is in the stack file #} + {% if instr.in_stack %} + Stack ✓ + {% else %} + Stack ○ + {% endif %} + + {# grid — only coloured if this instrument is in the grid file #} + {% if instr.in_grid %} + Grid ✓ + {% else %} + Grid ○ + {% endif %} +
+
+ {% if instr.skipped %}skipped{% endif %} +
+ + +

3 — Instrument summary

+

+ Variables and record length are read from stage 3 files where available, + otherwise stage 2. The P badge is shown as present + whether pressure was directly measured or interpolated from neighbouring + instruments — see Table 6 (QC flag summary) to distinguish. Instruments + marked skip: true are listed with the skip reason where provided. +

+ + + + + + + + + + + + + + + + + + + + + + {% for instr in instruments %} + + + + + + {% if instr.nc and instr.nc.get("error") %} + + {% elif instr.nc and instr.nc.get("t_start") %} + + + + + + {% for label, present in instr.nc.shorthands %} + + {% endfor %} + {% elif instr.skipped %} + + {% else %} + + {% endif %} + + {% endfor %} + +
#TypeS/NHab (m)First sampleLast sampleN recordsΔt (s)P range (dbar)TCPUV
{{ loop.index }}{{ instr.instr_type }}{% if instr.report_exists %}{{ instr.serial }}{% else %}{{ instr.serial }}{% endif %}{{ "%.1f"|format(instr.hab) }}Error: {{ instr.nc.error }}{{ instr.nc.t_start }}{{ instr.nc.t_end }}{{ "{:,}".format(instr.nc.n_records) }} + {% set dt = instr.nc.dt_s %} + {% if dt == dt %}{# NaN check: NaN != NaN #} + {{ "%.0f"|format(dt) }}{% if instr.dt_mismatch %} *{% endif %} + {% else %} + + {% endif %} + + {% if instr.nc.p_min is not none and instr.nc.p_max is not none %} + {% if instr.nc.get("pressure_interpolated") %} + ({{ "%.0f"|format(instr.nc.p_min) }}, {{ "%.0f"|format(instr.nc.p_max) }}) + {% else %} + ({{ "%.0f"|format(instr.nc.p_min) }}, {{ "%.0f"|format(instr.nc.p_max) }}) + {% endif %} + {% else %} + + {% endif %} + + {% if label == "P" and present and instr.nc.get("pressure_interpolated") %} + {{ label }} + {% else %} + {{ label }} + {% endif %} + skipped{% if instr.skip_reason %} — {{ instr.skip_reason }}{% endif %}no processed file
+ +{% set dt_mismatches = instruments | selectattr("dt_mismatch") | list %} +{% if dt_mismatches %} +

+ * Δt mismatch (YAML vs observed p90): + {% for instr in dt_mismatches %} +
  {{ instr.serial }}: + YAML gives Δt of {{ instr.yaml_interval_s }} s; + stage file shows Δt of {{ "%.0f"|format(instr.nc.dt_s) }} s. + {% endfor %} +

+{% endif %} + +{% if grid_p_start is not none and grid_p_end is not none %} +

+ Recommended pressure range for oceanarray grid, derived from the + min/max pressure across all instruments (rounded outward to the nearest 20 dbar): +

+
--pmin {{ grid_p_start }} --pmax {{ grid_p_end }}
+{% endif %} + + +

3.5 — Deployment timing

+

+ Stage 1 and Sugg. (raw) columns are in the raw + instrument clock (uncorrected). Sugg. UTC columns + apply the constant clock offset (and drift, for the end) and are safe to + paste into the YAML deployment_time / + recovery_time fields. Suggested UTC cells are highlighted + amber when the + pressure-derived suggested time differs from the YAML time by more than + 2 Δt (indicating a sinking/rising transient was detected). + The Stage 1 last cell is highlighted + orange when the + raw record ended more than 2 Δt before the YAML recovery time + (instrument may have stopped early). + Serial numbers link to the per-instrument report; the + 6 h link jumps directly to the start/end window plots. +

+

+ Spot-check recommended: open the 6 h start/end + window plots for each instrument (click 6 h) and visually + confirm that the orange suggested line (or green YAML line if they match) + sits at a plausible transition in the pressure (or temperature) record + before updating the YAML times. +

+
+ + + + + + + + + + + + + + + + + + + + {% for instr in instruments %} + {% set tm = instr.timing %} + + + + + {% if tm %} + {# Anchor dates for deduplication: show only time when on the same day #} + {% set d_start = (tm.get("stage1_start") or "")[:10] %} + {% set d_end = (tm.get("stage1_end") or "")[:10] %} + {# Stage1 first — always full date+time; anchors deduplication for start columns #} + + {# Sugg. start (raw) — only present for pressure instruments; time only when same date #} + {% set v = tm.get("sugg_start") or "" %} + + {{- v[11:] if (v and v[:10] == d_start) else (v or "—") -}} + + {# Sugg. start (UTC) #} + {% set v = tm.get("sugg_start_utc") or "" %} + + {{- v[11:] if (v and v[:10] == d_start) else (v or "—") -}} + + {# Stage1 last — orange-amber when instrument stopped early #} + + {{- tm.get("stage1_end") or "—" -}} + + {# Sugg. end (raw) #} + {% set v = tm.get("sugg_end") or "" %} + + {{- v[11:] if (v and v[:10] == d_end) else (v or "—") -}} + + {# Sugg. end (UTC) #} + {% set v = tm.get("sugg_end_utc") or "" %} + + {{- v[11:] if (v and v[:10] == d_end) else (v or "—") -}} + + {% elif instr.skipped %} + + {% else %} + + {% endif %} + + {% endfor %} + + {% if rec_deploy_sec or rec_recover_sec %} + + + + + + + + + + + + + {% endif %} +
#TypeS/NStartEnd
Stage1 firstSugg. start (raw)Sugg. start (UTC)Stage1 lastSugg. end (raw)Sugg. end (UTC)
{{ loop.index }}{{ instr.instr_type }} + {{ instr.serial }} + 6 h + {{ tm.get("stage1_start") or "—" }}skippedno stage 2 file
MooringMooring start{{ rec_deploy_sec or "—" }}Mooring end{{ rec_recover_sec or "—" }}
+
+ +{% if yaml_deploy_time or yaml_recover_time or rec_deploy or rec_recover %} +
+{% if rec_differs %} +

+ Deployment times — pressure-based instruments suggest times that differ + from the current YAML. Copy the suggested snippet (blue border) into your YAML file. + Times are given to the minute. +

+
+
+

+ Current YAML

+ +
+
+

+ Suggested (copy → paste into YAML)

+ +
+
+{% else %} +

+ ✓ Current YAML times match the pressure-based suggestion from the instruments.

+

+ Current YAML times

+ +{% endif %} +
+{% endif %} + + +

4 — Clock corrections

+

+ Positive drift/offset = instrument was slow (behind UTC); correction shifts times later. + Negative = instrument was fast (ahead of UTC); correction shifts times earlier. +

+ + + + + + + + + + + + + + + + {% for instr in instruments %} + + + + + + + + + + + + {% endfor %} + +
#TypeS/NHab (m)Offset (s)Computer time at recoveryInstrument time at recoveryDrift (s)Source
{{ loop.index }}{{ instr.instr_type }}{{ instr.serial }}{{ "%.1f"|format(instr.hab) }} + {% if instr.clock.offset_s == 0 %} + + {% elif instr.clock.offset_s > 0 %} + +{{ "%.1f"|format(instr.clock.offset_s) }} + {% else %} + {{ "%.1f"|format(instr.clock.offset_s) }} + {% endif %} + {% if instr.clock.computer_time %}{{ instr.clock.computer_time }}{% else %}{% endif %}{% if instr.clock.instrument_time %}{{ instr.clock.instrument_time }}{% else %}{% endif %} + {% if instr.clock.drift_s is none or instr.clock.drift_s == 0 %} + + {% elif instr.clock.drift_s > 0 %} + +{{ "%.1f"|format(instr.clock.drift_s) }} + {% else %} + {{ "%.1f"|format(instr.clock.drift_s) }} + {% endif %} + + {% if instr.clock.method == "none" %} + none + {% else %} + {{ instr.clock.method }} + {% endif %} +
+ +{% if fig_clock_check_b64 %} +

Clock alignment check

+

+ Overlaid temperature records zoomed to the first and last 30 minutes of the deployment. + If instrument clocks are misaligned the temperature curves will appear shifted in time. + Data are from stage‑3 (or stage‑2 if stage‑3 is not yet available). + Instruments with no temperature data are omitted. +

+Clock alignment check +{% endif %} + + +

5 — Sensor calibration

+ +{% set has_sensors = instruments | selectattr("sensors") | list %} +{% if has_sensors %} + + + + + + + + + + + + + + + {% set ns = namespace(idx=0) %} + {% for instr in instruments %} + {% if instr.sensors %} + {% set ns.idx = ns.idx + 1 %} + {% for sensor in instr.sensors %} + + + + + + + + + + + {% endfor %} + {% endif %} + {% endfor %} + +
#TypeInstr S/NSensorModelSensor S/NCal dateCoefficients
{% if loop.index0 == 0 %}{{ ns.idx }}{% endif %}{% if loop.index0 == 0 %}{{ instr.instr_type }}{% endif %}{% if loop.index0 == 0 %}{{ instr.serial }}{% endif %}{{ sensor.sensor_type | title }}{{ sensor.sensor_model }}{{ sensor.sensor_serial }}{{ sensor.cal_date }} + {% if sensor.coefficients %} +
+ show +
{{ sensor.coefficients }}
+
+ {% else %} + + {% endif %} +
+{% else %} +

No sensor calibration metadata found in processed files.

+{% endif %} + + +

6 — QC flag summary

+ +
+ good (1) + prob. good (2) + suspect (3) + bad (4) + interp. (8) + missing (9) +
+{% set has_qc = instruments | selectattr("qc_summary") | list %} +{% if has_qc %} + + + + + + + + + + + + + + + + + + {% set ns = namespace(idx=0) %} + {% for instr in instruments %} + {% if instr.qc_summary %} + {% set ns.idx = ns.idx + 1 %} + {% for row in instr.qc_summary %} + {% set good = row.flags | selectattr("flag", "eq", 1) | first %} + {% set susp = row.flags | selectattr("flag", "eq", 3) | first %} + {% set bad = row.flags | selectattr("flag", "eq", 4) | first %} + {% set interp = row.flags | selectattr("flag", "eq", 8) | first %} + {% set miss = row.flags | selectattr("flag", "eq", 9) | first %} + + + + + + + + + + + + + + {% endfor %} + {% endif %} + {% endfor %} + +
#TypeS/NVariableNGood %Suspect %Bad %Interp. %Missing %Distribution
{% if loop.index0 == 0 %}{{ ns.idx }}{% endif %}{% if loop.index0 == 0 %}{{ instr.instr_type }}{% endif %}{% if loop.index0 == 0 %}{{ instr.serial }}{% endif %}{{ row.var }}{{ "{:,}".format(row.total) }} + {{ good.pct }} + {% if susp.pct > 0 %}{{ susp.pct }}{% else %}–{% endif %}{% if bad.pct > 0 %}{{ bad.pct }}{% else %}–{% endif %}{% if interp.pct > 0 %}{{ interp.pct }}{% else %}–{% endif %}{% if miss.pct > 0 %}{{ miss.pct }}{% else %}–{% endif %} +
+ {% for f in row.flags %} + {% if f.pct > 0 %} +
+ {% endif %} + {% endfor %} +
+
+{% else %} +

No stage 3 QC files found — run oceanarray stage3 first.

+{% endif %} + +{% if fig_knockdown_hab_b64 or fig_knockdown_anomaly_b64 or fig_knockdown_displacement_b64 %} + +

Mooring knockdown

+
+{% if fig_knockdown_hab_b64 %} +
+

Nominal HAB (x) vs. measured pressure (y). The dashed line shows expected pressure + (water depth − HAB). Instruments below the line were knocked down. + Interpolated pressure (QC flag 8) excluded.

+Knockdown HAB vs pressure +
+{% endif %} +{% if fig_knockdown_anomaly_b64 %} +
+

IQR of pressure anomaly (measured − nominal) per instrument. + Positive = deeper than design depth. + Green < 100 dbar, + yellow 100–200, + amber 200–300, + red > 300 dbar.

+Knockdown pressure anomaly +
+{% endif %} +
+{% if fig_knockdown_displacement_b64 %} +

Estimated horizontal displacement (m) vs. measured pressure. + Left: scatter per instrument; right: normalised 2-D density across all instruments. + Displacement derived from the rigid-pendulum approximation: + x = √(habnom² − habmeas²).

+Knockdown horizontal displacement +{% endif %} +{% endif %} + +{% if diagram_b64 %} + +

Mooring diagram

+ +{% endif %} + + +{% if issues.any %} +

Issues for cruise report

+ +{% if issues.skipped %} +

Skipped / missing instruments

+
    +{% for i in issues.skipped %} +
  • SN {{ i.serial }} ({{ i.itype }}, {{ i.depth_str }}): {{ i.reason }}
  • +{% endfor %} +
+{% endif %} + +{% if issues.stopped_early %} +

Stopped early

+
    +{% for i in issues.stopped_early %} +
  • SN {{ i.serial }} ({{ i.itype }}, {{ i.depth_str }}): last sample {{ i.t_end }}, expected {{ i.expected }} ({{ i.delta_h }} h short)
  • +{% endfor %} +
+{% endif %} + +{% if issues.qc_flagged %} +

Data quality flags

+
    +{% for i in issues.qc_flagged %} +
  • SN {{ i.serial }} ({{ i.itype }}): {{ i.note }}
  • +{% endfor %} +
+{% endif %} + +{% endif %} + + + + + +{% endblock %} diff --git a/oceanarray/reports/templates/recovery_table.html b/oceanarray/reports/templates/recovery_table.html new file mode 100644 index 0000000..b9ed151 --- /dev/null +++ b/oceanarray/reports/templates/recovery_table.html @@ -0,0 +1,129 @@ +{% extends "base.html" %} +{% block title %}{{ mooring_name }} – Mooring Recovery Table{% endblock %} +{% block masthead_class %}masthead-plain{% endblock %} +{% block masthead_title %}{{ mooring_name }} ({{ year }}){% endblock %} +{% block masthead_type %}Mooring Recovery{% endblock %} +{% block masthead_nav %}{% endblock %} +{% block masthead_meta %} + {% if latitude %}
Latitude
{{ latitude }}
{% endif %} + {% if longitude %}
Longitude
{{ longitude }}
{% endif %} +
Water depth
{{ waterdepth }} m
+
Deployment
{{ deploy_time }}
+
Recovery
{{ recover_time }}
+
Duration
{{ duration }}
+ {% endblock %} +{% block page_styles %} + table { + border-collapse: collapse; + width: 100%; + font-size: 9.5pt; + } + th { + background: var(--ocean); + color: #fff; + padding: 0.35rem 0.5rem; + text-align: left; + font-weight: bold; + vertical-align: bottom; + white-space: pre-line; + } + th.num { text-align: right; } + td { + padding: 0.28rem 0.5rem; + border-bottom: 1px solid #ccc; + vertical-align: top; + } + tr:nth-child(even) td { background: var(--seafoam); } + tr.nonlog td { + background: #f0f0f0; + font-style: italic; + color: #444; + border-bottom: 1px solid #aaa; + } + td.ts { font-size: 8.5pt; white-space: pre; font-family: monospace; } + td.drift { font-family: monospace; font-size: 9pt; text-align: center; } + td.star { text-align: center; font-weight: bold; } + .notes { margin-top: 1.2rem; font-size: 9.5pt; } + .notes h3 { font-size: 10pt; margin-bottom: 0.4rem; } + .notes dl { margin: 0; } + .notes dt { font-weight: bold; margin-top: 0.4rem; } + .notes dd { margin: 0 0 0.2rem 1.2rem; } + .footer { margin-top: 1.5rem; font-size: 8.5pt; color: #888; border-top: 1px solid #ddd; padding-top: 0.5rem; } + @media print { + body { padding: 0; max-width: 100%; font-size: 9pt; } + th { -webkit-print-color-adjust: exact; print-color-adjust: exact; } + table { page-break-inside: auto; } + tr { page-break-inside: avoid; } + .footer { display: none; } + } +{% endblock %} +{% block content %} + + + + + + + + + + + + + + + + + {% for row in rows %} + {% if row.nonlog %} + + + + + + {% else %} + + + + + + + + + + + + {% endif %} + {% endfor %} + +
Height +(above +bottom) +(m)Depth +(nominal) +(m)InstrumentParam.Sample +int. +(s)Start/stop +time UTCClock +driftFirst good / +Last good recordNotes
{{ row.hab if row.hab is not none else "" }}{{ row.depth if row.depth is not none else "" }}{{ row.description }}
{{ row.hab if row.hab is not none else "?" }}{{ row.depth if row.depth is not none else "?" }}{{ row.instrument }}{{ row.params }}{{ row.interval_s if row.interval_s else "" }}{{ row.start_stop }}{{ row.clock_drift }}{{ row.first_last }}{{ "*" if row.comment else "" }}
+ +{% if comments %} +
+

Notes

+
+ {% for c in comments %} +
SN {{ c.serial }} ({{ c.instrument }}, {{ c.depth_str }})
+
{{ c.comment }}
+ {% endfor %} +
+
+{% endif %} + + + +{% endblock %} diff --git a/oceanarray/reports/templates/stack.html b/oceanarray/reports/templates/stack.html new file mode 100644 index 0000000..0a0ee73 --- /dev/null +++ b/oceanarray/reports/templates/stack.html @@ -0,0 +1,317 @@ +{% extends "base.html" %} +{% block title %}Stack report – {{ mooring_name }}{% endblock %} +{% block page_styles %} + :root { --accent:#2980b9; --accent-link:#cef; } + .fig { width:100%; border:1px solid #dce; border-radius:4px; margin-bottom:1.5rem; } + .var-table, .instr-table { width:100%; border-collapse:collapse; font-size:0.82rem; margin-bottom:1.5rem; } + .var-table th, .instr-table th { background:var(--seafoam); text-align:left; + padding:0.4rem 0.6rem; border-bottom:2px solid #cde; } + .var-table td, .instr-table td { padding:0.3rem 0.6rem; border-bottom:1px solid #eef; vertical-align:top; } + .var-table tr:nth-child(even) td, .instr-table tr:nth-child(even) td { background:#f4f9fc; } + .var-table tr:hover td, .instr-table tr:hover td { background:#e8f4f8; } +{% endblock %} +{% block masthead_type %}Stacked{% endblock %} +{% block masthead_meta %} +
Cruise
{{ cruise }}
+
Ship
{{ ship }}
+
Latitude
{{ latitude }}
+
Longitude
{{ longitude }}
+
Water depth
{{ waterdepth }}{% if waterdepth != '—' %} m{% endif %}
+
Deployment
{{ deploy_time }}
+
Recovery
{{ recover_time }}
+
Duration
{{ duration }}
+
Samp. Δt
{{ dt_seconds }}{% if dt_seconds != '—' %} s{% endif %}
+
Records
{{ n_time }}
+
Instruments
{{ n_instr }}
+
Source file
{{ nc_file }}
+ {% endblock %} +{% block content %} + + + + +{% if history_entries %} +

Processing history

+
    + {% for e in history_entries %} +
  • + {{ e.timestamp }} + {{ e.text }} +
  • + {% endfor %} +
+{% endif %} + + +

Instruments (deep-first)

+ + + + {% for row in instr_rows %} + + + + + + + + {% endfor %} + +
#TypeSerialHAB (m)~Depth (m)
{{ loop.index0 }}{{ row.instr_type }}{% if row.report_exists %}{{ row.serial }}{% else %}{{ row.serial }}{% endif %}{{ row.hab }}{{ row.depth }}
+ + +{% if fig_pressure_b64 %} +

Pressure records (all instruments)

+

Values with QC flag ≥ 3 (suspect/bad) masked to NaN before plotting. All data values are in {{ nc_file }} without masking.

+Pressure time series +{% endif %} + + +{% if fig_temp_b64 %} +

Temperature (all instruments)

+

Values with QC flag ≥ 3 (suspect/bad) masked to NaN before plotting. All data values are in {{ nc_file }} without masking.

+Temperature time series +{% endif %} + + +{% if fig_sal_b64 %} +

Salinity (all instruments)

+

Values with QC flag ≥ 3 (suspect/bad) masked to NaN before plotting. All data values are in {{ nc_file }} without masking.

+Salinity time series +{% endif %} + +{% if fig_dissolved_oxygen_b64 %} +

Dissolved oxygen (all instruments)

+

One line per instrument with dissolved oxygen data (SBE ODO sensor); QC flags ≥ 3 masked. Units: µmol L⁻¹. % saturation available in per-instrument reports.

+Dissolved oxygen time series +{% endif %} + + +{% if fig_east_vel_b64 %} +

East velocity (U)

+

ENU frame. Values with velocity_flag ≥ 3 masked to NaN before plotting. All data values are in {{ nc_file }} without masking. Instruments without velocity data omitted.

+East velocity time series +{% endif %} + +{% if fig_north_vel_b64 %} +

North velocity (V)

+

ENU frame. Values with velocity_flag ≥ 3 masked to NaN before plotting. All data values are in {{ nc_file }} without masking.

+North velocity time series +{% endif %} + +{% if fig_up_vel_b64 %} +

Vertical velocity (W)

+

ENU frame. Values with velocity_flag ≥ 3 masked to NaN before plotting. All data values are in {{ nc_file }} without masking.

+Vertical velocity time series +{% endif %} + +{% if fig_turbidity_b64 %} +

Turbidity

+

One line per instrument with turbidity data; QC flags ≥ 3 masked. Dots overlaid to reveal individual samples near zero. Units from file attrs (verify: NTU, FTU, or V depending on sensor).

+Turbidity time series +{% endif %} + +{% if fig_trajectories_b64 or fig_adcp_trajectories_b64 %} +

Particle trajectories

+

+ Pseudo-Lagrangian displacement: east/north velocity integrated over time + (Euler forward; NaN velocities set to zero). All trajectories share a common origin (0, 0). + Aquadopp: colour shows temperature (shared scale); end points labelled with serial and HAB. + ADCP: per-bin trajectories coloured by height above bottom; bins entirely below the seabed are omitted. +

+
+ {% if fig_trajectories_b64 %} +
+

Aquadopp

+ Aquadopp trajectories +
+ {% endif %} + {% if fig_adcp_trajectories_b64 %} +
+

ADCP

+ ADCP trajectories +
+ {% endif %} +
+{% endif %} + +{% if fig_speed_profile_b64 %} +

Aquadopp speed profile

+

+ Horizontal boxplot per Aquadopp, positioned at its nominal height above bottom + (per-instrument design value; does not account for mooring knockdown). + Box = interquartile range; line = median; whiskers = 1.5×IQR; dots = outliers. + Computed from east/north velocity components if current_speed is not stored. +

+Aquadopp speed profile +{% endif %} + +{% if fig_analog_b64 %} +

Analog channels

+

+ Full-record time series of analog channel variables containing non-zero, non-NaN data. + One panel per channel. +

+Analog channels +{% endif %} + +{% if fig_ts_stack_b64 %} +

T-S diagram

+

Left: scatter coloured by pressure. Middle: 2-D count heatmap. Right (when oxygen data present): scatter coloured by O₂ saturation (%). Bad (flag 4) and missing (flag 9) excluded; interpolated pressure (flag 8) retained.

+T-S diagram +{% endif %} + +{% if fig_rose_grid_b64 %} +

Current rose diagrams

+

Direction the current flows toward (oceanographic convention, 0°=N). Speed coloured light→dark blue (slow→fast). QC-flagged samples excluded. Title shows serial number and height above bottom (m).

+{% if rose_declination_warn %} +

+ ⚠ Magnetic declination could not be applied to + {% if rose_declination_missing_serials %}Aquadopp(s) s/n {{ rose_declination_missing_serials | join(', ') }}{% else %}one or more Aquadopps{% endif %} + — latitude/longitude are missing or all-zero in the mooring YAML (check seabed_latitude, deployment_latitude, or latitude/longitude). + Re-run oceanarray process … --stage 3 for the affected instrument(s) after fixing the YAML. + Affected ENU velocities currently use 0° declination (magnetic north, not true north). +

+{% endif %} +{% if rose_declination_note %}

{{ rose_declination_note }}

{% endif %} +Current rose grid +{% endif %} + +{% if fig_aquadopp_tilt_b64 %} +

Aquadopp tilt (|pitch| / |roll| / pressure estimate)

+

+ One panel per Aquadopp (deep-first). Blue = |pitch|, green = |roll|, orange dashed = tilt + estimated from pressure difference between the Aquadopp and the nearest instrument ≥10 m above + with valid pressure (arccos(ΔP / rope length)). All curves are non-negative. + Horizontal lines: orange dashed = suspect threshold, red dotted = fail threshold (read from file attrs). + Pitch and roll are stored unmasked in the stack file; use pitch_qc / + roll_qc to filter. Plots show all available values. +

+Aquadopp tilt panels +{% endif %} + +{% if fig_spacing_b64 %} +

Adjacent instrument spacing

+

Distribution of pressure differences between adjacent instrument pairs (pairs < 2 dbar apart excluded as co-located).

+Instrument spacing histogram +{% endif %} + +{% if fig_clock_check_b64 %} +

Clock alignment check

+

+ Temperature records from all instruments overlaid, zoomed to the first and last + 10 minutes of the deployment. A horizontal shift between curves indicates + a clock offset between instruments. Data are from stage‑3 (or stage‑2 + if stage‑3 is not yet available). Instruments without temperature are omitted. +

+Clock alignment check +{% endif %} + + +

NetCDF dimensions — {{ nc_file }}

+{% if nc_meta.dims %} + + + + {% for dim, size in nc_meta.dims.items() %} + + {% endfor %} + +
DimensionSize
{{ dim }}{{ "{:,}".format(size) }}
+{% endif %} + + +

NetCDF variables — {{ nc_file }}

+{% if nc_meta.get("error") %} +

Could not read file: {{ nc_meta.error }}

+{% else %} + +

Variables

+ + + + + + {% for v in nc_meta.time_vars %} + {% if not v.is_qc %} + + + + + + + + + + + + + {% endif %} + {% endfor %} + +
VariableTypeDimsNValidMin / MaxUnitsLong nameStandard nameQC flag
{{ v.name }}{{ v.dtype }}{{ v.dims }}{{ "{:,}".format(v.n) }}{{ "{:,}".format(v.n_valid) if v.n_valid is defined else "—" }}{% if v.v_min is not none %}{{ v.v_min }} / {{ v.v_max }}{% else %}—{% endif %}{{ v.units }}{{ v.long_name }}{{ v.standard_name }}{% if v.has_qc %}{% else %}–{% endif %}
+ +{% if nc_meta.scalar_vars %} +

Scalar metadata variables

+ + + + + + {% for v in nc_meta.scalar_vars %} + + + + + + + + {% endfor %} + +
VariableTypeValueUnitsLong name
{{ v.name }}{{ v.dtype }}{{ v.value }}{{ v.units }}{{ v.long_name }}
+{% endif %} + +{% if nc_meta.global_attrs %} +

Global attributes

+ + + + {% for k, v in nc_meta.global_attrs.items() %} + + + + + {% endfor %} + +
AttributeValue
{{ k }}{{ v }}
+{% endif %} + +{% endif %} + + + +{% endblock %} diff --git a/pyproject.toml b/pyproject.toml index 32fd00b..c478215 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,7 @@ oceanarray = [ "config/*.mplstyle", # vendored report.mplstyle — MPLSTYLE_PATH resolves it at runtime "config/*.yaml", "config/legacy/*.yaml", + "reports/templates/*.html", # report page templates loaded via FileSystemLoader ] [tool.setuptools.dynamic] diff --git a/tests/fixtures/golden/dune2/dune2_1_2026_grid_report.html b/tests/fixtures/golden/dune2/dune2_1_2026_grid_report.html index 82d0e46..2eb41e2 100644 --- a/tests/fixtures/golden/dune2/dune2_1_2026_grid_report.html +++ b/tests/fixtures/golden/dune2/dune2_1_2026_grid_report.html @@ -3,39 +3,42 @@ -Grid report – dune2_1_2026 +Grid report – dune2_1_2026 + -
-

dune2_1_2026Gridded

-

generated

+
+

dune2_1_2026Gridded

+

generated

Reports:SummaryStackGrid
Aquadopp:9920
Microcat:2941
+
Cruise
OdB
Ship
Odon de Buen
Latitude
65 43.913
@@ -80,9 +101,13 @@

oceanarray

+ +