What’s New#
v0.5.3 - 7 September 2026#
Fixed invalid PyPI classifiers blocking publication#
'Topic :: Scientific/Engineering :: Remote Sensing' and 'Topic :: Scientific/Engineering :: Marine Science' are not real Trove classifiers, which
caused the v0.5.2 PyPI upload to fail with a 400 error before anything was
published. Replaced with valid equivalents: Oceanography, Hydrology, and the
existing GIS.
v0.5.2 - 26 August 2026#
SPM mass column renamed and now reported in tonnes; new sum_SPM_in_the_plume_area column#
Results.csv’s mass column is renamed from mass_SPM_in_the_plume_area_in_g_m
(kilograms, despite the name) to mass_SPM_in_the_plume_area_in_t (tonnes),
and is now computed from a new sum_SPM_in_the_plume_area column (raw
pixel-wise sum of SPM concentration) rather than the mean, with every unit
conversion step (pixel area, an assumed 1 m water-column depth, g→t)
explicit and documented in code. Cloud-gap pixels within the plume mask are
no longer imputed from the mean of the observed pixels; they now contribute
zero to the mass total. This is a breaking change to the Results.csv
schema and to the numeric values of the mass column on any day with
cloud-gap pixels; see CHANGES.md for full detail.
Test coverage restored to ~99%, and a CI gap fixed#
Codecov’s coverage check had dropped below the project’s usual threshold. The main cause was less
a real regression and more a blind spot: testing/test_plume_algorithm.py had never actually been
wired into the CI test run, and the multithreaded scene-loading path and the new
plotting diagnostics (both added in v0.5.1) had shipped with comparatively little dedicated test coverage of
their own. Both gaps are now closed — CI runs the previously-omitted test file, and around 140 new
unit and rendering tests were added covering runner.py’s multi-core loading and error-handling
paths, several flood_fill / find_SPM_threshold / remove_post_shrink_widening /
remove_close_river_mouth algorithm edge cases that had no prior tests at all, and the new
plotting overlays in make_the_plot. Overall coverage is now around 99%.
Bug fix: extract_time_from_nc_file never actually read SEXTANT time attributes#
The image_reference_time and DSD_entry_id branches read map_data._attrs, a private,
implementation-specific attribute, instead of the public .attrs mapping. On the xarray version
this package targets, _attrs doesn’t exist, so both branches always raised AttributeError
before a time string could be returned. Fixed to use .attrs.
Cleanup: removed defensive code for dependencies that are always installed#
psutil, geopandas, multiprocess, and bathyreq are declared as required dependencies in
pyproject.toml, not optional ones, so the try/except ImportError fallbacks around their
imports in runner.py and utils.py, along with the is None guards that assumed those imports
could silently fail, could never actually trigger. Both are now plain imports with no fallback
branches or dead guard clauses. A separately dead branch in compute_directional_gradients
(plume_algorithm.py), for a 3-dimensional input array that an earlier line in the same function
already makes impossible, was also removed.
v0.5.1 - 11 August 2026#
New: dynamic-threshold and resuspension-filter diagnostics on the plume detection panel#
The right-hand (“Plume detection”) panel of every per-timestep PNG map, and therefore every GIF
frame, now shows the internal steps that produce each river’s plume mask, not just the final
result. Per river mouth: every pixel sampled by the dynamic-threshold gradient search is drawn as a
small grey dot, with the subset actually retained for the threshold calculation highlighted in that
river’s colour with a black outline; a dashed circle marks the resuspension filter’s distance limit
from the plume core (minimal_distance_from_estuary_for_zone_with_resuspension); a contour line
marks its bathymetry limit (maximal_bathymetric_for_zone_with_resuspension); and pixels the
resuspension filter actually removed are shaded in the river’s own colour with a hatch pattern. The
secondary-channel exclusion mask (river_mouth_to_exclude) is now rendered too: make_the_plot
already supported drawing it, but main_process never passed the computed mask through.
All of these render regardless of whether a plume was found that day, since that is exactly when this information is most useful for understanding why not. No configuration changes are required: all new information is derived from parameters and intermediate results the pipeline already computes.
Bug fix: flood fill could not follow a plume that curved back on itself#
Each river’s plume detection grows outward from the river mouth using a “fan” of 5 of the 8
possible directions (for example southward_fan), chosen per river to stop the flood fill
spilling backward into the estuary. This also meant that a plume genuinely curving back against
its configured fan, such as the Grand Rhone’s plume bending north after extending west and south,
had that portion of its extent cut off by an artificial straight edge instead of following the
true shape of the signal. The flood fill now grows in all 8 directions from the seed point; the
existing area-polygon, core-shape-selection, and resuspension/shallow-water filters still contain
its growth exactly as before. This replaces the inverted-fan second flood fill introduced in
v0.2.x, which only partially compensated for the same limitation.
Bug fix: cloud-gap pixels inside a removed resuspension patch stayed in the plume mask#
A cloud gap enclosed by real high-SPM pixels is promoted to “plume” before the resuspension
filter runs. That filter, which removes shallow water far from the river mouth, tested each pixel
against its raw SPM value, a test a cloud gap can never pass, so a promoted gap sitting inside an
otherwise-removed resuspension patch survived as an untouched pixel in Results.csv and
PlumeMasks.nc. The filter now tests against the current plume mask instead, so a cloud-filled
gap is removed together with the real pixels around it.
v0.5.0 - 10 August 2026#
Breaking change: Results.csv and PlumeMasks.nc now report per-river plume statistics#
Every zone tracks 2-3 rivers, and until now their independently-detected plume masks were silently unioned into a single combined mask before any statistic was computed or written out — there was no way to tell how much of a day’s detected plume area belonged to which river mouth.
Results.csv now has one row per (date, river), plus one additional river='ALL' row per date
carrying the previous combined-mask (union) statistics, so a zone total remains available without
re-deriving it downstream. Because each river’s flood fill is entirely independent, two rivers’
masks can legitimately overlap; a new n_pixel_shared_with_other_rivers column makes that overlap
auditable rather than silently double-counted if the per-river rows are summed. The previous wide
SPM_threshold_<rivername> columns (one per river, on every row) collapse into a single
SPM_threshold column (that row’s own river; NaN on the ALL row).
PlumeMasks.nc gains a river dimension — plume_mask is now (time, river, lat, lon), with one
boolean layer per river plus an 'ALL' union layer. The file is now written with zlib compression
by default (masks are mostly zeros, so this substantially offsets the size increase from the extra
dimension); the compression level is configurable via the new mask_compression_level config key
(set to null to disable compression entirely).
Per-timestep PNG maps (and the GIF assembled from them) now render each river’s plume mask in its own shade of red, with a legend at the bottom of the figure identifying which shade belongs to which river, instead of one flat red region for the whole zone.
'ALL' is now a reserved river name; load_run_config raises if a configured river is named
ALL. Existing Results.csv/PlumeMasks.nc outputs from before this change are not migrated —
re-run with overwrite: true to regenerate them under the new schema.
v0.4.3 — 4 August 2026#
Tweaks to default zone internal parametres#
The four zones that ship with panache do not all behave similarly. As anticipated. This does however require that each is treated with slightly different internal parametres. These tweaks have been made and tested against the full Sextant (1998 - 2025) dataset.
v0.4.2 — 3 August 2026#
Shortened overly long function and method names#
Many function and method names in plume_algorithm.py (and a few in utils.py,
runner.py, config.py) were written as full sentences, some over 80 characters
long (e.g. remove_parts_of_the_plume_area_with_very_high_SPM_on_the_edge_of_the_searching_zone).
The 26 worst offenders (names of 30 or more characters) were shortened to concise
verb-object identifiers; the explanatory detail dropped from each name stays in
that function’s docstring, so nothing was lost, just moved to where it already
belonged. Three further names that broke snake_case with a capitalized first word
(Set_cloudy_regions_to_True, Check_if_the_area_is_too_cloudy,
Pipeline_to_delineate_the_plume), easily mistaken for class names, were fixed to
proper lowercase form at the same time. This is a pure rename with no behavioural
change: the full unit test suite (233 tests) and the smoke test pipeline were run
afterwards to confirm the package still works end to end.
v0.4.1 — 17 July 2026#
Performance: each input file is now read from disk exactly once#
Previously, a full batch run could read every input file from disk two or
three times: once while pre-computing the SPM threshold and colourbar limits
from the full dataset, and again inside main_process for each scene’s
detection pass. runner.py now loads every valid input file into memory a
single time (_load_all_scenes), before the batch starts, and reuses that
same in-memory xr.DataArray for threshold/colourbar pre-computation and for
per-scene plume detection — including under nb_cores > 1, where the
already-loaded array is handed to the worker process instead of a file path.
Breaking change: removed lat_new_resolution / lon_new_resolution#
The optional resolution-downsampling feature (lat_new_resolution and
lon_new_resolution config keys, and the reduce_resolution coarsening step)
has been removed. panache now always operates at the native resolution of
the input product. Configs that still set these keys can simply drop them;
they are no longer read.
Bug fix: PlumeMasks.nc assembly crash on large batches#
Assembling PlumeMasks.nc at the end of a run could raise
ValueError: 'date_for_plot' not present in all datasets and coords='different'
on large batches (observed on a 10,000+ file run). Each scene’s mask carried a
date_for_plot scalar coordinate left over from the source SPM map, which is
redundant with the time coordinate assigned to the mask itself; xarray’s
xr.concat only complains when that coordinate has differing values on some
scenes and is absent on others, which is data-dependent and did not surface on
small test batches. _mask_with_time now drops date_for_plot from every
scene’s mask before it is returned, so the coordinate can never reach the
final xr.concat.
Per-scene log messages now report the scene date, not the input filename#
Batch progress lines ([n/N] Completed ...) and the “too cloudy” / “no plume
detected” skip messages now print the scene’s date (e.g. 2024-01-01) rather
than the input file’s name.
v0.4.0 — 17 July 2026#
New: PlumeMasks.nc — aggregated daily pixel masks#
Each panache run now writes a PlumeMasks.nc file alongside Results.csv
in the output directory. The file stores the per-pixel plume mask for every
processed day as a three-dimensional (time, lat, lon) array of int8 values
(0 = no plume, 1 = plume). Days on which the area was too cloudy or no plume
was detected are represented as all-zero slices, so the time axis covers every
input file regardless of detection outcome.
When overwrite: false is set and a PlumeMasks.nc file already exists from
a prior run, the new batch results are merged into it by date, mirroring the
existing Results.csv resume logic.
This output makes it straightforward to use the detected plume footprint as a spatial mask on any co-registered gridded product — for example, to extract daily in-plume SPM concentration distributions from the original Sextant files without re-running the detection algorithm. See the new Using PlumeMasks.nc vignette on the documentation site for a worked example.
v0.3.5 — 17 July 2026#
Bug fix: static threshold pipeline accessing non-existent dynamic arguments#
When dynamic_threshold: false, the pipeline in runner.py was still attempting
to read per-plume near-mouth threshold bounds that are only computed during a
dynamic-threshold run, raising a KeyError on any static-threshold batch.
The call has been removed from the static path and the corresponding dead branch
in plume_algorithm.py has been pruned.
Bug fix: date parsed from filename before internal NetCDF metadata#
io.py now extracts the scene date from the filename first and falls back to
the time coordinate inside the NetCDF only when no date is recoverable from
the name. This fixes processing of the full 2005 Sextant SPM archive, where an
upstream data error caused every file’s internal time coordinate to reference
an incorrect date while the filename remained correct.
Documentation site: workflow vignette#
A new Workflow Vignette page has been added to the documentation site, showing real L4 SPM plume-detection outputs for 1 January 2024 (winter) and 1 July 2024 (summer) across all four built-in zones (Bay of Seine, Bay of Biscay, Gulf of Lion, Southern Brittany). Each zone is illustrated under both the dynamic gradient-based threshold and the global p95 threshold, placed side by side using two-column card layouts so the sensitivity difference between the two methods is immediately visible.
Documentation site: theme switcher, README-driven index, codecov badge#
The documentation site has been updated with a light/dark theme toggle in the top navigation bar. The main landing page now mirrors the project README exactly, so any update to the README is automatically reflected on the site. The Codecov coverage badge is displayed on both the README and the documentation landing page.
v0.3.x — July 2026#
Stable release: four zones tested end-to-end#
The detection algorithm has been validated across all four built-in zone presets
(BAY_OF_SEINE, BAY_OF_BISCAY, GULF_OF_LION, SOUTHERN_BRITTANY) and the
Var estuary as an additional edge case. Bathymetry alignment, cloud screening,
threshold selection, and plume delineation all produce physically reasonable
output for these regions.
Codecov integration and 90% test coverage#
Continuous integration now uploads coverage reports to
Codecov on every push to main.
The test suite covers 90% of the source code, spanning unit tests for all major
pipeline components and a synthetic smoke test that runs the full pipeline
without real satellite data.
Documentation website#
A Sphinx documentation site (this site) is now deployed automatically from the
main branch via GitHub Actions. It includes installation instructions, a
quick-start guide, configuration reference, and a full API reference generated
from docstrings.
Bug fix: empty-slice warnings in find_SPM_threshold#
Three early-return guards were added to prevent np.nanquantile from receiving
an empty array. These edge cases arise when every transect pixel is clipped to
the same maximal_threshold (storm events with saturated SPM), when heavy cloud
cover breaks all gradient transects, or when the 90% steepness filter eliminates
every gradient point. In all three cases the function now returns
minimal_threshold immediately.
Bug fix: NaN start point in flood fill#
find_nearest_valid_start now relocates the BFS seed pixel to the nearest
finite neighbour when the configured starting_points coordinate falls on a
cloud-masked (NaN) pixel. Applies to both the dynamic-threshold transect origin
and the flood-fill entry point.
Bug fix: empty-mask statistics#
return_stats_dictionnary checks for finite SPM pixels within the plume mask
before calling np.nanmean / np.nanstd. When the detected area is entirely
cloud-covered, the function returns an empty-stats dict via make_an_empty_dict
rather than raising a warning.
New: river mouth labels and plume core markers on daily PNG maps#
Each PNG now shows a grey × at each core_of_the_plumes coordinate and a
labelled black + at each starting_points coordinate, with the river-mouth
name in a white annotation box. A subtitle legend at the lower-left explains
the two marker types.
New: global bounding-box threshold#
When global_threshold_quantile is set in the config, panache now loads the
full spatial bounding box for each input file to compute the quantile threshold,
rather than reading only the plume-detection sub-region. This produces a more
representative ambient-background threshold for the whole scene.
New: inverted-fan second flood fill#
Pipeline_to_delineate_the_plume performs a second BFS pass in the opposite
fan direction (e.g. southward_fan → northward_fan) and OR-merges the two
masks. This removes linear artifacts caused by the directional constraint of the
first pass.
New: five threshold parameters are now optional#
REQUIRED_PARAMETER_KEYS has been reduced from 15 to 10 keys. The following
five keys are now optional in a custom parameters block and are filled with
sensible defaults by build_parameters when absent: maximal_threshold,
minimal_threshold, quantile_to_use, fixed_threshold, and
river_mouth_to_exclude.
v0.2.x — June–July 2026#
input_path replaces input_glob / input_root#
The two previous input arguments input_glob and input_root have been merged
into a single input_path key. Pass a glob string (/data/spm/*.nc), a
directory path, or a single file path — panache figures out which it is.
Results compiled from all _statistics.csv files#
After processing, run_batch now searches all subdirectories of output_dir
for _statistics.csv files written in previous runs and merges them with the
current batch. This means interrupted runs can be resumed with overwrite: false
and the final Results.csv will always contain the full time series.
GIF is now optional#
Set "gif": false in the config to skip animated GIF generation. Useful for
large batches where GIF assembly adds significant wall-clock time.
Error handling for empty or malformed files#
Files that cannot be opened or that contain no valid data are now caught and
logged as Failed in manifest.csv rather than raising an unhandled exception
and aborting the batch.
Flood fill seed point improvements#
The BFS starting pixel can now be configured more intuitively. The algorithm accepts any water pixel near the river mouth and handles edge cases where the initial pixel sits at the boundary of the bathymetry mask.
v0.1.x — May 2026#
Initial release#
panache is a Python package for detecting river plumes in gridded satellite
data (SPM in NetCDF format). It implements a BFS flood-fill algorithm with
dynamic or fixed SPM thresholds, bathymetry-based land and cloud masking, and
directional fan constraints.
Key capabilities in the initial release:
Reads NetCDF files directly via
xarray; common SPM variable names are detected automatically.Two config modes:
zone(built-in presets) andparameters(fully custom).Four built-in zone presets:
BAY_OF_SEINE,BAY_OF_BISCAY,GULF_OF_LION,SOUTHERN_BRITTANY.Four fan directions:
northward_fan,southward_fan,eastward_fan,westward_fan.Three SPM threshold modes: dynamic (per-scene gradient), global quantile (dataset-wide), and fixed (per-plume constant).
Single-core and multi-core batch processing via
multiprocess.Outputs:
Results.csv,manifest.csv, per-file PNG maps, optional GIF.Automatic bathymetry download via
bathyreqwhen the pickle is absent.Installable from PyPI as
panache-riomar; CLI entry point ispanache.