Python API
The primary public entry point is ctdcast.report().
Main entry point
- ctdcast.reports._index.report(nc_dir: Path, out_dir: Path, *, profiles_path: Path | None = None, section_yaml: Path | None = None, ladcp_dir: Path | None = None, ladcp_profiles_path: Path | None = None, ladcp_pattern: str | None = None, ship_track_nc: Path | None = None, generate: dict[str, bool] | None = None, force: bool = False, skip_existing: bool = False, section_style: str = 'pcolormesh', timeseries_style: str = 'pcolormesh', vmin_override: dict[str, float] | None = None, vmax_override: dict[str, float] | None = None, cruise_info: dict[str, Any] | None = None, config: ReportConfig | None = None, cast_filter: int | list[int] | None = None, sal_range: tuple[float, float] | None = None, trim_soak: bool = False, dbar_step: int = 1, drop_stub: bool = False) int[source]
Generate the full ctdcast HTML report suite.
- Parameters:
nc_dir – Directory containing per-cast
.ncfiles.out_dir – Root output directory.
profiles_path – Path to compiled
profiles.nc. Required for section and time series pages.section_yaml – Path to the sections/timeseries YAML file (
ctd_sections.yaml).ladcp_dir – Directory containing processed LADCP
.matfiles namedNNN.matorNNNb.mat(letter-suffix variants supported).ladcp_profiles_path – Path to compiled
ladcp_profiles.nc. When present, the summary page gains a Velocity section (cruise-wide U/V overview panels) and a data-inventory pill for the file.ladcp_pattern – Optional filename glob for non-standard LADCP naming, e.g.
"msm_142_1_*.mat". The*is replaced with the zero-padded cast number. Seefind_ladcp_file().ship_track_nc – Path to a ship-track netCDF for the Leaflet map background line.
generate – Dict of booleans controlling which page types to build. Keys:
"stations","sections","timeseries","index","map". Missing keys default toTrue.force – Regenerate all pages regardless of file modification times.
skip_existing – If True, skip any page whose output HTML already exists, regardless of whether the source files are newer. Use this to fill in only missing pages without touching anything already generated. Takes precedence over the mtime check but is overridden by
force.section_style –
"pcolormesh"or"contourf"for section figures.timeseries_style –
"pcolormesh"or"contourf"for time series figures.vmin_override – Per-variable colormap limit overrides (e.g.
{"SA": 34.5}).vmax_override – Per-variable colormap limit overrides (e.g.
{"SA": 34.5}).cruise_info – Cruise metadata dict (from the
cruise_info:block inconfig.yaml).config – Frozen display configuration (GEBCO path, figsizes, map bounds, colormaps). Defaults to DEFAULT_REPORT_CONFIG.
cast_filter – If set, rebuild only the station page for this cast number (implies
generate={"stations": True, rest False}).sal_range –
(sal_min, sal_max)— records withsalinity_1outside this range are excluded from all station page plots. The NC files are not modified. Excluded record count is shown in each page header.trim_soak – If True, apply pre-soak detection on each cast: cut the first 60 s (pump activation) and any records up to the last near-surface record after pump-on. Passed through to
generate_station_page().dbar_step – Subsample the pressure axis by this step for section and timeseries plots (default 1, full 1-dbar resolution).
build_profiles()always stores 1-dbar data; this controls plot-time resolution only.drop_stub – If True, a cast-page section that applies but whose figures all failed to render is dropped and the survivors renumber over it, instead of keeping the heading with an “unavailable” placeholder. Passed through to
generate_station_page().
- Returns:
The number of pages that failed to build (0 on full success), so the CLI can exit non-zero when a requested page could not be generated.
- Return type:
int
Cast identity
Cast identity: cast-number parsing, expansion, and compact formatting.
Single home for the cast-identity operations shared across the package:
parse a cast number and letter suffix from a filename (cast_id_from_name()),
expand a cast_numbers config spec to order-preserving pairs or ints
(expand_cast_ids() / expand_cast_numbers()), format a single cast id as
a zero-padded string (format_cast_id()), and format a list of cast numbers
compactly with collapsed ranges (compact_cast_list()).
- ctdcast.identity.cast_id_from_name(name: str) tuple[int, str] | None[source]
Extract
(cast_num, cast_suffix)from a cast filename stem.Uses the last 3+-digit group in the stem as the cast number, so cruise or leg numbers earlier in the name (e.g. the
142inmsm_142_1_001_1sec) are not mistaken for the cast number. Letter suffixes are recognised whether directly appended (mixsed2_004b) or underscore-separated (mixsed2_004_b). ReturnsNonewhen no 3+-digit group is present.
- ctdcast.identity.compact_cast_list(nums: list[int]) str[source]
Format a cast number list compactly, collapsing consecutive runs into ranges.
Example: [131, 133, 134, 136, 163] → “131, 133–134, 136, 163”.
- ctdcast.identity.expand_cast_ids(cast_numbers: list) list[tuple[int, str]][source]
Expand a
cast_numbersspec to(number, suffix)pairs, preserving order.A plain cast NNN and its lettered sibling NNNb are distinct events, so identity is the pair, not the bare number. A bare int or range names plain events only (suffix
""); a “NNNb” string names the lettered event. Input order is preserved and duplicates are kept — section ordering relies on the author’s given order, so sorting and de-duplication are not applied.Raises
ValueErroron a malformed entry, so a bad config fails loudly rather than silently dropping or coercing casts.
- ctdcast.identity.expand_cast_numbers(cast_numbers: list) list[int][source]
Expand a
cast_numbersspec to a flat list of station numbers, in order.The integer view of
expand_cast_ids()— the letter suffix is dropped, so a “10b” entry contributes station10. Use this where callers key on the integer station (LADCP files, map positions, membership); useexpand_cast_ids()where a plain cast must be distinguished from its lettered sibling (section and timeseries profile selection).
- ctdcast.identity.format_cast_id(cast_num: int, cast_suffix: str = '') str[source]
Format a cast identity as a zero-padded id string, e.g.
(10, "b") -> "010b".The single formatter for the
NNN/NNNbconvention used in output filenames (cast_010b.html), page labels, and cast pills. Changing the pad width or suffix rule here changes it everywhere.
Figure builders
draw_*_fig functions build and return a matplotlib Figure (or None when the
dataset lacks the required variables).
Figure builders: each draw_*_fig returns a matplotlib Figure or None.
Encoding to base64 PNG for embedding in a page is done separately by the
_make_*_b64 wrappers in ctdcast.reports._plots.
- ctdcast.plotters.plots.draw_all_sections_map_fig(sections_data: list[dict[str, Any]], all_lats: list[float], all_lons: list[float], legend_outside: bool = False, *, target_h: float = 4.5, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a Figure showing all section tracks coloured by section.
- ctdcast.plotters.plots.draw_aux_profiles_fig(ds: Dataset, *, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a O₂ sat, fluorescence, turbidity profiles Figure (downcast + pale upcast).
- ctdcast.plotters.plots.draw_cruise_map_fig(all_meta: list[dict], *, target_h: float = 4.0, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a Figure of all cast positions (no single-cast highlight).
- ctdcast.plotters.plots.draw_ct_sa_sigma0_fig(ds: Dataset, *, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a CT, SA, σ₀ profiles side-by-side Figure (downcast + grey upcast).
- ctdcast.plotters.plots.draw_ladcp_bottomtrack_fig(ladcp_path: Path | None, *, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a LADCP bottom-track U and V vs depth Figure.
- ctdcast.plotters.plots.draw_overview_panel_fig(ds_prof: Dataset, var: str, label: str, bathy_depths: ndarray | None = None, style: str = 'pcolormesh', vmin: float | None = None, vmax: float | None = None, cast_groups: dict[str, list[int]] | None = None, *, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a Figure of var vs pressure × cast number (cruise overview panel).
- ctdcast.plotters.plots.draw_pressure_time_fig(ds: Dataset, *, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a pressure vs elapsed time Figure (cast trajectory + bottle stops).
- ctdcast.plotters.plots.draw_section_fig(ds_prof: Dataset, var: str, label: str, x_vals: ndarray, x_label: str, title: str = '', style: str = 'pcolormesh', bathy_depths: ndarray | None = None, bathy_x: ndarray | None = None, cast_labels: list | None = None, vmin: float | None = None, vmax: float | None = None, figsize: tuple[float, float] | None = None, *, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a Figure of var vs pressure × x_vals.
- ctdcast.plotters.plots.draw_section_map_fig(lats: list[float], lons: list[float], cast_nums: list[int], title: str = '', min_margin: float = 0.03, min_margin_lon: float | None = None, *, fig_w: float = 4.5, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a GEBCO map Figure with the section track.
- ctdcast.plotters.plots.draw_section_ts_histogram_fig(ds_prof: Dataset, *, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a CT–SA 2-D count histogram Figure (log₁₀ colour) for section profiles.
- ctdcast.plotters.plots.draw_section_ts_o2_fig(ds_prof: Dataset, *, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a CT–SA histogram Figure coloured by median O₂ saturation per bin.
- ctdcast.plotters.plots.draw_section_ts_profiles_fig(ds_prof: Dataset, x_vals: ndarray, *, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a Figure of per-cast CT–SA profiles coloured by along-track distance.
- ctdcast.plotters.plots.draw_sensor_diff_fig(ds: Dataset, *, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a primary minus secondary sensor difference profiles Figure.
- ctdcast.plotters.plots.draw_stability_fig(ds: Dataset, *, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a N² and Turner angle (2-panel) Figure.
- ctdcast.plotters.plots.draw_station_map_fig(lat: float, lon: float, all_meta: list[dict], target_h: float = 4.5, *, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a GEBCO map Figure with all casts and this cast highlighted.
- ctdcast.plotters.plots.draw_timeseries_fig(ds_prof: Dataset, var: str, label: str, style: str = 'pcolormesh', vmin: float | None = None, vmax: float | None = None, figw: float | None = None, *, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a Figure of var vs cast time × pressure, both down and upcast.
- ctdcast.plotters.plots.draw_ts_density_fig(ds: Dataset, ladcp_path: Path | None = None, *, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a CT/SA/σ₀ profiles Figure, optionally alongside LADCP U/V.
- ctdcast.plotters.plots.draw_ts_diagram_fig(ds: Dataset, *, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a T-S diagram Figure colored by O₂ saturation.
- ctdcast.plotters.plots.draw_ts_diagram_timeseries_fig(ds_ts: Dataset, *, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a CT–SA diagram Figure for all timeseries profiles, coloured by time.
- ctdcast.plotters.plots.draw_ts_updown_fig(ds: Dataset, *, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a CT–SA scatter Figure: downcast in blue, upcast in red, σ₀ contours.
- ctdcast.plotters.plots.draw_updown_diff_fig(ds: Dataset, *, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Figure | None[source]
Return a downcast minus upcast profiles Figure: ΔCT, ΔSA, Δσ₀.
- ctdcast.plotters.plots.map_panel(ax: Any, cax: Any, xl0: float, xl1: float, yl0: float, yl1: float, *, cfg: ReportConfig) None[source]
Draw the GEBCO depth field into ax and its ‘Depth (m)’ colorbar into cax.
cax is a pre-placed colorbar axes from
_map_layout(); when there is no GEBCO to draw it is hidden so no empty box remains.
- ctdcast.plotters.plots.section_figsize_and_slot(p_max_dbar: float, dist_km: float) tuple[tuple[float, float], str][source]
Return figure size and CSS slot class for a section pcolormesh plot.
The figure width is always a canonical slot width (one of
SLOTS), so the rendered PNG fills its slot at the same oversample as every other figure and the browser never rescales it — otherwise a section rendered at a between-slots width is squeezed into the nearest slot box, shrinking its baked-in fonts.Aspect is calibrated (
_SECTION_STRETCH) so KTout (416 dbar, 94 km) is short at full width. The widest slot whose resulting height stays within_MAX_SECTION_His chosen; height then follows the data aspect (floored at_MIN_SECTION_H). A section too deep for even the narrowest slot keeps that slot’s width and accepts the height cap.- Parameters:
p_max_dbar – Maximum pressure in the section (dbar).
dist_km – Total along-track distance of the section (km).
- Returns:
tuple of
((fig_w, fig_h), css_slot)wherefig_wequals the slot’scanonical inch width and
css_slotis one of"slot-full","slot-twothirds","slot-half", or"slot-third".
Layer-1 primitives
ax-taking primitives that draw into a caller-supplied axes and create no Figure, so
a panel shared by more than one page type has a single implementation.
Layer-1 ax-taking primitives that draw into a provided axes and create no Figure.
- ctdcast.plotters.primitives.mesh_field(ax: Any, fig: Any, x: ndarray, y: ndarray, data2d: ndarray, *, cmap: Any, norm: Any, cmap_name: str, bounds: ndarray, style: str, cbar_label: str = '') Any[source]
Draw a pcolormesh/contourf field with a matched discrete colorbar into ax; return the colorbar.
The colorbar has a fixed inch width (not a fraction of the host axes), so its thickness and the resulting right margin are identical on every field figure regardless of slot width, and its labels are ~6 round values (
nice_colorbar_ticks()) rather than one per discretisation boundary, with cbar_label written as a title on top (unit_colorbar()) so it does not widen the figure.make_axes_locatable(viaunit_colorbar(reserve=True)) is used only because this axes is free-aspect. It attaches the colorbar to the divider of the axes’ box at layout time; if something resizes that box afterwards —set_aspect("equal", adjustable="box"), or a hand-placed map layout — the cax tracks the pre-resize box and ends up the wrong size. The rule (see.claude/notes/2026-08-14-consistent-cruise-maps.md):make_axes_locatablefor free-aspect axes; hand-reserved inches whenever the aspect is locked or the axes are hand-placed. Do not addset_aspect("equal")to a figure that colorbars through here without switching to the reserved-inches path.
- ctdcast.plotters.primitives.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
BoundaryNormbar can still show ~6 round labels (e.g. 34.8, 34.9, … 35.2) instead of one label per boundary. UsesMaxNLocatorwith 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).
- Returns:
Tick positions, clipped to
[vmin, vmax].- Return type:
numpy.ndarray
- ctdcast.plotters.primitives.sigma0_isopycnals(ax: Any, x: ndarray, y: ndarray, data2d: ndarray) None[source]
Overlay the 27.7 and 27.8 σ₀ isopycnal contours (labelled) on ax, swallowing contour failures.
- ctdcast.plotters.primitives.unit_colorbar(target: Any, mappable: Any, *, unit: str = '', ticks: ndarray | None = None, extend: str = 'neither', reserve: bool = False, title_loc: str = 'center') Any[source]
Draw the report-standard colorbar with the unit as a title on top.
One entry point, two placement strategies so the appearance (bar width, gap, tick choice, unit-on-top) is set in a single place regardless of how the axes was laid out.
- Parameters:
target (matplotlib Axes) – When reserve is False, a colorbar axes already reserved by a hand layout. When reserve is True, the host plot axes, into which a fixed-inch cax is appended — allowed only for free-aspect axes (see
mesh_field()).mappable (matplotlib ScalarMappable) – The artist to map (
pcolormesh,contourfset, …).unit (str) – Text placed above the bar (
cax.set_title) rather than as a rotated side label — reads cleanly and, unlike a side label, does not widen the figure. A unit ("m s⁻¹") or a full label ("CT (°C)"); empty renders no title.ticks (numpy.ndarray, optional) – Explicit tick positions (e.g. from
nice_colorbar_ticks()).extend (str) –
"neither"/"both"/"min"/"max"— pointed ends for out-of-range.reserve (bool) – Append a fixed-inch cax to target instead of treating it as the cax.
title_loc (str) – Horizontal anchor for the on-top title —
"center"(default),"left"or"right"."left"anchors the title at the thin bar’s left edge so it extends right into the margin, clear of a figure’s top-left annotations (e.g. the cast-marker strip on field figures); centering it over the thin bar would instead overhang the plot.
- Return type:
matplotlib.colorbar.Colorbar
Base64 encoders
_make_*_b64 wrappers render a figure builder’s Figure to an embedded base64 PNG,
returning None on any exception so a missing figure never prevents a page from
being written.
Base64 PNG encoders — thin wrappers that render a Figure for a page.
Each _make_*_b64 builds a figure via a draw_*_fig in
ctdcast.plotters.plots and encodes it with render_b64(). Two use a
custom wrapper instead of render_b64(): _make_all_sections_map_b64 still
delegates drawing to its draw_*_fig but needs a post-tight_layout
adjustment, and _make_ladcp_section_b64 does its own plotting because it
returns a list of RenderedPanel rather than a single figure.
- class ctdcast.reports._plots.RenderedPanel(b64: str | None, title: str = '', short: str = '', figsize: tuple[float, float] | None = None, slot: str | None = None)[source]
A rendered figure plus the layout metadata the HTML template needs.
- b64: str | None
Base64-encoded PNG string, or
Nonewhen the figure could not be rendered.
- figsize: tuple[float, float] | None = None
Figure dimensions
(width, height)in inches, orNonewhen not recorded.
- short: str = ''
Short label used in
<figcaption>elements (e.g."CT","U").
- slot: str | None = None
CSS slot class (e.g.
"slot-full") matching the PNG aspect ratio, orNone.
- title: str = ''
Long descriptive title (used in
altattributes and headings).
Plotting parameters
Package-wide constants: plotting parameters, CNV aliases, variable metadata, CCHDO conventions.
Compile-time constants only — the per-run display settings (GEBCO path,
clean_spines, figsizes, map bounds, colormap overrides) live in the frozen
ctdcast.config.report_config.ReportConfig, built once and threaded down.
Section headers mark what kind of constant each block holds, because that determines who may change it and what breaks when they do:
- Contract — changing it makes output wrong or non-conformant.
Requires a code review and a version bump.
- Science — changing it gives a different but equally valid answer.
Per-cruise overrides go in
display.variables:in the cruiseconfig.yaml; usectdcast.config.loader.load_display_config().Derived — computed from another constant; must live here to avoid drift. Deferred — belongs in
oceanvisonce that package exists.
- ctdcast.config.parameters.SECTION_BIOGEO_VARS: tuple[str, ...] = ('ctd_oxygen_1', 'ctd_fluor', 'ctd_turbidity')
Biogeochemical variables drawn on section, overview, and timeseries pages, in order.
- ctdcast.config.parameters.SECTION_PHYSICS_VARS: tuple[str, ...] = ('conservative_temperature', 'absolute_salinity', 'sigma0')
Physics variables drawn on section, overview, and timeseries pages, in order. Use
vlabel(var)for the axis/panel label andVARIABLES[var]["label"]for the short caption. Defined here so the three report modules share one source of truth and cannot silently diverge from each other or from VARIABLES.
- ctdcast.config.parameters.resolve_sensor_var(ds: xr.Dataset, var: str) str[source]
Return the name to use for var in ds, applying the single/dual-sensor rule.
A variable may be stored plain (single sensor, e.g.
ctd_oxygen) or suffixed (dual sensor, e.g.ctd_oxygen_1). This resolves whichever form ds actually holds: a suffixed var falls back to its plain form, and a plain var falls back to the_1form. Returns var unchanged when neither is present, so the caller’s draw function then returnsNonefor the missing variable.- Parameters:
ds – The dataset whose variables to resolve against.
var – The requested variable name (plain or
_1/_2suffixed).
- Returns:
The name present in ds, or var unchanged if neither form is found.
- Return type:
str
- ctdcast.config.parameters.vlabel(var: str, prefix: str = '') str[source]
Return a matplotlib axis label for var using the VARIABLES registry.
Format is
"Label (units)"whenlabel_unitsis non-empty, or just"Label"when there are no units (e.g. dimensionless quantities). Units are always in the Unicode display form fromlabel_units— never the ASCII-safeunitsstring used for netCDF attributes.- Parameters:
var – VARIABLES key (e.g.
"conservative_temperature").prefix – Optional prefix prepended to the label component only, not the units. Use
"Δ"to produce difference labels such as"ΔCT (°C)".
- Returns:
Ready-to-use axis label string. Falls back to var itself when var is not in VARIABLES.
- Return type:
str
- ctdcast.config.parameters.vlabel_html(var: str, prefix: str = '') str[source]
Return
vlabel()as HTML-ready text — mathtext subscripts as Unicode.Use this wherever a variable label is written into HTML (a figure caption, a table cell): matplotlib needs the mathtext form, but HTML must show
σ₀, not the literal$\sigma_0$. One helper so the label-form choice lives in one place.
- ctdcast.config.parameters.vunit(var: str) str[source]
Return the Unicode display unit for var, or
""when dimensionless.The unit half of
vlabel(), for field colorbars that place the unit as a title on top of the bar rather than a"Label (units)"side label. Uses thelabel_unitsdisplay form (never the ASCIIunitsnetCDF string). Works for both canonical and single-/dual-sensor-resolved names (both carry the samelabel_unitsinVARIABLES).
Section manifest
Each report page is described by a Profile of
Section and
Panel entries, resolved by
resolve() into a numbered, rendered report. The
model is package-neutral; each page’s concrete registry lives in its own generator
module. _anchors maps the old hand-authored #s-* anchors onto the new
section ids for one release.
Section-manifest model and resolver for report pages.
A report page is described by a Profile: an ordered sequence of
Section (and Expand) entries, each naming Panel ids
that render figures or tables. resolve() walks a profile once against a
render context and returns a ResolvedReport whose sections are numbered
over the rendered subset — absent sections leave no gap, and identity is the
section id (a stable slug), not the integer.
This module holds only the model and the resolution algorithm — it is
package-neutral (names no variable, page, or science) and is vendored
byte-identical to the sister repos. Each page’s concrete registry (its
Ctx builder, panels, sections, and profiles) lives in that page’s own
module — grid’s in reports/_grid.py, and so on — never in a plural
_manifests.py companion. The design rationale is in
.claude/notes/2026-08-15-report-section-manifest-design.md.
- class ctdcast.reports._manifest.Expand(over: Callable[[Any], Sequence[Any]], section: Callable[[Any], Section])[source]
A single manifest entry that becomes N sections at resolution time.
- Parameters:
over (Callable) – Returns the sequence of items to expand over (e.g. the variables present on the page, in display order).
section (Callable) – Builds one
Sectionper item yielded byover.
- over: Callable[[Any], Sequence[Any]]
- class ctdcast.reports._manifest.Panel(id: str, render: ~typing.Callable[[~typing.Any], str | None], kind: ~typing.Literal['figure', 'html', 'table'] = 'figure', slot: str | ~typing.Callable[[~typing.Any], str] | None = 'full', caption: str | None = None, applies_to: ~typing.Callable[[~typing.Any], bool] = <function _always>, unavailable_if: ~typing.Callable[[~typing.Any], str | None] = <function _unavailable_never>)[source]
One figure or table, addressable by id.
- Parameters:
id (str) – Unique panel identifier, e.g.
"temperature_field". Also the anchor a caption or cross-reference can point at.render (Callable) – Adapter that returns the panel’s payload — a base64 PNG string for a
"figure", or ready-to-emit markup for"html"/"table"— orNonewhen the panel applies but no output could be produced (a plot raised, or the variable is absent), which the resolver turns into a.warnstub. Figure adapters wrap the existing_make_*_b64functions unchanged.kind ({"figure", "html", "table"}, optional) – Content discriminator. The template macro branches on it so that only
"html"/"table"payloads are emitted|safe; a"figure"payload is always escaped into an imagesrc.slot (str or Callable or None, optional) –
SLOTSkey giving the panel’s display width, or a callablectx -> slotthat computes it (e.g. from a section’s aspect ratio). Belongs to the panel, not the section, so a panel is the same width wherever it is placed. The resolver calls it when callable.Nonemarks a figure that is not rendered through the slot system: it is emitted as a bare.figthat fills the content column (noslot-*class, so no rendered-width contract).caption (str, optional) – Caption text rendered beneath the panel.
applies_to (Callable, optional) – Predicate deciding whether the panel is attempted at all.
Falseomits the panel silently (it is excluded, not merely unavailable).unavailable_if (Callable, optional) – Precondition checked before
render: given the context, return a reason string when the panel applies but cannot be produced (e.g. a required metadata field is missing), orNoneto proceed. A returned reason becomes a.warnstub with that reason andrenderis not called. This is the channel for defects knowable from context; arenderthat still returnsNonegets the generic stub reason, because that is the “should have worked and didn’t” case.
- applies_to() bool
Return True for any context (the default
applies_topredicate).
- caption: str | None = None
- id: str
- kind: Literal['figure', 'html', 'table'] = 'figure'
- render: Callable[[Any], str | None]
- slot: str | Callable[[Any], str] | None = 'full'
Return None for any context (the default
unavailable_ifpredicate).
- class ctdcast.reports._manifest.PanelGroup(over: Callable[[Any], Sequence[Any]], panel: Callable[[Any], Panel])[source]
A
Section.panelsentry that expands to N panels at resolution time.Places a data-driven run of panels — one per item — under a single heading without inflating the section count. Use this (not
Expand) when the run is over a numeric or unbounded set, e.g. one panel per isopycnal; reserveExpand’s section-level expansion for a closed editorial vocabulary. Numbering reflects editorial structure, not data cardinality.- Parameters:
over (Callable) – Returns the sequence of items to expand over, in display order.
panel (Callable) – Builds one
Panelper item yielded byover.
- over: Callable[[Any], Sequence[Any]]
- ctdcast.reports._manifest.PanelKind
Panel content kinds.
"figure"renders a base64 PNG (escaped as an imagesrc);"html"and"table"render pre-built markup that the template macro emits with|safe. The discriminator keeps theautoescape=Trueboundary to one auditable branch — a figure payload is never|safe-d.alias of
Literal[‘figure’, ‘html’, ‘table’]
- class ctdcast.reports._manifest.Profile(numbering: str = 'flat', entries: tuple[~ctdcast.reports._manifest.Section | ~ctdcast.reports._manifest.Expand, ...]=<factory>)[source]
An ordered page description plus a numbering policy.
- Parameters:
- numbering: str = 'flat'
- class ctdcast.reports._manifest.ResolvedPanel(id: str, kind: Literal['figure', 'html', 'table'], slot: str | None, payload: str | None, caption: str | None, stub_reason: str | None = None)[source]
A panel after rendering: a payload (figure b64 or markup) or a
.warnstub.- caption: str | None
- id: str
- property is_stub: bool
True when the panel applied but produced no output.
- kind: Literal['figure', 'html', 'table']
- payload: str | None
- slot: str | None
- stub_reason: str | None = None
- class ctdcast.reports._manifest.ResolvedReport(sections: tuple[ResolvedSection, ...], not_applicable: tuple[str, ...])[source]
The full resolved page: numbered sections plus the not-applicable list.
- anchor(section_id: str) str | None[source]
Return the display number for section_id, or None if not rendered.
- not_applicable: tuple[str, ...]
- sections: tuple[ResolvedSection, ...]
- class ctdcast.reports._manifest.ResolvedSection(id: str, number: str, title: str, level: int, intro: str | None, panels: tuple[ResolvedPanel, ...], role: str, layout: str | None = None)[source]
A section after resolution: numbered heading plus resolved panels.
- id: str
- intro: str | None
- layout: str | None = None
- level: int
- number: str
- panels: tuple[ResolvedPanel, ...]
- role: str
- title: str
- class ctdcast.reports._manifest.Section(id: str, title: str, panels: tuple[str | PanelGroup, ...], level: int = 2, intro: str | None = None, applies_to: Callable[[Any], bool] | None = None, role: str = 'content', layout: str | None = None)[source]
A numbered heading and the panels beneath it.
- Parameters:
id (str) – Unique section identifier, e.g.
"hydrography". Doubles as the anchor and the cross-reference key.title (str) – Human-readable heading text (without a number — the number is computed).
panels (tuple of (str or PanelGroup)) – Panel ids, in display order. An entry may instead be a
PanelGroup, which expands to a data-driven run of panels under this one heading.level (int, optional) – Heading level (
2for<h2>).intro (str, optional) – Introductory prose rendered under the heading, before the panels.
applies_to (Callable, optional) – Predicate deciding whether the section is included. When
None(default), the section applies if any of its panels apply. A section dropped here is named in the report’s “not applicable” footer line.role (str, optional) –
"content"(numbered1..N) or"appendix"(numberedA..), so appendix material such as a NetCDF-variable table does not pad the science numbering.layout (str, optional) – Within-section panel arrangement.
None(default) stacks panels vertically;"row"lays them out in a wrapping flex row, each panel in a cell sized by itsslot(so twoslot="half"figures sit side-by-side and aslot="full"one wraps to its own line).
- applies_to: Callable[[Any], bool] | None = None
- id: str
- intro: str | None = None
- layout: str | None = None
- level: int = 2
- panels: tuple[str | PanelGroup, ...]
- role: str = 'content'
- title: str
- ctdcast.reports._manifest.resolve(profile: Profile, ctx: Any, panels: dict[str, Panel], *, drop_stub: bool = False) ResolvedReport[source]
Resolve profile against ctx into a numbered
ResolvedReport.One pass: expand
Expandentries, drop sections whoseapplies_tois false (collecting them for the not-applicable footer), resolve each kept section’s panels (Nonerender →.warnstub), then number the survivors —contentsections1..NandappendixsectionsA...- Parameters:
profile (Profile) – The page’s ordered section description and numbering policy.
ctx (Any) – The render context passed to every predicate and render callable (dataset, config, paths — whatever the page’s panels need).
panels (dict of str to Panel) – The panel registry; section
panelsentries are ids into this map.drop_stub (bool, optional) – When True, a section that applies but whose panels are all stubs (applicable-but-entirely-unavailable) is dropped and the survivors renumber over it, so the page closes cleanly. Default False keeps the heading with its stub, which surfaces the failure rather than hiding it. A section dropped this way is not added to
not_applicable— that list stays reserved for genuinely-not-applicable-to-this-deployment sections, so a plot failure never reads as “not applicable”. A section with any non-stub panel is never dropped.
- Returns:
Numbered, rendered sections plus the titles of any omitted sections.
- Return type:
- Raises:
NotImplementedError – If
profile.numbering == "grouped"(reserved for a later branch).KeyError – If a section references a panel id absent from panels.
Legacy anchor aliases for report pages — a transition shim for the D3 fix.
Section ids became page anchors, so the old hand-authored #s-* anchors change
(#s-profiles/#s-physics/#s-hydro all collapse onto #hydrography).
The committed demo pages under docs/source/_static/demo/ deep-link the old
ids, and external links (papers, issues, cruise reports) may too. For one
release each page emits an empty <span id="old"> for every old anchor whose
new section is rendered on it, so those links keep resolving.
Remove this module and the one template call that uses it once the old links are gone. The set was audited complete against the templates and the demo pages.
- ctdcast.reports._anchors.LEGACY_ANCHORS: dict[str, str] = {'s-aux': 'biogeochemistry', 's-biogeo': 'biogeochemistry', 's-diagnostics': 'diagnostics', 's-hydro': 'hydrography', 's-ladcp': 'velocity', 's-map': 'map', 's-overview': 'overview', 's-physics': 'hydrography', 's-profiles': 'hydrography', 's-sensors': 'sensors', 's-stability': 'stability', 's-ts': 'ts_diagram'}
Old
#s-*anchor -> new section id (which is the new anchor). Multiple old anchors mapping to one new id is the D3 defect being retired: Hydrography wass-profiles(cast),s-physics(section/timeseries) ands-hydro(index); Biogeochemistry wass-aux(cast) ands-biogeo(elsewhere).
- ctdcast.reports._anchors.legacy_anchor_spans(rendered_ids: set[str]) str[source]
Return empty
<span id="old">aliases for rendered sections’ old anchors.- Parameters:
rendered_ids – The set of section ids actually rendered on the page. A legacy anchor is aliased only when its new section is present, so no alias dangles at a section the page does not have.
- Returns:
Concatenated empty spans, one per matching legacy anchor (possibly empty).
- Return type:
str
Page generators
Tier-2: generate a per-cast HTML report page.
- ctdcast.reports._cast.CAST_DEFAULT: Profile = Profile(numbering='flat', entries=(Section(id='overview', title='Overview', panels=('ts_density', 'station_map', 'ts_updown'), level=2, intro='CT · SA · σ₀ profiles, station location, and T–S down-vs-up.', applies_to=None, role='content', layout=None), Section(id='hydrography', title='Hydrography', panels=('ct_sa_sigma0',), level=2, intro='CT · SA · σ₀ vs pressure — downcast in colour, upcast in grey.', applies_to=<function _has_ts>, role='content', layout=None), Section(id='biogeochemistry', title='Biogeochemistry', panels=('aux',), level=2, intro='O₂ saturation · fluorescence · turbidity.', applies_to=<function _has_biogeo>, role='content', layout=None), Section(id='ts_diagram', title='T–S diagram', panels=('ts_diagram',), level=2, intro='Coloured by O₂ saturation — downcast only.', applies_to=<function _has_ts>, role='content', layout=None), Section(id='stability', title='Stability', panels=('stability',), level=2, intro='N² and Turner angle — downcast only.', applies_to=<function _has_ts>, role='content', layout=None), Section(id='velocity', title='Velocity (bottom track)', panels=('ladcp_bottomtrack',), level=2, intro=None, applies_to=<function <lambda>>, role='content', layout=None), Section(id='diagnostics', title='Diagnostics', panels=('pressure_time', 'sensor_diff', 'updown_diff'), level=2, intro=None, applies_to=None, role='content', layout=None), Section(id='sensors', title='Sensors', panels=('sensors_table',), level=2, intro=None, applies_to=<function <lambda>>, role='appendix', layout=None), Section(id='data_ranges', title='netCDF data ranges', panels=('data_ranges',), level=2, intro='Min · max · valid count for every variable in the cast file on disk.', applies_to=None, role='appendix', layout=None)))
same figures, same grouping — the manifest only renumbers (closing the D1 gaps), generates the jump-nav, and turns section ids into anchors (the D3 fix).
- Type:
The cast page profile. Conservative port of the current page
- ctdcast.reports._cast.CAST_PANELS: dict[str, Panel] = {'aux': Panel(id='aux', render=<function <lambda>>, kind='figure', slot='full', caption=None, applies_to=<function _always>, unavailable_if=<function _unavailable_never>), 'ct_sa_sigma0': Panel(id='ct_sa_sigma0', render=<function <lambda>>, kind='figure', slot='full', caption=None, applies_to=<function _always>, unavailable_if=<function _unavailable_never>), 'data_ranges': Panel(id='data_ranges', render=<function <lambda>>, kind='table', slot='full', caption=None, applies_to=<function _always>, unavailable_if=<function _unavailable_never>), 'ladcp_bottomtrack': Panel(id='ladcp_bottomtrack', render=<function <lambda>>, kind='figure', slot='third', caption=None, applies_to=<function _always>, unavailable_if=<function _unavailable_never>), 'pressure_time': Panel(id='pressure_time', render=<function <lambda>>, kind='figure', slot='third', caption='Cast trajectory: pressure vs elapsed time', applies_to=<function _always>, unavailable_if=<function _unavailable_never>), 'sensor_diff': Panel(id='sensor_diff', render=<function <lambda>>, kind='figure', slot='twothirds', caption='T₁−T₂, S₁−S₂: primary minus secondary sensor. Ideal: scatter around zero with ±0.01 spread.', applies_to=<function _has_dual_sensors>, unavailable_if=<function _unavailable_never>), 'sensors_table': Panel(id='sensors_table', render=<function <lambda>>, kind='table', slot='full', caption=None, applies_to=<function _always>, unavailable_if=<function _unavailable_never>), 'stability': Panel(id='stability', render=<function <lambda>>, kind='figure', slot='twothirds', caption=None, applies_to=<function _always>, unavailable_if=<function _unavailable_never>), 'station_map': Panel(id='station_map', render=<function <lambda>>, kind='figure', slot='two-fifths', caption=None, applies_to=<function _always>, unavailable_if=<function _unavailable_never>), 'ts_density': Panel(id='ts_density', render=<function <lambda>>, kind='figure', slot='three-fifths', caption=None, applies_to=<function _always>, unavailable_if=<function _unavailable_never>), 'ts_diagram': Panel(id='ts_diagram', render=<function <lambda>>, kind='figure', slot='third', caption='Contours: σ₀ (kg m⁻³) — potential density referenced to surface', applies_to=<function _always>, unavailable_if=<function _unavailable_never>), 'ts_updown': Panel(id='ts_updown', render=<function <lambda>>, kind='figure', slot='two-fifths', caption=None, applies_to=<function _always>, unavailable_if=<function _unavailable_never>), 'updown_diff': Panel(id='updown_diff', render=<function <lambda>>, kind='figure', slot='full', caption='ΔCT, ΔSA, Δσ₀ downcast minus upcast on 1-dbar grid — measures hysteresis from pump lag or sensor response time', applies_to=<function _always>, unavailable_if=<function _unavailable_never>)}
Cast panel registry — each wraps an existing
_make_*_b64adapter unchanged, reading only fromPageCtx. Slots mirror the current cast.html layout.
- ctdcast.reports._cast.CAST_SECTION_COLUMNS: dict[str, int] = {'overview': 1}
Presentational intra-section layout, kept out of the layout-neutral manifest model. Maps a section id to the panel index from which the trailing panels stack in a right-hand
fig-col(rather than wrapping onto their own row).overview: 1puts the CT·SA·σ₀ profiles on the left and stacks the station map above the T–S down-vs-up plot in the right column.
- class ctdcast.reports._cast.PageCtx(ds: Any, cfg: ReportConfig, lat: float, lon: float, all_meta: list[dict[str, Any]], ladcp_path: Path | None, ladcp_configured: bool, ladcp_exists: bool, sensor_info: list[dict[str, Any]], nc_path: Path)[source]
Per-cast render context: the frozen inputs every cast panel/predicate reads.
Wraps the frozen
ReportConfig(cfg) with the values derived once per cast, so a panel’srender/applies_todepends only on this object. Keeping it frozen and section-independent is what makes “derived context must not depend on section inclusion” enforceable rather than aspirational.- all_meta: list[dict[str, Any]]
- cfg: ReportConfig
- ds: Any
- ladcp_configured: bool
- ladcp_exists: bool
- ladcp_path: Path | None
- lat: float
- lon: float
- nc_path: Path
- sensor_info: list[dict[str, Any]]
- ctdcast.reports._cast.generate_station_page(nc_path: Path, out_dir: Path, all_meta: list[dict[str, Any]], prev_cast_str: str | None = None, next_cast_str: str | None = None, force: bool = False, ladcp_dir: Path | None = None, ladcp_pattern: str | None = None, cast_num_str: str | None = None, sal_range: tuple[float, float] | None = None, trim_soak: bool = False, cast_notes: list[str] | None = None, cruise_info: dict | None = None, drop_stub: bool = False, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Path | None[source]
Generate a per-cast HTML report page and write it to out_dir/casts/.
- Parameters:
nc_path – Path to a single cast
.ncfile.out_dir – Root output directory.
all_meta – List of dicts with keys
lat,lonfor all casts (used for map).prev_cast_str – Full cast identifier string of the previous cast for nav links, e.g.
"010"or"004b"(or None for no previous link).next_cast_str – Full cast identifier string of the next cast for nav links (or None).
force – Overwrite existing file if True.
ladcp_dir – Directory containing processed LADCP
.matfiles namedNNN.matorNNNb.mat. If None or no matching file exists, LADCP panels are omitted.ladcp_pattern – Optional filename glob for non-standard LADCP naming conventions, e.g.
"msm_142_1_*.mat". The*is replaced with the zero-padded cast number. Falls back to glob-based discovery when omitted.cast_num_str – Full cast identifier string, e.g.
"011"or"004b". Derived from nc_path if not provided.sal_range –
(sal_min, sal_max)— records withsalinity_1outside this range are excluded from all plots (but the NC file is not modified). The count of excluded records is shown in the page header.trim_soak – If True, apply pre-soak detection via
find_soak_end(). Finds the last record within 10 dbar of the surface before the cast maximum depth, crawls back up to 20 seconds to the shallowest point preceding the real descent, and trims everything up to that point. Applied before sal_range trimming. NC files are not modified.cast_notes – Optional list of free-text notes for this cast (e.g. “SBE43 malfunction”). Rendered as warning banners near the top of the page.
- Return type:
Path to the written HTML file, or None on failure.
- ctdcast.reports._cast.resolve_cast(ctx: PageCtx, *, drop_stub: bool = False) ResolvedReport[source]
Resolve the cast profile against ctx into numbered, rendered sections.
drop_stub (from the
--drop-stubCLI flag) drops an applicable section whose panels all failed to render, instead of keeping its heading with a stub.
Tier-2: generate a per-section HTML report page.
- ctdcast.reports._section.SECTION_DEFAULT: Profile = Profile(numbering='flat', entries=(Section(id='map', title='Map', panels=('section_map',), level=2, intro='The section track and the CTD stations it comprises.', applies_to=None, role='content', layout=None), Section(id='hydrography', title='Hydrography', panels=(PanelGroup(over=<function <lambda>>, panel=<function _field_panel>),), level=2, intro='Sections of conservative temperature (CT), absolute salinity (SA) and potential density (σ₀) against distance along the section. The σ₀ panel carries the 27.7 and 27.8 kg m⁻³ isopycnals (black, labelled). Open triangles along the top of each panel mark the profiles; station numbers are labelled at intervals.', applies_to=None, role='content', layout=None), Section(id='biogeochemistry', title='Biogeochemistry', panels=(PanelGroup(over=<function _biogeo_present>, panel=<function <lambda>>),), level=2, intro='Sections of the biogeochemical sensors present on this section — oxygen, fluorescence and turbidity where available — against distance along the section.', applies_to=None, role='content', layout=None), Section(id='velocity', title='Velocity (U east, V north)', panels=(PanelGroup(over=<function <lambda>>, panel=<function _ladcp_panel>),), level=2, intro='Eastward (U) and northward (V) velocity from the LADCP, against distance along the section.', applies_to=None, role='content', layout=None), Section(id='ts_diagram', title='T–S diagrams', panels=(PanelGroup(over=<function <lambda>>, panel=<function _ts_panel>),), level=2, intro='Water-mass structure of the section in temperature–salinity space.', applies_to=None, role='content', layout=None)))
The section page profile. Same order as the previous hand-authored page — Map, Hydrography, Biogeochemistry, then the former “extra cards” Velocity and T–S — now numbered and anchored by the resolver. Physics/biogeo are PanelGroups over their variables; Velocity is a PanelGroup over the pre-rendered LADCP panels.
- ctdcast.reports._section.SECTION_PANELS: dict[str, Panel] = {'section_map': Panel(id='section_map', render=<function <lambda>>, kind='figure', slot='half', caption=None, applies_to=<function <lambda>>, unavailable_if=<function _unavailable_never>)}
String-addressable section panels. Only the Map is fixed; field panels (physics/biogeo), LADCP and T–S panels are all data-driven PanelGroups.
- class ctdcast.reports._section.SectionPageCtx(ds_sec: Any, x_vals: Any, x_label: str, section_style: str, bathy_depths: Any, bathy_x: Any, cast_labels: list[int], vmin: dict[str, float], vmax: dict[str, float], section_figsize: tuple[float, float], section_slot_key: str, section_name: str, lats: list[float], lons: list[float], map_b64: str | None, ts_panels: tuple[RenderedPanel, ...], ladcp_panels: tuple[RenderedPanel, ...], cfg: ReportConfig)[source]
Per-section render context: the frozen inputs every section panel reads.
Holds the values derived once per section (selected profiles, x-axis, bathy, colour limits, the computed figure geometry) so a panel’s
renderdepends only on this object.- bathy_depths: Any
- bathy_x: Any
- cast_labels: list[int]
- cfg: ReportConfig
- ds_sec: Any
- ladcp_panels: tuple[RenderedPanel, ...]
- lats: list[float]
- lons: list[float]
- map_b64: str | None
- section_figsize: tuple[float, float]
- section_name: str
- section_slot_key: str
- section_style: str
- ts_panels: tuple[RenderedPanel, ...]
- vmax: dict[str, float]
- vmin: dict[str, float]
- x_label: str
- x_vals: Any
- ctdcast.reports._section.generate_section_page(section_name: str, section_cfg: dict[str, Any], profiles_path: Path, out_dir: Path, force: bool = False, section_style: str = 'pcolormesh', vmin_override: dict[str, float] | None = None, vmax_override: dict[str, float] | None = None, ladcp_dir: Path | None = None, ladcp_pattern: str | None = None, dbar_step: int = 1, prev_name: str | None = None, next_name: str | None = None, cruise_info: dict[str, Any] | None = None, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Path | None[source]
Generate a section HTML report page.
- Parameters:
section_name – Key from
ctd_sections.yaml, e.g."KTout".section_cfg – Dict with keys
description,cast_numbers,color.profiles_path – Path to
profiles.nc(built bycnv_build_profiles.py).out_dir – Root output directory.
force – Overwrite existing file if True.
section_style –
"pcolormesh"or"contourf"— passed through to each section figure.vmin_override – Per-variable colormap limit overrides (e.g.
{"SA": 34.5}).vmax_override – Per-variable colormap limit overrides (e.g.
{"SA": 34.5}).ladcp_dir – Directory containing processed LADCP
.matfiles. If None, the LADCP velocity section panel is omitted.ladcp_pattern – Filename pattern for LADCP files, e.g.
"msm_142_1_*.mat". The*is replaced with the zero-padded cast number. Falls back toNNN.matif not given.dbar_step – Subsample the pressure axis by this step before plotting (default 1, no subsampling).
build_profiles()always stores 1-dbar data; this controls plot-time resolution only.prev_name – Name of the preceding section (for the ← nav button). None omits the button.
next_name – Name of the following section (for the → nav button). None omits the button.
- Return type:
Path to the written HTML file, or None on failure.
- ctdcast.reports._section.resolve_section(ctx: SectionPageCtx) ResolvedReport[source]
Resolve the section profile against ctx into numbered, rendered sections.
Tier-2: per-group timeseries HTML report pages.
A timeseries in this codebase is a named group of repeat casts at one location (yoyo CTDs, e.g. 24 h of repeated profiles). Config is analogous to sections: a name, a description, and a list of cast numbers.
The cruise-wide stacked overview plots (all casts, station-number x-axis) live on index.html, generated by _index.py. They are not timeseries pages.
- ctdcast.reports._timeseries.TIMESERIES_DEFAULT: Profile = Profile(numbering='flat', entries=(Section(id='map', title='Map', panels=('ts_location_map',), level=2, intro='Location of the repeat CTD casts in this group.', applies_to=None, role='content', layout=None), Section(id='hydrography', title='Hydrography', panels=(PanelGroup(over=<function <lambda>>, panel=<function _ts_field_panel>),), level=2, intro='Time series of conservative temperature (CT), absolute salinity (SA) and potential density (σ₀) against cast time. The σ₀ panel carries the 27.7 and 27.8 kg m⁻³ isopycnals (black, labelled). Triangles along the top mark each profile — downward for downcasts, upward for upcasts — with station numbers labelled at intervals above the downcasts.', applies_to=None, role='content', layout=None), Section(id='biogeochemistry', title='Biogeochemistry', panels=(PanelGroup(over=<function _ts_biogeo_present>, panel=<function <lambda>>),), level=2, intro='Time series of the biogeochemical sensors present in this group — oxygen, fluorescence and turbidity where available — against cast time.', applies_to=None, role='content', layout=None), Section(id='velocity', title='Velocity (U east, V north)', panels=(PanelGroup(over=<function <lambda>>, panel=<function _ts_ladcp_panel>),), level=2, intro='Eastward (U) and northward (V) velocity from the LADCP, against hours since the first cast.', applies_to=None, role='content', layout=None), Section(id='ts_diagram', title='T–S diagram', panels=(PanelGroup(over=<function <lambda>>, panel=<function _ts_diagram_panel>),), level=2, intro='Water-mass structure over the occupation in temperature–salinity space, profiles coloured by time.', applies_to=None, role='content', layout=None)))
The timeseries page profile — the same shape and section ids as the section page (so the shared anchors and the legacy #s-* aliases resolve identically), but with time-axis panels and cast-count-driven widths.
- ctdcast.reports._timeseries.TIMESERIES_PANELS: dict[str, Panel] = {'ts_location_map': Panel(id='ts_location_map', render=<function <lambda>>, kind='figure', slot='third', caption=None, applies_to=<function <lambda>>, unavailable_if=<function _unavailable_never>)}
Only the location map is fixed; fields, LADCP and T–S are data-driven groups.
- class ctdcast.reports._timeseries.TimeseriesPageCtx(ds_ts: Any, section_style: str, vmin: dict[str, float], vmax: dict[str, float], ts_figw: float, ts_slot_key: str, fig_location_b64: str | None, ts_panels: tuple[RenderedPanel, ...], ladcp_panels: tuple[RenderedPanel, ...], cfg: ReportConfig)[source]
Per-timeseries render context: the frozen inputs every panel reads.
Holds the values derived once per group (the selected profiles, colour limits, the cast-count-driven figure width and slot, and the pre-rendered location map, LADCP and T–S panels) so a panel’s
renderdepends only on this object.- cfg: ReportConfig
- ds_ts: Any
- fig_location_b64: str | None
- ladcp_panels: tuple[RenderedPanel, ...]
- section_style: str
- ts_figw: float
- ts_panels: tuple[RenderedPanel, ...]
- ts_slot_key: str
- vmax: dict[str, float]
- vmin: dict[str, float]
- ctdcast.reports._timeseries.generate_timeseries_page(ts_name: str, ts_cfg: dict, profiles_path: Path, out_dir: Path, force: bool = False, section_style: str = 'contourf', vmin_override: dict[str, float] | None = None, vmax_override: dict[str, float] | None = None, all_meta: list[dict] | None = None, ladcp_dir: Path | None = None, ladcp_pattern: str | None = None, dbar_step: int = 1, prev_name: str | None = None, next_name: str | None = None, cruise_info: dict[str, Any] | None = None, cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Path | None[source]
Generate a per-timeseries HTML page for a named group of repeat casts.
Plots both downcast and upcast profiles sorted by time_start on a shared time x-axis. Output:
<out_dir>/timeseries/timeseries_<ts_name>.html.- Parameters:
ts_name – Group name (used in filename and page title).
ts_cfg – Dict with
cast_numbers,description, andcolorkeys.profiles_path – Path to
profiles.nc.out_dir – Root output directory.
force – Overwrite existing file if True.
section_style –
"pcolormesh"or"contourf".vmin_override – Per-variable colormap limit overrides.
vmax_override – Per-variable colormap limit overrides.
all_meta – List of per-cast metadata dicts (keys
lat,lon,cast_num); used to render a cruise-context location map. If None, no map is shown.ladcp_dir – Directory containing processed LADCP
.matfiles. If None, the LADCP velocity panel is omitted.ladcp_pattern – Filename pattern for LADCP files, e.g.
"msm_142_1_*.mat". The*is replaced with the zero-padded cast number. Falls back toNNN.matif not given.dbar_step – Subsample the pressure axis by this step before plotting (default 1, no subsampling).
build_profiles()always stores 1-dbar data; this controls plot-time resolution only.prev_name – Name of the preceding timeseries group (for the ← nav button). None omits the button.
next_name – Name of the following timeseries group (for the → nav button). None omits the button.
- Return type:
Path to the written HTML file, or None if skipped or failed.
- ctdcast.reports._timeseries.resolve_timeseries(ctx: TimeseriesPageCtx) ResolvedReport[source]
Resolve the timeseries profile against ctx into numbered, rendered sections.
Interactive map
Tier-2: self-contained interactive cruise map using Leaflet.js.
Leaflet JS/CSS (~160 KB) is bundled in ctdcast/reports/leaflet/ as package
data so the generated leaflet.html requires no internet access at either
generation or view time.
If a GEBCO path is configured, GEBCO bathymetry for the cruise region is rendered as an embedded PNG image layer using discrete depth bands (standard oceanographic levels: 0, 100, 200, 500, 1000, 2000, 3000, 4000, 6000 m).
- Interaction:
Hover cast dot or section line → info panel updates (bottom-left).
Click cast dot → navigate directly to station page.
Click section line → navigate directly to section page.
Scroll or +/− buttons to zoom.
Shift+drag to box-zoom (Leaflet built-in).
- ctdcast.reports._leaflet.generate_leaflet_map(all_meta: list[dict[str, Any]], sections_cfg: dict[str, Any], out_dir: Path, force: bool = False, ship_track_nc: Path | None = None, cruise: str = 'UNK', cfg: ReportConfig = ReportConfig(gebco_path=None, clean_spines=True, profile_figsize=(7.0, 10.0), overview_figsize=(9.0, 4.5), map_lat_min=None, map_lat_max=None, map_lon_min=None, map_lon_max=None, var_cmaps=mappingproxy({'ctd_temperature_1': 'RdYlBu_r', 'ctd_temperature_2': 'RdYlBu_r', 'ctd_temperature': 'RdYlBu_r', 'ctd_salinity_1': 'YlGnBu_r', 'ctd_salinity_2': 'YlGnBu_r', 'ctd_salinity': 'YlGnBu_r', 'ctd_oxygen_1': 'RdYlGn', 'ctd_oxygen_2': 'RdYlGn', 'ctd_oxygen': 'RdYlGn', 'oxygen_saturation': 'RdYlGn', 'ctd_fluor': 'YlGn', 'ctd_turbidity': 'YlOrBr', 'conservative_temperature': 'RdYlBu_r', 'absolute_salinity': 'YlGnBu_r', 'sigma0': 'Purples', 'AOU': 'RdBu_r'}))) Path | None[source]
Generate a Leaflet.js interactive cruise map at
<out_dir>/leaflet.html.Always regenerates (force is accepted for API symmetry but ignored). If
ship_track_ncis provided and the file exists, the ship track is loaded, subsampled, and rendered as a grey polyline behind cast markers.cruiseshould be the resolved cruise identifier (fromcruise_infoor the NC file attribute); it is displayed in the map page title. Returns the output path, or None if all_meta is empty.
Analysis helpers
Derived physical quantities computed from raw CTD measurements.
All functions use GSW — the same library oceanographers use directly.
Per-cast (1-D, dim=time) functions
derive_salinity SP from conductivity/temperature/pressure derive_SA Absolute Salinity from SP/pressure/lat/lon derive_CT Conservative Temperature from SA/in-situ-T/pressure derive_sigma0 Potential density anomaly from SA/CT derive_AOU Apparent Oxygen Utilization from oxygen_saturation (% sat) derive_teos10 Convenience: SA + CT + sigma0 + optional O2 unit conversion
Profiles (2-D, dims N_PROF × pressure) functions
derive_teos10_profiles SA + CT + sigma0 for compiled profiles datasets
Output variable names match the VARIABLES registry in
ctdcast.config.parameters: absolute_salinity,
conservative_temperature, sigma0.
Variable resolution
Functions accept both the canonical CCHDO names (ctd_temperature,
ctd_salinity, ctd_oxygen) and the suffixed dual-sensor names
(ctd_temperature_1, ctd_salinity_1, ctd_oxygen_1). Old
pre-rename names (temperature_1, salinity_1, oxygen_1)
are accepted for backward compatibility with NC files written before
the stage1-normalise rename.
- ctdcast.analysis.derive.derive_AOU(ds: Dataset) Dataset[source]
Return ds with AOU added as 100 - oxygen_saturation (O₂ saturation deficit, % sat).
Note: this is a saturation-deficit proxy, not the traditional AOU in µmol/kg, because it uses
oxygen_saturation(% saturation) rather than dissolved O₂ in µmol/kg.Returns ds unchanged if no oxygen saturation variable is present or
AOUalready exists. Acceptsoxygen_saturation(canonical) oroxsat_1(pre-rename name).- Parameters:
ds – Dataset (any dimensionality) with
oxygen_saturationoroxsat_1in % saturation.- Returns:
New Dataset with
AOUadded; input is not mutated.- Return type:
xr.Dataset
- ctdcast.analysis.derive.derive_CT(ds: Dataset) Dataset[source]
Return ds with Conservative Temperature (CT) added.
Requires
ds["absolute_salinity"]to already be present (callderive_SA()first). Usesgsw.CT_from_twith the first available temperature variable (ctd_temperature,ctd_temperature_1, ortemperature_1) andpressure.- Parameters:
ds – Per-cast Dataset (dim=time) with
absolute_salinity, a temperature variable, andpressure.- Returns:
New Dataset with
ds["conservative_temperature"]added; input is not mutated.- Return type:
xr.Dataset
- ctdcast.analysis.derive.derive_SA(ds: Dataset) Dataset[source]
Return ds with Absolute Salinity (SA) added.
Uses
gsw.SA_from_SPwith the first available salinity variable (ctd_salinity,ctd_salinity_1, orsalinity_1),pressure(dbar), and the cast’s median latitude/longitude.- Parameters:
ds – Per-cast Dataset (dim=time) with a salinity variable,
pressure,latitude,longitude.- Returns:
New Dataset with
ds["absolute_salinity"]added; input is not mutated.- Return type:
xr.Dataset
- ctdcast.analysis.derive.derive_salinity(ds: Dataset) Dataset[source]
Re-compute practical salinity from conductivity, temperature, pressure.
Uses
gsw.SP_from_C, which expects conductivity in mS/cm. Stored conductivity is mS/cm since stage1; a file still carrying S/m (unitsnot mS/cm) is converted by ×10. Call this after any conductivity calibration so that salinity reflects the calibrated conductivity.Does nothing if
conductivity_1or any temperature variable is absent.Writes output to
ctd_salinity_1/ctd_salinity_2(CCHDO canonical names). Records the conversion method in the variable’s attrs.- Parameters:
ds – Per-cast Dataset (dim=time) containing at minimum
conductivity_1, a temperature variable, andpressurein their expected units (conductivity in mS/cm, temperature in °C ITS-90, pressure in dbar).- Returns:
New Dataset with updated
ctd_salinity_1(andctd_salinity_2whenconductivity_2is present); input is not mutated.- Return type:
xr.Dataset
- ctdcast.analysis.derive.derive_sigma0(ds: Dataset) Dataset[source]
Return ds with potential density anomaly (sigma0) added.
Requires
ds["absolute_salinity"]andds["conservative_temperature"]to already be present. Usesgsw.sigma0.- Parameters:
ds – Per-cast Dataset (dim=time) with
absolute_salinityandconservative_temperature.- Returns:
New Dataset with
ds["sigma0"]added; input is not mutated.- Return type:
xr.Dataset
- ctdcast.analysis.derive.derive_teos10(ds: Dataset) Dataset[source]
Return ds with SA, CT, sigma0 added (1-D per-cast Dataset, dim=time).
Convenience function that calls
derive_SA()→derive_CT()→derive_sigma0()in order. Also derivesoxygen_saturation(% sat) from the first available oxygen variable (ctd_oxygen,ctd_oxygen_1, oroxygen_1) when that variable carries molar units.- Parameters:
ds – Per-cast Dataset (dim=time) with a salinity variable, a temperature variable,
pressure,latitude,longitude.- Returns:
New Dataset with SA, CT, sigma0 added; input is not mutated.
- Return type:
xr.Dataset
- ctdcast.analysis.derive.derive_teos10_profiles(ds: Dataset) Dataset[source]
Return ds with SA, CT, sigma0 added (2-D profiles Dataset).
Expects
pressureas a 1-D coordinate and a temperature variable, a salinity variable,latitude,longitudewith dims(N_PROF,)or(N_PROF, pressure). Returns ds unchanged if SA, CT, and sigma0 are already present.- Parameters:
ds – Profiles Dataset (dims N_PROF × pressure).
- Returns:
New Dataset with SA, CT, sigma0 added; input is not mutated.
- Return type:
xr.Dataset
Cast geometry: along-track distance and section orientation.
Pure computation — no matplotlib, no HTML.
- ctdcast.analysis.geometry.along_track_km(lats: list[float], lons: list[float]) tuple[ndarray, str][source]
Return (cumulative_distance_km, x_axis_label) for a list of positions.
- ctdcast.analysis.geometry.distance_from_km(key_lat: float, key_lon: float, lats: list[float], lons: list[float]) ndarray[source]
Return great-circle distance in km from a key position to each position.
Used for
key_castsection ordering: each cast’s x-coordinate is its distance from the chosen key cast. Usesgsw.distanceper pair so the convention matchesalong_track_km(). A position identical to the key yields 0. Agsw.distancefailure is allowed to propagate rather than being silently substituted with a fabricated distance.
- ctdcast.analysis.geometry.section_orientation(lats: list[float], lons: list[float]) bool[source]
Return True if the section x-axis should be flipped for geographic convention.
Convention: west on the left for E–W-dominant sections; north on the left for N–S-dominant sections. Dominance is determined by comparing the end-to-end longitude span against the latitude span.
- Parameters:
lats – Latitude of each cast in the section, in cast order.
lons – Longitude of each cast in the section, in cast order.
- Returns:
True if
x_vals(cumulative along-track distance from first cast) should be replaced byx_total - x_valsbefore plotting.- Return type:
bool
GEBCO bathymetry loading and interpolation.
Pure computation — no matplotlib, no HTML. GEBCO stores elevation as negative
below sea level; this module returns depth as positive below sea level
(depth = -elevation).
- ctdcast.analysis.bathymetry.dense_bathy_along_track(lats: list[float], lons: list[float], x_vals: ndarray, path: Path | None = None, n_per_segment: int = 20) tuple[ndarray | None, ndarray | None][source]
Return
(dense_x, dense_depths)interpolated between cast positions.Generates n_per_segment equally-spaced points along each segment between consecutive casts, giving a smooth GEBCO bathymetry fill rather than the stepped appearance produced by one sample per cast. Returns
(None, None)when GEBCO is unavailable or fewer than two cast positions are supplied.
- ctdcast.analysis.bathymetry.interpolate_bathy_at_casts(lats: list[float], lons: list[float], path: Path | None = None) ndarray | None[source]
Return GEBCO water depth (m, positive below sea level) at each cast position.
Uses bilinear interpolation via xarray. Returns
Noneif GEBCO is not available or on any error. Land points (elevation > 0) are clamped to 0.
- ctdcast.analysis.bathymetry.load_gebco(lat_lo: float, lat_hi: float, lon_lo: float, lon_hi: float, margin: float = 0.05, path: Path | None = None) tuple[ndarray, ndarray, ndarray] | None[source]
Return a GEBCO subset as (lons, lats, depth_m) or None if unavailable.
If
preload_gebcohas been called for path, subsets from the in-memory numpy cache (fast). Otherwise opens the file from disk (slow).- Parameters:
path – Path to GEBCO_2025.nc. Pass
cfg.gebco_pathfrom the caller. Returns None if not provided or file not found.
- ctdcast.analysis.bathymetry.preload_gebco(path: Path, lat_lo: float, lat_hi: float, lon_lo: float, lon_hi: float, margin: float = 1.0) bool[source]
Load a GEBCO region into memory once for the cruise area.
Call this at report-generation start with the full lat/lon extent of all casts. Subsequent
load_gebcocalls then subset from numpy arrays (no disk I/O) instead of reopening the file for every map figure.- Parameters:
path – Path to GEBCO netCDF file.
lat_lo – Bounding box of the cruise area.
lat_hi – Bounding box of the cruise area.
lon_lo – Bounding box of the cruise area.
lon_hi – Bounding box of the cruise area.
margin – Extra degrees around the bounding box. Default 1.0 deg.
- Returns:
True if the file was found and cached successfully.
- Return type:
bool
Cast processing
Stage 1 — CNV-to-netCDF conversion.
Defines the CtdBackend Protocol, concrete backend implementations, and
stage1(), the public function that converts a directory of CNV files
to per-cast netCDF. To add a new backend implement CtdBackend and add a
branch in get_ctd_backend() — nothing else changes.
The converters module re-exports these names for backward compatibility.
- class ctdcast.processors.stage1.CtdBackend(*args, **kwargs)[source]
Protocol for per-cast CNV-to-netCDF converters.
- convert_cast(cnv_path: Path, nc_path: Path, *, force: bool = False) bool[source]
Convert one CNV file to netCDF.
- Parameters:
cnv_path – Path to the raw SBE CNV input file.
nc_path – Desired output netCDF path.
force – If True, overwrite an existing nc_path.
- Returns:
True if the file was written; False if skipped.
- Return type:
bool
- ctdcast.processors.stage1.get_ctd_backend(name: str) CtdBackend[source]
Return a CtdBackend instance for the given backend name.
- Parameters:
name – Currently only
"seasenselib".- Raises:
ValueError – If name is not a recognised backend.
ImportError – If the requested backend’s package is not installed.
- ctdcast.processors.stage1.run(cnv_dir: Path, nc_dir: Path, *, force: bool = False, dry_run: bool = False, cast_tags: set[str] | None = None, **kw: object) int[source]
Run stage1 (CNV → netCDF) for explicit input and output directories.
Called by
ctdcast.processors.process()withstage=1orstage="stage1".- Parameters:
cnv_dir – Directory containing raw SBE CNV files.
nc_dir – Output directory for per-cast netCDF files (created if absent).
force – Overwrite existing NC files.
dry_run – Print what would be converted without writing any files.
cast_tags – If given, process only files whose stem contains one of the zero-padded 3-digit cast numbers (e.g.
{"042", "043"}).**kw – Passed to
stage1()(e.g.backend,pattern).
- Returns:
Number of files written (0 for dry_run).
- Return type:
int
- ctdcast.processors.stage1.stage1(cnv_dir: Path, nc_dir: Path, *, backend: str = 'seasenselib', force: bool = False, cast_filter: int | list[int] | None = None, pattern: str = '*.cnv') int[source]
Convert per-cast CNV files to netCDF using the specified backend.
- Parameters:
cnv_dir – Directory containing raw SBE CNV files.
nc_dir – Output directory for per-cast netCDF files (created if absent).
backend – Backend name (currently only
"seasenselib").force – Overwrite existing netCDF files.
cast_filter – If given, convert only files whose stem contains the zero-padded cast number. Accepts a single int or a list of ints for multi-cast filtering.
pattern – Filename glob pattern applied within
cnv_dir(default:"*.cnv").
- Returns:
Number of files written (skipped files not counted).
- Return type:
int
- Raises:
ImportError – If the chosen backend’s package is not installed.
Stage 2 — trim.
Downcast/upcast splitting and soak / back-on-deck detection. This is processing, not analysis: it decides which scans belong to the real cast.
apply_stage2() is the pipeline entry point: it sets QARTOD flag 4 on soak
and post-recovery deck records and records the parameters used in
ds.attrs["history"]. find_soak_end and find_cast_end are kept public
because reports._cast calls them directly for report-time plot trimming (that
coupling will be removed in a later phase when reports read flags from the files).
The turnaround convention (last pressure within 2 dbar of the maximum) and the soak/deck algorithms are deliberate — see the individual docstrings.
- ctdcast.processors.stage2.apply_stage2(ds: Dataset, *, near_surface_dbar: float = 10.0, search_seconds: float = 20.0, deck_window_seconds: float = 20.0, margin_dbar: float = 0.5, max_deck_dbar: float = 20.0) Dataset[source]
Apply QARTOD flag 4 to soak and post-recovery deck records.
Creates
{var}_qcarrays (int8, flag 1=pass) for each physical data variable, then sets flag 4 (fail) on the pre-descent soak window and the post-recovery on-deck window. Records the parameters used inds.attrs["history"]so the treatment is reproducible from the output file alone.- Parameters:
ds – Per-cast Dataset (dim=time).
near_surface_dbar – Passed to
find_soak_end— pressure threshold for last near-surface crossing before the real descent.search_seconds – Passed to
find_soak_end— backward-crawl window width.deck_window_seconds – Passed to
find_cast_end— tail window for on-deck reference pressure.margin_dbar – Passed to
find_cast_end— added to on-deck median to form the cut.max_deck_dbar – Passed to
find_cast_end— if on-deck median exceeds this, no trim.
- Returns:
New Dataset; input is not mutated.
- Return type:
xr.Dataset
- ctdcast.processors.stage2.find_cast_end(pressure: ndarray, times: ndarray, deck_window_seconds: float = 20.0, margin_dbar: float = 0.5, max_deck_dbar: float = 20.0) int[source]
Return the exclusive end index, trimming post-recovery deck records.
Algorithm:
Take the median pressure of the last deck_window_seconds seconds as the on-deck reference pressure. Using a median handles sensor offset (the pressure sensor may not read exactly 0 dbar when the CTD is in the air) and is robust to brief oscillations on deck.
Find the first index after the pressure maximum where pressure falls at or below
p_deck_median + margin_dbar. Trim from that index onward.
Returns
len(pressure)(no trim) if:the record is empty or the CTD never returned near the surface (
p_deck_median > max_deck_dbar), orpressure never drops to the threshold on the upcast.
- Parameters:
pressure – Pressure array in dbar.
times – Time coordinate array (
numpy.datetime64or numeric seconds).deck_window_seconds – Duration of the tail window used to estimate on-deck pressure.
margin_dbar – Added to the on-deck median to form the cut threshold.
max_deck_dbar – If the on-deck median exceeds this value the CTD is considered not to have returned to the surface and no trim is applied.
- Returns:
Exclusive end index; slice with
ds.isel(time=slice(None, idx)).- Return type:
int
- ctdcast.processors.stage2.find_soak_end(pressure: ndarray, times: ndarray, near_surface_dbar: float = 10.0, search_seconds: float = 20.0) int[source]
Return the index at which the real downcast begins (exclusive end of soak).
Algorithm (three steps):
Find
i_max, the index of the global pressure maximum (deepest point of the cast). Searching only inpressure[0:i_max+1]keeps the upcast recovery — when the CTD returns to the surface at the end of the cast — from being confused with the pre-soak position.Within
pressure[0:i_max+1], find the last index wherepressure < near_surface_dbar. For a typical MSM-style cast this falls on the early real descent, just as the CTD passesnear_surface_dbargoing downward.Crawl backward from that index within
search_secondsto find the minimum pressure — the shallowest point (closest to the surface) just before the real descent began. Return the index immediately after that minimum as the start of the real downcast.
In bad-weather conditions where the CTD soaks at depth and is never raised back to the surface, step 3 finds the minimum within the soak window and removes only the first
search_secondsof the soak. The operator- visible effect is a truncation of the pre-soak data, not a clean removal.- Parameters:
pressure – Pressure array in dbar (1-D, same length as times).
times – Time coordinate array. May be
numpy.datetime64or numeric seconds; elapsed time is computed relative totimes[0].near_surface_dbar – Pressure threshold used to find the last near-surface crossing before the main descent. Default is 10 dbar (≈10 m), safely below the typical soak depth of 8–10 m.
search_seconds – Width of the backward-crawl window (seconds) used to find the pre-descent surface minimum. Default is 20 s.
- Returns:
Index of the first record to keep; slice with
ds.isel(time=slice(idx, None)). Returns 0 if the cast never reaches belownear_surface_dbar(no trim applied).- Return type:
int
- ctdcast.processors.stage2.run(nc_dir: Path, *, force: bool = False, dry_run: bool = False, cast_tags: set[str] | None = None, **kw: object) int[source]
Apply stage2 (soak/deck flagging) to NC files in nc_dir.
Reads each
*.ncfile, appliesapply_stage2(), and writes the result back in place usingctdcast.writers.netcdf.write(). Called byctdcast.processors.process()withstage=2orstage="stage2".- Parameters:
nc_dir – Directory of per-cast netCDF files (read and written in place).
force – Reprocess files that already carry
_qcflag variables. Withoutforce, already-flagged files are skipped.dry_run – Print which files would be processed without writing any output.
cast_tags – If given, process only files whose stem contains one of the zero-padded 3-digit cast numbers (e.g.
{"042", "043"}).**kw – Passed to
apply_stage2()(e.g.near_surface_dbar).
- Returns:
Number of files written (0 for dry_run).
- Return type:
int
- Raises:
FileNotFoundError – If nc_dir does not exist or is not a directory.
- ctdcast.processors.stage2.split_cast(ds: Dataset) tuple[Dataset, Dataset][source]
Split ds (individual cast file, dim=time) into (downcast, upcast).
Uses the turnaround convention: last index where pressure is within 2 dbar of its maximum.
Stage 3 — QC, calibration, and derived-variable orchestrator.
Stage3 is iterative: re-run it as calibration improves. Each run applies gross-range QC, then any conductivity calibration present in the cruise config, then re-derives salinity from the calibrated conductivity.
Sea-Bird processing-chain calibration (hex-level, frequency coefficients) is Phase 5 scope; this module covers only the post-conversion treatment.
- ctdcast.processors.stage3.run(nc_dir: Path, *, force: bool = False, dry_run: bool = False, cast_tags: set[str] | None = None, **kw: object) int[source]
Apply stage3 (QC + calibration) to NC files in nc_dir.
Reads each
*.ncfile, appliesstage3(), and writes the result back in place usingctdcast.writers.netcdf.write(). Called byctdcast.processors.process()withstage=3orstage="stage3".- Parameters:
nc_dir – Directory of per-cast netCDF files (read and written in place).
force – Reprocess files that already carry QC flag variables. Without
force, already-QC’d files are skipped.dry_run – Print which files would be processed without writing any output.
cast_tags – If given, process only files whose stem contains one of the zero-padded 3-digit cast numbers (e.g.
{"042", "043"}).**kw – Passed to
stage3()(e.g.cruise_cfg).
- Returns:
Number of files written (0 for dry_run).
- Return type:
int
- Raises:
FileNotFoundError – If nc_dir does not exist or is not a directory.
- ctdcast.processors.stage3.stage3(ds: Dataset, cruise_cfg: dict | None = None) Dataset[source]
Apply QC and calibration to a per-cast Dataset.
Applies the following in order:
Gross-range QC (
qc.apply_gross_range), usingGROSS_RANGE_DEFAULTSmerged with any overrides fromcruise_cfg["qc"]["gross_range"].Conductivity calibration slope, if
cruise_cfg["calibration"]["conductivity_slope"]is present. Multipliesconductivity_1(andconductivity_2if present) by the slope and records it in the variable’s attributes.Re-derives
salinity_1(andsalinity_2) from the calibrated conductivity usingderive_salinity()— only when a conductivity calibration was applied.
- Parameters:
ds – Per-cast Dataset (dim=time), already through stage1 and stage2.
cruise_cfg – Optional dict with sub-keys
qcandcalibration. Passcfg.get("processing")or the full cruise config dict.
- Returns:
New Dataset; input is not mutated.
- Return type:
xr.Dataset
Stage QC — gross-range flagging.
Sets QARTOD flag 3 (suspect) on any record outside the configured
physical range. Operates on per-cast Datasets (dim=time); call after
apply_stage2 so the flag arrays already exist.
- ctdcast.processors.qc.apply_gross_range(ds: Dataset, thresholds: dict[str, tuple[float, float]] | None = None) Dataset[source]
Set QARTOD flag 3 on records outside gross-range bounds.
Creates
{var}_qcarrays (int8, initialised 1=pass) if they do not already exist. MergesGROSS_RANGE_DEFAULTSwith any caller-suppliedthresholds; caller wins per variable. Records each threshold used inds.attrs["history"].- Parameters:
ds – Per-cast Dataset (dim=time). Modified variables are those present in both
dsand the merged threshold dict.thresholds – Per-variable overrides:
{"ctd_salinity_1": (30.0, 40.0)}. Caller-supplied values replace the defaults for that variable.
- Returns:
New Dataset; input is not mutated.
- Return type:
xr.Dataset
Cruise-level profile compiler: per-cast netCDF → profiles.nc.
Reads all per-cast netCDF files in a directory, splits each into downcast and
upcast halves, bins to a common 1-dbar grid, and writes a single
(N_PROF × pressure) netCDF. The converters module re-exports
build_profiles for backward compatibility.
- ctdcast.processors.profiles.build_profiles(nc_dir: Path, profiles_path: Path, *, force: bool = False, gebco_path: Path | None = None) bool[source]
Compile per-cast netCDF files into a single profiles.nc on a 1-dbar grid.
Reads all
*.ncfiles in nc_dir, splits each cast into downcast and upcast halves, bins to a common 1-dbar pressure grid, and writes a single (N_PROF × pressure) netCDF. N_PROF is a plain integer index (0, 1, 2, …); cast identity is carried bycast_number,cast_suffix, andcast_directionvariables.Per-cast scalar variables added to the output:
max_pressure_dbar— maximum pressure recorded over the full cast.gebco_depth_m— GEBCO bathymetry depth (m, positive down) at the max-pressure lat/lon position; NaN when gebco_path is None or the file is unavailable.
The
altimeterchannel (when present in the input files) is binned onto the 1-dbar grid as a standard 2-D variable.- Parameters:
nc_dir – Directory containing per-cast netCDF files.
profiles_path – Output path for the compiled profiles netCDF.
force – Overwrite an existing profiles_path.
gebco_path – Path to a GEBCO_2025.nc file. Used to look up water depth at each cast’s max-pressure position. Pass
cfg.gebco_pathwhen calling from report generation code. Silently omitted when None.
- Returns:
True if profiles.nc was written; False if skipped (existed, force=False).
- Return type:
bool
- Raises:
ValueError – If no recognised cast files are found in nc_dir.
- ctdcast.processors.profiles.run(nc_dir: Path, profiles_path: Path, *, force: bool = False, dry_run: bool = False, **kw: object) bool[source]
Build
profiles.ncfrom NC files in nc_dir.Called by
ctdcast.processors.process()withstage="profiles".- Parameters:
nc_dir – Directory of per-cast netCDF files.
profiles_path – Output path for the compiled profiles netCDF.
force – Overwrite an existing profiles.nc.
dry_run – Print what would be built without writing any output.
**kw – Passed to
build_profiles()(e.g.gebco_path).
- Returns:
True if profiles.nc was written; False if skipped (or dry_run).
- Return type:
bool
Readers
Reader for LDEO IXv14 LADCP .mat files.
Locates the .mat file for a cast (find_ladcp_file()), loads it with a
single set of scipy.io.loadmat options (read_ladcp()), and maps the
result struct to a single-cast xarray.Dataset on the native 10 m depth
grid (read_ladcp_cast()). The .mat is the LDEO IX velocity solution;
its ~50 fields are mapped to the compiled-dataset schema in
.claude/notes/2026-08-17-ladcp-compiled-dataset.md.
- ctdcast.readers.ladcp.find_ladcp_file(ladcp_dir: Path, cast_num: int, cast_suffix: str = '', ladcp_pattern: str | None = None) Path | None[source]
Return the .mat file for cast_num in ladcp_dir, or
Noneif absent.If ladcp_pattern is given (e.g.
"msm_142_1_*.mat"), the*wildcard is replaced with the zero-padded cast number (and optional suffix) and that name is tried first. Falls back to standard names (NNN.mat,NNNb.mat) then a*_NNN.matglob for cruise-prefixed filenames. The first glob match (lexicographic) is returned when multiple files match.
- ctdcast.readers.ladcp.read_ladcp(path: Path | str) dict[str, Any][source]
Load an LDEO IXv14 LADCP
.matfile.Uses
squeeze_me=Trueandstruct_as_record=Falseso the LADCP result struct is reachable asread_ladcp(path)["dr"]with attribute access.
- ctdcast.readers.ladcp.read_ladcp_cast(path: Path | str, *, cast_num: int, cast_suffix: str = '') Dataset[source]
Map an LDEO LADCP
.matto a single-cast Dataset on the native 10 m grid.Returns a Dataset with dimension
depth(uniform 10 m, positive down), an auxiliarypressure(depth)coordinate (dbar, the bridge toprofiles.nc), the inverse (u/v) and shear (u_shear/v_shear/w_shear) velocity solutions, per-instrument down/up-looker profiles, per-cast scalars (barotropic, bottom-track, position,instrument_config), and the LADCP processing provenance as global attributes. Velocity carries the CFeastward/northward/error_sea_water_velocitystandard names.
Reader for per-cast sensor metadata written by seasenselib.
Parses the raw_metadata global attribute (a JSON blob) into a list of sensor
descriptors for the cast page.
- ctdcast.readers.metadata.parse_sensor_info(ds: Dataset) list[dict[str, str]][source]
Extract sensor serial numbers and calibration dates from ds.
Parses the
raw_metadataglobal attribute (a JSON string written by seasenselib) and returns one entry per sensor channel that has both asensor_typeand aserial_number.- Parameters:
ds – Per-cast Dataset as opened from a netCDF file.
- Returns:
Each dict has keys
sensor_type(human-readable label),serial_number, andcalibration_date. Returns[]ifraw_metadatais absent, unparseable, or contains no usable sensors.- Return type:
list[dict[str, str]]