asp-plot 3.3.0__tar.gz → 3.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {asp_plot-3.3.0 → asp_plot-3.4.0}/.github/workflows/release.yml +3 -1
  2. {asp_plot-3.3.0 → asp_plot-3.4.0}/.gitignore +7 -1
  3. {asp_plot-3.3.0 → asp_plot-3.4.0}/.readthedocs.yaml +2 -2
  4. {asp_plot-3.3.0 → asp_plot-3.4.0}/AGENTS.md +46 -2
  5. {asp_plot-3.3.0 → asp_plot-3.4.0}/ARCHITECTURE.md +8 -6
  6. {asp_plot-3.3.0 → asp_plot-3.4.0}/CHANGELOG.md +26 -0
  7. {asp_plot-3.3.0 → asp_plot-3.4.0}/PKG-INFO +1 -1
  8. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/cli/asp_report.py +11 -0
  9. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/cli/dem_benchmark.py +58 -5
  10. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/dem_benchmark.py +512 -73
  11. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/icesat2_source.py +62 -36
  12. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/report.py +72 -6
  13. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/report_captions.py +2 -2
  14. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/report_pipeline.py +5 -0
  15. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/stereo.py +58 -15
  16. {asp_plot-3.3.0 → asp_plot-3.4.0}/pyproject.toml +1 -1
  17. {asp_plot-3.3.0 → asp_plot-3.4.0}/.flake8 +0 -0
  18. {asp_plot-3.3.0 → asp_plot-3.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  19. {asp_plot-3.3.0 → asp_plot-3.4.0}/.github/ISSUE_TEMPLATE/problem-report.yml +0 -0
  20. {asp_plot-3.3.0 → asp_plot-3.4.0}/.github/workflows/claude-code-review.yml +0 -0
  21. {asp_plot-3.3.0 → asp_plot-3.4.0}/.github/workflows/claude.yml +0 -0
  22. {asp_plot-3.3.0 → asp_plot-3.4.0}/.github/workflows/run-tests.yml +0 -0
  23. {asp_plot-3.3.0 → asp_plot-3.4.0}/.pre-commit-config.yaml +0 -0
  24. {asp_plot-3.3.0 → asp_plot-3.4.0}/CLAUDE.md +0 -0
  25. {asp_plot-3.3.0 → asp_plot-3.4.0}/LICENSE +0 -0
  26. {asp_plot-3.3.0 → asp_plot-3.4.0}/README.md +0 -0
  27. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/__init__.py +0 -0
  28. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/alignment.py +0 -0
  29. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/altimetry.py +0 -0
  30. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/altimetry_plots.py +0 -0
  31. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/altimetry_source.py +0 -0
  32. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/asp_log.py +0 -0
  33. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/bodies.py +0 -0
  34. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/bundle_adjust.py +0 -0
  35. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/cli/__init__.py +0 -0
  36. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/cli/bundle_adjust_cameras.py +0 -0
  37. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/cli/csm_camera_plot.py +0 -0
  38. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/cli/gallery.py +0 -0
  39. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/cli/request_planetary_altimetry.py +0 -0
  40. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/cli/stereo_geom.py +0 -0
  41. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/csm_analysis.py +0 -0
  42. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/csm_camera.py +0 -0
  43. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/csm_io.py +0 -0
  44. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/gallery.py +0 -0
  45. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/mapproject.py +0 -0
  46. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/planetary_source.py +0 -0
  47. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/processing_parameters.py +0 -0
  48. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/scenes.py +0 -0
  49. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/selections.py +0 -0
  50. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/sensors/__init__.py +0 -0
  51. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/sensors/aster.py +0 -0
  52. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/sensors/base.py +0 -0
  53. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/sensors/dimap.py +0 -0
  54. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/sensors/dimap_v1.py +0 -0
  55. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/sensors/rpc.py +0 -0
  56. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/sensors/worldview.py +0 -0
  57. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/stereo_geometry.py +0 -0
  58. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/stereopair_metadata_parser.py +0 -0
  59. {asp_plot-3.3.0 → asp_plot-3.4.0}/asp_plot/utils.py +0 -0
  60. {asp_plot-3.3.0 → asp_plot-3.4.0}/conda-forge-recipe/meta.yaml +0 -0
  61. {asp_plot-3.3.0 → asp_plot-3.4.0}/environment.yml +0 -0
@@ -37,7 +37,9 @@ jobs:
37
37
  - name: Get previous tag
38
38
  id: prev_tag
39
39
  run: |
40
- PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
40
+ # --match 'v*' so the dated reports-<date> tags (issue #201) are not
41
+ # mistaken for the previous version and compared against in the notes.
42
+ PREV_TAG=$(git describe --tags --abbrev=0 --match 'v*' 2>/dev/null || echo "")
41
43
  echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
42
44
 
43
45
  - name: Check if tag exists
@@ -146,6 +146,12 @@ reports/regenerate_reports.sh
146
146
  # Figure-selections sidecars written next to each report by `asp_report`. These are
147
147
  # regeneration byproducts that hardcode absolute local paths (dem_filename,
148
148
  # parquet_cache), so they are not committed; a sanitized example is documented in
149
- # docs/cli/asp_report.md instead.
149
+ # docs/cli/asp_report.md instead. They are attached to each reports-<date>
150
+ # release alongside the PDFs they generated.
150
151
  reports/*_figure_selections.yml
152
+ # The example reports themselves. PDFs do not delta-compress, so every
153
+ # regeneration became a permanent new blob -- 7 files, 116 objects, 846 MB of
154
+ # history. They are published as GitHub Release assets instead, and the docs
155
+ # build fetches them (docs/fetch_example_reports.sh). See issue #201.
156
+ reports/*.pdf
151
157
  *.csv
@@ -15,8 +15,8 @@ build:
15
15
  - cp notebooks/Mars_MRO/*.ipynb docs/examples/notebooks/
16
16
  - mkdir -p docs/_extra/examples/figures
17
17
  - cp notebooks/figures/* docs/_extra/examples/figures/
18
- - mkdir -p docs/_static/reports
19
- - cp reports/*.pdf docs/_static/reports/
18
+ # Reports are GitHub Release assets, not committed to git (issue #201).
19
+ - bash docs/fetch_example_reports.sh
20
20
 
21
21
  sphinx:
22
22
  configuration: docs/conf.py
@@ -30,14 +30,58 @@ Flake8 config is in `.flake8` (extends ignore: E203, E701); pre-commit further i
30
30
  To build the docs locally (Sphinx + MyST; hosted on ReadTheDocs, auto-built on push to `main`):
31
31
 
32
32
  ```bash
33
- # One-time: copy notebooks, reports, and figures for local preview
33
+ # One-time: copy notebooks and figures, fetch the reports, for local preview
34
34
  mkdir -p docs/examples/notebooks && cp notebooks/**/*.ipynb docs/examples/notebooks/
35
- mkdir -p docs/_static/reports && cp reports/*.pdf docs/_static/reports/
36
35
  mkdir -p docs/_extra/examples/figures && cp notebooks/figures/* docs/_extra/examples/figures/
36
+ bash docs/fetch_example_reports.sh # downloads the reports from their Release
37
37
 
38
38
  sphinx-autobuild docs docs/_build/html --open-browser # or sphinx-build for a one-off
39
39
  ```
40
40
 
41
+ ## Example Reports Are Release Assets, Not Committed Files
42
+
43
+ `reports/*.pdf` is gitignored. PDFs do not delta-compress, so every regeneration
44
+ was a wholly new permanent blob — seven report files had become 116 immortal
45
+ objects and 846 MB of history, most of a 1.8 GB clone (issue #201). They are
46
+ published as GitHub Release assets instead, on a dated `reports-<date>` tag that
47
+ is bumped only when the reports change — deliberately *not* the per-version
48
+ releases, since `release.yml` creates one per `pyproject.toml` bump and the
49
+ reports do not change per version.
50
+
51
+ To publish a regenerated set:
52
+
53
+ ```bash
54
+ bash reports/regenerate_reports.sh # gitignored; needs ~/Desktop/asp-plot-examples
55
+ gh release create reports-$(date +%F) \
56
+ reports/*.pdf reports/*_figure_selections.yml reports/regenerate_reports.sh \
57
+ --target main --latest=false \
58
+ --title "Example reports, $(date +%F)" --notes "What changed and why."
59
+ # then bump REPORTS_RELEASE in docs/fetch_example_reports.sh and commit that one line
60
+ ```
61
+
62
+ Four things matter here:
63
+
64
+ - **Publish the release before pushing the commit** that points at it. The docs
65
+ build uses `curl -f`, so a tag that does not exist yet fails the build.
66
+ - **`--latest=false`**, or the reports release takes the "Latest" badge from the
67
+ current version release.
68
+ - **Attach the `*_figure_selections.yml` sidecars too.** They are gitignored (they
69
+ hardcode absolute local paths) but they are what makes regeneration
70
+ figure-stable and offline (issue #121), so keeping them with the PDFs they
71
+ produced makes a release self-describing.
72
+ - **Attach `regenerate_reports.sh` as well, for the same reason and one more.**
73
+ It is gitignored too, which means it is in no clone *and in no backup* — a
74
+ mirror of this repository contains zero objects for it, because git never
75
+ tracked it. The release is therefore its only durable copy, and a release that
76
+ carries the reports, the selections that shaped them, and the commands that
77
+ produced them can be reproduced by someone who has the example datasets and
78
+ nothing else.
79
+
80
+ Always regenerate with `--reuse-selections` so pages stay figure-for-figure
81
+ comparable and no SlideRule request is made. Reports are capped at 200 effective
82
+ dpi at their placed width (`asp_report --figure-max-dpi`, `0` disables), which is
83
+ what keeps the assets small.
84
+
41
85
  ## Gotchas
42
86
 
43
87
  - **ASP tools must be on PATH** for the workflows that wrap them: `stereo`/`parallel_stereo`, `bundle_adjust`, `point2dem`, `pc_align`, `geodiff`, `dg_mosaic` (called via `run_subprocess_command()`). **Append** the ASP bin directory to PATH rather than prepending: the ASP release bundles its own `python`, which would shadow your environment's interpreter and break imports.
@@ -216,11 +216,12 @@ The package is organized by functionality, with each module focused on a specifi
216
216
  - Not wired into the main `asp_report` PDF report (standalone class + `gallery` CLI)
217
217
 
218
218
  **`dem_benchmark.py`** - `DEMBenchmark` class (issue #169)
219
- - Scores *many* DEMs against *one* altimetry sample so scene combinations, processing flows (joint MVS vs. pairwise + `dem_mosaic`) and parameter sweeps are compared on the same points. Replaces the ad hoc loop in the Atlanta MVS notebook: one `Altimetry` per DEM (kept in `self.altimetry[label]` for follow-up per-DEM figures), the same parquet replayed via `load_atl06sr_from_parquet` (or `load_planetary_csv`), the report's WorldCover water filter and outlier cut (30-NMAD gate + 3σ)
219
+ - Scores *many* DEMs against *one* altimetry sample so scene combinations, processing flows (joint MVS vs. pairwise + `dem_mosaic`) and parameter sweeps are compared on the same points. Replaces the ad hoc loop in the Atlanta MVS notebook: one `Altimetry` per DEM (kept in `self.altimetry[label]` for follow-up per-DEM figures), the same parquet replayed via `load_atl06sr_from_parquet` (or `load_planetary_csv`), the report's WorldCover water filter (`filter_out`, a group name or a list such as `["water", "trees"]` for stable terrain; validated against `icesat2_source.WORLDCOVER_GROUPS`, CLI `--filter-out`, repeatable) and outlier cut (30-NMAD gate + 3σ)
220
220
  - `run()` → `stats_df`, one row per DEM, columns `STATS_COLUMNS`: coverage (`valid_pct` / `valid_area_km2`) inside a common AOI (default the **intersection** of all DEM extents in the first DEM's CRS, so differing crop windows compare fairly; falls back to own extents with a warning when footprints don't all overlap), IntersectionErr median/NMAD from the `point2dem` sibling raster (`intersection_error_path()`; NaN for mosaics), altimetry-minus-DEM n/median/NMAD/RMSE before and after a per-DEM `pc_align --compute-translation-only` (translation + N-E-D shifts from `Alignment.pc_align_report`), and optional DEM-minus-reference stats via `Raster.compute_difference`
221
+ - **Shared point set and paired block bootstrap** (issue #199). Each DEM keeps different points (voids, per-DEM outlier cut), and scoring on them changes the ranking, so after the per-DEM loop `_shared_stats()` intersects the point ids every DEM kept (`shared_ids`) and rescores all of them there (`*_shared_*` columns; the figure and the sort use these, the per-DEM `dh_*` columns stay as secondary numbers). Points are identified across the per-DEM `Altimetry` objects by a positional `benchmark_point_id` column stamped right after the parquet/CSV load (`_tag_points` / `_tag_planetary_points`; the tables share a non-unique time index, and every DEM replays the same file through the same ingest so row order is identical). `paired_block_bootstrap()` then resamples whole blocks — ICESat-2 beam tracks `(rgt, cycle, spot)`, MOLA `ORBIT`; LOLA has no track id and falls back to points with a warning — and evaluates every DEM on the same replicate, giving `median_ci_*` / `nmad_ci_*` (95 %), `nmad_vs_best_ci_*` (paired difference to the lowest-NMAD DEM; includes 0 = not separable) and `p_best` at the ranked stage (`bootstrap["stage"]`: aligned when any DEM has it). Track blocks, not points, because ATL06-SR segments overlap (40 m posted at 20 m) and residuals cluster with terrain; paired differences, not overlapping per-DEM intervals, because DEMs on the same points share most of their sampling variation
221
222
  - Window reads (`_window_for_bounds` / `_read_window`) are downsampled past `MAX_WINDOW_PIXELS` so a 1 m 20k×20k DEM stays at ~16 M samples (valid fraction becomes a subsample estimate; area scales from it)
222
223
  - pc_align products and the translated DEM go under `<directory>/dem_benchmark/<label>/` (via `Alignment.apply_dem_translation(output_fn=...)`), never next to the candidate DEMs; an existing `*-transform.txt` there is reused (a re-run makes no pc_align call and works offline); a missing `pc_align` binary (`FileNotFoundError`) degrades to pre-alignment scoring with one warning
223
- - Figures: `summary_plot()` (rows = DEMs sorted best-first by post-alignment NMAD; panels coverage / IntersectionErr / dh median / dh NMAD as before→after dumbbells; the NMAD panel is anchored at zero and its markers coincide by construction for a translation-only alignment) and `histogram_plot()` (overlaid residual outlines). `label_from_dem_path()` / `parse_dem_specs()` back the CLI's `LABEL=PATH` specs (an ASP `run-DEM.tif` is labelled by its folder)
224
+ - Figures: `summary_plot()` (rows = DEMs sorted best-first by post-alignment shared-point NMAD; panels coverage / dh median / dh NMAD as before→after dumbbells (IntersectionErr is table-only: across geometries it ranks opposite to accuracy, #199) with the bootstrap interval as an error bar on the ranked marker and printed in brackets, `best` / `≈ best` labels in the NMAD panel; the NMAD panel is anchored at zero and its markers coincide by construction for a translation-only alignment) and `histogram_plot()` (overlaid residual outlines on the shared points). `label_from_dem_path()` / `parse_dem_specs()` back the CLI's `LABEL=PATH` specs (an ASP `run-DEM.tif` is labelled by its folder)
224
225
  - Not wired into the `asp_report` PDF (standalone class + `dem_benchmark` CLI)
225
226
 
226
227
  ## CLI Tools
@@ -287,8 +288,8 @@ All CLI tools are in `asp_plot/cli/` and use Click for argument parsing:
287
288
 
288
289
  **`dem_benchmark.py`** - Multi-DEM altimetry benchmark tool (`dem_benchmark` command)
289
290
  - Wrapper for `DEMBenchmark`; positional `DEMS` as paths or `LABEL=PATH`
290
- - `--parquet` (Earth, the ATL06-SR cache) or `--altimetry-csv` (Moon/Mars); `--directory` (where `dem_benchmark/<label>/` products go), `--reference`, `--no-pc-align`, `--own-extent`, `--title`, `--output-directory/--output-filename`
291
- - Writes the summary figure, a `_histogram.png` twin, and the stats table as `.csv` with the same stem, and prints the table
291
+ - `--parquet` (Earth, the ATL06-SR cache) or `--altimetry-csv` (Moon/Mars); `--directory` (where `dem_benchmark/<label>/` products go), `--reference`, `--no-pc-align`, `--own-extent`, `--n-bootstrap` (0 skips), `--title`, `--output-directory/--output-filename`
292
+ - Writes the summary figure, a `_histogram.png` twin, and the stats table as `.csv` with the same stem, and prints the headline (shared-point) columns of the table
292
293
 
293
294
  ## Documentation Website
294
295
 
@@ -316,7 +317,7 @@ docs/
316
317
  figures/ # Committed doc figures (e.g. example_gallery.png); referenced from .md via ../figures/
317
318
  contributing.md # Dev setup, testing, release process
318
319
  changelog.md # Includes top-level CHANGELOG.md via MyST include
319
- _static/reports/ # .gitignored — copied from reports/ during build
320
+ _static/reports/ # .gitignored — fetched from the reports-<date> Release during build
320
321
  _extra/examples/figures/ # .gitignored — copied from notebooks/figures/ during build
321
322
  _templates/footer.html # RTD ethical ads placement in footer
322
323
  requirements.txt # Docs-only pip dependencies for RTD
@@ -324,7 +325,8 @@ docs/
324
325
 
325
326
  ### Key Design Decisions
326
327
 
327
- - **Notebooks and reports stay at top level** (`notebooks/`, `reports/`). RTD's `pre_build` jobs in `.readthedocs.yaml` copy them into the docs tree. The `docs/examples/notebooks/`, `docs/_static/reports/`, and `docs/_extra/` directories are `.gitignore`d.
328
+ - **Notebooks stay at top level** (`notebooks/`) and RTD's `pre_build` jobs in `.readthedocs.yaml` copy them into the docs tree. The `docs/examples/notebooks/`, `docs/_static/reports/`, and `docs/_extra/` directories are `.gitignore`d.
329
+ - **Reports are not in git at all.** `reports/*.pdf` is gitignored and published as assets on a dated `reports-<date>` GitHub Release; `docs/fetch_example_reports.sh` downloads them into `_static/reports/` during the build, replacing what used to be a `cp` (issue #201). They must be self-hosted rather than linked directly because GitHub serves release assets as `content-disposition: attachment`, which an `<iframe>` cannot render. The script pins one `REPORTS_RELEASE` tag, so a docs build of an older commit keeps fetching the reports of its era.
328
330
  - **sphinx-autoapi** generates API reference from static analysis — no GDAL/rasterio needed on RTD. This is why RTD installs only `docs/requirements.txt` instead of the full package.
329
331
  - **`html_extra_path`** is used to serve `notebooks/figures/` at the correct relative path for notebook `<img src>` references.
330
332
  - **`docs/` is excluded from sdist** in `pyproject.toml` so docs never ship in the PyPI/conda package.
@@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.4.0] - 2026-09-29
9
+
10
+ In this release:
11
+
12
+ - The DEM benchmark compares DEMs on the same altimetry points, reports which differences between them are distinguishable, and can drop more than one land-cover group from the reference sample ([#199](https://github.com/uw-cryo/asp_plot/issues/199)).
13
+ - The report states what the intersection error measures ([#199](https://github.com/uw-cryo/asp_plot/issues/199)).
14
+ - The example reports moved out of git and into release assets, and figures embedded in a report are capped at 200 dpi ([#201](https://github.com/uw-cryo/asp_plot/issues/201)).
15
+ - The report no longer fails on a stereo directory whose `*-L.tif` or `*-D.tif` has been deleted ([#202](https://github.com/uw-cryo/asp_plot/issues/202)).
16
+
17
+ Repository size work, part one ([#201](https://github.com/uw-cryo/asp_plot/issues/201)). A `git clone` of this repository is 1.8 GB against a ~275 MB checkout, and the largest single cause is the committed example reports: PDFs do not delta-compress, so every regeneration is a wholly new permanent object, and seven report files had become 116 of them and 846 MB of history. This stops that growing. It does not shrink what is already there — that needs a history rewrite, which is the other half of #201.
18
+
19
+ ### Added
20
+ - **A cap on the resolution of figures embedded in PDF reports** (`asp_plot.report.FIGURE_MAX_DPI`, `asp_report --figure-max-dpi`, `0` disables). Figures are plotted at 220 dpi and then scaled down to fit the page, so the resolution that actually lands in the PDF is set by the placed size, not the save dpi: a 24-inch-wide multi-panel figure squeezed into 186 mm of a Letter page arrives at ~400 dpi. Measuring the Atlanta MVS report found 21.3 of its 21.4 MB was image data and 13.6 MB of it sat at 300–500 dpi — detail no screen shows and no printer reproduces, stored in full. `compile_report()` now downsamples each figure to a ceiling applied at its placed width, defaulting to 200 dpi. This takes the seven example reports from 79.9 MB to 59.0 MB, and it is a **change in output for every user**: reports are smaller and figures are capped at 200 dpi unless `--figure-max-dpi 0` is passed. 200 dpi rather than the ~150 dpi Ghostscript's `/ebook` preset targets, because that preset re-encodes plots as JPEG and rings around axis lines and tick labels; resolution-only downsampling loses nothing the page can show. PIL is already a dependency and fpdf2's `image()` takes a `PIL.Image`, so this adds no dependency and writes no temp files.
21
+ - **`docs/fetch_example_reports.sh`**, which downloads the example reports into `docs/_static/reports/` at docs-build time. It pins one `REPORTS_RELEASE` tag, so a docs build of an older commit fetches the reports of its era, and uses `curl -f` so a missing or renamed asset fails the build loudly instead of shipping a broken iframe.
22
+ - **`DEMBenchmark` scores every DEM on the points valid in all of them, and says which differences are real** ([#199](https://github.com/uw-cryo/asp_plot/issues/199)). Each DEM used to be scored on its own surviving points: its voids differ from the next DEM's, and the outlier cut runs per DEM, so a blend that fills voids was scored on harder ground (steep, tree-covered) than a single pair that skips it, and the ranking depends on that. Restricted to the shared points, the Atlanta 10-pair median mosaic moves from 8th to 3rd, and at UCSD the five-scene run referenced on the 8° scene moves from 4th to 1st while the pair that had been 1st drops to 4th. `run()` now also intersects the point ids every DEM kept (`bench.shared_ids`, `n_shared`) and rescores all of them there; those `*_shared_*` columns are what `summary_plot()` draws and sorts on, and the per-DEM `dh_*` columns stay as secondary numbers. Residuals along an ICESat-2 beam are correlated (ATL06-SR segments are 40 m long and posted every 20 m, and errors cluster with terrain and land cover), so the number of points overstates the evidence; a paired block bootstrap (`paired_block_bootstrap()`, `n_bootstrap=1000`, `--n-bootstrap`) resamples whole beam tracks (rgt, cycle, spot), or MOLA orbits, and evaluates every DEM on the same replicate. It gives 95 % intervals on each DEM's median and NMAD (`median_ci_*`, `nmad_ci_*`), the interval on its NMAD *difference* to the best DEM (`nmad_vs_best_ci_*`; includes zero means not separable) and the fraction of replicates on which it ranks first (`p_best`). Paired differences rather than overlapping intervals, because DEMs scored on the same points share most of their sampling variation: at Atlanta every top DEM's interval overlaps every other, yet the best DEM ranks first on 99 % of replicates. The figure shows the interval as an error bar on the ranked marker, prints it in brackets, and labels the best DEM `best` and every DEM not separable from it `≈ best`; the histogram uses the shared points; the CLI prints the headline columns and writes every column to the CSV. LOLA CSVs carry no track id, so on the Moon the bootstrap resamples points and warns that the intervals are optimistic. The IntersectionErr panel is dropped from the figure: across DEMs of different geometry the triangulation error ranks opposite to accuracy (the 5° pairs have the smallest, the five-ray runs the largest), so a panel of it beside the residuals misled; the `ie_*` columns stay in the table as a consistency check on a run.
23
+ - **The land-cover filter takes a list of groups** ([#199](https://github.com/uw-cryo/asp_plot/issues/199)). `Altimetry.filter_esa_worldcover(filter_out=...)` and `retain_only` accepted one ESA WorldCover group; they now also accept a list, and `DEMBenchmark(filter_out=["water", "trees"])` and `dem_benchmark --filter-out water --filter-out trees` pass one through (`--filter-out none` keeps every return). Dropping trees as well as water is the stable-terrain choice: years usually separate the imagery from the ICESat-2 passes and canopy height changes over that time, and at Atlanta a ranking on the tree points alone moved the best DEM from 1st to 7th. The filter runs on the altimetry sample before any DEM is scored, so every DEM is still compared on the same points. The default stays `"water"`, the report's setting, and a benchmark figure made with any other choice says so in its subtitle. `DEMBenchmark` raises on a name that is not a group (`"tree"` for `"trees"`) instead of scoring an unfiltered sample; the groups are `asp_plot.icesat2_source.WORLDCOVER_GROUPS`.
24
+
25
+ ### Changed
26
+ - **The report says what the intersection error is and is not** ([#199](https://github.com/uw-cryo/asp_plot/issues/199)). The caption of the DEM results page now defines it (the closest distance between the rays at the triangulated point; for a multi-view run, twice the mean distance from the point to its rays) and states that it measures how consistently the rays intersect, not how accurate the height is: it does not reflect the convergence angle, so it serves to find camera or correlation problems within a run and not to compare runs of different geometry. In the benchmark the 5° pairs had the smallest intersection error and the largest height error. The hillshade page's caption called the areas its zoomed subsets are picked from "low, medium, and high uncertainty"; it now says "intersection error", which is what they are picked by.
27
+ - **The example reports are no longer committed to git.** `reports/*.pdf` is gitignored and `git rm --cached`ed; the reports are published as assets on a dated [`reports-<date>`](https://github.com/uw-cryo/asp_plot/releases/tag/reports-2026-09-18) GitHub Release in this repository, together with the `*_figure_selections.yml` sidecars that generated them. A dated tag rather than the per-version releases, because `release.yml` creates a release per `pyproject.toml` bump and the reports do not change per version. `.readthedocs.yaml`'s `cp reports/*.pdf` step becomes `bash docs/fetch_example_reports.sh`; the reports page, the report links in the notebooks, and the local docs recipe in AGENTS.md all work as before, because the build still self-hosts the PDFs under `_static/reports/` — GitHub serves release assets as `content-disposition: attachment`, which an `<iframe>` cannot render. Nothing about generating a report locally changes. Publishing a regenerated set is documented in AGENTS.md; note that the release must be published *before* the commit that points at it.
28
+
29
+ ### Fixed
30
+ - **`release.yml` no longer picks a `reports-<date>` tag as the previous version.** Its previous-tag step was `git describe --tags --abbrev=0`, which returns the most recent tag reachable from `HEAD` — and the reports releases are tagged on `main`, so the next version release would have linked a changelog comparing `reports-2026-09-18...v3.4.0`. Now `--match 'v*'`. The tag-exists check greps for `v<version>` and was never affected.
31
+ - **`StereoFiles` no longer crashes on a stereo directory with no top-level `*-L.tif`** ([#202](https://github.com/uw-cryo/asp_plot/issues/202)). The constructor passed the result of `glob_file()` straight to `Raster()` to decide whether the run was mapprojected, so a missing left image raised `TypeError: expected str, bytes or os.PathLike object, not NoneType` instead of falling back to the missing-file placeholders the plots already draw. A missing left image now means not mapprojected. This happens in trimmed example directories (it blocked regenerating the Atlanta MVS report) and in multi-view runs, which keep their aligned images in `<prefix>-pairN/`; in that layout the lookup is now quiet, like its neighbours.
32
+ - **The match-point and disparity figures fall back to `*-lMask.tif` when `*-L.tif` and `*-D.tif` have been deleted** ([#202](https://github.com/uw-cryo/asp_plot/issues/202)). Both figures read the full-resolution file only for its size (or GSD, for mapprojected runs) to rescale onto the sub-sampled images, and showed the missing-files placeholder without it. ASP writes the left mask on the same grid and with the same georeferencing as the left image, and it is much smaller, so it tends to survive when disk space is reclaimed. The mask is also used to decide whether the run is mapprojected. The Atlanta MVS example directory had lost `L.tif` and `D.tif` in every pair, and its per-pair figures render again without re-running stereo.
33
+
8
34
  ## [3.3.0] - 2026-09-18
9
35
 
10
36
  The first piece of the scene-combination benchmark ([#169](https://github.com/uw-cryo/asp_plot/issues/169)): a way to score *many* DEMs against *one* altimetry sample. The report assesses a single DEM; the question of which scene combination, which processing flow (joint multi-view triangulation vs. pairwise stereo merged with `dem_mosaic`), or which parameter setting gives the best DEM needs every candidate scored against exactly the same points, side by side. The Atlanta MVS notebook had been doing that with an ad hoc loop; it is now a class, a figure, and a `dem_benchmark` command, and the notebook uses them to score six DEMs at once, where the single pairs explain the mosaic's result (the 5° pair lowers its accuracy) and the five-scene run has the lowest bias but not the lowest spread. No new dependencies; one new entry point, so the conda-forge feedstock recipe needs `dem_benchmark` added by hand this release.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: asp_plot
3
- Version: 3.3.0
3
+ Version: 3.4.0
4
4
  Summary: Package for plotting outputs Ames Stereo Pipeline processing
5
5
  Project-URL: Homepage, https://github.com/uw-cryo/asp_plot
6
6
  Project-URL: Documentation, https://asp-plot.readthedocs.io
@@ -1,5 +1,6 @@
1
1
  import click
2
2
 
3
+ from asp_plot.report import FIGURE_MAX_DPI
3
4
  from asp_plot.report_pipeline import ReportConfig, run_report
4
5
 
5
6
 
@@ -128,6 +129,14 @@ def _reconstruct_command():
128
129
  default=None,
129
130
  help="Title for the report. Default: Directory name of ASP processing.",
130
131
  )
132
+ @click.option(
133
+ "--figure-max-dpi",
134
+ prompt=False,
135
+ default=FIGURE_MAX_DPI,
136
+ show_default=True,
137
+ type=int,
138
+ help="Resolution ceiling for figures embedded in the PDF, applied at the size each figure is placed on the page. Figures are plotted at 220 dpi and then scaled down to the page, so wide multi-panel figures otherwise land at 400+ effective dpi and inflate the file for detail no screen or printer shows. Pass 0 to embed the full-resolution figures.",
139
+ )
131
140
  def main(
132
141
  directory,
133
142
  bundle_adjust_prefix,
@@ -146,6 +155,7 @@ def main(
146
155
  reuse_selections,
147
156
  report_filename,
148
157
  report_title,
158
+ figure_max_dpi,
149
159
  ):
150
160
  """
151
161
  Generate a comprehensive report of ASP processing results.
@@ -172,6 +182,7 @@ def main(
172
182
  reuse_selections=reuse_selections,
173
183
  report_filename=report_filename,
174
184
  report_title=report_title,
185
+ figure_max_dpi=figure_max_dpi,
175
186
  report_command=_reconstruct_command(),
176
187
  )
177
188
  run_report(config)
@@ -3,6 +3,7 @@ import os
3
3
  import click
4
4
 
5
5
  from asp_plot.dem_benchmark import DEMBenchmark
6
+ from asp_plot.icesat2_source import WORLDCOVER_GROUPS
6
7
 
7
8
 
8
9
  @click.command()
@@ -51,6 +52,29 @@ from asp_plot.dem_benchmark import DEMBenchmark
51
52
  help="Compute coverage and triangulation-error statistics over each DEM's "
52
53
  "own extent instead of the intersection of all DEM footprints.",
53
54
  )
55
+ @click.option(
56
+ "--filter-out",
57
+ prompt=False,
58
+ multiple=True,
59
+ default=("water",),
60
+ type=click.Choice(sorted(WORLDCOVER_GROUPS) + ["none"]),
61
+ help="ESA WorldCover group to drop from the ICESat-2 points before "
62
+ "differencing; repeat the option for several. Giving the option replaces "
63
+ "the default, so name water as well to keep dropping it: '--filter-out "
64
+ "water --filter-out trees' is the stable-terrain choice when years "
65
+ "separate the imagery from the ICESat-2 passes. 'none' keeps every return "
66
+ "and cannot be combined with a group. Earth DEMs only. Default: water.",
67
+ )
68
+ @click.option(
69
+ "--n-bootstrap",
70
+ prompt=False,
71
+ default=1000,
72
+ type=int,
73
+ help="Replicates of the paired block bootstrap that puts 95 % intervals on "
74
+ "the shared-point median and NMAD of every DEM and on its NMAD difference "
75
+ "to the best DEM (whole ICESat-2 beam tracks, or MOLA orbits, are "
76
+ "resampled). 0 skips it. Default: 1000.",
77
+ )
54
78
  @click.option(
55
79
  "--title",
56
80
  prompt=False,
@@ -79,6 +103,8 @@ def main(
79
103
  reference,
80
104
  no_pc_align,
81
105
  own_extent,
106
+ filter_out,
107
+ n_bootstrap,
82
108
  title,
83
109
  output_directory,
84
110
  output_filename,
@@ -89,11 +115,19 @@ def main(
89
115
  DEMS are paths, optionally labelled as LABEL=PATH (e.g.
90
116
  "MVS=stereo_mvs3/run-DEM.tif"); an unlabelled ASP run-DEM.tif is labelled
91
117
  by its folder. Every DEM gets: coverage inside the common footprint, the
92
- median triangulation error from its IntersectionErr raster when present,
93
- and the altimetry-minus-DEM median / NMAD / RMSE before and (unless
94
- --no-pc-align) after a pc_align translation. Writes a one-row-per-DEM
95
- summary figure, an overlaid residual histogram, and the stats table as CSV.
118
+ median triangulation error from its IntersectionErr raster when present
119
+ (table only; it is a consistency check, not a quality ranking), and the
120
+ altimetry-minus-DEM median / NMAD / RMSE before and (unless
121
+ --no-pc-align) after a pc_align translation, on its own points and on the
122
+ points valid in every DEM, with bootstrap intervals on the latter. Writes
123
+ a one-row-per-DEM summary figure, an overlaid residual histogram, and the
124
+ stats table as CSV.
96
125
  """
126
+ if "none" in filter_out and len(set(filter_out)) > 1:
127
+ raise click.UsageError(
128
+ "--filter-out none keeps every return and cannot be combined with "
129
+ "a land-cover group."
130
+ )
97
131
  directory = os.path.expanduser(directory)
98
132
  if output_directory is None:
99
133
  output_directory = directory
@@ -106,6 +140,8 @@ def main(
106
140
  altimetry_csv=altimetry_csv,
107
141
  reference=reference,
108
142
  aoi=None if own_extent else "intersection",
143
+ filter_out=[g for g in filter_out if g != "none"] or None,
144
+ n_bootstrap=n_bootstrap,
109
145
  title=title,
110
146
  )
111
147
  stats = bench.run(pc_align=not no_pc_align)
@@ -115,7 +151,24 @@ def main(
115
151
  bench.summary_plot(save_dir=output_directory, fig_fn=output_filename)
116
152
  bench.histogram_plot(save_dir=output_directory, fig_fn=f"{stem}_histogram.png")
117
153
 
118
- shown = stats.drop(columns=["dem_fn"])
154
+ # The headline columns; the CSV has every column of STATS_COLUMNS.
155
+ shown = stats[
156
+ [
157
+ "label",
158
+ "valid_pct",
159
+ "ie_median_m",
160
+ "n_points",
161
+ "n_shared",
162
+ "dh_shared_median_m",
163
+ "dh_shared_nmad_m",
164
+ "dh_aligned_shared_median_m",
165
+ "dh_aligned_shared_nmad_m",
166
+ "nmad_ci_low_m",
167
+ "nmad_ci_high_m",
168
+ "p_best",
169
+ ]
170
+ ]
171
+ shown = shown.dropna(axis=1, how="all")
119
172
  print("\n" + shown.to_string(index=False, float_format=lambda v: f"{v:.2f}"))
120
173
  print(
121
174
  f"\nSummary figure: {os.path.join(output_directory, output_filename)}"