oceanarray API reference

Load and process moored oceanographic time series data from raw instrument format to array-integrated products.

Public entry point

process

Top-level process() function, STAGES registry, and resolve_stage() dispatcher. These are the primary public API for driving the pipeline from Python code.

Pipeline stage registry and public process() entry point.

The STAGES tuple is the single source of truth for pipeline ordering, scope, and dispatch. It is used by process(), the CLI, the re-run rule (plan §7), and the parametrised re-run tests (test plan §7d).

Example usage:

import oceanarray

oceanarray.process("dsG3_1_2026", proc_dir="/data/proc")               # all five stages
oceanarray.process("dsG3_1_2026", stage=1, proc_dir="/data/proc",
                   raw_dir="/data/raw")                                  # stage 1 only
oceanarray.process("dsG3_1_2026", stage="grid", proc_dir="/data/proc")  # grid only
oceanarray.processors.STAGES: tuple[Stage, ...] = (Stage(name='stage1', number=1, scope='instrument', run=<function _run_stage1>), Stage(name='stage2', number=2, scope='instrument', run=<function _run_stage2>), Stage(name='stage3', number=3, scope='instrument', run=<function _run_stage3>), Stage(name='stack', number=None, scope='mooring', run=<function _run_stack>), Stage(name='grid', number=None, scope='mooring', run=<function _run_grid>))

Pipeline stages in execution order. Single source of truth for process(), the CLI, the re-run rule (plan §7), and the parametrised re-run tests (test plan §7d).

class oceanarray.processors.Stage(name: str, number: int | None, scope: Literal['instrument', 'mooring'], run: Callable[[...], bool])[source]

One pipeline stage.

Parameters:
  • name (str) – Canonical stage name used on the CLI and in log messages. For the numbered stages this matches the output filename suffix (*_stage3.nc); stack and grid write stack.nc and grid.nc.

  • number (int or None) – Position for the numbered stages; None for stack and grid. This is what makes stage=1 resolvable and stage=4 a ValueError rather than a silent wrong dispatch.

  • scope (Scope) – Whether the stage runs once per instrument ("instrument") or once per mooring ("mooring"). Drives which staleness sources the re-run rule consults (plan §7).

  • run (Callable) – Normalised entry point: run(mooring, proc_dir, *, force, **kw) -> bool.

name: str
number: int | None
run: Callable[[...], bool]
scope: Literal['instrument', 'mooring']
oceanarray.processors.process(mooring: str, stage: int | str | list[int | str] | None = None, *, proc_dir: PathLike, raw_dir: PathLike | None = None, force: bool = False, **kw: Any) bool[source]

Run one or more pipeline stages, or every stage in order, for mooring.

Parameters:
  • mooring (str) – Mooring name (the subdirectory under proc_dir).

  • stage (int, str, list of int/str, or None) – Which stage(s) to run — 1, 2, 3, "stage1", "stack", "grid", etc. Pass a list to run a specific subset; the subset is always executed in STAGES order regardless of the order of elements in the list. None (the default) runs all five stages in STAGES order.

  • proc_dir (path-like) – Processing root directory (parent containing per-mooring subdirectories).

  • raw_dir (path-like, optional) – Raw-data root. Required only when stage includes stage 1.

  • force (bool, default False) – Re-run even when the output is newer than its inputs.

  • **kw – Extra keyword arguments forwarded to the stage’s run callable (e.g. serials, dt_seconds, p_start, p_end, dp).

Returns:

True if every requested stage succeeded, False if any failed.

Return type:

bool

oceanarray.processors.resolve_stage(stage: int | str) Stage[source]

Return the Stage named or numbered by stage.

Parameters:

stage (int or str) – 1, 2, or 3; or a canonical name — "stage1", "stage2", "stage3", "stack", "grid". Names are matched case-insensitively. Integer 4 is deliberately an error: stack and grid have no number so there is no valid integer shorthand for them.

Returns:

The matching Stage entry from STAGES.

Return type:

Stage

Raises:

ValueError – If stage matches no entry. The message lists every valid value.

I/O and shared tools

readers

Supplementary data readers (Nortek CSV, RODB legacy format).

NetCDF and legacy-format readers for mooring instrument data.

oceanarray.tools.readers.load_dataset(source: str | Path | List[str | Path]) Dataset | List[Dataset][source]

Load one or more observational data files and return as xarray Datasets.

Dispatches based on file extension or known formats.

Parameters:

source (str, Path, or list of str/Path) – Single file or list of files to load.

Returns:

Loaded dataset(s). A single dataset is returned if one file is given; a list of datasets is returned for multiple files.

Return type:

xarray.Dataset or list of xarray.Dataset

Raises:

ValueError – If file type is unrecognized.

oceanarray.tools.readers.load_nortek_csv(file_path: str | Path, header_file: str | None = None) Dataset[source]

Load Nortek CSV data exported from AquaPro software.

Parameters:
  • file_path (str or Path) – Path to the semicolon-delimited CSV data file (e.g. “Average Velocity DF3.csv”).

  • header_file (str, optional) – Path to the accompanying Units.csv file (reserved for future metadata use).

Returns:

Dataset with time coordinate and variables for velocity beams, amplitude, correlation, and environmental channels.

Return type:

xr.Dataset

oceanarray.tools.readers.rodbload_old(filepath: Path, variables: list[str]) Dataset[source]

Load a RODB-style file into an xarray.Dataset.

Parameters:
  • filepath (Path) – Path to the .use, .raw or .dat file

  • variables (list of str) – Variables to extract (must be present in columns= line)

Returns:

ds – Dataset containing requested variables

Return type:

xr.Dataset

writers

NetCDF output helpers.

NetCDF write helpers for OceanSITES-compliant mooring datasets.

oceanarray.tools.writers.save_OS_instrument(ds: Dataset, data_dir: Path) Path[source]

Save OceanSITES dataset to netCDF using the ‘id’ global attribute as filename.

Parameters:
  • ds (xarray.Dataset) – Dataset with OceanSITES-compliant global attributes including ‘id’.

  • data_dir (pathlib.Path) – Directory to save the netCDF file.

Returns:

Full path to the saved NetCDF file.

Return type:

Path

oceanarray.tools.writers.save_dataset(ds: Dataset, output_file: str = '../test.nc') bool[source]

Save a dataset to NetCDF, converting unsupported attribute types on retry.

If a TypeError occurs due to invalid attribute values, converts the offending attributes to strings and retries the save operation.

Parameters:
  • ds (xarray.Dataset) – The dataset to be saved.

  • output_file (str, optional) – The path to the output NetCDF file. Defaults to ‘../test.nc’.

Returns:

True if the dataset was saved successfully, False otherwise.

Return type:

bool

Notes

This function is based on a workaround for issues with saving datasets containing attributes of unsupported types. See: https://github.com/pydata/xarray/issues/3743

rapid interpolation

Physics-informed vertical interpolation (RAPID array scheme).

Physics-informed vertical interpolation scheme for sparse ocean profiles.

This module implements the core algorithm used in the RAPID array, which reconstructs full-depth temperature and salinity profiles using climatological vertical gradients.

Functions

  • spacing : Generate evenly spaced pressure levels from start to end.

  • save_climatology : Save climatology fields as a NetCDF dataset.

  • smooth_climatology : Apply running mean smoothing to climatology fields.

  • interpolate_internal : Interpolate between two sparse profile points using gradient fields.

  • extrapolate_boundary : Extrapolate into unsampled regions near profile boundaries.

References

  • Vertical interpolation based on climatological gradients (Johns et al., 2001).

  • Adapted and translated from Matlab routines by T. Kanzow (2000).

oceanarray.tools.rapid_interp.build_climatology(ds: Dataset, standard_pressures: ndarray, temp_key: str = 'CT', salt_key: str = 'SA', pres_key: str = 'PRESSURE', time_key: str = 'TIME', min_profiles_per_bin: int = 5, temp_bins: ndarray | None = None) Dataset[source]

Build a seasonal climatology of dT/dP and dS/dP as a function of temperature.

This function processes hydrographic profiles to compute monthly climatologies of temperature and salinity vertical gradients. It bins gradients by temperature and month, and returns an xarray.Dataset with dTdp and dSdp fields indexed by temperature and time of year.

Parameters:
  • ds (xr.Dataset) – Dataset containing temperature, salinity, pressure, and time variables.

  • standard_pressures (np.ndarray) – Vertical pressure grid [dbar] to interpolate profiles before gradient calculation.

  • temp_key (str, optional) – Variable name for Conservative Temperature, by default “CT”.

  • salt_key (str, optional) – Variable name for Absolute Salinity, by default “SA”.

  • pres_key (str, optional) – Variable name for Pressure, by default “PRESSURE”.

  • time_key (str, optional) – Variable name for time coordinate, by default “TIME”.

  • min_profiles_per_bin (int, optional) – Minimum number of valid profiles per (month, temp) bin to accept, by default 5.

  • temp_bins (np.ndarray, optional) – Temperature bin edges for grouping (°C). If None, defaults to -2 to 35.5°C in 0.5°C bins.

Returns:

Climatology dataset with ‘dTdp’ and ‘dSdp’ indexed by (‘month’, ‘TEMP’).

Return type:

xr.Dataset

Notes

  • Interpolates all input profiles to standard_pressures before calculating gradients.

  • Based on the physics-informed interpolation approach described in Johns et al. (2001).

  • Original implementation adapted from Matlab code by T. Kanzow (2000).

See also

verticalnn.rapid_interp.smooth_climatology

Applies smoothing to output climatology.

verticalnn.rapid_interp.save_climatology

Saves climatology dataset to disk.

verticalnn.rapid_interp.interpolate_profiles

Uses this climatology for profile interpolation.

oceanarray.tools.rapid_interp.extrapolate_boundary(T: float, S: float, P: float, p_bound: float, dtdp_func, dsdp_func, int_step: float = 20.0) Tuple[ndarray, ndarray, ndarray][source]

Extrapolate temperature and salinity profiles from a boundary point using climatological gradients.

This function integrates vertical gradients of temperature (dT/dP) and salinity (dS/dP) downward or upward from a single observed point to reach a specified pressure boundary.

Parameters:
  • T (float) – Starting temperature [°C].

  • S (float) – Starting salinity [g/kg].

  • P (float) – Starting pressure [dbar].

  • p_bound (float) – Target pressure to extrapolate to [dbar].

  • dtdp_func (function) – Callable that returns dT/dP as a function of T.

  • dsdp_func (function) – Callable that returns dS/dP as a function of T.

  • int_step (float, optional) – Integration step size [dbar], by default 20.0.

Returns:

  • T_profile (np.ndarray) – Extrapolated temperature profile.

  • S_profile (np.ndarray) – Extrapolated salinity profile.

  • P_profile (np.ndarray) – Corresponding pressure levels.

Notes

This function integrates stepwise from the boundary point toward the pressure bound. The step direction is automatically inferred from the sign of (p_bound - P).

Based on the original Matlab routine t_bound0.m by T. Kanzow (2000). Method described in Johns et al. (2001).

See also

verticalnn.rapid_interp.interpolate_internal

Used for interpolation between data points.

verticalnn.rapid_interp.interpolate_profiles

Combines extrapolation and interpolation.

oceanarray.tools.rapid_interp.interpolate_internal(T: ndarray, S: ndarray, P: ndarray, dtdp_func, dsdp_func, int_step: float = 20.0) Tuple[ndarray, ndarray, ndarray][source]

Interpolate between observed data points using climatological vertical gradients.

This function fills gaps between observed pressure levels by integrating temperature and salinity gradients from both the upper and lower sensors and blending the results. It implements the logic of the original t_int0.m routine by Kanzow (2000).

Parameters:
  • T (np.ndarray) – Observed temperature values [°C].

  • S (np.ndarray) – Observed salinity values [g/kg].

  • P (np.ndarray) – Observed pressure values [dbar].

  • dtdp_func (callable) – Function that returns dT/dP given temperature.

  • dsdp_func (callable) – Function that returns dS/dP given temperature.

  • int_step (float, optional) – Pressure increment for integration [dbar], by default 20.0.

Returns:

  • np.ndarray – Interpolated temperature values between observations.

  • np.ndarray – Interpolated salinity values between observations.

  • np.ndarray – Corresponding pressure values.

Notes

This function performs dual integration (from above and below) and blends the two estimates using linear weighting. Duplicate pressure values are removed in the final output. - Adapted from original Matlab function t_int0.m. - Author: T. Kanzow, 4 April 2000. - Part of the vertical interpolation scheme described in Johns et al. (2001):

“The Kuroshio east of Taiwan: Moored transport observations from the WOCE PCM-1 Array.” This version translated to Python and adapted for TEOS-10 Conservative Temperature and Absolute Salinity.

See also

verticalnn.rapid_interp.extrapolate_boundary

For extrapolation above or below the data range.

verticalnn.rapid_interp.interpolate_profiles

Applies this interpolation to all profiles in a dataset.

oceanarray.tools.rapid_interp.interpolate_profiles(ds: Dataset, clim_ds: Dataset, temp_key: str = 'CT', salt_key: str = 'SA', pres_key: str = 'PRES', time_key: str = 'TIME', p_grid: ndarray | None = None, int_step: float = 20.0, extrapolate: bool = True) Dataset[source]

Interpolate sparse moored profiles onto a regular vertical grid using climatological gradients.

This function performs both interpolation between observed levels and extrapolation beyond observed bounds using seasonal climatologies of dT/dP and dS/dP, following the physics-informed method described by Johns et al. (2001) and originally implemented in Matlab by T. Kanzow (2000).

Parameters:
  • ds (xr.Dataset) – Input dataset containing sparse temperature, salinity, and pressure profiles.

  • clim_ds (xr.Dataset) – Climatology dataset with vertical gradients (variables: ‘dTdp’, ‘dSdp’) indexed by TEMP and month.

  • temp_key (str, optional) – Variable name for Conservative Temperature, by default “CT”.

  • salt_key (str, optional) – Variable name for Absolute Salinity, by default “SA”.

  • pres_key (str, optional) – Variable name for pressure, by default “PRES”.

  • time_key (str, optional) – Variable name for profile time coordinate, by default “TIME”.

  • p_grid (np.ndarray, optional) – Target vertical grid for output profiles [dbar], by default np.arange(0, 5000, 20).

  • int_step (float, optional) – Step size for vertical integration [dbar], by default 20.

  • extrapolate (bool, optional) – Whether to extrapolate above and below observed pressures, by default True.

Returns:

Interpolated dataset with gridded CT, SA, and SIGMA0 on the pressure grid.

Return type:

xr.Dataset

Notes

  • Uses temperature-dependent vertical gradients from clim_ds.

  • Combines logic of con_tprof0.m, t_int0.m, and t_bound0.m (Kanzow 2000).

  • Final dataset includes derived potential density (SIGMA0) using TEOS-10.

See also

verticalnn.rapid_interp.build_climatology

Generates the dT/dP and dS/dP climatology.

verticalnn.rapid_interp.interpolate_internal

Fills gaps between sensors.

verticalnn.rapid_interp.extrapolate_boundary

Fills above/below observed range.

oceanarray.tools.rapid_interp.plot_climatology(clim_ds: Dataset, var: str = 'dTdp', clim_ds_smoothed: Dataset | None = None, fig: Any = None, ax: Any = None) Tuple[Any, Any][source]

Plot the seasonal climatology of dT/dP or dS/dP, optionally with a smoothed overlay.

Lives here because it visualises the climatological gradient field this module builds. Follows the composable fig=None, ax=None convention.

Parameters:
  • clim_ds (xarray.Dataset) – Raw climatology dataset with dTdp and/or dSdp.

  • var (str, optional) – Variable to plot ("dTdp" or "dSdp"). Default "dTdp".

  • clim_ds_smoothed (xarray.Dataset, optional) – Smoothed climatology dataset to overlay. When given, the raw climatology is drawn in grey behind it. Default None.

  • fig (matplotlib.figure.Figure, optional) – Existing figure to draw on; a new one is created when None.

  • ax (matplotlib.axes.Axes, optional) – Existing axes to draw on; new axes are created when None.

Returns:

The figure and axes drawn on.

Return type:

tuple of (Figure, Axes)

oceanarray.tools.rapid_interp.save_climatology(clim_ds: Dataset, output_path: str | Path) None[source]

Save a climatology dataset containing vertical gradients to a NetCDF file.

This function writes the input dataset—typically containing monthly fields of dT/dP and dS/dP as a function of temperature—to disk, adding metadata.

Parameters:
  • clim_ds (xr.Dataset) – Dataset containing climatological temperature and salinity gradients. Should include variables like ‘dTdp’, ‘dSdp’, and coordinate ‘TEMP’, ‘month’.

  • output_path (str or pathlib.Path) – Destination file path for the NetCDF file.

Notes

Adds metadata to the saved dataset: - description: Short description of contents. - generated_by: Marks source module (verticalnn.rapid_interp). - created_on: ISO-formatted timestamp of when the file was written.

See also

verticalnn.rapid_interp.smooth_climatology

Smooths this dataset along the temperature axis.

verticalnn.rapid_interp.build_climatology

Creates the dataset to be saved.

oceanarray.tools.rapid_interp.smooth_climatology(clim_ds: Dataset, window: int = 3) Dataset[source]

Apply rolling smoothing to vertical gradient climatology along the temperature axis.

This function smooths the dT/dP and dS/dP climatology fields using a centered moving average along the temperature bins, applied independently for each month.

Parameters:
  • clim_ds (xr.Dataset) – Input climatology dataset with variables ‘dTdp’ and ‘dSdp’ on (month, TEMP) grid.

  • window (int, optional) – Rolling window size (number of temperature bins), by default 3.

Returns:

Smoothed climatology dataset with the same coordinates and attributes.

Return type:

xr.Dataset

Notes

Rolling averages are computed with center=True and min_periods=1, preserving edge values. The smoothing is purely horizontal (along TEMP), not temporal.

See also

verticalnn.rapid_interp.save_climatology

saves smoothed output to disk.

oceanarray.tools.rapid_interp.spacing(p_start: float, p_end: float, step: float) ndarray[source]

Create an array of pressures from p_start to p_end using approximately uniform spacing.

This helper function generates a 1D pressure array using either increasing or decreasing steps depending on the ordering of p_start and p_end. It ensures that the end point is included.

Parameters:
  • p_start (float) – Starting pressure [dbar].

  • p_end (float) – Ending pressure [dbar].

  • step (float) – Approximate pressure increment [dbar].

Returns:

1D array of pressure values from p_start to p_end (inclusive).

Return type:

np.ndarray

Notes

This function is used internally by the vertical interpolation routines to construct pressure grids between adjacent observations.

See also

verticalnn.rapid_interp.interpolate_internal

Uses this function to build vertical grids.

utilities

General utilities for file management, logging, and parsing ASCII metadata.

Shared utility helpers used across the oceanarray processing pipeline.

oceanarray.utilities.apply_defaults(default_source: str, default_files: List[str]) Callable[source]

Decorate a function to apply default values for source and file_list parameters.

Parameters:
  • default_source (str) – Default source URL or path.

  • default_files (list of str) – Default list of filenames.

Returns:

A wrapped function with defaults applied.

Return type:

Callable

oceanarray.utilities.cast_output_dtypes(ds: Dataset) Dataset[source]

Cast every variable in ds to its optimal storage dtype.

Calls find_best_dtype() per variable and rebuilds only those that change. Attributes are preserved. The input dataset is not modified.

Parameters:

ds (xr.Dataset) – Dataset to cast.

Returns:

New dataset with optimised dtypes, ready for NetCDF output.

Return type:

xr.Dataset

oceanarray.utilities.check_necessary_variables(ds: Dataset, vars: list) None[source]

Check that all required variables are present in a dataset.

Parameters:
  • ds (xarray.Dataset) – Dataset that should be checked

  • vars (list) – List of variables

Raises:

KeyError: – Raises an error if all vars not present in ds

Notes

Original Author: Callum Rollo

oceanarray.utilities.concat_with_scalar_vars(datasets: List[Dataset], dim: str, scalar_vars: List[str] | None = None) Dataset[source]

Concatenate datasets along a dimension, preserving scalar variables.

Scalar (0-D) variables would normally be broadcast to the concatenation dimension by xr.concat; this helper strips them beforehand and re-attaches the first occurrence after the concat so they remain 0-D.

Parameters:
  • datasets (list of xarray.Dataset) – Datasets to concatenate.

  • dim (str) – Dimension along which to concatenate.

  • scalar_vars (list of str, optional) – List of variable names to treat as scalars. If None, auto-detect scalar variables (those with ndim == 0 in any dataset).

Returns:

Concatenated dataset with scalar variables re-attached as 0-D DataArrays.

Return type:

xarray.Dataset

oceanarray.utilities.drop_all_zero_vars(ds: Dataset, prefixes: list[str]) Dataset[source]

Drop variables whose finite values are all zero.

Parameters:
  • ds (xr.Dataset) – Dataset to filter.

  • prefixes (list of str) – Variable name prefixes to check (e.g. ["amplitude_beam", "analog_input_"]). Any variable whose name starts with one of these prefixes and whose finite values are all zero (or has no finite values) is dropped.

Returns:

Dataset with all-zero prefix-matched variables removed.

Return type:

xr.Dataset

oceanarray.utilities.extract_inline_instruments(inline_list: List[Dict[str, Any]]) List[Dict[str, Any]][source]

Extract processable instrument entries from the mooring YAML inline list.

Most inline entries describe passive hardware (ropes, shackles, floats). This function filters for entries that have an instrument field and either a filename (to be processed) or skip: true (to be reported as skipped).

Normalisation applied to each matching entry:

  • hab is set from hab_bottom if hab is not already present. For a downward-looking instrument the transducer is at the bottom of the housing (hab_bottom); for an upward-looking one use hab_top.

  • serial is split on the first comma and the first token is used as the primary serial number. Any remaining tokens are joined and stored under beacon_id in the returned dict.

  • source: "inline" is added to distinguish these entries from clamp entries in downstream logging.

Serial parsing is fragile: the convention serial: 16430, R01-024 assumes the first comma-separated token is the instrument serial and the rest is a transponder/beacon ID. If a YAML author places the beacon serial first, the wrong value will be used as the output filename stem. The validator emits a WARNING for any inline entry whose serial contains a comma so operators can confirm the ordering is correct.

Parameters:

inline_list – The raw list parsed from the inline YAML key.

Returns:

List of normalised instrument-config dicts ready for stage processing.

oceanarray.utilities.find_best_dtype(var_name: str, da: DataArray) type[source]

Determine the optimal storage dtype for a variable.

Parameters:
  • var_name (str) – Variable name.

  • da (xr.DataArray) – Data array to inspect.

Returns:

Recommended numpy dtype.

Return type:

type

Notes

Rules applied in order:

  • String / datetime / object variables: unchanged.

  • time in name: unchanged (preserve datetime64 / float encoding).

  • *_qc suffix or flag in name: int8.

  • serial_number or serial: int32.

  • latitude / longitude in name: float64.

  • Integer input: downsize to int32 if stored as int64, else unchanged.

  • float64 input: float32.

  • Anything else: unchanged.

oceanarray.utilities.format_latlon(lat: float, lon: float, *, ndp: int = 4) tuple[str, str][source]

Return (lat, lon) as hemisphere-suffixed strings from signed decimal degrees.

The sign selects the hemisphere and the magnitude is shown unsigned, so a formatted value can never carry both a minus sign and a hemisphere letter (e.g. the malformed "-27.8 W"). Latitude uses N/S, longitude E/W; a value of exactly 0 is rendered on the positive hemisphere.

Parameters:
  • lat (float) – Latitude and longitude in signed decimal degrees.

  • lon (float) – Latitude and longitude in signed decimal degrees.

  • ndp (int, optional) – Number of decimal places for the magnitude (default 4 ≈ 11 m).

Returns:

(lat_str, lon_str), e.g. ("65.7319° N", "27.8000° W").

Return type:

tuple[str, str]

oceanarray.utilities.get_dims(ds_gridded: Dataset) tuple[source]

Extract pressure key, time key, and their dimensions from a dataset.

Parameters:

ds_gridded (xarray.Dataset) – Dataset containing the variables and dimensions.

Returns:

  • pres_key (str or None) – Key for the pressure (or depth) variable; None if not found.

  • time_key (str) – Key for the time variable.

  • pres_dim (str or None) – Dimension associated with the pressure variable; None if not found.

  • time_dim (str or None) – Dimension associated with the time variable.

oceanarray.utilities.get_time_key(ds: Dataset) str[source]

Return the name of the time coordinate or variable in an xarray.Dataset.

Parameters:

ds (xarray.Dataset) – The dataset to inspect.

Returns:

The name of the time coordinate or variable.

Return type:

str

Raises:

ValueError – If no time dimension or coordinate is found.

oceanarray.utilities.is_iso8601_utc(timestr: str) bool[source]

Validate whether a string is in ISO8601 UTC format: YYYY-MM-DDTHH:MM:SSZ.

Parameters:

timestr (str) – Input time string.

Returns:

True if valid ISO8601 UTC format, False otherwise.

Return type:

bool

oceanarray.utilities.iso8601_duration_from_seconds(seconds: float) str[source]

Convert a duration in seconds to an ISO 8601 duration string.

Parameters:

seconds (float) – Duration in seconds.

Returns:

ISO 8601 duration string, e.g., ‘PT1H’, ‘PT30M’, ‘PT15S’.

Return type:

str

oceanarray.utilities.nice_colorbar_ticks(vmin: float, vmax: float, *, max_ticks: int = 6) ndarray[source]

Return at most max_ticks nicely-rounded tick positions in [vmin, vmax].

Decoupled from the colorbar’s colour discretisation: a 20-level BoundaryNorm bar can still show ~6 round labels (e.g. 34.8, 34.9, … 35.2) instead of one label per boundary. Uses matplotlib.ticker.MaxNLocator with round step multiples so labels land on clean values.

Parameters:
  • vmin (float) – Data range of the colorbar.

  • vmax (float) – Data range of the colorbar.

  • max_ticks (int) – Maximum number of ticks (approximate; the locator may return a few fewer). Default 6.

Returns:

Tick positions, clipped to [vmin, vmax].

Return type:

numpy.ndarray

oceanarray.utilities.parse_latlon(cfg: dict) tuple[float, float][source]

Return (lat, lon) in decimal degrees from a mooring config dict.

Parameters:

cfg (dict) – Mooring configuration dictionary (from a .mooring.yaml file).

Returns:

(latitude, longitude) in decimal degrees.

Return type:

tuple[float, float]

oceanarray.utilities.parse_latlon_with_source(cfg: dict) tuple[float, float, str][source]

Return (lat, lon, source_key) from a mooring config dict.

source_key is the YAML key pair used, e.g. 'seabed_latitude/seabed_longitude', or 'unknown (defaulting to 0, 0)' if none found.

Zero values (lat == 0 and lon == 0) are treated as unfilled placeholders and skipped so that a later key with a real location is used instead.

Parameters:

cfg (dict) – Mooring configuration dictionary (from a .mooring.yaml file).

Returns:

(latitude, longitude, source_key) where source_key identifies which YAML key pair was used.

Return type:

tuple[float, float, str]

oceanarray.utilities.period_axis_ticks(p_min_days: float, p_max_days: float) tuple[list[float], list[str]][source]

Return human-readable period tick values and labels for a log-period axis.

Canonical tick list covers 1 min through 1 yr. Only ticks within [p_min_days, p_max_days] are returned. Intended for spectral and wavelet period axes (both x and y) so that all figures share the same tick positions.

Parameters:
  • p_min_days – Minimum visible period in days (Nyquist end of the axis).

  • p_max_days – Maximum visible period in days (long-period end of the axis).

Returns:

Pair of (values_in_days, labels) filtered to the visible range.

Return type:

tuple[list[float], list[str]]

oceanarray.utilities.should_skip_regeneration(output: Path, force: bool, skip_existing: bool, *sources: Path) bool[source]

Decide whether regenerating output can be skipped.

Three-mode logic (matching the report layer / ctd_report CLI design):

  • force=True → never skip (always regenerate).

  • skip_existing=True → skip whenever output exists, regardless of source modification times (the fast, mtime-agnostic behaviour).

  • default → skip only if output exists and is newer than every path in sources (mtime-based staleness). A source newer than output — for example an edited mooring YAML — forces regeneration.

Parameters:
  • output (Path) – The file that would be (re)generated.

  • force (bool) – If True, never skip.

  • skip_existing (bool) – If True, skip whenever output exists regardless of source mtimes.

  • *sources (Path) – Input files output is derived from (raw data, previous-stage NetCDF, the mooring YAML). Non-existent sources are ignored.

Returns:

True if regeneration can be skipped; False if output must be rebuilt.

Return type:

bool

paths

Path resolution helpers for raw and processed directory trees.

Filesystem path and filename conventions for the oceanarray pipeline.

Single source of truth for folder resolution, the stage output-filename pattern, and turning instrument serial numbers into filename-safe tokens. Every processing stage derives its read/write locations from here so the logic is defined once rather than copied per stage.

exception oceanarray.paths.LegacyLayoutError[source]

Raised when a directory uses the removed moor/proc (basedir) layout.

oceanarray.paths.mooring_proc_dir(proc_root: str | Path, mooring: str) Path[source]

Return the mooring-level processed-data directory.

Under the current layout this is proc_root/<mooring>.

Parameters:
  • proc_root (str or Path) – Cruise-level processed-data root (the --proc-dir value).

  • mooring (str) – Mooring name.

Returns:

proc_root/<mooring>.

Return type:

Path

oceanarray.paths.raw_mooring_dir(raw_root: str | Path, mooring: str) Path[source]

Return the mooring-level raw-data directory.

Under the current layout this is raw_root/<mooring>.

Parameters:
  • raw_root (str or Path) – Cruise-level raw-data root (the --raw-dir value).

  • mooring (str) – Mooring name.

Returns:

raw_root/<mooring>.

Return type:

Path

oceanarray.paths.require_current_layout(proc_root: str | Path, mooring: str) None[source]

Raise if proc_root points at a removed legacy layout, naming the fix.

Under the current layout the mooring directory is proc_root/<mooring>. The removed basedir mode nested it one level down — under either proc_root/moor/proc/<mooring> or proc_root/proc/<mooring> (both shapes the old cli._get_proc_root accepted). When the current-layout directory is absent but a legacy one is present, raise an error that names the directory to use instead, rather than letting processing fail later with “no files found”. If neither exists this returns normally — a genuinely empty run is not this function’s concern.

Parameters:
  • proc_root (str or Path) – Cruise-level processed-data root (the --proc-dir value).

  • mooring (str) – Mooring name.

Raises:

LegacyLayoutError – If a legacy <proc_root>/moor/proc/<mooring> or <proc_root>/proc/<mooring> directory exists but the current <proc_root>/<mooring> directory does not.

oceanarray.paths.resolve_report_dir(mooring: str, outdir: str | Path | None, report_dir: str | Path | None, proc_root: str | Path) Path[source]

Return the directory a mooring’s HTML report pages are written to.

Single source of truth for report output-dir resolution, mirrored by both oceanarray.reports.MooringReport.generate() and the PDF combiner so the two never drift. Priority: explicit outdir wins; otherwise a central report_dir nests each mooring under report_dir/<mooring>; otherwise the default proc_root/<mooring>/report.

Parameters:
  • mooring (str) – Mooring name.

  • outdir (str or Path, optional) – Explicit output directory (--output-dir); takes precedence when set.

  • report_dir (str or Path, optional) – Central report root (--report-dir); each mooring nests below it.

  • proc_root (str or Path) – Cruise-level processed-data root, used for the default location.

Returns:

The resolved report directory.

Return type:

Path

oceanarray.paths.safe_serial(serial: Any) str[source]

Return a filename-safe token for an instrument serial number.

If the raw value contains a comma (e.g. "16430, R01-024"), only the first comma-separated token is the primary serial used in filenames and output; the remainder is a beacon id or annotation and is dropped. Any remaining characters that are illegal in filenames (e.g. * used as a YAML marker) are stripped.

Parameters:

serial (Any) – Raw serial value from the mooring YAML (str, int, or other).

Returns:

Sanitised serial token containing only word characters and hyphens.

Return type:

str

oceanarray.paths.stage_output_name(mooring: str, serial: Any, stage: int, tag: str = '') str[source]

Return the stage output filename for one instrument.

The pattern is {mooring}_{serial}{tag}_stage{N}.nc. The serial is cleaned with safe_serial(), so the raw YAML value may be passed directly.

Parameters:
  • mooring (str) – Mooring name.

  • serial (Any) – Raw instrument serial (cleaned internally via safe_serial()).

  • stage (int) – Processing stage number (1, 2, or 3).

  • tag (str, optional) – Extra filename tag (e.g. the ADCP-matlab file tag). Default "".

Returns:

The output filename, e.g. "dsG3_16430_stage1.nc".

Return type:

str

Instrument processing

stage 1 — standardisation

Convert raw instrument files (SeaBird, RBR, Nortek, RDI) to CF-NetCDF. Faithful to raw data; no QC.

Refactored stage1 processing for mooring data with improved readability.

class oceanarray.processors.stage1.MooringProcessor(*, raw_dir: str, proc_dir: str)[source]

Handles stage1 processing of mooring data.

COORDS_TO_REMOVE = {'sbe-ascii': ['depth', 'latitude', 'longitude'], 'sbe-cnv': ['depth', 'latitude', 'longitude']}
SUPPORTED_FILE_TYPES = frozenset({'adcp-matlab', 'nortek-ascii', 'nortek-csv', 'nortek-csv-oa', 'nortek-raw', 'rbr-dat', 'rbr-hex', 'rbr-matlab', 'rbr-matlab-legacy', 'rbr-rsk', 'rdi-raw', 'sbe-ascii', 'sbe-cnv', 'sbe-hex'})
VARS_TO_REMOVE = {'sbe-ascii': ['potential_temperature', 'julian_days_offset', 'density'], 'sbe-cnv': ['potential_temperature', 'julian_days_offset', 'density']}
process_mooring(mooring_name: str, output_path: str | None = None, serials: List[str] | None = None, force: bool = False) bool[source]

Process a single mooring’s data.

Parameters:
  • mooring_name – Name of the mooring to process

  • output_path – Optional custom output path. If None, uses default structure.

  • serials – Optional list of serial numbers to process; if None, process all.

  • force – Re-process even if output already exists.

Returns:

True if processing completed successfully, False otherwise

Return type:

bool

stage 2 — trimming and clock correction

Trim to the deployment window; apply linear clock-offset/drift correction.

Stage 2 processing for mooring data: apply clock corrections and trim to deployment.

Processing order per instrument

  1. Load Stage 1 _stage1.nc file.

  2. Resolve clock offset and drift from YAML.

  3. If either is non-zero, save time_orig (original instrument time) before correcting.

  4. Apply a linear correction that ramps from clock_offset at deployment to clock_drift_seconds at recovery. Both default to 0, so: - Only clock_offset set: uniform constant shift (same correction throughout). - Only clock_drift_seconds set: ramps from 0 at deployment to drift at recovery. - Both set: ramps from clock_offset at deployment to clock_drift_seconds at recovery.

  5. Trim record to deployment_timerecovery_time from the mooring YAML.

  6. Write _stage2.nc. time_orig is only present when a correction was applied.

Clock correction YAML keys (per instrument)

clock_offsetfloat, seconds

Total correction to apply at the start of the deployment (instrument clock error at deployment time). Positive = instrument was slow (behind UTC).

clock_drift_secondsfloat, seconds [Option A]

Total correction to apply at the end of the deployment (instrument clock error at recovery time). Positive = instrument was slow (behind UTC) at recovery. The correction ramps linearly from clock_offset at deployment to this value at recovery.

computer_clock_at_recovery / instrument_clock_at_recoveryISO-8601 str [Option B]

Two timestamps read off at recovery. drift = computer − instrument = total correction at recovery. Equivalent to setting clock_drift_seconds. Option B takes priority over Option A if both are present.

Sign convention

All clock values are the amounts added to instrument time to obtain corrected time.

  • Positive value → instrument was slow (behind real time); times shifted later.

  • Negative value → instrument was fast (ahead of real time); times shifted earlier.

class oceanarray.processors.stage2.Stage2Processor(*, proc_dir: str)[source]

Handles Stage 2 processing: clock correction and temporal trimming.

process_mooring(mooring_name: str, output_path: str | None = None, serials: List[str] | None = None, force: bool = False) bool[source]

Process Stage 2 for a single mooring.

Parameters:
  • mooring_name – Name of the mooring to process

  • output_path – Optional custom output path

  • serials – Optional list of serial numbers to process; if None, process all.

  • force – Re-process even if Stage 2 output already exists.

Returns:

True if processing completed successfully

Return type:

bool

oceanarray.processors.stage2.detect_deployment_window(ds: Dataset) tuple[datetime64 | None, datetime64 | None, str][source]

Estimate the deployed in-water window from a stage1 pressure record.

Important

Returns ``(None, None, …)`` when no pressure data are available. The caller must check for None before using the result. Suggested times are not written to the output file when pressure is absent — instruments without pressure receive no suggested_* attrs.

Important

All non-None timestamps are in the raw instrument clock.

The input ds is a stage 1 dataset whose "time" coordinate carries the uncorrected instrument clock. Add the YAML clock_offset to convert to UTC for copy-pasting into the YAML.

Pressure-based algorithm (middle-50 % pmin + 10 dbar):

  1. Middle-50 % reference window: skip the first and last 25 % of the record by time. This excludes any bench / surface period at either end regardless of its length, while avoiding a pmax-based threshold that would be biased by knockdown events (which push instruments deeper than the nominal deployment depth).

  2. pmin_deployed = 1st-percentile of pressure within that middle window (robust to brief sensor-zero artefacts that would drag the absolute minimum to ~0 dbar and make the threshold negative).

  3. threshold = pmin_deployed 10 dbar.

  4. Opening search window: the first 25 % of the record by time (mirrors the middle window, long enough to cover multi-day bench periods).

  5. Closing search window: the last 25 % of the record by time.

  6. Start: last sample at or below the threshold in the opening window → the next sample is the suggested deployment start.

  7. End: first sample at or below the threshold in the closing window → the preceding sample is the suggested recovery end.

This approach avoids two known failure modes of the original “skip first/last 12 h” middle window:

  • Bench data in the middle window: if an instrument recorded for days on deck before deployment, the 12 h skip was too short and pmin would equal the bench pressure (≈ 0 dbar), driving threshold negative so that nothing ever satisfied it.

  • ``pmax``-based threshold biased by knockdown: using 0.5 × pmax as a conservative-window threshold is biased because knockdowns push instruments deeper than their nominal position, inflating pmax above the nominal deployment pressure. 0.5 × pmax therefore sits deeper than intended, delaying the start of the conservative window. The middle-50 % approach uses the typical deployed pressure (median region of the time series), not the occasional extreme.

Returns (None, None, "no_pressure") when the dataset has no "pressure" variable, fewer than 10 records, all-NaN pressure, the middle window is empty, or the algorithm cannot produce a valid window.

Parameters:

ds (xr.Dataset) – Stage 1 dataset. Must contain a "time" coordinate in the raw instrument clock. The "pressure" variable (dbar) is required.

Returns:

(sug_start, sug_end, source) where source is "pressure_pmin10dbar" on success or "no_pressure" when no valid pressure data are available.

Return type:

tuple[Optional[np.datetime64], Optional[np.datetime64], str]

stage 3 — QC, rotation, and derived variables

Apply QC flags, rotate ADCP velocities to ENU, apply magnetic declination correction, and compute derived quantities (salinity, density, speed/direction).

Stage 3: pressure interpolation + QARTOD QC (gross-range and spike tests).

Processing order per instrument

  1. Load _stage2.nc.

  2. Pressure interpolation (targets only — instruments lacking pressure or whose pressure is flagged bad in the YAML): a. Near-neighbour if any source has |Δhab|HAB_THRESHOLD. b. Weighted bracketing from the closest source above and below. c. Extrapolation (with WARNING) when target is outside all source habs. Interpolated pressure gets pressure_qc = 8 (interpolated_value).

  3. QARTOD gross-range test on temperature, conductivity, pressure, and velocity components. Flags 4 (bad) or 3 (suspect) based on thresholds in parameters.QC_GROSS_RANGE (overrideable per mooring / per instrument in YAML via a qc_ranges key).

  4. QARTOD spike test on the same variables. Flags from parameters.QC_SPIKE (overrideable via qc_spike in YAML).

  5. Merge all QC flags using priority order: 9 > 4 > 3 > 8 > 2 > 1.

  6. Write _stage3.nc for all instruments (not only pressure targets).

Flag combination priority

Missing (9) > Bad (4) > Suspect (3) > Interpolated (8) > Prob-good (2) > Good (1)

This means that if interpolated pressure also fails the range test it is flagged 4 (bad), not 8 (interpolated).

YAML configuration keys

Top-level (mooring-wide):

qc_ranges : mapping of variable → {fail_span, suspect_span} qc_spike : mapping of variable → {suspect_threshold, fail_threshold}

Per-instrument (in a clamp entry):
qc_rangessame structure; overrides the mooring-level setting for

the variables listed (others fall back to mooring/global defaults)

qc_spike : same structure pressure_qc : int — mark this instrument’s own pressure as bad (≥3) so

stage3 replaces it with an interpolated value.

class oceanarray.processors.stage3.Stage3Processor(*, proc_dir: str)[source]

Pressure interpolation + QARTOD QC for all mooring instruments.

process_mooring(mooring_name: str, serials: List[str] | None = None, force: bool = False, dry_run: bool = False) bool[source]

Run Stage 3 QC and pressure interpolation for all instruments on a mooring.

pressure

Pressure interpolation helpers (HAB computation, gap-filling).

Pressure interpolation helpers for stage 3.

oceanarray.processors.pressure.compute_adcp_bin_pressure(ds: Dataset, lat: float, log_fn: Any = None) Dataset[source]

Compute pressure at each ADCP bin from transducer pressure and along-beam range.

For each time step, adds a pressure offset per bin based on the bin’s distance from the transducer. The offset is gsw.p_from_z(-range_m, lat), which approximates the hydrostatic pressure contribution of range_m metres of seawater at the given latitude.

Note on ``range`` units: the RDI WorkHorse firmware already converts slant range to vertical distance before writing to the raw output (using the nominal beam angle; see RDI ADCP Coordinate Transformation manual §4.2, Equation 8). Dolfyn reads these vertical distances directly, so range is already in metres of vertical depth — no beam-angle correction is needed.

Approximation (fixable post-OdB): gsw.p_from_z(-range_m, lat) computes the pressure of a water column of depth range_m measured from the sea surface, not the true pressure increment at the ADCP’s actual depth. Error at 300 m range from a 500 m transducer is ~2–3 dbar — within the seabed-QC margin (~20 dbar) for current moorings.

The sign follows instrument orientation:

  • Downward-looking (orientation == "down"): bins are deeper than the transducer → pressure increases with bin index.

  • Upward-looking (orientation == "up"): bins are shallower than the transducer → pressure decreases with bin index.

If orientation cannot be determined from dataset attributes a WARNING is emitted and "down" is assumed.

Parameters:
  • ds (xr.Dataset) – Stage 3 ADCP dataset. Must contain pressure(time) in dbar (pressure at the transducer head) and range(N_BINS) in metres (bin centre distance from the transducer face). Orientation is read from the orientation_yaml global attribute (preferred) or orientation_instrument (fallback).

  • lat (float) – Mooring latitude in decimal degrees (positive North), used by gsw.p_from_z for the gravitational/centrifugal correction.

  • log_fn (callable, optional) – Logging callback (e.g. logger.info).

Returns:

Input dataset with bin_pressure(time, N_BINS) added in dbar. The variable’s comment attribute records the formula and orientation used. Returns ds unchanged if pressure or range are absent.

Return type:

xr.Dataset

oceanarray.processors.pressure.interp_pressure(source_time: ndarray, source_pressure: ndarray, target_time: ndarray) ndarray[source]

Linearly interpolate source pressure onto target time axis.

Nearest-point extrapolation at the edges (no NaN fill outside range).

oceanarray.processors.pressure.interp_pressure_for_hab(hab_t: float, sorted_sources: List[Dict[str, Any]], target_time: ndarray, serial: str, log_fn: Any | None = None) tuple[ndarray, str][source]

Interpolate pressure for a single nominal HAB; return (p_array, method_str).

Parameters:
  • hab_t (float) – Target height above bottom in metres.

  • sorted_sources (list of dict) – Pressure source instrument dicts sorted by hab, each with 'ds', 'hab', 'pressure_var', 'instrument', and 'serial' keys.

  • target_time (np.ndarray) – Target datetime64 time axis.

  • serial (str) – Serial number of the target instrument (used in log messages only).

  • log_fn (callable, optional) – Logging callback; falls back to print when None.

Returns:

Interpolated pressure array and a human-readable method string.

Return type:

tuple[np.ndarray, str]

oceanarray.processors.pressure.interpolate_pressure(ds: Dataset, target_info: Dict[str, Any], sources: List[Dict[str, Any]], target_time: ndarray, pressure_bad_flag: bool, log_fn: Any | None = None) tuple[Dataset, str][source]

Interpolate pressure from sources onto target; return (ds, method_str).

If target_info contains hab_segments (a sorted list of (breakpoint_datetime64, hab_float) pairs), the record is split into time segments and each segment is interpolated with its own HAB, allowing for instruments that physically moved during the deployment.

HAB convention for segmented deployments

target_info["hab"] is the initial HAB — the height above bottom at which the instrument was deployed. Segment 0 runs from the start of the record up to (but not including) the first breakpoint, using this value. Each entry in hab_segments gives the new HAB at and after the breakpoint timestamp (i.e. records with T >= breakpoint use the new HAB; the breakpoint itself belongs to the new segment). This matches the YAML convention where hab: records the as-deployed position and hab_segments: records subsequent slides, with from: marking the first timestamp that belongs to the post-slide position.

param ds:

Stage 2 dataset for the target instrument.

type ds:

xr.Dataset

param target_info:

Instrument metadata dict with 'hab', 'hab_segments', 'serial', and 'qc_flags' keys.

type target_info:

dict

param sources:

Pressure source instrument dicts (each must have 'ds' loaded).

type sources:

list of dict

param target_time:

Target datetime64 time axis (must match ds["time"].values).

type target_time:

np.ndarray

param pressure_bad_flag:

When True, the original pressure is preserved as pressure_orig and pressure_orig_qc before being replaced.

type pressure_bad_flag:

bool

param log_fn:

Logging callback.

type log_fn:

callable, optional

returns:

Updated dataset and a human-readable method string.

rtype:

tuple[xr.Dataset, str]

qc

QARTOD quality-control tests.

QARTOD QC tests and CTD derivations for stage 3.

oceanarray.processors.qc.apply_enu_velocity_qc(ds: Dataset, gr_cfg: Dict[str, Any]) Dataset[source]

Apply QARTOD gross-range QC to ENU velocity vars and propagate w flags.

Must be called after apply_beam_to_enu (east/north/up_velocity must exist). No spike test is applied to velocity (burst-mode Aquadopps generate false positives at every burst boundary).

Propagates up_velocity_qc (flag 3 or 4) to east_velocity_qc and north_velocity_qc: if vertical velocity is implausibly large the whole 3-D velocity measurement is suspect/bad. Full bidirectional unification (large east/north flags → up_velocity_qc) is done by unify_velocity_qc, which must be called after apply_tilt_qc.

oceanarray.processors.qc.apply_qc_tests(ds: Dataset, gross_range: Dict[str, Any], spike: Dict[str, Any], flat_line: Dict[str, Any] | None = None) Dataset[source]

Apply QARTOD gross-range, spike, and flat-line tests, writing *_qc variables.

Three QARTOD tests are applied in sequence; the worst flag across all tests wins for each sample:

  • Gross-range: flags values outside physically plausible bounds as SUSPECT (3) or BAD (4) — e.g. temperature below -2.5 °C or above 40 °C.

  • Spike: flags isolated outliers that deviate from the surrounding record by more than a threshold — e.g. a brief salinity glitch from biofouling or a pressure transient.

  • Flat-line (stuck-sensor): flags runs of consecutive samples where the value does not change by more than tolerance — e.g. a pressure sensor frozen at 0 dbar after a failure. Threshold is expressed in sample counts (suspect_n, fail_n) and converted to seconds using the median sample interval. Applied by default to pressure only (see parameters.QC_FLAT_LINE).

Pre-existing *_qc values (e.g. pressure_qc = 8 set by the pressure-interpolation step in the stack) are preserved: the worst flag across the incoming value and the new test flags is written to the output.

Provenance: the thresholds actually applied are stored as attributes on each {var}_qc variable so the treatment can be reconstructed from the NetCDF file alone, without re-reading the YAML:

  • qc_gross_range_fail_min / qc_gross_range_fail_max

  • qc_gross_range_suspect_min / qc_gross_range_suspect_max

  • qc_spike_suspect_threshold / qc_spike_fail_threshold

  • qc_flat_line_suspect_count / qc_flat_line_fail_count

oceanarray.processors.qc.apply_tilt_qc(ds: Dataset, tilt_cfg: Dict[str, Any]) tuple[Dataset, int, int][source]

Flag velocity variables when pitch or roll exceeds QC thresholds.

Primary path — pitch_qc / roll_qc already exist (created by the gross-range QC step when pitch and/or roll appear in qc_ranges in the YAML):

The two flag arrays are merged element-wise (worst flag wins) and the result is propagated to every velocity variable. Any time step where pitch OR roll is flagged suspect (3) or bad (4) will flag the velocities with the same severity.

Fallback path — neither pitch_qc nor roll_qc exist:

tilt is computed as max(|pitch|, |roll|) and compared against tilt_cfg thresholds (suspect_threshold / fail_threshold).

In both paths tilt_suspect_threshold and tilt_fail_threshold are written to global attrs so the report can draw reference lines on the tilt time-series panel.

Returns (ds, n_suspect, n_bad). No-ops when both pitch and roll are absent.

oceanarray.processors.qc.compute_salinity_data(ds: Dataset, log_fn: Any = None) Dataset[source]

Compute Practical Salinity (SP) data values only — no QC flags yet.

Call this BEFORE apply_qc_tests so that salinity participates in the gross-range QC pass and gets its threshold attrs written to salinity_qc. Call merge_salinity_parent_qc afterward to fold in T/C/P parent flags.

oceanarray.processors.qc.derive_oxygen_saturation(ds: Dataset) Dataset[source]

Compute O2 % saturation and AOU from dissolved_oxygen, temperature, salinity, pressure.

Requires dissolved_oxygen (µmol/L), temperature (°C), salinity (PSU), and pressure (dbar) all present in ds. Returns ds unchanged when any variable is missing or when gsw is unavailable.

Unit conversion uses in-situ seawater density from gsw.rho(SA, CT, p) (kg m⁻³) — NOT freshwater density (~1000 kg m⁻³). Seawater density at typical mooring conditions is ~1025–1028 kg m⁻³, giving a ~2.5 % correction relative to freshwater. This correction is oceanographically significant and must not be skipped.

lon/lat for gsw.SA_from_SP are taken from global attrs (default 0.0 if absent; the SA error from a wrong position is typically < 0.05 g kg⁻¹ at open-ocean sites).

Derived variables stored

oxygen_saturation_pct%

100 × O2_measured(µmol kg⁻¹) / O2sol(µmol kg⁻¹) where O2sol is from gsw.O2sol_SP_pt(SP, pt).

apparent_oxygen_utilizationµmol kg⁻¹

O2sol − O2_measured (positive = oxygen-depleted water).

oceanarray.processors.qc.ensure_conductivity_units(ds: Dataset, log_fn: Any = None) Dataset[source]

Convert conductivity from S/m → mS/cm if needed.

QC thresholds in parameters.QC_GROSS_RANGE are in mS/cm. Some readers (notably sbe-ascii) write S/m; this normalises before QC is applied so thresholds are always compared against values in the same unit.

oceanarray.processors.qc.load_qc_config(mooring_cfg: Dict[str, Any], entry: Dict[str, Any]) tuple[Dict, Dict, Dict, Dict][source]

Return QC threshold dicts for one instrument.

Returns a 4-tuple (gross_range, spike, tilt, flat_line) built from package defaults with mooring-level and then instrument-level YAML overrides applied (later values win).

gross_range and spike map variable names (e.g. "temperature", "pressure") to threshold dicts understood by apply_qc_tests. tilt holds suspect_threshold / fail_threshold in degrees, used to flag Aquadopp velocity data when the instrument is tilted beyond acceptable limits. flat_line maps variable names to {suspect_n, fail_n, tolerance} dicts for the stuck-sensor test.

YAML override keys:

  • qc_ranges (mooring or instrument level) — gross-range and tilt spans

  • qc_spike — spike thresholds

  • tilt_qc — tilt thresholds (takes precedence over qc_ranges.tilt)

  • qc_flat_line — stuck-sensor thresholds

tilt is stripped from the gross-range dict before returning so it is not passed to the QARTOD gross-range test runner.

oceanarray.processors.qc.merge_salinity_parent_qc(ds: Dataset) Dataset[source]

Merge parent (T/C/P) QC flags into salinity_qc after QC tests have run.

apply_qc_tests sets salinity_qc from the gross-range test and stores the threshold attrs needed by the report histogram. Here we additionally fold in the worst flag from temperature_qc, conductivity_qc, and pressure_qc so that a bad/suspect input propagates to salinity.

oceanarray.processors.qc.set_qc_attrs(ds: Dataset, var: str, extra: Dict[str, Any] | None = None) Dataset[source]

Attach the OceanSITES flag attributes to {var}_qc in place.

The single owner of QC-flag metadata: call this wherever a _qc variable is created or updated so the CF flag attributes cannot be forgotten. The flag table is sourced from oceanarray.parameters (one source of truth) and the full legal set is declared, not only the values present. The status-flag standard_name ("{parent} status_flag") is attached only when the parent variable has a standard_name — it is skipped, never fabricated, when the parent has none (standard_name is an optional CF attribute).

Parameters:
  • ds (xarray.Dataset) – Dataset containing both var and its {var}_qc companion.

  • var (str) – Name of the data variable whose _qc companion is annotated.

  • extra (dict of str to Any, optional) – Additional attributes to attach (e.g. QC threshold provenance).

Returns:

ds, modified in place.

Return type:

xarray.Dataset

oceanarray.processors.qc.unify_velocity_qc(ds: Dataset) Dataset[source]

Unify ENU velocity QC flags so all three components share the worst flag.

After gross-range, up-velocity propagation, and tilt QC, each component may carry different flags. A large east/north velocity flags only that component; tilt flags all three; up-velocity flags east and north but not itself from the east/north gross-range test. This function computes the element-wise worst flag across east_velocity_qc, north_velocity_qc, and up_velocity_qc, then writes that combined flag back to all three. The result: masking on any single component produces an identical, consistent velocity mask.

coordinate

Coordinate system transformations (BEAM → XYZ → ENU, magnetic declination).

BEAM→ENU coordinate transforms and ADCP-specific QC for stage 3.

oceanarray.processors.coordinate.apply_adcp_seabed_qc(ds: Dataset, water_depth_m: float, lat: float, fail_margin_m: float = 20.0, log_fn: Any = None) Dataset[source]

Flag ADCP bins that are at or below the seabed.

Bins whose bin_pressure exceeds the estimated seabed pressure are flagged suspect (3); bins more than fail_margin_m metres below the seabed are flagged bad (4). The flag is stored as the standalone variable seabed_qc(time, N_BINS).

Flag scale (OceanSITES / QARTOD convention): 1 = good, 3 = suspect, 4 = bad, 9 = missing.

Standalone flag: seabed_qc is not merged into east_velocity_qc, north_velocity_qc, or up_velocity_qc. Downstream code that needs clean velocity data must explicitly include all relevant flags, for example:

clean = (ds.east_velocity_qc == 1) & (ds.seabed_qc == 1)

The seabed pressure threshold is computed via gsw.p_from_z(-water_depth_m, lat), consistent with compute_adcp_bin_pressure.

Parameters:
  • ds (xr.Dataset) – Stage 3 ADCP dataset containing bin_pressure(time, N_BINS) in dbar.

  • water_depth_m (float) – Water depth at the mooring site in metres (from YAML waterdepth key). If ≤ 0 the function is a no-op.

  • lat (float) – Mooring latitude in decimal degrees, used by gsw.p_from_z.

  • fail_margin_m (float) – Margin below the seabed (in metres) that separates suspect (3) from bad (4). Default 20 m. Bins between 0 and fail_margin_m below the seabed receive flag 3; bins more than fail_margin_m below receive flag 4.

  • log_fn (callable, optional) – Logging callback.

Returns:

Input dataset with seabed_qc(time, N_BINS) added. Returns ds unchanged if water_depth_m <= 0 or bin_pressure is absent.

Return type:

xr.Dataset

oceanarray.processors.coordinate.apply_adcp_surface_qc(ds: Dataset, lat: float, suspect_margin_m: float = 20.0, log_fn: Any = None) Dataset[source]

Flag ADCP bins that are at or above the sea surface.

Bins whose bin_pressure is ≤ 0 dbar are flagged bad (4); bins within suspect_margin_m metres of the surface (0 < bin_pressure < p_suspect) are flagged suspect (3). The result is stored as the standalone variable surface_qc(time, N_BINS).

This catches upward-looking ADCP bins that extend above the water surface when the instrument range exceeds the distance to the surface. Symmetric counterpart to apply_adcp_seabed_qc.

Flag scale (OceanSITES / QARTOD convention): 1 = good, 3 = suspect, 4 = bad.

Standalone flag: surface_qc is not merged into the velocity QC variables. Downstream consumers must combine flags explicitly:

clean = (ds.east_velocity_qc == 1) & (ds.surface_qc == 1) & (ds.seabed_qc == 1)
Parameters:
  • ds (xr.Dataset) – Stage 3 ADCP dataset containing bin_pressure(time, N_BINS) in dbar.

  • lat (float) – Mooring latitude in decimal degrees, used by gsw.p_from_z.

  • suspect_margin_m (float) – Depth (m) below the surface that defines the suspect zone. Bins with 0 < bin_pressure < p_from_z(-suspect_margin_m) receive flag 3; bins with bin_pressure ≤ 0 receive flag 4. Default 20 m.

  • log_fn (callable, optional) – Logging callback.

Returns:

Input dataset with surface_qc(time, N_BINS) added. Returns ds unchanged if bin_pressure is absent.

Return type:

xr.Dataset

oceanarray.processors.coordinate.apply_adcp_velocity_qc(ds: Dataset, gr_cfg: Dict[str, Any], prcnt_gd_bad: float, prcnt_gd_suspect: float, error_vel_threshold: float, log_fn: Any = None) Dataset[source]

Apply QC to ADCP 2D velocity variables (time × N_BINS).

Each QC criterion produces its own standalone variable; flags are not merged across variables. Downstream users combine them to mask data, e.g.:

good = (
    (ds.east_velocity_qc == 1)
    & (ds.percent_good_qc == 1)
    & (ds.error_velocity_qc == 1)
    & (ds.seabed_qc == 1)
    & (ds.surface_qc == 1)
)

QC variables produced

east/north/up_velocity_qc

Gross-range flag on the velocity component value itself (same fail_span / suspect_span thresholds as point instruments). Flag 1 means the velocity value is within the accepted range; it says nothing about acoustic quality.

percent_good_qc(time, N_BINS)

RDI ADCPs write four percent-good columns per ensemble per bin:

Column 3 (4-beam solutions) is the relevant quality indicator. Averaging all four columns is wrong: when data is perfect, col 3 ≈ 100 % and cols 0–2 ≈ 0 %, giving a mean of ~25 %, which falls below any reasonable suspect threshold and flags everything. This function therefore uses column 3 alone (falling back to the column mean for non-4-beam ADCPs that store fewer than 4 columns).

Flags: col3 < prcnt_gd_bad → bad (4); col3 < prcnt_gd_suspect → suspect (3); otherwise good (1).

error_velocity_qc(time, N_BINS)

For a 4-beam ADCP the error velocity is the difference between two independent estimates of vertical velocity from opposite beam pairs. It is zero for a perfect measurement; large values indicate beam decorrelation (e.g. fish, bubbles, mooring motion). |error_velocity| > error_vel_threshold → bad (4).

param ds:

Stage 2 dataset with 2-D ADCP velocity variables.

type ds:

xr.Dataset

param gr_cfg:

Gross-range config (same format as load_qc_config returns).

type gr_cfg:

dict

param prcnt_gd_bad:

4-beam percent good below this → flag 4 (bad). Percent.

type prcnt_gd_bad:

float

param prcnt_gd_suspect:

4-beam percent good below this (but above prcnt_gd_bad) → flag 3 (suspect). Percent.

type prcnt_gd_suspect:

float

param error_vel_threshold:

|error_velocity| above this → flag 4 (bad). m s⁻¹.

type error_vel_threshold:

float

param log_fn:

Logging callback.

type log_fn:

callable, optional

returns:

Input dataset with the following standalone QC variables added (where their parent data variables are present):

  • east_velocity_qc(time, N_BINS) — gross-range flag on east velocity

  • north_velocity_qc(time, N_BINS) — gross-range flag on north velocity

  • up_velocity_qc(time, N_BINS) — gross-range flag on up velocity

  • percent_good_qc(time, N_BINS) — 4-beam percent-good flag

  • error_velocity_qc(time, N_BINS) — error velocity magnitude flag

All flags use the OceanSITES scale: 1 = good, 3 = suspect, 4 = bad, 9 = missing. Variables are standalone; no merging across criteria is performed.

rtype:

xr.Dataset

oceanarray.processors.coordinate.apply_beam_to_enu(ds: Dataset, entry: Dict[str, Any], lat: float, lon: float, latlon_source: str = 'unknown', log_fn: Any = None) Dataset[source]

Transform BEAM or XYZ Nortek velocities to ENU geographic coordinates.

Adds east_velocity, north_velocity, up_velocity, current_speed, current_direction. Updates coordinate_system attr to ‘ENU’. No-ops for instruments already in ENU or with unknown coordinate system. Requires normalized variable names (heading, pitch, roll) — re-run stage1 if these are absent.

oceanarray.processors.coordinate.apply_declination_to_enu(ds: Dataset, lat: float, lon: float, latlon_source: str = 'unknown', log_fn: Any = None) Dataset[source]

Apply magnetic declination rotation to velocities already in ENU frame.

When a Nortek instrument is configured to output ENU coordinates internally, the heading reference used is magnetic north. This function rotates east_velocity and north_velocity by the declination angle so that north aligns with true (geographic) north.

Rotation (D = declination, positive = east):

u_true = u_mag * cos(D) + v_mag * sin(D) v_true = -u_mag * sin(D) + v_mag * cos(D)

No-ops if east_velocity or north_velocity are absent, or if magnetic declination has already been applied (magnetic_declination attr present).

oceanarray.processors.coordinate.xyz_to_enu(vx: ndarray, vy: ndarray, vz: ndarray, heading_deg: ndarray, pitch_deg: ndarray, roll_deg: ndarray, declination_deg: float = 0.0) tuple[ndarray, ndarray, ndarray][source]

Vectorised XYZ → ENU rotation per Nortek Support reference script.

R = H @ P where (with hdg = heading - 90 + declination):

H = [[cos(hdg), sin(hdg), 0], [-sin(hdg), cos(hdg), 0], [0, 0, 1]] P = [[cos(p), -sin(p)*sin(r), -cos(r)*sin(p)],

[0, cos(r), -sin(r)], [sin(p), sin(r)*cos(p), cos(p)*cos(r)]]

The -90 offset accounts for the Nortek Aquadopp instrument frame where heading=90° aligns X→East, Y→North (standard geography at zero tilt). Magnetic declination is added to convert magnetic heading to true north.

Returns (east, north, up) arrays of the same shape as the inputs.

caldip

Cal-dip cast processing (stub; full implementation in progress).

CalDip calibration-dip drift corrections for stage 3 (not yet implemented).

Mooring processing

stack

Interpolate multiple instruments onto a common time grid and stack into a single mooring dataset (oceanarray process MOORING --stage stack).

MooringStacker: interpolate all instruments on a mooring onto a common time grid.

class oceanarray.processors.stack.MooringStacker(*, proc_dir: str)[source]

Step 1: stack all instruments onto a common time axis → _stack.nc.

stack(mooring_name: str, dt_seconds: int = 60, force: bool = False) bool[source]

Stack all processed instruments for mooring_name onto a common time grid.

Reads _stage3.nc (falling back to _stage2.nc) for every instrument listed in the mooring YAML, resamples each to dt_seconds resolution, and writes {mooring}_stack.nc under the mooring proc directory.

Returns True on success, False if no instruments could be loaded or an unrecoverable error occurs.

grid

Interpolate stacked mooring data onto a regular pressure grid (oceanarray process MOORING --stage grid).

MooringGridder and TimeGriddingProcessor: grid mooring data onto regular time/depth axes.

class oceanarray.processors.grid.MooringGridder(*, proc_dir: str)[source]

Step 2: vertically interpolate stacked instruments onto a pressure grid → _grid.nc.

grid(mooring_name: str, p_start: float = 200.0, p_end: float = 1000.0, dp: float = 20.0, force: bool = False) bool[source]

Interpolate the stacked mooring dataset onto a regular pressure grid.

Reads {mooring}_stack.nc, interpolates all variables onto a pressure axis from p_start to p_end in steps of dp dbar, and writes {mooring}_grid.nc.

Returns True on success, False on error.

class oceanarray.processors.grid.TimeGriddingProcessor(*, proc_dir: str)[source]

Handles Step 1 processing: time gridding and optional filtering of mooring instruments.

process_mooring(mooring_name: str, output_path: str | None = None, file_suffix: str = '_stage2', vars_to_keep: List[str] = None, filter_type: str | None = None, filter_params: Dict[str, Any] | None = None) bool[source]

Process Step 1 for a single mooring: time gridding and optional filtering.

Parameters:
  • mooring_name – Name of the mooring to process

  • output_path – Optional custom output path

  • file_suffix – Suffix for input files (‘_stage2’ or ‘_raw’)

  • vars_to_keep – List of variables to include in combined dataset

  • filter_type – Type of time filtering to apply (‘lowpass’, ‘detide’, ‘bandpass’)

  • filter_params – Parameters for filtering

Returns:

True if processing completed successfully

Return type:

bool

oceanarray.processors.grid.process_multiple_moorings_time_gridding(mooring_list: List[str], proc_dir: str, file_suffix: str = '_stage2', filter_type: str | None = None, filter_params: Dict[str, Any] | None = None) Dict[str, bool][source]

Process Step 1 for multiple moorings.

Parameters:
  • mooring_list – List of mooring names to process

  • proc_dir – Cruise-level processed output directory (appends /{mooring}/)

  • file_suffix – Suffix for input files (‘_stage2’ or ‘_raw’)

  • filter_type – Optional time filtering to apply (‘lowpass’, ‘detide’, ‘bandpass’)

  • filter_params – Optional parameters for filtering

Returns:

Dict mapping mooring names to success status

oceanarray.processors.grid.time_gridding_mooring(mooring_name: str, proc_dir: str, output_path: str | None = None, file_suffix: str = '_stage2', filter_type: str | None = None, filter_params: Dict[str, Any] | None = None) bool[source]

Process Step 1 for a single mooring (convenience function).

Parameters:
  • mooring_name – Name of the mooring to process

  • proc_dir – Cruise-level processed output directory (appends /{mooring}/)

  • output_path – Optional output path override

  • file_suffix – Suffix for input files (‘_stage2’ or ‘_raw’)

  • filter_type – Optional time filtering to apply (‘lowpass’, ‘detide’, ‘bandpass’)

  • filter_params – Optional parameters for filtering

Returns:

True if processing completed successfully

Return type:

bool

mooring helpers

Shared helpers for position parsing, HAB computation, and instrument metadata.

Internal helper functions for mooring-level stack and grid operations.

Configuration and validation

parameters

Global processing parameters (QC thresholds, grid defaults, file paths, variable registry).

Package-level defaults for oceanarray.

Matplotlib appearance (font sizes, figure size, DPI, grid style) belongs in config/report.mplstyle — that is the right place for anything that maps to a matplotlib rcParam. There is a single style file: it is applied both by the report encoder and by plotters that set their own style context (via MPLSTYLE below), so the two can never diverge.

This file holds the things the mplstyle cannot express: instrument abbreviations, colorbar percentile clipping, downsample interval, and figure sizes that differ from the per-plot mplstyle default.

Import and reassign any value here to override it globally, e.g.:

import oceanarray.parameters as params
params.DOWNSAMPLE_SECONDS = 60
params.DEFAULT_COLORMAP = "viridis"

These values are read at call time, so assignment before calling a function is sufficient.

oceanarray.config.parameters.CMAPS_BY_VARIABLE: dict[str, str] = {'absolute_salinity': 'YlGnBu_r', 'conservative_temperature': 'RdBu_r', 'dissolved_oxygen': 'RdYlGn', 'dissolved_oxygen_ml_l': 'RdYlGn', 'east_velocity': 'RdBu_r', 'n2': 'plasma', 'north_velocity': 'RdBu_r', 'oxygen_saturation_pct': 'RdYlGn', 'potential_density': 'BuPu', 'salinity': 'YlGnBu_r', 'speed': 'plasma', 'temperature': 'RdBu_r', 'turbidity': 'YlOrBr', 'u': 'RdBu_r', 'up_velocity': 'RdBu_r', 'v': 'RdBu_r', 'w': 'RdBu_r'}

Colormap lookup derived from VARIABLES — single source of truth so the two cannot drift. Excludes entries where cmap is None.

oceanarray.config.parameters.GRID_PANEL_ROW_IN: float = 2.5

Height (inches) of one gridded-section / time-series panel row at full width. Matches the per-row height of the two-row “velocity at depth” figure (5” / 2), so stacked grid panels and single-row section figures share one scale and do not render over-tall.

oceanarray.config.parameters.LINE_CMAPS_BY_VARIABLE: dict[str, str] = {'east_velocity': 'Blues_r', 'north_velocity': 'Blues_r', 'pressure': 'Blues_r', 'salinity': 'YlGnBu_r', 'temperature': 'RdBu_r', 'up_velocity': 'Blues_r'}

Colormaps for colouring lines (one per instrument, deep-first) — distinct from the pcolormesh field maps in CMAPS_BY_VARIABLE, because a field map that is fine for a filled panel can be wrong for overlaid lines (e.g. a diverging map’s pale midpoint washes lines out). Sampled deep→shallow with washed-out colours skipped by luminance (see oceanarray.plotters.helpers.ordered_line_colors()). Directions are chosen so the deepest instrument gets the darkest/most-saturated colour: temperature blue(cold/deep)→red(warm/shallow); pressure dark→lighter blue (bathymetry convention, deep = dark); salinity starts at the blue end.

oceanarray.config.parameters.VARIABLES: dict[str, dict] = {'absolute_salinity': {'cmap': 'YlGnBu_r', 'label': 'Absolute salinity', 'label_units': 'g kg⁻¹', 'standard_name': 'sea_water_absolute_salinity', 'units': 'g kg-1'}, 'apparent_oxygen_utilization': {'cmap': None, 'label': 'Apparent oxygen utilization', 'label_units': 'µmol kg⁻¹', 'standard_name': 'apparent_oxygen_utilization', 'units': 'umol kg-1'}, 'conductivity': {'cmap': None, 'label': 'Conductivity', 'label_units': 'mS cm⁻¹', 'standard_name': 'sea_water_electrical_conductivity', 'units': 'mS cm-1', 'valid_max': 80.0, 'valid_min': 0.0}, 'conservative_temperature': {'cmap': 'RdBu_r', 'label': 'Conservative temperature', 'label_units': '°C', 'standard_name': 'sea_water_conservative_temperature', 'units': 'degree_Celsius', 'valid_max': 42.0, 'valid_min': -5.0}, 'depth': {'cmap': None, 'label': 'Depth', 'label_units': 'm', 'standard_name': 'depth', 'units': 'm', 'valid_max': 12000.0, 'valid_min': 0.0}, 'dissolved_oxygen': {'cmap': 'RdYlGn', 'label': 'Dissolved oxygen', 'label_units': 'µmol L⁻¹', 'standard_name': 'mole_concentration_of_dissolved_molecular_oxygen_in_sea_water', 'units': 'umol L-1'}, 'dissolved_oxygen_ml_l': {'cmap': 'RdYlGn', 'label': 'Dissolved oxygen', 'label_units': 'mL L⁻¹', 'standard_name': None, 'units': 'mL L-1'}, 'east_velocity': {'cmap': 'RdBu_r', 'label': 'Eastward velocity', 'label_units': 'm s⁻¹', 'standard_name': 'eastward_sea_water_velocity', 'units': 'm s-1'}, 'n2': {'cmap': 'plasma', 'label': 'N²', 'label_units': 's⁻²', 'standard_name': 'square_of_brunt_vaisala_frequency_in_sea_water', 'units': 's-2'}, 'north_velocity': {'cmap': 'RdBu_r', 'label': 'Northward velocity', 'label_units': 'm s⁻¹', 'standard_name': 'northward_sea_water_velocity', 'units': 'm s-1'}, 'oxygen_saturation_pct': {'cmap': 'RdYlGn', 'label': 'O₂ percent saturation', 'label_units': '%', 'standard_name': None, 'units': '%'}, 'potential_density': {'cmap': 'BuPu', 'label': 'σ₀', 'label_units': 'kg m⁻³', 'standard_name': 'sea_water_sigma_theta', 'units': 'kg m-3'}, 'pressure': {'cmap': None, 'label': 'Pressure', 'label_units': 'dbar', 'standard_name': 'sea_water_pressure', 'units': 'dbar', 'valid_max': 11000.0, 'valid_min': 0.0}, 'salinity': {'cmap': 'YlGnBu_r', 'label': 'Salinity', 'label_units': 'PSU', 'standard_name': 'sea_water_practical_salinity', 'units': '1'}, 'speed': {'cmap': 'plasma', 'label': 'Speed', 'label_units': 'm s⁻¹', 'standard_name': 'sea_water_speed', 'units': 'm s-1'}, 'temperature': {'cmap': 'RdBu_r', 'label': 'Temperature', 'label_units': '°C', 'standard_name': 'sea_water_temperature', 'units': 'degree_Celsius', 'valid_max': 42.0, 'valid_min': -5.0}, 'turbidity': {'cmap': 'YlOrBr', 'label': 'Turbidity', 'label_units': 'NTU', 'standard_name': 'sea_water_turbidity', 'units': 'NTU'}, 'u': {'cmap': 'RdBu_r', 'label': 'Eastward velocity', 'label_units': 'm s⁻¹', 'standard_name': 'eastward_sea_water_velocity', 'units': 'm s-1'}, 'up_velocity': {'cmap': 'RdBu_r', 'label': 'Upward velocity', 'label_units': 'm s⁻¹', 'standard_name': 'upward_sea_water_velocity', 'units': 'm s-1'}, 'v': {'cmap': 'RdBu_r', 'label': 'Northward velocity', 'label_units': 'm s⁻¹', 'standard_name': 'northward_sea_water_velocity', 'units': 'm s-1'}, 'w': {'cmap': 'RdBu_r', 'label': 'Vertical velocity', 'label_units': 'm s⁻¹', 'standard_name': 'upward_sea_water_velocity', 'units': 'm s-1'}}

Display metadata for each physical variable.

Each entry has:

label — human-readable name for plot titles and legends. label_units — Unicode units string for plot axis labels (e.g. "°C").

Use vlabel() to get the combined "Label (units)" string. Empty string for dimensionless quantities.

units — udunits-2 / CF-compliant ASCII units for NetCDF attributes

(e.g. "degree_Celsius"). May differ from label_units only in encoding (Unicode → ASCII).

standard_name — CF standard name. None when no standard name exists

(e.g. turbidity in NTU, which has no udunits-2 unit).

cmap — default matplotlib colormap name, or None for variables

without a natural diverging / sequential convention.

valid_min — CF valid_min attribute written to NetCDF output (physically valid_max valid range). Present only where OS1_vocab_attrs.yaml

provides an authoritative value; absent for other variables.

oceanarray.config.parameters.VAR_COLORS: dict[str, str] = {'absolute_salinity': '#E69F00', 'conductivity': '#44AA99', 'conservative_temperature': '#56B4E9', 'depth': '#000000', 'dissolved_oxygen': '#332288', 'dissolved_oxygen_ml_l': '#332288', 'east_velocity': '#D55E00', 'n2': '#000000', 'north_velocity': '#0072B2', 'oxygen_saturation_pct': '#332288', 'potential_density': '#009E73', 'pressure': '#000000', 'salinity': '#E69F00', 'speed': '#D55E00', 'temperature': '#56B4E9', 'turbidity': '#661100', 'u': '#D55E00', 'up_velocity': '#CC79A7', 'v': '#0072B2', 'w': '#CC79A7'}

One colourblind-safe colour per variable, for single-instrument panels where each variable is drawn as one line (not the multi-instrument stack, which uses LINE_CMAPS_BY_VARIABLE). Physics use the Okabe-Ito palette (Wong, Nature Methods 8:441, 2011); biogeochemistry uses the Paul Tol palette. Look up with var_color() (single fallback, no scattered literals). Mirrors ctdcast’s VAR_COLORS.

oceanarray.config.parameters.VAR_COLOR_DEFAULT: str = '#000000'

Colour for a variable with no VAR_COLORS entry — one canonical fallback so callers don’t scatter their own literal defaults (which drift).

oceanarray.config.parameters.var_color(var: str) str[source]

Return the line/marker colour for var from VAR_COLORS.

Falls back to VAR_COLOR_DEFAULT for an unregistered variable, so every caller shares one default rather than hard-coding its own.

Parameters:

var (str) – Variable name (key in VARIABLES / VAR_COLORS).

Returns:

A hex colour string.

Return type:

str

oceanarray.config.parameters.vlabel(var: str, prefix: str = '') str[source]

Return a matplotlib axis label for var from the VARIABLES registry.

Format is "{prefix}Label (units)" when label_units is non-empty, or "{prefix}Label (1)" for a dimensionless quantity — 1 is the CF / UDUNITS unit string for dimensionless (e.g. practical salinity, whose NetCDF units attribute is "1"), so the label matches the stored metadata rather than leaving the reader to wonder whether a unit was forgotten. The prefix is prepended to the label component only, not the units, so vlabel("temperature", prefix="Δ") produces "ΔTemperature (°C)".

Parameters:
  • var (str) – Variable name (key in VARIABLES).

  • prefix (str, optional) – Text prepended to the label component — e.g. "Gridded " or "Δ".

Returns:

Ready-to-use axis label. Falls back to "{prefix}{var}" when var is not in the registry so callers always get something useful.

Return type:

str

oceanarray.config.parameters.vunit(var: str) str[source]

Return the axis-label units string for var from VARIABLES.

This is the label_units component alone (e.g. "°C", "dbar"), suitable for a units-only colorbar title. Returns an empty string for a dimensionless quantity or an unknown variable.

Parameters:

var (str) – Variable name (key in VARIABLES).

Returns:

The unit string, or "" when none is registered.

Return type:

str

validation

Validate mooring YAML configuration files and check instrument type names.

Validation utilities for oceanarray mooring YAML configuration files.

The instrument field in each clamp/instruments entry is used as a subdirectory name when reading raw files and writing processed output:

<raw-dir>/<mooring_name>/<instrument>/<filename>
<proc-dir>/<mooring_name>/<instrument>/<output>.nc

Valid instrument names and their typical file types

Do NOT use hardware/model names as the instrument value:
  • sbe37 → use microcat

  • nortek → use aquadopp

Clock correction YAML fields (per-instrument, applied in Stage 2)

Stage 2 applies corrections in this order:

  1. Constant offset — applied uniformly across the entire record:

    clock_offset: 15        # seconds; positive = instrument was slow (behind)
    
  2. Linear drift — grows linearly from 0 at deployment to the full drift at recovery. Two equivalent ways to specify it:

    Option A — direct:

    clock_drift_seconds: 8   # positive = instrument was slow (behind at recovery)
    

    Option B — two timestamps read off at recovery (preferred; no sign errors):

    clock_computer_at_recovery:    '2026-07-11T10:23:30'
    clock_instrument_at_recovery:  '2026-07-11T10:23:22'
    # drift = computer − instrument = +8 s  (instrument was 8 s behind)
    

    If both Option A and Option B are present, Option B takes priority.

  3. Trimming — data outside deployment_time … recovery_time is discarded after all clock corrections, so corrections are applied to the full raw record.

Sign convention — both values are amounts added to instrument time:

Situation

clock_offset

clock_drift_seconds

Instrument clock was slow (behind real time)

positive (+)

positive (+)

Instrument clock was fast (ahead of real time)

negative (-)

negative (-)

The original, uncorrected time is always saved as time_orig in the output NetCDF alongside the corrected time coordinate. The history attribute records what was applied and when.

class oceanarray.config.validation.ValidationIssue(level: str, message: str)[source]

A single validation finding with a severity level and human-readable message.

level: str

Alias for field number 0

message: str

Alias for field number 1

oceanarray.config.validation.print_validation_report(yaml_path: str) bool[source]

Print a human-readable validation report. Returns True if no errors.

oceanarray.config.validation.validate_mooring_yaml(yaml_path: str) List[ValidationIssue][source]

Validate a mooring YAML configuration file.

Checks: - Required top-level keys are present - Each instrument entry uses a valid instrument name (not a model alias) - Each instrument entry with a file_type uses a recognised value - Instruments with filename also have file_type - Instruments without filename are flagged as warnings (not yet staged)

Returns a list of ValidationIssue named-tuples. An empty list means the file passed all checks.

Analysis

science

QC routines: flag_salinity_outliers, flag_temporal_spikes, flag_vertical_inconsistencies, run_qc.

QC and dataset processing functions for oceanographic instrument data.

Contains salinity/conductivity quality-control routines and a dataset processing helper. Hydrographic, spectral, time-series, and vector utilities have been split into dedicated submodules:

Backward-compatible re-exports at the bottom of this file preserve existing from oceanarray.analysis._science import usage.

oceanarray.analysis.science.flag_salinity_outliers(ds: Dataset, n_std: float = 4) DataArray[source]

Flag PSAL values more than n_std standard deviations from the mean.

Computed separately for each depth level.

Parameters:
  • ds (xarray.Dataset) – Dataset containing “PSAL” variable with dimensions including “DEPTH”.

  • n_std (float, optional) – Number of standard deviations from the mean to define an outlier (default is 4).

Returns:

Boolean array with True where salinity is flagged as an outlier.

Return type:

xarray.DataArray (bool)

oceanarray.analysis.science.flag_temporal_spikes(ds: Dataset, var: str = 'CNDC', threshold: float = 5) DataArray[source]

Flag large absolute differences in time for each depth.

threshold: maximum allowed difference in units of the variable.

oceanarray.analysis.science.flag_vertical_inconsistencies(ds: Dataset, var: str = 'CNDC', threshold: float = 2) DataArray[source]

Flag points that are very different from vertical neighbors.

threshold: max allowed difference between vertically adjacent sensors.

oceanarray.analysis.science.process_dataset(ds: Dataset, latlim: tuple[float, float] = (26.0, 27.0), lonlim: tuple[float, float] = (-77.0, -76.5), pgrid: ndarray = None) tuple[Dataset, Dataset][source]

Filter and process a hydrographic dataset for use in training.

This function selects a region of interest, extracts and downsamples profiles of temperature and salinity onto both standard and sparse pressure grids. It also computes potential density anomaly for both resolutions.

Parameters:
  • ds (xr.Dataset) – Input dataset containing hydrographic data including CT, SA, PRES, and metadata.

  • latlim (tuple of float, optional) – Latitude limits for filtering, by default (26.0, 27.0).

  • lonlim (tuple of float, optional) – Longitude limits for filtering, by default (-77.0, -76.5).

  • pgrid (np.ndarray, optional) – Target pressure levels (dbar) for vertical interpolation. When None, a 20 dbar grid is constructed automatically from 0 to the maximum observed pressure.

Returns:

  • ds_standard (xr.Dataset) – Dataset downsampled to standard pressure levels.

  • ds_sparse (xr.Dataset) – Dataset downsampled to sparse pressure levels.

See also

verticalnn.data_utils.downsample_to_sparse

Used to interpolate to target pressure levels.

verticalnn.config.STANDARD_PRESSURES

Standard pressure grid.

verticalnn.config.SPARSE_PRESSURES

Sparse pressure grid.

oceanarray.analysis.science.run_qc(ds: Dataset) Dataset[source]

Apply a sequence of QC tests and write combined CNDC_QC flag variable.

hydrographic

Salinity calculation, isopycnal tracking, cold-regime detection, and dataset differencing.

Hydrographic analysis utilities for oceanographic mooring data.

hydrographic.py provides salinity computation, isopycnal tracking, dataset differencing, and cold-regime detection functions extracted from the general science utilities.

Pairs with oceanarray.plotters.hydrography for figure output.

oceanarray.analysis.hydrographic.calc_ds_difference(ds1: Dataset, ds2: Dataset) Dataset[source]

Compute the variable-by-variable difference between two time-matched datasets.

oceanarray.analysis.hydrographic.calc_psal(ds: Dataset) Dataset[source]

Compute Practical Salinity from conductivity, temperature, and pressure.

Uses the Gibbs SeaWater (GSW) toolbox: gsw.SP_from_C applies the PSS-78 equation to derive Practical Salinity (dimensionless, roughly PSU) from conductivity (mS cm⁻¹), temperature (°C), and pressure (dbar).

If PSAL is already present in ds the function is a no-op.

Parameters:

ds (xarray.Dataset) – Dataset containing CNDC (conductivity, mS cm⁻¹), TEMP (temperature, °C), and PRES (pressure, dbar).

Returns:

Input dataset with PSAL added (same dimensions as CNDC), or unchanged if PSAL was already present.

Return type:

xarray.Dataset

oceanarray.analysis.hydrographic.find_cold_entry_exit(time: ndarray, temp: ndarray, quantile: float = 0.95, dwell_seconds: int = 1800, smooth_window: int = 5) tuple[Timestamp | None, Timestamp | None, float][source]

Identify first sustained entry into ‘cold’ regime and last sustained exit.

Parameters:
  • time (array-like of datetime64) – Time coordinate array.

  • temp (array-like of float) – Temperature values aligned with time.

  • quantile (float) – Percentile for threshold (e.g. 0.1 ~ 10th percentile).

  • dwell_seconds (int) – Minimum time in cold regime for it to count (seconds).

  • smooth_window (int) – Rolling median window length (samples).

Returns:

t_start, t_end, threshold

Return type:

tuple of (Timestamp or None, Timestamp or None, float)

oceanarray.analysis.hydrographic.isopycnal_dataset(ds: Dataset, sigma_var: str = 'sigma0', sigma_grid: ndarray | None = None) Dataset[source]

Build an isopycnal-tracking dataset from a gridded mooring file.

Tracks the pressure (and height above seabed) of density surfaces through time using the gridded sigma0 field. Calls isopycnal_pressure_series() column-by-column.

Parameters:
  • ds – Gridded mooring xr.Dataset with (pressure, time) dimensions and a sigma variable. A waterdepth global attribute (metres) is used for the height-above-seabed conversion; if absent both _height variables are all-NaN.

  • sigma_var – Name of the sigma variable in ds (default "sigma0").

  • sigma_grid – Target sigma0 values to track. If None, a 0.1 kg m⁻³ grid is computed from the observed data range.

Returns:

Dimensions (sigma0_level, time). Variables:

  • isopycnal_pressure — dbar, NaN where absent.

  • isopycnal_height — m above seabed, NaN where absent.

Return type:

xr.Dataset

oceanarray.analysis.hydrographic.isopycnal_pressure_series(sigma0_tp: ndarray, pressure: ndarray, sigma_grid: ndarray) ndarray[source]

Find the pressure of each target density surface at every time step.

Uses a “first crossing from shallow” approach: for each target σ₀ value, scans from the shallowest pressure downward and finds the first level where sigma0 transitions from below to at-or-above the target, then linearly interpolates between those two levels. This is robust to non-monotonic sigma0 columns (which occur in gridded fields during knockdowns) and always returns the shallowest crossing — the physically meaningful pycnocline.

Returns NaN only when the target density is absent from the entire column (too light or too dense for any observed level at that time step).

Note

Operates on σ₀ (potential density referenced to 0 dbar). If the mooring was processed with a different reference pressure (e.g. σ₂ at 2000 dbar), pass the corresponding sigma variable. A future option --sig-ref / density_reference will generalise this; for now the variable name in the dataset controls which reference is used.

Parameters:
  • sigma0_tp – Shape (time, pressure). NaN where missing.

  • pressure – 1-D array of pressure levels in dbar. Need not be sorted — the function sorts each time step’s finite values by pressure before scanning for crossings.

  • sigma_grid – Target sigma0 values (kg m⁻³) at which to find the pressure.

Returns:

Shape (time, len(sigma_grid)). NaN where the target isopycnal is absent from the observed column at that time step.

Return type:

np.ndarray

temporal

Lag correlation, histogram-based split value, T/S downsampling, and Tukey time-series filtering.

Time-series analysis utilities for oceanographic data.

temporal.py provides filtering, lag correlation, histogram-based splitting, and sparse downsampling operations used across instrument processing and reporting.

Pairs with oceanarray.plotters.timeseries for figure output.

oceanarray.analysis.temporal.downsample_to_sparse(temp_profiles: ndarray, salt_profiles: ndarray, full_pressures: ndarray, sparse_pressures: ndarray) tuple[ndarray, ndarray][source]

Downsample full T/S profiles to sparse pressure levels.

Parameters:
  • temp_profiles (np.ndarray) – Full temperature profiles, shape (n_profiles, n_pressures_full).

  • salt_profiles (np.ndarray) – Full salinity profiles, shape (n_profiles, n_pressures_full).

  • full_pressures (np.ndarray) – Full pressure levels corresponding to temp_profiles and salt_profiles, shape (n_pressures_full,).

  • sparse_pressures (np.ndarray) – Target sparse pressure levels to sample, shape (n_pressures_sparse,).

Returns:

  • temp_sparse (np.ndarray) – Sparse temperature profiles, shape (n_profiles, n_pressures_sparse). NaN where the target pressure is outside full_pressures.

  • salt_sparse (np.ndarray) – Sparse salinity profiles, same shape. NaN at the same out-of-range levels.

oceanarray.analysis.temporal.filter_sigma_tukey(data: ndarray, window_samples: int, alpha: float = 0.5) ndarray[source]

Apply a Tukey moving-average filter along axis=1 (time), NaN-aware.

Uses a finite-weight convolution: each output point is the weighted mean of the finite values within the window, so NaN gaps never contaminate adjacent points. Output is set to NaN where fewer than 10 % of the window weights are finite (edges of large data gaps).

Parameters:
  • data (np.ndarray) – 2-D array with shape (n_pressure, n_time). NaN marks missing values.

  • window_samples (int) – Length of the Tukey window in samples. Values < 3 or ≥ n_time return a copy of data unchanged.

  • alpha (float, optional) – Shape parameter of the Tukey window in [0, 1] (default 0.5). alpha=0 is a rectangular window; alpha=1 is a Hann window.

Returns:

Smoothed array with the same shape as data. Rows that are entirely NaN are returned unchanged.

Return type:

np.ndarray

oceanarray.analysis.temporal.lag_correlation(x: ndarray, y: ndarray, max_lag: int, min_overlap: int = 10) ndarray[source]

Pearson correlation at integer lags in [-max_lag, max_lag].

Parameters:
  • x (np.ndarray) – 1-D arrays of the same length. NaN values are excluded pairwise at each lag.

  • y (np.ndarray) – 1-D arrays of the same length. NaN values are excluded pairwise at each lag.

  • max_lag (int) – Maximum lag (in samples) to compute. Output has length 2 * max_lag + 1.

  • min_overlap (int, optional) – Minimum number of finite pairs required to compute a correlation at a given lag. Lags with fewer pairs return NaN (default 10).

Returns:

Correlation coefficients, shape (2 * max_lag + 1,). Positive lags mean x leads y; NaN where overlap is insufficient.

Return type:

np.ndarray

Raises:

ValueError – If x and y do not have the same shape.

oceanarray.analysis.temporal.split_value(data: ndarray, nbins: int = 30) float[source]

Find the histogram-based threshold between two data modes.

Computes a nbins-bin histogram, locates the two highest peaks, and returns the left edge of the minimum-count bin between them.

Parameters:
  • data (np.ndarray) – 1-D array (NaNs are removed before binning).

  • nbins (int, optional) – Number of histogram bins (default 30). Increase if the two modes are not resolved.

Returns:

Left edge of the minimum-count bin between the two dominant peaks.

Return type:

float

Raises:

ValueError – If fewer than two histogram peaks are detected. This occurs when the data are unimodal or when nbins is too coarse to resolve the two modes.

spectral

Gonella rotary spectra, Welch PSD, and continuous wavelet transforms.

Spectral analysis utilities for oceanographic time series.

Provides rotary spectrum decomposition (Gonella 1972), Welch PSD estimates with and without gap-awareness, and continuous wavelet transforms.

Pairs with oceanarray.plotters.spectrum for figure output.

oceanarray.analysis.spectral.compute_cwt(x: ndarray, dt_seconds: float, wavelet: str = 'morlet', dj: float = 0.25, significance_level: float = 0.95) dict[source]

Compute a continuous wavelet transform (CWT) on a 1-D time series.

Uses pycwt (Torrence & Compo 1998 method). The input array is gap-filled by linear interpolation before the transform; a boolean gap mask is returned so callers can overlay the filled regions.

If the AR(1) coefficient estimation fails (series too short or strongly trended), a WARNING is logged and the significance test falls back to a white-noise background (alpha=0); the wavelet itself is unaffected.

Parameters:
  • x – 1-D array of values. NaNs are treated as gaps and filled by linear interpolation before the transform.

  • dt_seconds – Sample interval in seconds.

  • wavelet"morlet" (default, complex — gives amplitude and phase, best for oscillatory signals) or "mexican_hat" (real — better for detecting edges/sharp features, useful for wave-skewness studies).

  • dj – Fractional octave spacing between scales. Smaller values give more scales (finer period resolution) at higher compute cost. Default 0.25 (4 scales per octave).

  • significance_level – Confidence level for the chi-squared significance test against a red-noise background. Default 0.95 (95 %).

Returns:

  • dict with keys

  • - ``power`` (2-D array (n_scales, n_time), real wavelet power.)

  • - ``periods`` (1-D array of periods in **days*.*)

  • - ``coi`` (1-D array of raw record-edge COI periods in **days* (pycwt output).*)

  • - ``effective_coi`` (1-D array of gap-aware COI periods in **days*. At each time*) – step this is the minimum of the record-edge COI and the COI contributed by the nearest gap boundary. Inside gap columns it is 0 (entire period range unreliable). Use this for hatching in plots.

  • - ``signif`` (1-D array (n_scales,) — significance threshold for each) – scale; power > signif[:,None] is significant.

  • - ``gap_mask`` (boolean 1-D array (n_time,) — True where the original) – data was NaN (gap-filled region).

  • - ``dt_days`` (sample interval in days (convenience).)

oceanarray.analysis.spectral.gonella_rotary_spectrum(u_col: ndarray, v_col: ndarray, fs: float, nperseg: int, noverlap: int) tuple[ndarray, ndarray, ndarray, ndarray][source]

Compute the Gonella (1972) rotary power spectrum from 1-D velocity components.

Both input arrays must be finite (no NaN). Calls scipy.signal.welch for the auto-spectra and scipy.signal.csd for the cross-spectrum, then applies the Gonella rotary decomposition.

Parameters:
  • u_col (np.ndarray) – 1-D east-velocity time series in m s⁻¹, finite (gap-filled).

  • v_col (np.ndarray) – 1-D north-velocity time series in m s⁻¹, finite (gap-filled).

  • fs (float) – Sampling frequency in cycles per day.

  • nperseg (int) – Number of samples per Welch segment.

  • noverlap (int) – Number of overlapping samples between adjacent Welch segments.

Returns:

(freq, s_cw, s_ccw, r) where:

  • freq — 1-D frequency array in cycles per day.

  • s_cw — clockwise power spectral density (≥ 0).

  • s_ccw — counter-clockwise power spectral density (≥ 0).

  • r — rotary coefficient (s_ccw - s_cw) / (s_ccw + s_cw), in [-1, 1]; 0 where both spectra are zero.

Return type:

tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]

References

Gonella, J. (1972). A rotary-component method for analysing meteorological and oceanographic vector time series. Deep-Sea Research, 19(12), 833–846.

oceanarray.analysis.spectral.welch_psd(x: ndarray, dt_days: float, segment_length: int, overlap: float = 0.5, window: str = 'hann') tuple[ndarray, ndarray][source]

Welch PSD estimate on a gap-filled (finite) time series.

Parameters:
  • x – 1-D array of evenly-spaced, finite values (no NaNs).

  • dt_days – Sample interval in days.

  • segment_length – Number of samples per Welch window (nperseg).

  • overlap – Fractional overlap between windows (default 0.5 → 50 %).

  • window – Window function name accepted by scipy.signal.welch.

Returns:

(frequencies_cpd, psd) where frequencies are in cycles per day.

Return type:

tuple[np.ndarray, np.ndarray]

oceanarray.analysis.spectral.welch_psd_gapaware(x: ndarray, dt_days: float, segment_length: int, overlap: float = 0.5, window: str = 'hann') tuple[ndarray | None, ndarray | None, int][source]

Welch PSD over contiguous finite runs only; windows straddling gaps are skipped.

Returns a sample-count-weighted average PSD across all valid windows from all contiguous finite segments. This avoids the low-frequency bias that arises from gap-filling (linear interpolation) before the Welch estimate.

Switching the LF panel to gap-aware is a one-line change: replace welch_psd(col_filled, ...) with welch_psd_gapaware(col, ...).

Parameters:
  • x – 1-D array of evenly-spaced values; NaN marks gaps.

  • dt_days – Sample interval in days.

  • segment_length – Number of samples per Welch window (nperseg).

  • overlap – Fractional overlap between windows (default 0.5 → 50 %).

  • window – Window function name accepted by scipy.signal.welch.

Returns:

(frequencies_cpd, psd, n_windows) where n_windows is the total number of valid Welch windows used. Returns (None, None, 0) when no contiguous run is long enough for a single window.

Return type:

tuple[np.ndarray | None, np.ndarray | None, int]

vector

XYZ→ENU rotation and progressive-vector trajectory computation.

Vector rotation and progressive-vector utilities for oceanographic current analysis.

Provides coordinate-system rotation (XYZ → ENU) and progressive-vector (pseudo-Lagrangian) trajectory computation extracted from instrument-specific processing code so they can be tested and reused independently.

Pairs with oceanarray.plotters.current for figure output.

oceanarray.analysis.vector.progressive_vector(east_2d: ndarray, north_2d: ndarray, dt_s: ndarray, pressure: ndarray) list[tuple[float, ndarray, ndarray]][source]

Compute pseudo-Lagrangian progressive-vector trajectories by pressure level.

Integrates east and north velocity time series using the Euler forward method to produce cumulative horizontal displacement from the origin. Levels with all-NaN velocity are silently skipped.

Parameters:
  • east_2d (np.ndarray) – East velocity array, shape (n_time, n_pressure) in m s⁻¹. NaN is treated as zero for the integration step in which it occurs.

  • north_2d (np.ndarray) – North velocity array, shape (n_time, n_pressure) in m s⁻¹. NaN is treated as zero for the integration step in which it occurs.

  • dt_s (np.ndarray) – 1-D array of time-step sizes in seconds, length n_time - 1.

  • pressure (np.ndarray) – 1-D pressure array of length n_pressure in dbar.

Returns:

One entry (p_val, x_km, y_km) per pressure level that has at least one finite velocity sample. p_val is the pressure in dbar; x_km and y_km are 1-D arrays of cumulative east and north displacement in km, length n_time.

Return type:

list of tuple[float, np.ndarray, np.ndarray]

oceanarray.analysis.vector.xyz_to_enu_2d(vx: ndarray, vy: ndarray, vz: ndarray, heading_deg: ndarray, pitch_deg: ndarray, roll_deg: ndarray, declination_deg: float = 0.0) tuple[ndarray, ndarray][source]

Rotate XYZ instrument-frame velocities to ENU using the Nortek heading convention.

The transformation is vectorised and handles arbitrary array shapes as long as all inputs broadcast together. The Nortek heading convention subtracts 90° before constructing the rotation matrix so that a heading of 0° (north) maps to the correct ENU orientation.

Parameters:
  • vx (np.ndarray) – Along-beam (X) velocity component, m s⁻¹.

  • vy (np.ndarray) – Lateral (Y) velocity component, m s⁻¹.

  • vz (np.ndarray) – Vertical (Z) velocity component, m s⁻¹.

  • heading_deg (np.ndarray) – Instrument heading in degrees, positive clockwise from north.

  • pitch_deg (np.ndarray) – Instrument pitch in degrees.

  • roll_deg (np.ndarray) – Instrument roll in degrees.

  • declination_deg (float, optional) – Magnetic declination to add to heading before rotation (degrees, positive east). Default is 0.0 (no correction).

Returns:

(east, north) velocity components in m s⁻¹.

Return type:

tuple[np.ndarray, np.ndarray]

Plotters

primitives

Tier-1 data-agnostic plot primitives (array-in / Figure-out).

Tier-1 data-agnostic plotting primitives.

These functions accept pre-processed arrays (not xarray Datasets) and have no knowledge of oceanographic variable naming conventions. They are the lowest layer in the three-tier plotters architecture:

Tier 1: primitives.py — generic, array-in / Figure-out Tier 2: _current.py etc. — domain wrappers (xr.Dataset-in / Figure-out) Tier 3: report/_plots.py — thin wrappers (path-in / base64-out)

Post-OdB: add plot_vector_heatmap, plot_section, plot_spectrum, plot_polar_histogram, plot_timeseries.

oceanarray.plotters.primitives.colorbar_norm(data: ndarray | None = None, *, vmin: float | None = None, vmax: float | None = None, n: int = 20, symmetric: bool = False) tuple[ndarray, BoundaryNorm][source]

Return (bounds, norm) for a discrete pcolormesh colorbar.

Computes percentile limits from data when vmin / vmax are not given. Explicit vmin / vmax override the percentile calculation. Pass symmetric=True to force the range symmetric about zero.

Parameters:
  • data (np.ndarray, optional) – Source array used for percentile-based limit computation. Ignored when both vmin and vmax are given.

  • vmin (float, optional) – Explicit color limits. Either one or both may be supplied; the other falls back to the percentile of data.

  • vmax (float, optional) – Explicit color limits. Either one or both may be supplied; the other falls back to the percentile of data.

  • n (int) – Target number of colorbar levels (default 20).

  • symmetric (bool) – If True, expand [vmin, vmax] to [-max, +max] before computing bounds.

Returns:

  • bounds (np.ndarray) – Boundary array for BoundaryNorm and colorbar ticks.

  • norm (matplotlib.colors.BoundaryNorm)

Raises:

ValueError – If neither data nor both vmin and vmax are provided.

oceanarray.plotters.primitives.date_axis(ax: Any) None[source]

Apply a concise auto-scaled date formatter to ax’s x-axis (offset left).

oceanarray.plotters.primitives.date_offset_left(ax: Any) None[source]

Move the x-axis date offset label (e.g. 2026-Jul) to the bottom-left.

matplotlib’s ConciseDateFormatter draws the year/month offset at the bottom-right. Only the offset’s vertical position is updated on each draw, so the left x-position set here persists. Call after setting the date formatter.

oceanarray.plotters.primitives.figure_title(fig: Any, text: str, **kwargs: Any) Any[source]

Set a figure-level title (suptitle), centred over all panels.

Use for a title that spans a multi-panel figure; per-panel titles use plot_title() (left-aligned). Keeping both in one module makes the left-vs-centre policy a single switch point.

Parameters:
  • fig (matplotlib.figure.Figure) – Figure whose suptitle is set.

  • text (str) – Title text.

  • **kwargs (Any) – Forwarded to matplotlib.figure.Figure.suptitle (e.g. y, fontsize).

Returns:

The created suptitle artist.

Return type:

matplotlib.text.Text

oceanarray.plotters.primitives.hodograph_panel(ax: Any, e_v: ndarray, n_v: ndarray, t_frac: ndarray, title: str, units: str) Any[source]

Draw a single velocity hodograph on a pre-squared ax; return its mappable.

Renders a time-coloured LineCollection trajectory (downsampled to <= 2 000 segments for performance) with start/end markers, symmetric -lim..lim limits, and a subtle grid. The panel does NOT draw its own colorbar — the caller owns one shared colorbar (all panels use the same 0->1 plasma time mapping); pass the returned ScalarMappable to unit_colorbar() on a shared cax from square_axes_grid().

e_v, n_v, and t_frac must already be filtered to the same finite-valid indices (no NaN, same length). ax is expected to be an exact square from square_axes_grid(); equal aspect is enforced with adjustable='datalim' so the box is never resized (which would strand the shared colorbar).

Parameters:
  • ax (matplotlib Axes) – Pre-squared target axes to draw on.

  • e_v (np.ndarray) – East and north velocity (finite values only, same length).

  • n_v (np.ndarray) – East and north velocity (finite values only, same length).

  • t_frac (np.ndarray) – Fractional deployment time 0 -> 1, same length as e_v.

  • title (str) – Axes title.

  • units (str) – Velocity unit string appended to axis labels, e.g. "m s^-1".

Returns:

The 0->1 plasma time mapping, for a shared colorbar.

Return type:

matplotlib.cm.ScalarMappable

oceanarray.plotters.primitives.pcolormesh_panel(fig: Any, ax: Any, data: ndarray, time: ndarray, pressure: ndarray, title: str, units: str = '', cmap: str = 'RdYlBu_r', style: str = 'pcolormesh', vmin: float | None = None, vmax: float | None = None, n: int = 20, cb_label: str | None = None, title_loc: str = 'left', date_fmt: bool = True) Any[source]

Draw one (pressure × time) field as a discrete-colorbar panel on ax.

A generic time–depth panel primitive. Computes percentile colour limits from data unless vmin / vmax are supplied. Applies a discrete BoundaryNorm colorbar, an inverted pressure axis, and (optionally) a concise date axis.

Parameters:
  • fig (matplotlib Figure and Axes) – Target figure and axes to draw on.

  • ax (matplotlib Figure and Axes) – Target figure and axes to draw on.

  • data (numpy.ndarray) – 2-D field shaped (pressure, time).

  • time (numpy.ndarray) – Time coordinate (length matching data’s second axis).

  • pressure (numpy.ndarray) – Pressure coordinate (length matching data’s first axis).

  • title (str) – Panel title.

  • units (str, optional) – Units string appended to cb_label when cb_label is None.

  • cmap (str, optional) – Colormap name. Default "RdYlBu_r".

  • style (str, optional) – "pcolormesh" (default) or "contourf".

  • vmin (float, optional) – Explicit colour limits. Computed from data percentiles when omitted.

  • vmax (float, optional) – Explicit colour limits. Computed from data percentiles when omitted.

  • n (int, optional) – Target number of colorbar levels. Default 20.

  • cb_label (str, optional) – Colorbar label. Defaults to "{title} ({units})" or "{title}".

  • title_loc (str, optional) – Horizontal alignment of the axes title. Default "left" (the report panel-title convention; pass "center" for a centred title).

  • date_fmt (bool, optional) – When True (default), apply date_axis() to ax. Pass False for stacked panels where only the last axis needs the formatter.

Returns:

The pcolormesh/contourf artist (useful for a shared colorbar).

Return type:

matplotlib collection

oceanarray.plotters.primitives.plot_title(ax: Any, text: str, *, loc: str = 'left', **kwargs: Any) Any[source]

Set an axes (panel) title, left-aligned by default.

The single place panel-title alignment is decided for report figures, so the left-vs-centre choice for every panel title can be switched here in one line. Figure-level titles that span several panels use figure_title() (centred) instead — do not route those through this helper. Colorbar unit labels (cb.ax.set_title) are not panel titles and must not use this helper either.

Parameters:
  • ax (matplotlib.axes.Axes) – Axes whose title is set.

  • text (str) – Title text.

  • loc (str, optional) – Horizontal alignment passed to set_title (default "left").

  • **kwargs (Any) – Forwarded to matplotlib.axes.Axes.set_title (e.g. fontsize).

Returns:

The created title artist.

Return type:

matplotlib.text.Text

oceanarray.plotters.primitives.plot_trajectory(x: ndarray, y: ndarray, color_data: ndarray | None = None, cmap: str = 'coolwarm', xlabel: str = 'East displacement (m)', ylabel: str = 'North displacement (m)', colorbar_label: str = '', colorbar_unit: str = '', title: str = '', *, width_in: float = 4.5) Figure[source]

Plot a 2D trajectory, optionally coloured per-segment by a scalar field.

When color_data is provided, segments are drawn as a LineCollection with colours mapped through cmap and a height-matched colorbar (unit as a title on top). When omitted, a plain line is drawn. Start and end are marked with green and red markers. The axes are laid out as an exact square via square_axes_grid() with square_limits(), so the colorbar height always matches the plotted square.

Parameters:
  • x (ndarray) – Trajectory coordinates (same length).

  • y (ndarray) – Trajectory coordinates (same length).

  • color_data (ndarray, optional) – Scalar values to map onto the line (same length as x/y).

  • cmap (str) – Matplotlib colormap name used when color_data is provided.

  • xlabel (str) – Axis labels.

  • ylabel (str) – Axis labels.

  • colorbar_label (str) – Fallback colorbar label placed on top when colorbar_unit is empty.

  • colorbar_unit (str) – Unit string placed above the colorbar (units-only on top, saves width).

  • title (str) – Figure title.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Return type:

matplotlib.figure.Figure

oceanarray.plotters.primitives.pressure_axis(ax: Any) None[source]

Configure ax as a standard pressure Y-axis: inverted, labelled, gridded.

oceanarray.plotters.primitives.square_axes_grid(fig_w: float, nrows: int, ncols: int, *, colorbar: bool = True, per_panel_colorbar: bool = False, top_pad_in: float = 0.0, bottom_pad_in: float = 0.0, wgap_in: float | None = None, cbar_txt_in: float | None = None, left_in: float | None = None) tuple[Figure, ndarray, Any][source]

Lay out an nrows × ncols grid of square axes deterministically in inches.

Every panel is an exact square whose side is computed from the usable width, so an equal-aspect plot fills the panel without set_aspect having to shrink the axes box — and a colorbar placed here shares each panel’s exact pixel height rather than the taller subplot cell. A single shared colorbar axes (spanning the full height of the panel stack) is reserved on the right when colorbar is true. fig._manual_layout is set so the base64 encoder skips tight_layout (which would re-flow these hand-placed axes).

Callers must NOT call set_aspect('equal', adjustable='box') on the returned axes: the box is already square and authoritative, so use symmetric limits (or adjustable='datalim') instead, or the box would resize and strand the shared colorbar again.

Parameters:
  • fig_w (float) – Figure width in inches — must equal the display slot so the PNG is not rescaled by the browser.

  • nrows (int) – Grid shape (both >= 1).

  • ncols (int) – Grid shape (both >= 1).

  • colorbar (bool) – Reserve and return a single shared colorbar axes on the right. Default True. Ignored when per_panel_colorbar is set.

  • per_panel_colorbar (bool) – Give each panel its own height-matched colorbar axes to its right (for figures where every panel encodes a different field, e.g. a T-S dot plot + count heatmap + O₂ panel). The third return value is then an (nrows, ncols) object array of colorbar axes instead of a single one.

  • top_pad_in (float) – Extra inches reserved above the panel-title strip, e.g. for a figure suptitle. Default 0.

  • bottom_pad_in (float) – Extra inches reserved below the x-tick strip, e.g. for rotated tick labels or a second x-axis label line. Default 0.

  • wgap_in (float, optional) – Override the inter-column gap (inches). Default (None) uses the standard gap, which reserves room for each column’s y-labels; a caller whose panels share a y-axis (right panel hides its y-labels) can pass a smaller value to close the gap.

  • cbar_txt_in (float, optional) – Override the reserved width (inches) for the shared colorbar’s tick labels and axis label. Default (None) uses the standard reserve; pass a larger value for a long colorbar label so it stays on-canvas.

  • left_in (float, optional) – Override the reserved left-margin width (inches) for the y-tick labels plus rotated y-axis label. Default (None) uses the standard reserve (_SQ_LABEL_IN), which fits up to ~4-character tick labels; pass a larger value when the y-tick labels are wide (e.g. thousands-of-km trajectory displacements) so the axis label stays on-canvas. Applies to the standard (non per_panel_colorbar) layout. Use ytick_reserve_in() to size it from the data range.

Returns:

The figure; an (nrows, ncols) object array of panel axes; and the colorbar axes — a single shared Axes (or None) normally, or an (nrows, ncols) object array when per_panel_colorbar is set.

Return type:

tuple of (matplotlib.figure.Figure, numpy.ndarray, object)

oceanarray.plotters.primitives.square_limits(x: ndarray, y: ndarray, *, pad_frac: float = 0.05) tuple[tuple[float, float], tuple[float, float]][source]

Return (xlim, ylim) framing x, y as an equal-extent square.

The larger of the x and y data ranges is applied to both axes (each centred on its own data midpoint), so an equal-aspect plot of the data is square and neither axis is a thin strip. Non-finite values are ignored; a degenerate (zero-extent) input falls back to a unit square. Choose the limits with this helper before placing a square axes so the colorbar sizing stays exact.

Parameters:
  • x (numpy.ndarray) – Data coordinates (any shape; flattened, non-finite dropped).

  • y (numpy.ndarray) – Data coordinates (any shape; flattened, non-finite dropped).

  • pad_frac (float) – Fractional padding added to the half-extent on all sides. Default 0.05.

Returns:

((x0, x1), (y0, y1)).

Return type:

tuple of (tuple of float, tuple of float)

oceanarray.plotters.primitives.unit_colorbar(cax: Any, mappable: Any, *, unit: str = '', ticks: ndarray | None = None, ticklabels: list[str] | None = None) Any[source]

Draw mappable’s colorbar into the pre-placed cax with the unit on top.

The unit is rendered as a title above the bar (cax.set_title) rather than a rotated side label, which saves horizontal width and reads cleanly — the same convention as the cruise-map depth colorbar. cax is expected to come from square_axes_grid() so its height already matches the plotted square.

Parameters:
  • cax (matplotlib Axes) – Pre-placed colorbar axes.

  • mappable (matplotlib ScalarMappable) – The artist (LineCollection, pcolormesh, ScalarMappable, …) to map.

  • unit (str) – Unit string placed above the bar (e.g. "m s⁻¹"). Empty renders no title.

  • ticks (numpy.ndarray, optional) – Explicit colorbar tick positions.

  • ticklabels (list of str, optional) – Explicit tick labels (same length as ticks), e.g. ["start", "end"] for a fractional-time bar.

Return type:

matplotlib.colorbar.Colorbar

oceanarray.plotters.primitives.ytick_reserve_in(y: ndarray | float) float[source]

Left-margin inches to fit the widest y-tick label plus rotated y-axis label.

Sizes the left_in reserve for square_axes_grid() from the y-data range instead of a fixed shrink: the base reserve (_SQ_LABEL_IN) already fits up to ~4-character tick labels, and each additional character (a larger magnitude, or a leading minus) adds _SQ_PER_CHAR_IN. Returns the base reserve unchanged for <=4-character labels, so a small-displacement trajectory keeps the full square (no 9% shrink) while a thousands-of-km one reserves just enough.

Parameters:

y (numpy.ndarray or float) – The y values (or the maximum absolute y-limit) about to be plotted.

Returns:

Left-margin reserve in inches, always >= _SQ_LABEL_IN.

Return type:

float

helpers

Shared colormap, normalisation, and style helpers used across Tier-2 modules.

Shared helpers for the plotters package.

Provides colormap helpers and rose-diagram rendering used across multiple Tier-2 plotter modules.

Note: _fig_to_base64 stays in reports/_html_helpers.py (called only by Tier-3 wrappers in reports/_plots.py; plotters/ never serialises to base64).

oceanarray.plotters.helpers.OKABE_ITO: list[str] = ['#000000', '#E69F00', '#56B4E9', '#009E73', '#F0E442', '#0072B2', '#D55E00', '#CC79A7']

Okabe-Ito 8-colour qualitative palette — the accessibility-standard colourblind-safe set (distinguishable under protan/deutan/tritan CVD). Note the yellow (#F0E442) is pale on a white background; the linewidth tiers in distinct_line_styles() and the linestyle help keep it legible.

oceanarray.plotters.helpers.distinct_line_styles(n: int) list[tuple[str, str, float]][source]

Return n colourblind-safe (color, linestyle, linewidth) line styles.

Cycles the 8-colour OKABE_ITO palette and advances the linestyle every 8 lines (solid → dashed → dash-dot → dotted), so up to 32 lines each get a unique (color, linestyle) pair — enough to tell apart a mooring’s worth of instruments where colour alone (max 8–20 hues) collides. Linewidth increases with the linestyle group so the sparser styles (dash-dot, dotted) stay as visible as the solid ones: solid thinnest, dashed thin, dash-dot and dotted thicker. Beyond 32 lines the linestyle group is clamped (styles repeat) rather than raising.

Parameters:

n (int) – Number of line styles to return (>= 0).

Returns:

(hex_color, linestyle, linewidth) per line, in order.

Return type:

list of (str, str, float)

oceanarray.plotters.helpers.grid_despine(ax: plt.Axes, *, axis: str = 'both') None[source]

Turn the grid on and hide the top and right spines (report convention).

The report style keeps axes.grid off by default and figures opt in; when they do, the top and right spines are redundant clutter. Call this instead of ax.grid(True) so the two always travel together. Grid appearance (dotted, faint) comes from the active mplstyle, not hard-coded here, so a single style change restyles every grid.

Parameters:
  • ax (matplotlib.axes.Axes) – Axes to style.

  • axis ({"both", "x", "y"}, optional) – Which gridlines to draw (default "both"). Bar/profile plots that want one-directional gridlines pass "x" or "y" and still get the top/right spines hidden.

oceanarray.plotters.helpers.ordered_line_colors(cmap_name: str, n: int, *, max_luminance: float = 0.72) list[source]

Return n colours from cmap_name, in colormap order, skipping pale ones.

Samples the colormap on a fine grid, keeps only positions whose relative luminance is <= max_luminance (so no line washes out against white), then returns n colours evenly spaced across the usable positions. For a diverging colormap this drops the pale midpoint, leaving two saturated arcs; for a sequential one it drops the pale end. Callers assign the colours in a fixed order (e.g. deep-first) so the darkest end maps to the intended extreme.

Parameters:
  • cmap_name (str) – Matplotlib colormap name.

  • n (int) – Number of colours to return (>= 1).

  • max_luminance (float) – Rec. 709 relative-luminance ceiling in [0, 1]; positions lighter than this are excluded. Default 0.72 — low enough that the least-saturated remaining colour on a diverging map (the arc boundary either side of the excluded pale centre) is still legible on white.

Returns:

n colours (length exactly max(n, 1)).

Return type:

list of RGBA tuples

oceanarray.plotters.helpers.tukey_smooth(arr: ndarray, window_n: int) ndarray[source]

Zero-phase Tukey (cosine-tapered) smooth, NaN-gap aware.

Uses a convolution-based approach so NaN gaps do not propagate: output points near gaps are weighted only by the finite neighbours that fall within the window. Output is set to NaN where fewer than 10 % of the window weights are finite (edges of large data gaps).

Requires scipy.

Parameters:
  • arr (np.ndarray) – 1-D array, possibly containing NaNs.

  • window_n (int) – Window length in samples.

Returns:

Smoothed array, same shape as arr.

Return type:

np.ndarray

current

ADCP velocity plots: hodographs, current roses, stick plots, depth-time panels.

Tier-2 domain wrappers for current/velocity instrument plots.

These functions accept xarray Datasets and delegate rendering to Tier-1 primitives in _primitives.py. They understand oceanarray variable naming conventions but do not serialise to base64 (that happens in report/_plots.py).

Pairs with oceanarray.analysis.vector for coordinate transformations.

Public draw_* functions (migrated from report/_plots.py): - draw_instrument_rose: rose diagram grid for a single Aquadopp instrument. - draw_rose_grid: grid of current roses for instruments in a stack dataset. - draw_grid_rose: grid of current roses by pressure level for the grid report. - draw_grid_trajectory: pseudo-Lagrangian trajectory by pressure for the grid report. - draw_adcp_velocity: stacked colour panels for the ADCP per-instrument HTML page. - draw_adcp_rose: current rose panels for an ADCP per-instrument report. - draw_adcp_hodograph: two-depth hodograph for an ADCP per-instrument report. - draw_grid_hodograph: two-depth hodograph for the grid report.

oceanarray.plotters.current.draw_adcp_hodograph(nc_path: str, lp_days: float = 4.0, smooth_hours: float = 24.0, *, width_in: float = 9.0) Figure | None[source]

Two-depth hodograph for an ADCP per-instrument report; return a Figure.

Picks the bins nearest the 25th and 75th percentile of the valid range and renders a 2×2 figure: top row = far bin (75th pctile — typically shallower for upward-looking), bottom row = near bin (25th pctile). Left column = Tukey-smoothed raw; right column = eddy (LP mean removed). Returns None if east/north velocity are absent, 1-D, or too few valid bins.

Accepts both the current naming convention (east_velocity / north_velocity) and the legacy dolfyn names (u / v) so that files produced before the stage1 renaming was introduced can still be visualised. Files need to be regenerated with the current stage1 to get the canonical names.

Parameters:
  • nc_path (str) – Path to a stage-3 ADCP NetCDF file.

  • lp_days (float) – Low-pass filter cutoff in days for eddy extraction.

  • smooth_hours (float) – Tukey smoothing window in hours for the raw panel.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

Figure, or None if velocity data are absent or insufficient.

Return type:

plt.Figure or None

oceanarray.plotters.current.draw_adcp_rose(nc_path: str, *, width_in: float = 9.0) Figure | None[source]

Current rose panels for an ADCP: depth-average plus percentile-selected bins.

Selects the depth-average and up to four individual range bins at the 10th, 37th, 63rd, and 90th percentile positions of the valid bin indices (bins with at least 5 % of time steps having finite data). Returns None if east/north velocity are absent or too few samples exist.

Parameters:
  • nc_path (str) – Path to a stage-3 ADCP NetCDF file.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

Figure, or None if east/north velocity are absent or too few samples exist.

Return type:

plt.Figure or None

oceanarray.plotters.current.draw_adcp_velocity(nc_path: str, *, width_in: float = 9.0) Figure | None[source]

Stacked colour panels for the ADCP per-instrument HTML report page; return a Figure.

Reads the stage-3 NetCDF file at nc_path and produces a multi-panel time × range pcolormesh figure.

Coordinate convention: velocities are in geographic ENU (East–North–Up), after magnetic declination correction applied in stage 3. Positive east = rightward facing north; positive north = toward True North.

Direction convention (current_direction panel): direction toward which the water flows, clockwise from True North (0° = northward, 90° = eastward). This is the oceanographic convention, opposite to meteorological “direction from”. Computed as atan2(east, north) mod 360.

Panels rendered for each variable present in the file:

Diverging panels (east/north/up/error) share symmetric colormap bounds set to ±max(|2nd pctile|, |98th pctile|) of all finite ENU velocity values.

Bins flagged at or below the seabed (seabed_qc >= 3) are masked to NaN. Y-axis (range coordinate) is inverted for downward-looking instruments (pressure increases into the page); non-inverted for upward-looking.

Parameters:
  • nc_path (str) – Path to a stage-3 NetCDF file for a single ADCP instrument.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

Figure, or None if no velocity data are present or range coordinate is absent.

Return type:

plt.Figure or None

oceanarray.plotters.current.draw_grid_hodograph(ds: Dataset, smooth_hours: float = 24.0, *, width_in: float = 9.0) Figure | None[source]

Two-depth hodograph for the grid report; return a Figure.

Takes an already-loaded xr.Dataset (not a path). Picks the pressure levels nearest the 25th and 75th percentile of the valid gridded pressure range and renders a 1×2 figure (shallow / deep, each showing the smooth_hours-smoothed hodograph). Returns None if east/north velocity are absent or fewer than two valid levels exist.

Parameters:
  • ds (xr.Dataset) – Gridded mooring dataset with east_velocity and north_velocity.

  • smooth_hours (float) – Tukey smoothing window in hours.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

Figure, or None if velocity data are absent or insufficient.

Return type:

plt.Figure or None

oceanarray.plotters.current.draw_grid_rose(ds: Dataset, max_roses: int = 4, *, width_in: float = 9.0) Figure | None[source]

Grid of current roses, one per pressure level, for the grid report.

Shows up to max_roses pressure levels (at most 1/5th of valid levels, capped at 4), each labelled with its pressure (dbar). Levels with no finite ENU velocity are skipped. Returns None when east/north velocity are absent or all-NaN.

Parameters:
  • ds (xr.Dataset) – Gridded dataset with dimensions (time, pressure) containing east_velocity and north_velocity in m s⁻¹.

  • max_roses (int) – Maximum number of rose panels to draw (default 4).

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Return type:

plt.Figure or None

oceanarray.plotters.current.draw_grid_trajectory(ds: Dataset, *, width_in: float = 4.5) Figure | None[source]

Pseudo-Lagrangian current-vector integral by pressure level for the grid report.

For each pressure level, integrates east and north velocity over time using the Euler forward method to produce a cumulative displacement trajectory from the origin (0, 0). Returns None when east/north velocity are absent or all-NaN.

Parameters:
  • ds (xr.Dataset) – Gridded dataset with dimensions (time, pressure), containing east_velocity and north_velocity in m s⁻¹.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Return type:

plt.Figure or None

oceanarray.plotters.current.draw_instrument_rose(nc_path: Path, *, width_in: float = 9.0) Figure | None[source]

Rose diagram grid for a single Aquadopp instrument; return Figure or None.

Loads the stage-3 NetCDF at nc_path, builds one polar panel per available velocity QC tier (ENU magnetic, ENU good, suspect, fail), and returns the Figure. Returns None when no velocity data are found.

Parameters:
  • nc_path (Path) – Path to a stage-3 NetCDF file for a single Aquadopp instrument.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Return type:

matplotlib.figure.Figure or None

oceanarray.plotters.current.draw_rose_grid(ds: Dataset, serial_list: list, *, width_in: float = 9.0) tuple[Figure, int] | None[source]

Grid of current roses (max 4 per row) for instruments with ENU velocity data.

Parameters:
  • ds (xr.Dataset) – Stack dataset with east_velocity and north_velocity.

  • serial_list (list) – Serial numbers corresponding to the instrument axis of the velocity arrays.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

Figure and the number of rose panels rendered, or None if no velocity data.

Return type:

tuple of (plt.Figure, int) or None

oceanarray.plotters.current.plot_adcp_trajectories(ds: Dataset, u_var: str = 'east_velocity', v_var: str = 'north_velocity', instr_type_var: str = 'instrument_type', hab_var: str = 'hab', seabed_qc_var: str = 'seabed_qc', percent_good_qc_var: str = 'percent_good_qc', *, width_in: float = 4.5) Figure | None[source]

Lagrangian per-bin trajectories for ADCP data, coloured by HAB.

Each depth bin integrated by Euler-forward from the origin. Bins that are entirely below the seabed (all seabed_qc >= 3) are silently omitted. QC masking is applied before integration so bad pings are treated as zero velocity (do not accumulate spurious displacement).

Parameters:
  • ds (xr.Dataset) – Stacked mooring dataset. ADCP bins are identified by instrument_type == "ADCP".

  • u_var (str) – Eastward and northward velocity variable names (m s⁻¹).

  • v_var (str) – Eastward and northward velocity variable names (m s⁻¹).

  • instr_type_var (str) – Coordinate variable names.

  • hab_var (str) – Coordinate variable names.

  • seabed_qc_var (str) – QC variable for seabed proximity; bins with all values >= 3 are skipped.

  • percent_good_qc_var (str) – Ping-quality QC; timesteps flagged >= 3 are zeroed before integration.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

None if no ADCP bins are found.

Return type:

matplotlib.figure.Figure or None

oceanarray.plotters.current.plot_aquadopp_speed_profile(ds: Dataset, speed_var: str = 'current_speed', u_var: str = 'east_velocity', v_var: str = 'north_velocity', instr_type_var: str = 'instrument_type', serial_var: str = 'serial', hab_var: str = 'hab', *, width_in: float = 4.5) Figure | None[source]

Horizontal speed boxplots for all Aquadopps, one per instrument at its HAB.

X-axis: current speed. Y-axis: height above bottom (m). All Aquadopps appear on the same axes so the speed distribution can be compared across depths.

If speed_var is not present in ds, speed is computed from u_var and v_var as sqrt(u² + v²).

Parameters:
  • ds (xr.Dataset) – Stacked mooring dataset with shape (time, N_LEVELS).

  • speed_var (str) – Preferred speed variable name; computed from u/v if absent.

  • u_var (str) – Used to compute speed when speed_var is not present.

  • v_var (str) – Used to compute speed when speed_var is not present.

  • instr_type_var (str) – Dimension-coordinate variable names.

  • serial_var (str) – Dimension-coordinate variable names.

  • hab_var (str) – Dimension-coordinate variable names.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

None if no Aquadopp instruments are found.

Return type:

matplotlib.figure.Figure or None

Notes

Adapted from 02_aqdp_ploter.py by L. Moscatel (lmoscat), Universitat de Barcelona.

oceanarray.plotters.current.plot_hodograph(ds: Dataset, u_var: str = 'east_velocity', v_var: str = 'north_velocity', lp_days: float = 4.0, smooth_hours: float = 3.0, *, width_in: float = 9.0) Figure[source]

Two-panel hodograph: Tukey-smoothed raw and eddy-only, coloured by time.

Panel 1: smooth_hours-Tukey-smoothed raw east-vs-north velocity. Panel 2: eddy component — raw minus a lp_days-day rolling-mean low-pass, then smooth_hours-Tukey smoothed to suppress instrument noise.

Points are coloured by fractional time through the record (0 = start, 1 = end) using a discrete viridis colorbar so the temporal evolution of rotary motion can be followed by eye. The figure title shows the instrument id from ds.attrs["id"] when available.

If u_var or v_var are absent, a placeholder figure is returned with an explanatory message — the caller always receives a renderable image.

Parameters:
  • ds (xr.Dataset) – Per-instrument dataset containing east and north velocity variables.

  • u_var (str) – Eastward velocity variable name (m s⁻¹).

  • v_var (str) – Northward velocity variable name (m s⁻¹).

  • lp_days (float) – Low-pass window length in days for the eddy-component panel.

  • smooth_hours (float) – Tukey smoothing window in hours applied to both panels.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Return type:

matplotlib.figure.Figure

oceanarray.plotters.current.plot_multi_aquadopp_trajectories(ds: Dataset, u_var: str = 'east_velocity', v_var: str = 'north_velocity', temp_var: str = 'temperature', instr_type_var: str = 'instrument_type', serial_var: str = 'serial', title: str = '', *, width_in: float = 4.5) Figure | None[source]

Multi-instrument Lagrangian trajectories for all Aquadopps, coloured by temperature.

Each trajectory starts at the origin and is built by integrating the east/north velocity over time (Euler forward; NaN velocities set to zero). All trajectories share a single temperature colour scale so instruments can be compared directly. End points are annotated with the instrument serial.

Parameters:
  • ds (xr.Dataset) – Stacked mooring dataset with shape (time, N_LEVELS). Must contain instr_type_var, serial_var, u_var, v_var.

  • u_var (str) – Eastward and northward velocity variables (m s⁻¹).

  • v_var (str) – Eastward and northward velocity variables (m s⁻¹).

  • temp_var (str) – Temperature variable for colouring; omitted if not present in ds.

  • instr_type_var (str) – Dimension-coordinate variable names identifying each instrument.

  • serial_var (str) – Dimension-coordinate variable names identifying each instrument.

  • title (str) – Optional figure title; falls back to the dataset id attribute.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

None if no Aquadopp instruments are found in the dataset.

Return type:

matplotlib.figure.Figure or None

Notes

Adapted from 02_aqdp_ploter.py by L. Moscatel (lmoscat), Universitat de Barcelona.

oceanarray.plotters.current.plot_speed_boxplot(ds: Dataset, speed_var: str = 'current_speed', *, width_in: float = 3.0) object[source]

Boxplot of current speed with printed percentile statistics.

Prints the 5th, 25th, 50th, 75th and 95th percentiles to stdout. NaN values are excluded before plotting.

Parameters:
  • ds (xr.Dataset) – Dataset containing the speed variable.

  • speed_var (str) – Name of the current speed variable.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Return type:

matplotlib.figure.Figure

oceanarray.plotters.current.plot_temperature_trajectory(ds: Dataset, u_var: str = 'east_velocity', v_var: str = 'north_velocity', temp_var: str = 'temperature', title: str = '') object[source]

Lagrangian particle trajectory coloured per-segment by temperature.

Integrates u_var and v_var (east/north velocity) over time using a forward Euler scheme. NaN velocity values are treated as zero so the trajectory is not interrupted.

The displacement is in metres; for long deployments the trajectory will drift far from origin and should be interpreted as a pseudo-Lagrangian tracer, not a real particle path.

Note: the velocity integration step will move to oceanarray.tools post-OdB so that it can be reused independently of plotting.

Parameters:
  • ds (xr.Dataset) – Dataset containing east/north velocity and temperature variables.

  • u_var (str) – Name of the eastward velocity variable (m s⁻¹).

  • v_var (str) – Name of the northward velocity variable (m s⁻¹).

  • temp_var (str) – Name of the temperature variable used for colouring.

  • title (str) – Optional figure title; defaults to the dataset id attribute.

Return type:

matplotlib.figure.Figure

timeseries

Grid and mooring time-series figures: T/S/density pcolormesh, velocity panels, N² sections.

Tier-2 domain wrappers for time-series and gridded-section plots.

This module provides the draw_* functions used by the HTML report pipeline to render instrument time-series panels and mooring-grid section plots.

Pairs with oceanarray.analysis.temporal for time-series analysis.

Grid section panels (data on a pressure × time grid):

Instrument time-series panels:

Post-OdB: migrate the following from plotter.py and report/_plots.py:

plot_microcat_raw, plot_aquadopp_raw, plot_mooring_timeseries (the three kept plotter.py functions; §11), plot_aquadopp_quick, build_instrument_fig (was _build_fig_from_ds), plot_instrument_windows.

oceanarray.plotters.timeseries.draw_analog_timeseries(nc_path: Path, analog_vars: List[str], *, width_in: float = 9.0) plt.Figure | None[source]

Full-record time series for analog channel variables, one panel per variable.

Returns None when the dataset lacks a time dimension.

Parameters:
  • nc_path (Path) – Path to the stage-3 or stack NetCDF file.

  • analog_vars (list of str) – Variable names to plot (caller must ensure the list is non-empty).

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Return type:

plt.Figure or None

oceanarray.plotters.timeseries.draw_grid_fig(da: xr.DataArray, title: str, units: str, cmap: str, style: str = 'pcolormesh', contour_levels: list | None = None, symmetric: bool = False, vmin: float | None = None, vmax: float | None = None, *, width_in: float = 9.0) plt.Figure[source]

Render a grid figure from da (dims time × pressure); return a Figure.

Parameters:
  • da (xr.DataArray) – Data array with time and pressure dimensions.

  • title (str) – Panel title and colorbar label prefix.

  • units (str) – Unit string appended to the colorbar label.

  • cmap (str) – Matplotlib colormap name.

  • style (str) – "pcolormesh" (default) or "contourf".

  • contour_levels (list, optional) – If given, overlay black contour lines at these levels.

  • symmetric (bool) – If True, force a symmetric (diverging) color range.

  • vmin (float, optional) – Override the automatic percentile-based color limits.

  • vmax (float, optional) – Override the automatic percentile-based color limits.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Return type:

plt.Figure

oceanarray.plotters.timeseries.draw_grid_hydro(ds: xr.Dataset, var_bounds: dict | None = None, *, width_in: float = 9.0) plt.Figure | None[source]

Stacked temperature / salinity pcolormesh panels for the grid report; return a Figure.

Both panels have pressure (dbar) on the Y-axis (inverted, surface at top). Colorbar bounds are clipped to the COLORBAR_PLOWCOLORBAR_PHIGH percentiles of the data to reduce the influence of outliers on color scaling.

Temperature (°C, colormap RdYlBu_r): sea water temperature on the gridded pressure–time grid.

Salinity (PSU, colormap YlGnBu_r): taken from the salinity variable if present. If absent but both conductivity (mS cm⁻¹) and temperature (°C) are present, Practical Salinity is derived via gsw.SP_from_C (PSS-78). Note: this is Practical Salinity, not Absolute Salinity (g kg⁻¹); the latter would require pressure and longitude via gsw.SA_from_SP.

Panels are rendered only for variables that are present in ds or derivable.

Parameters:
  • ds (xr.Dataset) – Gridded mooring dataset with dimensions (time, pressure).

  • var_bounds (dict, optional) – Pre-computed colorbar limits keyed by variable name, e.g. {"t_lim": (vmin, vmax), "s_lim": (vmin, vmax), "o2_lim": (vmin, vmax)}. When a key is present its limits are used instead of computing from the data. Intended for passing the T-S diagram axis limits so both figures share scales.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

Figure, or None if no hydrographic data are present.

Return type:

plt.Figure or None

oceanarray.plotters.timeseries.draw_grid_n2(ds: xr.Dataset, lat: float = 0.0, *, width_in: float = 9.0) plt.Figure | None[source]

Compute and plot buoyancy frequency squared N² on the pressure-time grid.

Returns None when temperature or salinity are absent.

oceanarray.plotters.timeseries.draw_grid_sigma(ds: xr.Dataset, *, width_in: float = 9.0) plt.Figure | None[source]

Stacked sigma0 pcolormesh panel(s) for the stratification section.

Returns None when no sigma variables are present.

oceanarray.plotters.timeseries.draw_grid_timeseries(ds: xr.Dataset, *, width_in: float = 9.0) plt.Figure | None[source]

Velocity time series at the depth of maximum time-mean current speed.

Two stacked panels (shared time axis):

  • Speed (black, m s⁻¹): sqrt(east² + north²); always ≥ 0.

  • East and North velocity (same axes, m s⁻¹): east in Okabe-Ito blue (#0072B2), north in Okabe-Ito orange (#E69F00). Both signed components are plotted together so the relationship between along- and cross-stream flow is immediately visible.

The target pressure level is chosen automatically as the level with the highest time-mean current speed and at least 70 % non-NaN coverage. The selected pressure is annotated in the figure title.

Parameters:
  • ds (xr.Dataset) – Gridded dataset with dimensions (time, pressure) containing at minimum east_velocity and north_velocity in m s⁻¹.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

Figure, or None if horizontal velocity data are absent or all-NaN.

Return type:

plt.Figure or None

oceanarray.plotters.timeseries.draw_grid_velocity_stacked(ds: xr.Dataset, *, width_in: float = 9.0) plt.Figure | None[source]

Stacked east / north / up velocity pcolormesh panels for the grid report.

All three panels share the time axis and show pressure (dbar) on the Y-axis (inverted, surface at top). East and north share symmetric diverging bounds; up uses its own symmetric bounds (open-ocean vertical velocities are typically 1–2 cm s⁻¹ vs. tens of cm s⁻¹ horizontal). Returns None when no velocity variables are present.

Parameters:
  • ds (xr.Dataset) – Gridded dataset with dimensions (time, pressure).

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Return type:

plt.Figure or None

diagnostic

Diagnostic plots: boxplots, clock-alignment checks, deployment-boundary windows.

Tier-2 domain wrappers for diagnostic plots (T-S, histograms, spectra, QC).

Knockdown plots

  • plot_knockdown_pressure() — IQR of measured pressure vs. nominal design depth, equal aspect ratio with 1:1 reference line.

  • plot_knockdown_hab() — IQR of measured pressure vs. nominal HAB, equal aspect ratio with expected-pressure reference line.

  • plot_knockdown_anomaly() — IQR of pressure anomaly (measured minus nominal) per instrument, colour-coded by knockdown severity.

  • plot_knockdown_displacement() — scatter and 2-D heatmap of estimated horizontal displacement vs. measured pressure.

Clock-alignment check

Deployment window figures (moved from report._plots)

Post-OdB: migrate the following from report/_plots.py:

plot_ts_diagram (was _make_ts_diagram), plot_stack_ts_diagram, plot_grid_ts_diagram, plot_data_histogram (was _make_data_histogram), plot_spectrum (was _make_spectrum_fig_b64).

Tier-1 primitives: plot_vector_heatmap (for T-S, U-V, any pair), plot_spectrum (any 1D time series), plot_polar_histogram (current rose).

oceanarray.plotters.diagnostic.draw_data_histogram(nc_path: Path, *, width_in: float = 9.0) plt.Figure | None[source]

Histogram of data values for each main variable; return a Figure.

Each panel shows grey bars (all finite data) and blue bars (kept, not bad/missing), with QC range threshold lines overlaid.

Parameters:
  • nc_path (Path) – Path to a stage-3 NetCDF file.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

Figure, or None if no plottable variables are found.

Return type:

plt.Figure or None

oceanarray.plotters.diagnostic.draw_velocity_iqr_profile(ds: xr.Dataset, *, width_in: float = 9.0) plt.Figure | None[source]

Percentile-profile figure for gridded ADCP velocity data; return a Figure.

Three side-by-side panels, all with pressure (dbar) on the Y-axis (inverted, surface at top). All velocity units are m s⁻¹.

Left — current speed (always ≥ 0):

Shaded percentile profile: outer band p2.5–p97.5 (95% range of the distribution); inner band IQR p25–p75; median p50. Wide IQR at a given depth indicates high velocity variability (e.g. an eddy-active layer or a strong tidal signal); narrow IQR with a large median indicates a persistent mean flow. current_speed is computed from sqrt(east² + north²) if not already present in the dataset.

Middle — east and north velocity (can be negative):

Median and IQR (p25/p50/p75) for each component. East velocity in Okabe-Ito blue (#0072B2); north velocity in Okabe-Ito orange (#E69F00); both colours are distinguishable for common colour-vision deficiencies. Positive east = rightward facing north (geographic ENU after declination correction); positive north = toward True North. A median near zero with large IQR suggests rotary motion (e.g. tides or near-inertial oscillations); a non-zero median indicates a mean current.

Right — count (dimensionless):

Number of non-NaN time steps at each pressure level. Use this panel to assess data coverage before interpreting velocity statistics: pressure levels with very few records produce unreliable percentile estimates.

Parameters:
  • ds (xr.Dataset) – Gridded dataset with dimensions (time, pressure) containing at minimum one of current_speed, east_velocity, or north_velocity in m s⁻¹.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

Figure, or None if no velocity data are present.

Return type:

plt.Figure or None

oceanarray.plotters.diagnostic.draw_windows(nc_path: Path, instr_type: str, hours: int = 6, show_qc: bool = True, vlines: list | None = None, stage1_nc: Path | None = None, panels: list | None = None, *, width_in: float = 9.0) plt.Figure | None[source]

Combined start + end window figure: (nrows × 2) — left = first N h, right = last N h.

Parameters:
  • nc_path (Path) – Path to the processed (stage2 or stage3) NetCDF file.

  • instr_type (str) – Instrument type string (used in the figure title).

  • hours (int) – Width of each window in hours (default 6).

  • show_qc (bool) – Overlay QC flag markers on the data.

  • vlines (list of (time_val, color, label), optional) – Vertical marker lines to draw on both panels. time_val may be a numpy.datetime64, an ISO-8601 string, or a pandas.Timestamp. Lines are only drawn when they fall inside the plotted window. Labels appear as small rotated text at the top of the first row only to avoid excessive clutter.

  • stage1_nc (Path, optional) – Path to the stage1 NC file. When provided, the raw stage1 data is plotted as a light-grey background trace in each window panel so the full pre/post-deployment record is visible (bench → deployment in the left panel; deployment → recovery in the right panel). The x-axis limits are taken from the stage1 window extent so that the recovery transition appears even if the stage2/3 YAML trim cut it off. The y-axis limits are taken from the primary (stage2/3) data only so that bench-pressure outliers (p ≈ 0) do not squish the deployment-depth view.

  • panels (list, optional) – Subset of _instrument_panels tuples to draw. When given, only these rows are rendered (used to paginate a tall window figure across several images); otherwise every panel for the instrument is drawn on one figure.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

Figure, or None if the dataset is too short to show windows.

Return type:

plt.Figure or None

oceanarray.plotters.diagnostic.plot_clock_offset_check(nc_paths: Dict[str, Path], deploy_dt: datetime | None, recover_dt: datetime | None, window_minutes: int = 30, *, width_in: float = 9.0) matplotlib.figure.Figure | None[source]

Overlaid, per-instrument normalised temperature around deploy and recover.

Plots a ±window_minutes window centred on deployment and on recovery for every instrument with a temperature variable, so clock alignment between instruments can be assessed visually. If an instrument’s clock is offset the temperature signal appears shifted in time relative to the others.

Each instrument’s trace is standardised over the plotted window (subtract the window mean, divide by the window standard deviation) so instruments with different absolute temperatures and amplitudes overlay on a common std y-axis and their timing can be compared directly.

Two sub-panels are produced side by side:

  • Left: deploy_dt ± window_minutes

  • Right: recover_dt ± window_minutes

When deploy_dt or recover_dt is None, only the available window is produced. A shared legend below both panels lists all instruments. An instrument with zero variance in a window (flat/constant) is skipped for that window (no timing information, and normalisation is undefined).

Parameters:
  • nc_paths (dict of {serial: Path}) – Paths to stage-2 or stage-3 NetCDF files keyed by serial number. Only instruments with temperature data are included in the plot.

  • deploy_dt (datetime or None) – Deployment time (UTC).

  • recover_dt (datetime or None) – Recovery time (UTC).

  • window_minutes (int) – Duration of each zoom window in minutes.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

None when fewer than two instruments have temperature data, or if any other error prevents plotting.

Return type:

matplotlib.figure.Figure or None

oceanarray.plotters.diagnostic.plot_knockdown_anomaly(ds: xr.Dataset, *, width_in: float = 4.5) matplotlib.figure.Figure | None[source]

IQR of pressure anomaly (measured − nominal) per instrument.

Each non-ADCP instrument is shown as a horizontal box-and-whisker at its nominal pressure on the y-axis; the box spans the IQR of the pressure anomaly distribution (actual − nominal, positive = knocked down deeper).

A vertical reference line at x = 0 marks zero knockdown. Box colour indicates the magnitude of the median knockdown:

Interpolated pressure (QC flag 8) is excluded. Rendered at half-width in the stack report.

Parameters:
  • ds (xr.Dataset) – Stack dataset; same requirements as plot_knockdown_pressure().

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Return type:

matplotlib.figure.Figure or None

oceanarray.plotters.diagnostic.plot_knockdown_displacement(ds: xr.Dataset, *, width_in: float = 9.0) matplotlib.figure.Figure | None[source]

Scatter and heatmap of estimated horizontal displacement vs. measured pressure.

For each non-ADCP instrument the horizontal displacement is estimated at every time step as:

x_horiz = sqrt(max(0, hab_nom² − hab_meas²))

where hab_nom is the nominal height-above-bottom (m) and hab_meas = waterdepth pressure is the measured HAB.

Left panel — scatter of (x_horiz, measured_pressure) per instrument, coloured by serial number. y-axis is measured pressure (dbar, increasing downward); x-axis is horizontal displacement (m). Square axes (equal aspect, adjustable data limits).

Right panel — 2-D count heatmap of the same points across all instruments combined. Discrete colorbar (up to 15 levels).

Parameters:
  • ds (xr.Dataset) – Stack dataset; same requirements as plot_knockdown_pressure().

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Return type:

matplotlib.figure.Figure or None

oceanarray.plotters.diagnostic.plot_knockdown_hab(ds: xr.Dataset, *, width_in: float = 4.5) matplotlib.figure.Figure | None[source]

IQR of measured pressure vs. nominal HAB, equal aspect ratio.

Companion to plot_knockdown_pressure(). The y-axis is identical (measured pressure, dbar, increasing downward); only the x-axis changes from nominal pressure to nominal height-above-bottom (m).

Each non-ADCP instrument is shown as a vertical box-and-whisker positioned at its nominal HAB on the x-axis; the box spans the IQR of the measured pressure distribution. The dashed reference line shows the expected pressure for each HAB (pressure = waterdepth hab). Boxes below the line were pulled deeper than their design height by current drag.

With 1 dbar ≈ 1 m, equal aspect keeps the reference line near 45° and the departure from it is directly related to horizontal displacement.

Parameters:
  • ds (xr.Dataset) – Stack dataset with dimensions (time, N_LEVELS) containing pressure, hab, serial, instrument_type, and optionally pressure_qc. The waterdepth global attribute must be present and non-zero.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Return type:

matplotlib.figure.Figure or None

oceanarray.plotters.diagnostic.plot_knockdown_pressure(ds: xr.Dataset) matplotlib.figure.Figure | None[source]

IQR of measured pressure vs. nominal design depth, equal aspect ratio.

Each non-ADCP instrument is shown as a vertical box-and-whisker positioned at its nominal pressure on the x-axis; the box spans the IQR of the actual measured pressure distribution on the y-axis (increasing downward).

The axes have equal aspect (ax.set_aspect("equal")) and both run from 0 to the maximum value, so the 1:1 reference line appears at 45°. Instruments on the diagonal are at their design depth; boxes that drop below the diagonal experienced knockdown (measured deeper than nominal).

Boxes are coloured blue; interpolated pressure (QC flag 8) is excluded. Rendered at half-width in the stack report.

Parameters:

ds (xr.Dataset) – Stack dataset with dimensions (time, N_LEVELS) containing pressure, hab, serial, instrument_type, and optionally pressure_qc. The waterdepth global attribute must be present and non-zero.

Return type:

matplotlib.figure.Figure or None

hydrography

T-S diagrams and isopycnal overlay figures.

Tier-2 domain wrappers for hydrographic section and isopycnal plots.

hydrography.py contains:
  • draw_isopycnal_ts_fig: isopycnal height-above-seabed time series.

  • draw_isopycnal_coverage: three-panel isopycnal diagnostic.

  • draw_overflow_temperature_fig: temperature time series at ~100 m above seabed.

Pairs with oceanarray.analysis.hydrographic for density computations.

oceanarray.plotters.hydrography.draw_isopycnal_coverage(ds: xr.Dataset, *, width_in: float = 9.0) plt.Figure | None[source]

Three-panel isopycnal diagnostic; return a Figure.

Panel 0 — Distribution: horizontal histogram of all gridded σ₀ values (all time steps × all pressure levels), binned at 0.1 kg m⁻³. Shows how the water column is distributed in density space.

Panel 1 — Coverage: for each σ₀ value at 0.1 kg m⁻³ spacing, the percentage of valid time steps during which the target surface lies within the measured column (min(sigma0_column) target max(sigma0_column)). Colour-coded: green ≥ 80 %, amber 50–80 %, red < 50 %. Dashed reference at 80 %.

Panel 2 — Depth distribution: for each target surface, the median height above seabed (or pressure when waterdepth is unavailable), with the IQR (25th–75th percentile) as a thick bar and the 5th–95th percentile as a thin whisker.

The shared y-axis (σ₀) is clipped to the 2.5th–99.99th percentile of the distribution — this removes rare light-water outliers from the top while retaining all of the dense water at the bottom. Currently selected params.SIGMA_GRID targets are marked with orange diamonds (panel 1) and dotted guide lines (panel 2).

Parameters:
  • ds – Gridded mooring xr.Dataset containing a variable whose name starts with "sigma" and has pressure and time dimensions.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

Figure, or None if required sigma data are absent.

Return type:

plt.Figure or None

oceanarray.plotters.hydrography.draw_isopycnal_ts_fig(ds_iso: xr.Dataset, *, width_in: float = 9.0) plt.Figure | None[source]

Isopycnal height-above-seabed time series; return a Figure.

Plots a 1-hour running median of each σ₀ surface’s height above seabed. NaN gaps break the line naturally (pandas rolling preserves NaN boundaries). Colormap: Blues — light blue = lower density (shallower), dark = denser (deeper).

Parameters:
  • ds_iso – Output of isopycnal_dataset() — must contain isopycnal_height (sigma0_level, time) and the sigma0_level coordinate.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

Figure, or None if required data are absent.

Return type:

plt.Figure or None

oceanarray.plotters.hydrography.draw_overflow_temperature_fig(ds: xr.Dataset, *, width_in: float = 9.0) plt.Figure | None[source]

Temperature time series at ~100 m above the seabed; return a Figure.

Selects the grid pressure level nearest to waterdepth - 100 dbar and plots a 1-hour running median temperature time series. Returns None if waterdepth is missing, temperature is absent, or all values are NaN.

Parameters:
  • ds – Gridded mooring xr.Dataset. Must have a waterdepth global attribute (metres) and a temperature variable with pressure and time dimensions.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

Figure, or None if required data are absent.

Return type:

plt.Figure or None

spectrum

Power spectra and rotary spectrum figures.

Tier-2 domain wrappers for spectral diagnostics.

spectrum.py contains:
  • wavelet_panel: Tier-1 primitive for rendering a single wavelet scalogram.

  • draw_spectrum: Two-panel Welch PSD of gridded temperature (migrated from report/_plots.py).

  • draw_wavelet: Continuous wavelet transform scalogram for gridded temperature (migrated from report/_plots.py).

  • draw_grid_rotary_spectrum: Two-panel rotary velocity spectrum (migrated from report/_plots.py).

Pairs with oceanarray.analysis.spectral for spectral computations.

Post-OdB remaining migrations from report/_plots.py:

plot_grid_fig (was _make_grid_fig_b64), plot_grid_n2 (was _make_grid_n2_b64).

oceanarray.plotters.spectrum.draw_grid_rotary_spectrum(ds: xr.Dataset, lat: float = 0.0, *, width_in: float = 9.0) plt.Figure | None[source]

Two-panel rotary velocity spectrum for the grid report; return a Figure.

Left panel: CW (solid, reds) and CCW (dashed, blues) power spectra on the same axes, one line per selected pressure level. Right panel: rotary coefficient r = (CCW - CW) / (CCW + CW) on a linear [-1, 1] scale. Welch PSD with Hann window, 14-day segments, 50% overlap.

Level selection: min(4, max(1, n_valid_levels // 5)) evenly-spaced levels from those with >= 5% finite data in both east and north velocity.

Parameters:
  • ds (xr.Dataset) – Gridded dataset with east_velocity and north_velocity on (time, pressure) dimensions.

  • lat (float) – Mooring latitude (degrees, positive north) used for the inertial period marker.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

Figure, or None if insufficient velocity data.

Return type:

plt.Figure or None

oceanarray.plotters.spectrum.draw_spectrum(da_temp: xr.DataArray, dt_seconds: float, lat: float = 0.0, hf_segment_days: float = 1.0, hf_x_max_days: float = 3.0, *, width_in: float = 9.0) plt.Figure | None[source]

Two-panel Welch PSD of gridded temperature, one line per depth level.

Left panel: low-frequency overview using 14-day Hann windows. Right panel: high-frequency zoom using hf_segment_days Hann windows, giving more (shorter) windows and a smoother estimate at tidal and inertial frequencies.

Parameters:
  • da_temp – Gridded temperature DataArray with dimensions (pressure, time).

  • dt_seconds – Uniform time step of the grid in seconds.

  • lat – Mooring latitude in decimal degrees; used to compute the inertial frequency marker. Pass 0 to omit.

  • hf_segment_days – Window length for the HF panel in days. Default 2 days (~180 windows per year, ~7x more than the LF panel). Reduce to focus on higher frequencies, e.g. hf_segment_days=1/24 for 1-hour windows on sub-hourly data.

  • hf_x_max_days – Upper x-axis limit (longest period shown) for the HF panel in days. When <= 3 the HF x-axis is displayed in hours; otherwise in days.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Notes

The LF panel uses gap-filled interpolation before calling welch_psd (current behaviour). The HF panel uses welch_psd_gapaware, which operates only on contiguous finite runs and skips any window that straddles a gap – avoiding the low-pass bias that linear interpolation introduces at high frequencies.

To switch the LF panel to gap-aware as well, replace the two lines in the computation loop that read:

col_filled = col.copy()
if not good.all():
    col_filled = np.interp(...)
f, p = welch_psd(col_filled, dt_days, segment_length_lf)

with the single line:

f, p, _ = welch_psd_gapaware(col, dt_days, segment_length_lf)
oceanarray.plotters.spectrum.draw_wavelet(da_temp: xr.DataArray, dt_seconds: float, wavelet: str = 'morlet', *, width_in: float = 9.0) plt.Figure | None[source]

Continuous wavelet transform scalogram for gridded temperature; return a Figure.

Produces three stacked wavelet + time-series panel pairs. Depth levels are selected from 100-dbar multiples that have at least 75 % data coverage (sparse near-bottom levels are excluded). Levels within 100 dbar of the shallowest valid level are also excluded to avoid gappy near-surface data. From the remaining candidates the deepest, an upper-middle, and a lower-middle level are chosen (biased toward the deeper water column).

Each wavelet panel shows log10(Morlet power) as a filled contour plot; the gap-aware cone of influence (COI) is hatched – this covers both the record edges and the COI wings that spread out from every data gap. The y-axis is trimmed per-panel to the longest period that is reliable at any time step, so gappier levels automatically get a tighter period range. A 95 % significance contour is drawn in black (falls back to white-noise background if AR(1) estimation fails). Below each wavelet panel a short temperature time series for that depth level shares the x-axis.

Parameters:
  • da_temp – Gridded temperature DataArray with dimensions (pressure, time).

  • dt_seconds – Sample interval in seconds.

  • wavelet"morlet" (default, Morlet omega_0=6) or "mexican_hat".

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

Figure, or None if the dataset has insufficient temperature data.

Return type:

plt.Figure or None

oceanarray.plotters.spectrum.wavelet_panel(ax: matplotlib.axes.Axes, times: np.ndarray, periods: np.ndarray, power: np.ndarray, coi: np.ndarray, signif: np.ndarray | None = None, title: str = '') mcolors.ScalarMappable[source]

Draw one wavelet scalogram panel onto ax.

Renders log10(power) as a filled contour plot with period on a log y-axis (inverted so short periods are at the top), time on x.

The COI region (period > coi[t]) is hatched with diagonal lines. Callers should pass effective_coi from compute_cwt rather than the raw pycwt COI — the effective COI already incorporates gap-edge wings so that one hatching call covers record edges, gap columns, and the surrounding unreliable region.

The y-axis is trimmed to coi.max() so entirely-hatched long-period rows are not shown; gappier records automatically get a tighter period range.

An optional 95 % significance contour is drawn in black.

Parameters:
  • ax – Target axes object.

  • times – 1-D array of time values (any type accepted by matplotlib, e.g. numpy datetime64 or float index).

  • periods – 1-D array of wavelet periods in days (length n_scales).

  • power – 2-D array of real wavelet power, shape (n_scales, n_time).

  • coi – 1-D array of COI periods in days, length n_time. Pass effective_coi from compute_cwt to include gap-edge wings.

  • signif – Optional 1-D significance threshold array, length n_scales. Where power[i, t] > signif[i] the transform is significant at the chosen confidence level. If None, no significance contour is drawn.

  • title – Axes title string (e.g. depth label).

Returns:

Mappable suitable for passing to fig.colorbar().

Return type:

matplotlib.cm.ScalarMappable

ts

T-S scatter and density-coloured scatter figures.

T-S diagram and thermohaline structure plot functions.

oceanarray.plotters.ts.draw_grid_ts_diagram(ds: xr.Dataset, n_bins: int = 60, *, width_in: float = 9.0) tuple[plt.Figure, dict] | None[source]

T-S diagram for gridded mooring data; return a (Figure, bounds_dict) tuple.

Left panel: 2-D count heatmap (log₁₀ samples per T-S bin). Right panel (when oxygen_saturation_pct is present): median O₂ saturation per T-S bin, computed with scipy.stats.binned_statistic_2d. Bins with fewer than 5 samples are masked white.

This lets you see which water masses (T-S combinations) are oxygen-rich vs oxygen-depleted at this mooring — a compact water-mass characterisation.

Parameters:
  • ds (xr.Dataset) – Gridded dataset with at least temperature and salinity variables.

  • n_bins (int) – Number of bins per axis for the 2-D histogram.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

bounds_dict contains the axis/colorbar limits computed from the data so the hydro pcolormesh panels can share the same scales:

  • "t_lim" : (vmin, vmax) for temperature

  • "s_lim" : (vmin, vmax) for salinity

  • "o2_lim" : (vmin, vmax) for oxygen_saturation_pct, or None

Return type:

tuple of (plt.Figure, bounds_dict) or None

oceanarray.plotters.ts.draw_stack_ts_diagram(ds: xr.Dataset, *, width_in: float = 9.0) plt.Figure | None[source]

T-S diagram for a stacked dataset; return a Figure.

Scatter-by-pressure, count heatmap, and (when present) scatter-by-AOU. Panels are arranged in a single row. The AOU panel is included when apparent_oxygen_utilization is present in ds (written by stage3 for instruments with dissolved oxygen data).

QC masking: bad (flag 4) and missing (flag 9) excluded; interpolated pressure (flag 8) is kept as usable colour data.

Parameters:
  • ds (xr.Dataset) – Stacked mooring dataset containing temperature and salinity.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

Figure, or None if temperature or salinity are absent.

Return type:

plt.Figure or None

oceanarray.plotters.ts.draw_ts_diagram(nc_path: Path, *, width_in: float = 9.0) plt.Figure | None[source]

T-S diagram from a NetCDF path; return a Figure.

Scatter by pressure, 2-D count heatmap, and (when present) scatter by O2 saturation.

Parameters:
  • nc_path (Path) – Path to a stage-3 NetCDF file.

  • width_in (float, optional) – Figure width in inches – the display-slot width the report builder resolves; standalone callers get the full content width.

Returns:

Figure, or None if temperature or salinity are absent.

Return type:

plt.Figure or None

animation

Animated GIF output via matplotlib.animation.

Animated plot functions for the oceanarray plotters package.

This module is intentionally separate from the static (Tier-1/2/3) pipeline. It relies on matplotlib.animation and requires the pillow package as a GIF writer. scipy is required for the Tukey smoothing window.

Future interactive equivalents (plotly, bokeh) should live in a companion interactive.py module so the animation and interactive layers stay clearly distinct.

Tier classification: Tier-2 domain functions (xr.Dataset-in / file-out).

oceanarray.plotters.animation.animate_hodograph(ds: Dataset, output_path: str | Path, u_var: str = 'east_velocity', v_var: str = 'north_velocity', lp_days: float = 4.0, smooth_hours: float = 3.0, frame_hours: float = 6.0, fps: int = 20, dpi: int = 100) Path | None[source]

Write an animated GIF of the smoothed hodograph drawing itself through time.

Each frame advances by frame_hours of real deployment time and reveals the smoothed velocity trajectory accumulated up to that moment, so the viewer watches rotary/eddy structure build up through the record.

Signal processing (applied once to the full record before animating):

  • Raw panelsmooth_hours-Tukey filter of the raw east/north signal.

  • Eddy panellp_days-day rolling-mean removed, then smooth_hours- Tukey filter to suppress instrument noise.

A timestamp header on each frame shows the real date and time.

Requires pillow (pip install pillow) as the GIF writer and scipy for the Tukey window.

Parameters:
  • ds (xr.Dataset) – Per-instrument dataset with east/north velocity variables and a time dimension.

  • output_path (str or Path) – Destination file path; .gif extension recommended.

  • u_var (str) – Eastward velocity variable name (m s⁻¹).

  • v_var (str) – Northward velocity variable name (m s⁻¹).

  • lp_days (float) – Low-pass window length in days for the eddy-component panel.

  • smooth_hours (float) – Tukey smoothing window in hours applied to both panels.

  • frame_hours (float) – Time step between frames in hours (default 6 h → one frame per quarter-day of deployment).

  • fps (int) – Frames per second in the output GIF.

  • dpi (int) – Resolution of each frame in dots per inch.

Returns:

  • Path – The resolved output path on success.

  • None – If u_var or v_var are absent from ds, or if the pillow writer is unavailable.