Skip to content

Add SAL precipitation figures (S–A scatter and case-snapshot maps) to the publication workflow - #223

Draft
Louis-Frey wants to merge 62 commits into
mainfrom
paper/sal-figures
Draft

Add SAL precipitation figures (S–A scatter and case-snapshot maps) to the publication workflow#223
Louis-Frey wants to merge 62 commits into
mainfrom
paper/sal-figures

Conversation

@Louis-Frey

Copy link
Copy Markdown
Contributor

What this adds

This PR integrates the two SAL-based precipitation figures for the Varda paper as first-class figures in the publication-figures workflow: an S–A (Structure–Amplitude) scatter across the candidate and the ICON baselines, and a case-snapshot map panel (candidate forecast vs KENDA-CH1 analysis) for the selected cases. Both are driven exactly like the existing figures / scoremaps / meteogram figures — resolved from the manifest, rendered via python -m evalml.publication <figure>, and wired into publication_all behind per-figure enabled switches.

The SAL scores themselves come from the SAL verification core (compute-only) in the SAL-core PR (feat/MRB-921); this PR adds only the plotting and the config/manifest/CLI plumbing to consume it.

Status — draft / work in progress

Plumbing landed so far:

  • publication: config — PublicationSalScatterConfig and PublicationCaseSnapshotsConfig, plus coherence checks in validate_publication (gridded truth, cases within dates, producible lead times, matching experiment.sal params/lead times); schema regenerated.
  • Manifest — run_sal/baseline_sal path templates and a per-participant sal_template; experiment.sal settings carried into the manifest; schema version bumped to 2.
  • Resolver — Manifest.sal_path() and validate_request branches for both figures.
  • CLI — sal-scatter and case-snapshots subcommands.

Still to come (tracked in PLAN_PR2_sal_figures.md):

  • Figure scripts workflow/scripts/publication_sal_scatter.py (incl. the SAL-CSV reshaping adapter) and publication_case_snapshots.py.
  • Snakemake rules publication_sal_scatter / publication_case_snapshots and their wiring into publication_all.
  • experiment.sal + publication: blocks in config/varda-single_paper_analysis.yaml.
  • Unit tests (config, manifest, CSV adapter) and verification (schema/ruff/tests, --dry-run, standalone smoke test).

Relationship to the SAL-core PR and the base branch

This branch targets paper-figures and is stacked on it. Because the SAL core is not yet on main, this branch merges feat/MRB-921 in so the figures have data to consume. For review that means the diff currently includes the SAL-core changes — please review those in their own PR and focus here on the figure-specific commits. Once the SAL core lands on main and main flows into paper-figures, this branch will be rebased so its diff shows only the figure work (a one-time git rebase --onto if the core is squash-merged).

Provenance note

Both figures use the config's candidate Varda-Single (the 1-hourly temporal downscaler — the true Varda output). Scores and the exact case selection may shift slightly once the final model run is available; the selected cases are expected to remain valid and will be re-confirmed then.

clairemerker and others added 30 commits June 11, 2026 11:34
AI-assisted implementation!

This PR adds meteograms for publication, centralises some ploting style
things, uses long names for variables and tries out some colors :)

Any feedback and changes welcome!

To run only plotting: 
```
evalml publication config/Varda-Single_paper.yaml -- \
  --allowed-rules publication_all publication_meteogram publication_figures \
                  data_download_eckit_geo_grids \
  --forcerun publication_meteogram publication_figures
``` 

Current version of the plots:

<img width="2957" height="1685" alt="image"
src="https://github.com/user-attachments/assets/218f849e-4bd3-41d2-b19d-124dfe156e50"
/>
<img width="2335" height="2056" alt="image"
src="https://github.com/user-attachments/assets/25d1d544-996a-4adf-869c-54ae054cfcf8"
/>

---------

Co-authored-by: Jonas Bhend <jonas.bhend@meteoswiss.ch>
Co-authored-by: Daniele Nerini <daniele.nerini@meteoswiss.ch>
Co-authored-by: Michele Cattaneo <44707621+MicheleCattaneo@users.noreply.github.com>
Co-authored-by: Jonas Bhend <jonasbhend@users.noreply.github.com>
)

AI assisted implementation!

- Derive the jretrievedwh obs marker from --station inside the script
instead of passing a separate --obs argument. The marker value contains
'=' (e.g. jretrievedwh:locations=KLO), which marimo's cli_args drops, so
obs silently fell back to the KLO default and any non-KLO station was
broken (also via the snakemake rule). The rule no longer passes --obs.
- Import `time` in the entry cell and thread it through: the timing log
lines used time.perf_counter() but `time` was never imported (NameError
at runtime).
AI assisted implementation! 

Changes the plotting workflow for publication
to be able to generate them from the snakemake workflow as well as
interactively rendering through a manifest .

A documentation is currently available in `docs/publication_figures.md`
adestefani8 and others added 30 commits July 17, 2026 14:30
This PR adds a standalone script that plots an areal-mean meteogram over
a region polygon, styled to match the paper figures. It masks the truth
points inside a region shapefile, averages over them at each valid time,
and plots the series, reusing `publication_style` (`line_style`,
`param_label`), `publication.mplstyle`, and the lead-time x-axis of
`publication_meteogram` so it reads consistently with the other paper
figures.

The script is standalone (not wired to snakemake) and truth-only: it
reads the analysis zarr directly, with no inference or verification. It
works for any region shapefile and parameter.

For the Valais precipitation case study it plots the areal-mean hourly
precipitation of the ICON-CH1 analysis over the Valais region. The
Valais shapefile is on store at
`/store_new/mch/msopr/ml/regions/cantons/valais.shp`. It can also be
generated with the included `make_region_shapefile.py`, which pulls the
canton boundary from GADM and reprojects it to EPSG:2056:

```
python workflow/scripts/make_region_shapefile.py --canton Valais --outfn valais.shp
```

## Output
With
```
python workflow/scripts/plot_meteogram_region.py \
  --truth /store_new/mch/msopr/ml/datasets/mch-ich1-1km-2024-2025-1h-pl13-v1.0.zarr \
  --truth_label "ICON-CH1 analysis" \
  --shapefile /store_new/mch/msopr/ml/regions/cantons/valais.shp \
  --date 202506271800 --steps 0/120/1 --param TOT_PREC --outfn output/results/valais.png
```
you get:

<img width="1560" height="585" alt="valais_final"
src="https://github.com/user-attachments/assets/779cef33-3894-473b-8d6f-890379f0659a"
/>
SAL (Structure–Amplitude–Location) precipitation scoring uses
pysteps.verification.salscores; scikit-image is its transitive
requirement. uv.lock is gitignored, so no lockfile update.
Framework-agnostic helpers for Structure–Amplitude–Location scoring:
build a near-isotropic regular lat–lon raster, nearest-neighbour remap
native (triangular ICON / KENDA) fields onto it via
verification.spatial.spherical_nearest_neighbor_indices, and a thin
wrapper over pysteps' sal() that gates dry windows. S/A/L are normalized
ratios, hence invariant to a constant precipitation rescaling; the raster
is required because the Location term assumes square pixels.
New SalConfig (enabled/params/leadtimes, detection thresholds, raster
extent+spacing) under experiment.sal, mirroring ScoreMapsConfig, plus a
validate_sal_leadtimes model validator. Regenerate config.schema.json.
verification_sal.py computes per-init SAL over all init times for a given
(participant, param, lead time), remapping forecast and truth onto a common
raster, and writes a CSV (one row per init: S,A,L + domain means, with a
commented metadata header). New verification_sal / verification_sal_baseline
rules produce these CSVs for every run and baseline; experiment_all requests
them when experiment.sal.enabled (compute-only — plotting lives in the
publication-figures workflow). Add an example sal block to
varda-single-1.0.yaml (disabled; requires gridded truth).
Cover the raster builder, nearest-neighbour remap, and the SAL wrapper
(identical-field zero, dry-window NaN, amplitude sign, location sign,
unit-invariance), plus SalConfig parsing/validation and the leadtime
validator. 12 tests.
config.schema.json was committed in 57a6b26 without the regenerated SAL
entries, so config.py (with SalConfig) and the checked-in schema were out
of sync and the pydantic-schema pre-commit hook failed in CI. Regenerate
from config.py to add the SalConfig definition and the experiment.sal field.
Add a SAL block for all example configs, and adjust the one for
varda-single-1.0.yaml such that it works with INCA.
1a87453 added pysteps and scikit-image to pyproject.toml but did not
regenerate the lockfile. Resolve them in uv.lock (+170 lines, no removals)
so the locked environment matches the declared dependencies. uv.lock is
tracked despite the stale .gitignore entry, matching repo convention (3e7660b).
The SAL rules listed only sal.py + data_input as code inputs, so edits to
verification/spatial.py (the remap kernel, imported via verification.sal) or
verification/__init__.py would not retrigger SAL, risking stale results. Add
both as inputs. Also drop the baseline rule cpus_per_task from 24 (copied from
scoremaps) to 2 for the single-threaded KDTree+pysteps job.
Use type(item).model_fields instead of the deprecated instance attribute in
the SAL and scoremaps leadtime validators (Pydantic 2.11). Extend the
grid_extent validator to reject non-finite values and implausible lat/lon
ranges, not just wrong length/ordering.
SAL needs a resolved field; remapping sparse station truth (jretrieve, ~150
points) onto the ~1 km raster yields meaningless scores. Add a point-density
guard (point_density_per_km2 + MIN_TRUTH_POINT_DENSITY) that fails fast on the
first init. Also guard compute_sal against all-NaN input, drop the unused
SEASONS constant, and add unit tests for the density guard and grid_extent.
SAL is defined for precipitation only, but sal.params accepted any parameter
and the driver would silently compute meaningless scores on e.g. T_2M. Add a
lenient guard requiring params to start with TOT_PREC (period-accumulated like
TOT_PREC6 and bare cumulative TOT_PREC both allowed): a SalConfig field
validator that fails fast at config load, plus a matching check in
verification_sal.py for direct invocations. Add unit tests.

If SAL should be deployed later on for different variables as well
(which is conceivable but unlikely), this guard would need to be relaxed again.
Extract the shared steps-vs-requested-leadtimes check from the scoremaps
and SAL validators into a single _reject_unproducible_leadtimes() helper,
parameterized by the config-block label used in the error message. Both
validators become thin enabled-guards, so future features validating a
leadtimes field reuse one implementation instead of copy-pasting.
They are redundant information, the init hour is given in the first column.
The leadtime-validation dedup (ab3c497) hand-wrapped the
_reject_unproducible_leadtimes signature across three lines;
ruff collapses it to one. Without this the ruff-format CI hook fails.
…nto paper/sal-figures

# Conflicts:
#	src/evalml/config.py
#	workflow/tools/config.schema.json
Introduce PublicationSalScatterConfig and PublicationCaseSnapshotsConfig
under PublicationConfig, and extend ConfigModel.validate_publication with
coherence checks: gridded (zarr) truth, cases within `dates`, producible
lead times, and matching experiment.sal params/lead times. Regenerate the
config JSON schema.
Add run_sal/baseline_sal path templates and a per-participant sal_template
to build_manifest, carry experiment.sal settings into the manifest, and
bump the manifest schema version to 2. Add Manifest.sal_path() and
validate_request branches for the sal_scatter and case_snapshots figures.
Two `python -m evalml.publication` subcommands that resolve data from the
manifest (SAL CSVs for the scatter; candidate GRIB, truth and SAL
annotation for the case snapshots) and invoke the figure scripts, mirroring
the existing scoremaps/meteogram commands. The figure scripts themselves
land in a follow-up commit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants