asp-plot 3.3.0__tar.gz → 3.5.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.5.0}/.github/workflows/release.yml +3 -1
  2. {asp_plot-3.3.0 → asp_plot-3.5.0}/.gitignore +10 -2
  3. {asp_plot-3.3.0 → asp_plot-3.5.0}/.readthedocs.yaml +2 -2
  4. {asp_plot-3.3.0 → asp_plot-3.5.0}/AGENTS.md +67 -5
  5. {asp_plot-3.3.0 → asp_plot-3.5.0}/ARCHITECTURE.md +10 -7
  6. {asp_plot-3.3.0 → asp_plot-3.5.0}/CHANGELOG.md +48 -0
  7. {asp_plot-3.3.0 → asp_plot-3.5.0}/PKG-INFO +1 -1
  8. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/cli/asp_report.py +19 -0
  9. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/cli/dem_benchmark.py +58 -5
  10. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/dem_benchmark.py +541 -77
  11. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/icesat2_source.py +62 -36
  12. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/report.py +72 -6
  13. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/report_captions.py +2 -2
  14. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/report_pipeline.py +36 -1
  15. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/stereo.py +58 -15
  16. {asp_plot-3.3.0 → asp_plot-3.5.0}/pyproject.toml +1 -1
  17. {asp_plot-3.3.0 → asp_plot-3.5.0}/.flake8 +0 -0
  18. {asp_plot-3.3.0 → asp_plot-3.5.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  19. {asp_plot-3.3.0 → asp_plot-3.5.0}/.github/ISSUE_TEMPLATE/problem-report.yml +0 -0
  20. {asp_plot-3.3.0 → asp_plot-3.5.0}/.github/workflows/claude-code-review.yml +0 -0
  21. {asp_plot-3.3.0 → asp_plot-3.5.0}/.github/workflows/claude.yml +0 -0
  22. {asp_plot-3.3.0 → asp_plot-3.5.0}/.github/workflows/run-tests.yml +0 -0
  23. {asp_plot-3.3.0 → asp_plot-3.5.0}/.pre-commit-config.yaml +0 -0
  24. {asp_plot-3.3.0 → asp_plot-3.5.0}/CLAUDE.md +0 -0
  25. {asp_plot-3.3.0 → asp_plot-3.5.0}/LICENSE +0 -0
  26. {asp_plot-3.3.0 → asp_plot-3.5.0}/README.md +0 -0
  27. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/__init__.py +0 -0
  28. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/alignment.py +0 -0
  29. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/altimetry.py +0 -0
  30. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/altimetry_plots.py +0 -0
  31. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/altimetry_source.py +0 -0
  32. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/asp_log.py +0 -0
  33. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/bodies.py +0 -0
  34. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/bundle_adjust.py +0 -0
  35. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/cli/__init__.py +0 -0
  36. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/cli/bundle_adjust_cameras.py +0 -0
  37. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/cli/csm_camera_plot.py +0 -0
  38. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/cli/gallery.py +0 -0
  39. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/cli/request_planetary_altimetry.py +0 -0
  40. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/cli/stereo_geom.py +0 -0
  41. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/csm_analysis.py +0 -0
  42. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/csm_camera.py +0 -0
  43. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/csm_io.py +0 -0
  44. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/gallery.py +0 -0
  45. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/mapproject.py +0 -0
  46. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/planetary_source.py +0 -0
  47. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/processing_parameters.py +0 -0
  48. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/scenes.py +0 -0
  49. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/selections.py +0 -0
  50. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/sensors/__init__.py +0 -0
  51. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/sensors/aster.py +0 -0
  52. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/sensors/base.py +0 -0
  53. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/sensors/dimap.py +0 -0
  54. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/sensors/dimap_v1.py +0 -0
  55. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/sensors/rpc.py +0 -0
  56. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/sensors/worldview.py +0 -0
  57. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/stereo_geometry.py +0 -0
  58. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/stereopair_metadata_parser.py +0 -0
  59. {asp_plot-3.3.0 → asp_plot-3.5.0}/asp_plot/utils.py +0 -0
  60. {asp_plot-3.3.0 → asp_plot-3.5.0}/conda-forge-recipe/meta.yaml +0 -0
  61. {asp_plot-3.3.0 → asp_plot-3.5.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
@@ -140,12 +140,20 @@ notebooks/**/*.parquet
140
140
  notebooks/**/*.csv
141
141
  CLAUDE/
142
142
  .claude/**
143
- scripts/
143
+ # Study driver scripts live in a separate repo; scripts is often a symlink to it,
144
+ # which a trailing-slash pattern (directories only) does not match.
145
+ /scripts
144
146
  /*.parquet
145
147
  reports/regenerate_reports.sh
146
148
  # Figure-selections sidecars written next to each report by `asp_report`. These are
147
149
  # regeneration byproducts that hardcode absolute local paths (dem_filename,
148
150
  # parquet_cache), so they are not committed; a sanitized example is documented in
149
- # docs/cli/asp_report.md instead.
151
+ # docs/cli/asp_report.md instead. They are attached to each reports-<date>
152
+ # release alongside the PDFs they generated.
150
153
  reports/*_figure_selections.yml
154
+ # The example reports themselves. PDFs do not delta-compress, so every
155
+ # regeneration became a permanent new blob -- 7 files, 116 objects, 846 MB of
156
+ # history. They are published as GitHub Release assets instead, and the docs
157
+ # build fetches them (docs/fetch_example_reports.sh). See issue #201.
158
+ reports/*.pdf
151
159
  *.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,76 @@ 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
+
56
+ # Switch the Zenodo webhook off for the publish (needs repo admin; see below)
57
+ HOOK=$(gh api repos/uw-cryo/asp_plot/hooks \
58
+ --jq '.[] | select(.config.url | contains("zenodo.org")) | .id')
59
+ gh api -X PATCH repos/uw-cryo/asp_plot/hooks/$HOOK -F active=false --jq .active
60
+
61
+ gh release create reports-$(date +%F) \
62
+ reports/*.pdf reports/*_figure_selections.yml reports/regenerate_reports.sh \
63
+ --target main --latest=false \
64
+ --title "Example reports, $(date +%F)" --notes "What changed and why."
65
+
66
+ # Confirm nothing was delivered for the new release, then switch it back on
67
+ gh api "repos/uw-cryo/asp_plot/hooks/$HOOK/deliveries?per_page=3" \
68
+ --jq '.[] | "\(.delivered_at) \(.event) \(.action)"'
69
+ gh api -X PATCH repos/uw-cryo/asp_plot/hooks/$HOOK -F active=true --jq .active
70
+ # then bump REPORTS_RELEASE in docs/fetch_example_reports.sh and commit that one line
71
+ ```
72
+
73
+ Five things matter here:
74
+
75
+ - **Keep the release off Zenodo.** The repository has a Zenodo webhook on
76
+ `release` events, and Zenodo archives every published release as a version of
77
+ the software: it cannot tell a reports release from a version release. The
78
+ `reports-2026-09-18` release was archived that way and, being the newest
79
+ record, became what the DOI badge in the README resolves to. Switch the
80
+ webhook off before publishing and back on afterwards, as above; GitHub does
81
+ not re-send the events it skipped. Always pass `--jq` when reading the hook,
82
+ because its URL embeds an access token that must not end up in a log or a
83
+ comment. A published Zenodo record can only be removed by Zenodo support.
84
+ - **Publish the release before pushing the commit** that points at it. The docs
85
+ build uses `curl -f`, so a tag that does not exist yet fails the build.
86
+ - **`--latest=false`**, or the reports release takes the "Latest" badge from the
87
+ current version release.
88
+ - **Attach the `*_figure_selections.yml` sidecars too.** They are gitignored (they
89
+ hardcode absolute local paths) but they are what makes regeneration
90
+ figure-stable and offline (issue #121), so keeping them with the PDFs they
91
+ produced makes a release self-describing.
92
+ - **Attach `regenerate_reports.sh` as well, for the same reason.** It is
93
+ gitignored too, so no clone of this repository contains it. Attaching it keeps
94
+ a copy with each set of reports, and a release that carries the reports, the
95
+ selections that shaped them, and the commands that produced them can be
96
+ reproduced by someone who has the example datasets and nothing else.
97
+
98
+ Always regenerate with `--reuse-selections` so pages stay figure-for-figure
99
+ comparable and no SlideRule request is made. Reports are capped at 200 effective
100
+ dpi at their placed width (`asp_report --figure-max-dpi`, `0` disables), which is
101
+ what keeps the assets small.
102
+
41
103
  ## Gotchas
42
104
 
43
105
  - **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.
@@ -49,9 +111,9 @@ sphinx-autobuild docs docs/_build/html --open-browser # or sphinx-build for a
49
111
  - **There is no "unadjusted" CSM state on disk after a `bundle_adjust` run** — it writes only `*.adjusted_state.json` for the *optimized* cameras, while the originals exist solely as vendor XML. To get the before-camera for `csm_camera_summary_plot()`, re-run with an identity transform: write a 4x4 identity to `identity.txt` and pass `--initial-transform identity.txt --apply-initial-transform-only` with the same images/XMLs into a separate `-o` prefix. That skips iterations, matching, and reports (seconds, not minutes), and the resulting `.adjust` files are exactly zero translation + identity quaternion, so you can verify the cameras really are untouched. Demonstrated in `notebooks/WorldView/worldview_spacenet_ucsd_stereo.ipynb`.
50
112
  - **CSM camera *differences* deliberately diverge from ASP's `orbit_plot.py`** (#53): `csm_analysis.read_angles_common_frame()` puts both cameras in one satellite body frame estimated from the *original* ephemeris, instead of estimating a frame per camera from its own positions the way `csm_io.read_angles()` (the ASP mirror) does. A solver perturbs positions *and* resamples the ephemeris finer, and over the resulting ~140 m central-difference baseline a 2 m perturbation tilts a per-camera frame by ~0.8° — which is what made the example figure look full of outliers. Angle diffs are also wrapped to [-180, 180). Don't "re-sync" either back to `read_angles()`; `csm_io.py` stays a verbatim ASP mirror and is not where this belongs.
51
113
  - **Airbus DIMAP quaternions are scalar-first** (`Q0` = scalar); they are reordered to the scalar-last `q1..q4` layout the roll/pitch/yaw code expects in `PleiadesMetadata.getAtt_df()`. Don't "fix" the reorder.
52
- - **The altimetry outlier cut is a 30-NMAD gate plus 3σ; both values are deliberate.** A whole ICESat-2 pass can be cloud (over UCSD one pass was a marine-layer return 150–210 m above ground, 21 % of the sample), which defeats a plain mean ± 3σ cut by inflating σ; the gate removes it. Don't "simplify" the cut to median ± 3 NMAD: on the heavy-tailed residuals a DSM has against ICESat-2 in a city that removes a fifth of the real points and lowers every reported NMAD by a third (`_outlier_mask` in `altimetry_source.py`; the case is described in the benchmark notebook's UCSD section). If a benchmark or report shows an RMSE far above its NMAD, look for a cloud pass in the residual map before touching the DEM.
53
- - **`asp_plot.sensors.rpc.read_rpc()` refuses any raster that carries a CRS**, on purpose: an RPC describes the raw image grid, and ASP's `mapproject` copies RPC metadata onto its outputs. However, GDAL sometimes writes a lat/lon geotransform from a NITF's corner coordinates when converting it (one of five CORE3D scenes came out that way), and the reader then calls a raw scene "already map-projected". Strip it with `gdal_edit.py -unsetgt -a_srs "" scene.tif`.
54
- - **SpaceNet CORE3D NITF scenes are JPEG2000, and the ASP 3.8.0-alpha build ships GDAL's JP2OpenJPEG driver as a plugin it never loads**: `gdalinfo` and rasterio both fail with *No JPEG2000 capable driver is available*. `GDAL_DRIVER_PATH=<ASP root>/lib` for one `gdal_translate` to GeoTIFF is enough; the older 3.7.0 build had the driver built in, which is why earlier UCSD runs never hit this.
114
+ - **The altimetry outlier cut is a 30-NMAD gate plus 3σ; both values are deliberate.** A whole ICESat-2 pass can be cloud (over UCSD one pass was a marine-layer return 150–210 m above ground, 21 % of the sample), which defeats a plain mean ± 3σ cut by inflating σ; the gate removes it. Don't "simplify" the cut to median ± 3 NMAD: on the heavy-tailed residuals a DSM has against ICESat-2 in a city that removes a fifth of the real points and lowers every reported NMAD by a third (`_outlier_mask` in `altimetry_source.py`). If a benchmark or report shows an RMSE far above its NMAD, look for a cloud pass in the residual map before touching the DEM.
115
+ - **`asp_plot.sensors.rpc.read_rpc()` refuses any raster that carries a CRS**, on purpose: an RPC describes the raw image grid, and ASP's `mapproject` copies RPC metadata onto its outputs.
116
+ - **SpaceNet CORE3D NITF scenes are JPEG2000, and the ASP 3.8.0-alpha build ships GDAL's JP2OpenJPEG driver as a plugin it never loads**: `gdalinfo` and rasterio both fail with *No JPEG2000 capable driver is available*. `GDAL_DRIVER_PATH=<ASP root>/lib` for one `gdal_translate` to GeoTIFF is enough; the older 3.7.0 build had the driver built in, which is why earlier UCSD runs never hit this. The conda-forge `asp-3.7.0` env on NAS ships it as a plugin too, under `$ASP_ROOT/lib/gdalplugins`.
55
117
  - **ASP multiview triangulation of mapprojected images needs `ISISROOT`** (observed with ASP 3.8.0-alpha, non-ISIS `-t pleiades` session): the joint triangulation aborts with an uncatchable `Isis::IException` (`$ISISROOT/IsisPreferences was not found`) that surfaces as a generic "Failed to run"/killed job. Workaround: `export ISISROOT=<ASP install root>` (the release bundles `IsisPreferences` there). Pair runs and raw-image multiview runs are unaffected; full write-up in a PR #155 comment.
56
118
 
57
119
  ## External Data Sources
@@ -62,6 +62,7 @@ The package is organized by functionality, with each module focused on a specifi
62
62
  - A declarative section registry (`REPORT_SECTIONS`) of `ReportSpec`s replaces the old inline plot-and-append wall: each spec pairs an `enabled(ctx)` predicate with a `build(ctx)` function returning the sections to append. `--no-geometry` / `--no-altimetry` / `--no-pc-align` gating are predicates; figure numbering comes from a per-run counter on the shared `ReportContext`, so section order and numbering are data, not source-line position. The alignment "Page B/C/D" follow-ups are one spec emitting several sections
63
63
  - Section builders: `_build_input_scenes`, `_build_stereo_geometry`, `_build_match_points`, `_build_bundle_adjust`, `_build_disparity`, `_build_dem_results`, `_build_detailed_hillshade`, `_build_altimetry` (→ `_build_altimetry_earth` / `_build_altimetry_planetary`)
64
64
  - `_numbered_sections()`: shared helper turning a plotter's saved-filename list into one `ReportSection` per figure (first gets the caption, continuations are titled "... (continued)") — used by the scenes/geometry/match/disparity builders, whose plotters save one figure per pair on multi-view runs (issue #160)
65
+ - `_keep_figures()`: with `--figures-directory`, copies each section's PNG out of `tmp_asp_report_plots/` as `NN_<title-slug>.png` after `compile_report()` has numbered the sections, and before the plots directory is deleted. The title is in the name because figure numbers shift when a section is gated off
65
66
 
66
67
  **`report_captions.py`** - Caption + description text for the report sections (issue #128)
67
68
  - All section captions and the Earth/planetary alignment descriptions as module constants / small builder functions, moved out of the CLI as a data module (verified byte-identical to the pre-refactor strings via AST comparison)
@@ -216,11 +217,12 @@ The package is organized by functionality, with each module focused on a specifi
216
217
  - Not wired into the main `asp_report` PDF report (standalone class + `gallery` CLI)
217
218
 
218
219
  **`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σ)
220
+ - 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
221
  - `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`
222
+ - **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
223
  - 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
224
  - 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)
225
+ - 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
226
  - Not wired into the `asp_report` PDF (standalone class + `dem_benchmark` CLI)
225
227
 
226
228
  ## CLI Tools
@@ -287,8 +289,8 @@ All CLI tools are in `asp_plot/cli/` and use Click for argument parsing:
287
289
 
288
290
  **`dem_benchmark.py`** - Multi-DEM altimetry benchmark tool (`dem_benchmark` command)
289
291
  - 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
292
+ - `--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`
293
+ - 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
294
 
293
295
  ## Documentation Website
294
296
 
@@ -316,7 +318,7 @@ docs/
316
318
  figures/ # Committed doc figures (e.g. example_gallery.png); referenced from .md via ../figures/
317
319
  contributing.md # Dev setup, testing, release process
318
320
  changelog.md # Includes top-level CHANGELOG.md via MyST include
319
- _static/reports/ # .gitignored — copied from reports/ during build
321
+ _static/reports/ # .gitignored — fetched from the reports-<date> Release during build
320
322
  _extra/examples/figures/ # .gitignored — copied from notebooks/figures/ during build
321
323
  _templates/footer.html # RTD ethical ads placement in footer
322
324
  requirements.txt # Docs-only pip dependencies for RTD
@@ -324,7 +326,8 @@ docs/
324
326
 
325
327
  ### Key Design Decisions
326
328
 
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.
329
+ - **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.
330
+ - **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
331
  - **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
332
  - **`html_extra_path`** is used to serve `notebooks/figures/` at the correct relative path for notebook `<img src>` references.
330
333
  - **`docs/` is excluded from sdist** in `pyproject.toml` so docs never ship in the PyPI/conda package.
@@ -364,7 +367,7 @@ Docs dependencies are in `pyproject.toml` under `[project.optional-dependencies]
364
367
  ## Example Notebooks
365
368
 
366
369
  Example notebooks demonstrating modular usage are organized by sensor type:
367
- - `notebooks/WorldView/` - WorldView examples (Earth-based). `worldview_spacenet_ucsd_stereo.ipynb` is the classic two-scene pair example; `worldview_utqiagvik_stereo.ipynb` (excluded from the docs build) and `worldview_uyuni_jitter_plots.ipynb` cover comprehensive modular usage and jitter plots; the two `*_scene_selection.ipynb` notebooks pick which scenes to process with `stereo_geom` before any ASP run. **`worldview_spacenet_benchmark.ipynb` is the scene-combination benchmark (issue #169)**: with `DEMBenchmark` on one ICESat-2 sample per site it scores every combination and flow — at Atlanta 21 DEMs (all ten pairs of the five same-pass scenes, six `dem_mosaic` blends incl. median / convergence-weighted / `VerticalStdDev`-weighted, six multi-view runs incl. two references) then at UCSD 17 DEMs (five multi-date WorldView-3 scenes over a steep crop of Mount Soledad: three multi-view references, ten pairs, four blends) (both sets of runs come from local driver scripts that are not in the repo; the notebook re-scores whatever is on disk). Crop and scene selection for UCSD are done in the notebook from metadata staged next to the data (`ucsd_mvs/selection/`). Its findings, in its two takeaways sections: at the same-pass flat site single-pair accuracy is set by convergence angle; at the steep multi-date site the point estimates depend more on which scene a pair contains than on its geometry, but a paired track bootstrap (#199) separates only the worst DEM from the rest, so those rules are suggested rather than established; at both sites the multi-view reference scene has the largest effect. The Atlanta example (`worldview_spacenet_atlanta_mvs.ipynb`) is a 3-scene same-pass multi-view run (`parallel_stereo --alignment-method affineepipolar` on the `wv_correct`ed `*_corr.tif` images, nadir13 reference) compared against the ASP-docs-recommended alternative: the three pairwise runs merged with `dem_mosaic` (coverage, DEM difference, ICESat-2 residuals). Two crop-window methods keep the ROIs matched: the reference crop window in mapprojected/ground coordinates converts to image pixels via GDAL's inverse RPC transform on the *original* `.tif` (the `wv_correct` `_corr.tif` carries no RPC metadata) — `gdal.Transformer(["METHOD=RPC", "RPC_HEIGHT=<m>"])` + `TransformPoint(1, lon, lat, h)` on the corners; and the `--left-image-crop-win` for a pair whose left image is *not* the reference is derived from the `bundle_adjust` clean match points that fall inside the reference crop window (no RPCs needed at all).
370
+ - `notebooks/WorldView/` - WorldView examples (Earth-based). `worldview_spacenet_ucsd_stereo.ipynb` is the classic two-scene pair example; `worldview_utqiagvik_stereo.ipynb` (excluded from the docs build) and `worldview_uyuni_jitter_plots.ipynb` cover comprehensive modular usage and jitter plots. The Atlanta example (`worldview_spacenet_atlanta_mvs.ipynb`) is a 3-scene same-pass multi-view run (`parallel_stereo --alignment-method affineepipolar` on the `wv_correct`ed `*_corr.tif` images, nadir13 reference) compared against the ASP-docs-recommended alternative: the three pairwise runs merged with `dem_mosaic` (coverage, DEM difference, ICESat-2 residuals). Two crop-window methods keep the ROIs matched: the reference crop window in mapprojected/ground coordinates converts to image pixels via GDAL's inverse RPC transform on the *original* `.tif` (the `wv_correct` `_corr.tif` carries no RPC metadata) — `gdal.Transformer(["METHOD=RPC", "RPC_HEIGHT=<m>"])` + `TransformPoint(1, lon, lat, h)` on the corners; and the `--left-image-crop-win` for a pair whose left image is *not* the reference is derived from the `bundle_adjust` clean match points that fall inside the reference crop window (no RPCs needed at all). Its benchmark cell, six DEMs scored with `DEMBenchmark` on one ICESat-2 sample, is the worked example of `DEMBenchmark`.
368
371
  - `notebooks/Pleiades/` - Airbus Pléiades Neo tri-stereo example over Marseille (Earth-based): DIMAP stereo-geometry analysis, bundle adjustment of all three scenes, and a 3-image multi-view stereo run (nadir reference, `--left-image-crop-win` derived from the DIMAP `Dataset_Extent` vertices; the ASP docs discourage MVS in favor of pairwise + `dem_mosaic`, which the notebook cites)
369
372
  - `notebooks/ASTER/` - ASTER examples with map-projection and jitter correction (Earth-based)
370
373
  - `notebooks/LRO_NAC/` - Lunar Reconnaissance Orbiter Narrow Angle Camera examples (Lunar)
@@ -5,6 +5,54 @@ 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
+ ## [Unreleased]
9
+
10
+ ## [3.5.0] - 2026-10-23
11
+
12
+ In this release:
13
+
14
+ - `asp_report --figures-directory` keeps a copy of each report figure, named by figure number and title, for use in notebooks and slides.
15
+ - `DEMBenchmark` reuses a translated DEM newer than its inputs instead of rewriting it on every run.
16
+ - Three study notebooks are no longer in the examples.
17
+ - The example reports are regenerated under 3.4.0, and a reports release is kept off Zenodo.
18
+
19
+ ### Added
20
+ - **`asp_report --figures-directory DIR` keeps a copy of each report figure** (`ReportConfig.figures_directory`). The report plots every figure to `tmp_asp_report_plots/` and deletes that directory once the PDF is written, so the only way to show a figure in a notebook or slides was to extract it from the PDF. With the option, each figure is also copied to `DIR` as `NN_<title>.png` (`05_dem_results.png`, `07_icesat_2_atl06_sr_map.png`), where `NN` is its figure number in the PDF. The title is in the name because the numbers shift when a section is skipped. Nothing is kept by default, so existing runs are unchanged.
21
+
22
+ ### Changed
23
+ - **The example reports are regenerated under 3.4.0** and published as [`reports-2026-09-29`](https://github.com/uw-cryo/asp_plot/releases/tag/reports-2026-09-29); the docs build fetches that set. All seven carry the intersection error captions of 3.4.0. `WorldView_Atlanta_MVS` is rebuilt for the first time since the 200 dpi cap, which the fix for deleted `*-L.tif` files ([#202](https://github.com/uw-cryo/asp_plot/issues/202)) made possible, and goes from 22.4 MB to 12.4 MB. Every report was rebuilt with `--reuse-selections`, so each page shows the same altimetry points and clips as before.
24
+ - **A reports release is kept off Zenodo.** Zenodo archives every published GitHub release as a version of the software, and the first reports release had become what the DOI badge resolves to. `AGENTS.md` now has the step that prevents it: the Zenodo webhook is switched off while a reports release is published.
25
+ - **`DEMBenchmark` reuses a translated DEM that is newer than its DEM and its `pc_align` output** instead of rewriting it on every run, so re-scoring from cached `pc_align` runs no longer writes a full-size copy of every DEM. A new copy is written under a temporary name and renamed, so an interrupted run cannot leave a partial file that a later run would reuse.
26
+
27
+ ### Removed
28
+ - **Three study notebooks are no longer examples: `worldview_spacenet_benchmark.ipynb`, `worldview_spacenet_atlanta_stereo_scene_selection.ipynb` and `worldview_spacenet_ucsd_stereo_scene_selection.ipynb`.** Each answered a research question (which scene combination or pair gives the best DEM) rather than showing how to use a feature, and each re-execution added a full new copy of the notebook to the repository's history. `worldview_spacenet_atlanta_mvs.ipynb` remains the worked example of `DEMBenchmark`; `docs/cli/stereo_geom.md` covers assessing the geometry of every pair in a multi-scene delivery; and the UCSD stereo and Atlanta MVS notebooks now say in their text how their pairs were chosen and how the candidates scored (markdown edits only; neither notebook was re-executed).
29
+
30
+ ## [3.4.0] - 2026-09-29
31
+
32
+ In this release:
33
+
34
+ - 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)).
35
+ - The report states what the intersection error measures ([#199](https://github.com/uw-cryo/asp_plot/issues/199)).
36
+ - 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)).
37
+ - 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)).
38
+
39
+ 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.
40
+
41
+ ### Added
42
+ - **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.
43
+ - **`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.
44
+ - **`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.
45
+ - **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`.
46
+
47
+ ### Changed
48
+ - **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.
49
+ - **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.
50
+
51
+ ### Fixed
52
+ - **`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.
53
+ - **`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.
54
+ - **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.
55
+
8
56
  ## [3.3.0] - 2026-09-18
9
57
 
10
58
  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.5.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,20 @@ 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
+ )
140
+ @click.option(
141
+ "--figures-directory",
142
+ prompt=False,
143
+ default=None,
144
+ help="Directory to keep a copy of each report figure in, as NN_<title>.png (e.g. 05_dem_results.png), where NN is the figure number in the PDF. A relative path is taken from the current directory, not from --directory. Use this to show figures in a notebook or slides without extracting them from the PDF. Files of the same name are overwritten. Default: None, the figures are not kept.",
145
+ )
131
146
  def main(
132
147
  directory,
133
148
  bundle_adjust_prefix,
@@ -146,6 +161,8 @@ def main(
146
161
  reuse_selections,
147
162
  report_filename,
148
163
  report_title,
164
+ figure_max_dpi,
165
+ figures_directory,
149
166
  ):
150
167
  """
151
168
  Generate a comprehensive report of ASP processing results.
@@ -172,6 +189,8 @@ def main(
172
189
  reuse_selections=reuse_selections,
173
190
  report_filename=report_filename,
174
191
  report_title=report_title,
192
+ figure_max_dpi=figure_max_dpi,
193
+ figures_directory=figures_directory,
175
194
  report_command=_reconstruct_command(),
176
195
  )
177
196
  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)}"