asp-plot 3.4.0__tar.gz → 3.6.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.
- {asp_plot-3.4.0 → asp_plot-3.6.0}/.gitignore +3 -1
- {asp_plot-3.4.0 → asp_plot-3.6.0}/AGENTS.md +30 -12
- {asp_plot-3.4.0 → asp_plot-3.6.0}/ARCHITECTURE.md +3 -1
- {asp_plot-3.4.0 → asp_plot-3.6.0}/CHANGELOG.md +31 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/PKG-INFO +1 -1
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/cli/asp_report.py +16 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/dem_benchmark.py +31 -6
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/report_pipeline.py +42 -6
- {asp_plot-3.4.0 → asp_plot-3.6.0}/pyproject.toml +1 -1
- {asp_plot-3.4.0 → asp_plot-3.6.0}/.flake8 +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/.github/ISSUE_TEMPLATE/problem-report.yml +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/.github/workflows/claude-code-review.yml +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/.github/workflows/claude.yml +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/.github/workflows/release.yml +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/.github/workflows/run-tests.yml +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/.pre-commit-config.yaml +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/.readthedocs.yaml +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/CLAUDE.md +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/LICENSE +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/README.md +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/__init__.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/alignment.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/altimetry.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/altimetry_plots.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/altimetry_source.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/asp_log.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/bodies.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/bundle_adjust.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/cli/__init__.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/cli/bundle_adjust_cameras.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/cli/csm_camera_plot.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/cli/dem_benchmark.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/cli/gallery.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/cli/request_planetary_altimetry.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/cli/stereo_geom.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/csm_analysis.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/csm_camera.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/csm_io.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/gallery.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/icesat2_source.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/mapproject.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/planetary_source.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/processing_parameters.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/report.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/report_captions.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/scenes.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/selections.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/sensors/__init__.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/sensors/aster.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/sensors/base.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/sensors/dimap.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/sensors/dimap_v1.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/sensors/rpc.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/sensors/worldview.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/stereo.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/stereo_geometry.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/stereopair_metadata_parser.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/asp_plot/utils.py +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/conda-forge-recipe/meta.yaml +0 -0
- {asp_plot-3.4.0 → asp_plot-3.6.0}/environment.yml +0 -0
|
@@ -140,7 +140,9 @@ 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
|
|
@@ -52,15 +52,35 @@ To publish a regenerated set:
|
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
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
|
+
|
|
55
61
|
gh release create reports-$(date +%F) \
|
|
56
62
|
reports/*.pdf reports/*_figure_selections.yml reports/regenerate_reports.sh \
|
|
57
63
|
--target main --latest=false \
|
|
58
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
|
|
59
70
|
# then bump REPORTS_RELEASE in docs/fetch_example_reports.sh and commit that one line
|
|
60
71
|
```
|
|
61
72
|
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
64
84
|
- **Publish the release before pushing the commit** that points at it. The docs
|
|
65
85
|
build uses `curl -f`, so a tag that does not exist yet fails the build.
|
|
66
86
|
- **`--latest=false`**, or the reports release takes the "Latest" badge from the
|
|
@@ -69,13 +89,11 @@ Four things matter here:
|
|
|
69
89
|
hardcode absolute local paths) but they are what makes regeneration
|
|
70
90
|
figure-stable and offline (issue #121), so keeping them with the PDFs they
|
|
71
91
|
produced makes a release self-describing.
|
|
72
|
-
- **Attach `regenerate_reports.sh` as well, for the same reason
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
produced them can be reproduced by someone who has the example datasets and
|
|
78
|
-
nothing else.
|
|
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.
|
|
79
97
|
|
|
80
98
|
Always regenerate with `--reuse-selections` so pages stay figure-for-figure
|
|
81
99
|
comparable and no SlideRule request is made. Reports are capped at 200 effective
|
|
@@ -93,9 +111,9 @@ what keeps the assets small.
|
|
|
93
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`.
|
|
94
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.
|
|
95
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.
|
|
96
|
-
- **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
|
|
97
|
-
- **`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.
|
|
98
|
-
- **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`.
|
|
99
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.
|
|
100
118
|
|
|
101
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)
|
|
@@ -246,6 +247,7 @@ All CLI tools are in `asp_plot/cli/` and use Click for argument parsing:
|
|
|
246
247
|
- `--no-altimetry`: Skip the altimetry comparisons plotted by default. Auto-detects planetary body from DEM CRS: Earth → ICESat-2 (requires internet), Moon → LOLA, Mars → MOLA. For planetary DEMs, requires `--altimetry-csv`.
|
|
247
248
|
- `--altimetry-csv`: Path to a LOLA/MOLA CSV from the ODE GDS API. **Mars: must be the `*_pts_csv.csv` (not `*_topo_csv.csv`) — the loader requires the `PLANET_RAD` column to avoid the oblate-areoid offset.** Moon accepts either the `*_topo_simple_csv.csv` (results=u) or the `*_pts_csv.csv` (results=p). Obtained via the `request_planetary_altimetry` CLI tool.
|
|
248
249
|
- `--no-pc-align`: Skips the `pc_align` step; by default (when altimetry is on) the report runs `pc_align` against the reference altimetry (ICESat-2 for Earth, MOLA for Mars, LOLA for Moon) after the existing altimetry plots and appends an alignment report. **Earth success path**: adds four pages (alignment report page, pre/post landcover histogram, aligned profile, aligned best/worst segments). **Planetary success path**: adds three pages (alignment report page, pre/post mapview, pre/post histogram). `insufficient_points` and `no_improvement` outcomes emit a single alignment report page on either branch. Skipped automatically when `--no-altimetry` is set.
|
|
250
|
+
- `--pc-align-min-points`: Fewest altimetry points on the DEM for the `pc_align` step to run (`ReportConfig.pc_align_min_points`); `None`, the default, keeps the alignment methods' own minimums (500 ICESat-2 points, 20 LOLA/MOLA points). Below the minimum the alignment page says the step was skipped.
|
|
249
251
|
- `--no-geometry`: Skip the stereo geometry plots drawn by default (used for planetary missions)
|
|
250
252
|
- `--subset-km`: Hillshade subset size in km (default: 1.0)
|
|
251
253
|
- `--atl06sr-time-range`: Time range for ICESat-2 ATL06-SR requests. `"all"` (default) for full mission, `"auto"` for scene metadata ±1 year, `"START,END"` for a custom range, or a single date (buffered by ±1 year).
|
|
@@ -366,7 +368,7 @@ Docs dependencies are in `pyproject.toml` under `[project.optional-dependencies]
|
|
|
366
368
|
## Example Notebooks
|
|
367
369
|
|
|
368
370
|
Example notebooks demonstrating modular usage are organized by sensor type:
|
|
369
|
-
- `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
|
|
371
|
+
- `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`.
|
|
370
372
|
- `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)
|
|
371
373
|
- `notebooks/ASTER/` - ASTER examples with map-projection and jitter correction (Earth-based)
|
|
372
374
|
- `notebooks/LRO_NAC/` - Lunar Reconnaissance Orbiter Narrow Angle Camera examples (Lunar)
|
|
@@ -5,6 +5,37 @@ 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.6.0] - 2026-10-30
|
|
11
|
+
|
|
12
|
+
In this release:
|
|
13
|
+
|
|
14
|
+
- `asp_report --pc-align-min-points` sets how many altimetry points the `pc_align` step needs before it runs, so small DEMs that ICESat-2 crosses only a few times can still be aligned.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **`asp_report --pc-align-min-points N` sets how many altimetry points the `pc_align` step needs before it runs** (`ReportConfig.pc_align_min_points`). The step skipped itself below a fixed 500 ICESat-2 points (20 for LOLA/MOLA), so a report on a DEM of about 1 km², which ICESat-2 crosses only a few times, compared against the points but never aligned to them. The default is unchanged.
|
|
18
|
+
|
|
19
|
+
## [3.5.0] - 2026-10-23
|
|
20
|
+
|
|
21
|
+
In this release:
|
|
22
|
+
|
|
23
|
+
- `asp_report --figures-directory` keeps a copy of each report figure, named by figure number and title, for use in notebooks and slides.
|
|
24
|
+
- `DEMBenchmark` reuses a translated DEM newer than its inputs instead of rewriting it on every run.
|
|
25
|
+
- Three study notebooks are no longer in the examples.
|
|
26
|
+
- The example reports are regenerated under 3.4.0, and a reports release is kept off Zenodo.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- **`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.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- **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.
|
|
33
|
+
- **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.
|
|
34
|
+
- **`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.
|
|
35
|
+
|
|
36
|
+
### Removed
|
|
37
|
+
- **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).
|
|
38
|
+
|
|
8
39
|
## [3.4.0] - 2026-09-29
|
|
9
40
|
|
|
10
41
|
In this release:
|
|
@@ -92,6 +92,12 @@ def _reconstruct_command():
|
|
|
92
92
|
default=False,
|
|
93
93
|
help="Skip the pc_align step (run by default against the reference altimetry -- ICESat-2 for Earth, MOLA for Mars, LOLA for Moon -- appending the alignment-report pages; skipped automatically with --no-altimetry).",
|
|
94
94
|
)
|
|
95
|
+
@click.option(
|
|
96
|
+
"--pc-align-min-points",
|
|
97
|
+
type=int,
|
|
98
|
+
default=None,
|
|
99
|
+
help="Fewest altimetry points on the DEM for the pc_align step to run; below it the alignment page says the step was skipped. Default: 500 for ICESat-2, 20 for LOLA/MOLA.",
|
|
100
|
+
)
|
|
95
101
|
@click.option(
|
|
96
102
|
"--no-geometry",
|
|
97
103
|
is_flag=True,
|
|
@@ -137,6 +143,12 @@ def _reconstruct_command():
|
|
|
137
143
|
type=int,
|
|
138
144
|
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
145
|
)
|
|
146
|
+
@click.option(
|
|
147
|
+
"--figures-directory",
|
|
148
|
+
prompt=False,
|
|
149
|
+
default=None,
|
|
150
|
+
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.",
|
|
151
|
+
)
|
|
140
152
|
def main(
|
|
141
153
|
directory,
|
|
142
154
|
bundle_adjust_prefix,
|
|
@@ -149,6 +161,7 @@ def main(
|
|
|
149
161
|
no_altimetry,
|
|
150
162
|
altimetry_csv,
|
|
151
163
|
no_pc_align,
|
|
164
|
+
pc_align_min_points,
|
|
152
165
|
no_geometry,
|
|
153
166
|
subset_km,
|
|
154
167
|
atl06sr_time_range,
|
|
@@ -156,6 +169,7 @@ def main(
|
|
|
156
169
|
report_filename,
|
|
157
170
|
report_title,
|
|
158
171
|
figure_max_dpi,
|
|
172
|
+
figures_directory,
|
|
159
173
|
):
|
|
160
174
|
"""
|
|
161
175
|
Generate a comprehensive report of ASP processing results.
|
|
@@ -176,6 +190,7 @@ def main(
|
|
|
176
190
|
plot_altimetry=not no_altimetry,
|
|
177
191
|
altimetry_csv=altimetry_csv,
|
|
178
192
|
pc_align=not no_pc_align,
|
|
193
|
+
pc_align_min_points=pc_align_min_points,
|
|
179
194
|
plot_geometry=not no_geometry,
|
|
180
195
|
subset_km=subset_km,
|
|
181
196
|
atl06sr_time_range=atl06sr_time_range,
|
|
@@ -183,6 +198,7 @@ def main(
|
|
|
183
198
|
report_filename=report_filename,
|
|
184
199
|
report_title=report_title,
|
|
185
200
|
figure_max_dpi=figure_max_dpi,
|
|
201
|
+
figures_directory=figures_directory,
|
|
186
202
|
report_command=_reconstruct_command(),
|
|
187
203
|
)
|
|
188
204
|
run_report(config)
|
|
@@ -54,6 +54,7 @@ Examples
|
|
|
54
54
|
>>> bench.summary_plot(save_dir="atlanta_mvs", fig_fn="dem_benchmark.png")
|
|
55
55
|
"""
|
|
56
56
|
|
|
57
|
+
import glob
|
|
57
58
|
import logging
|
|
58
59
|
import os
|
|
59
60
|
import re
|
|
@@ -235,6 +236,14 @@ def _safe_dirname(label):
|
|
|
235
236
|
return re.sub(r"[^\w.-]+", "_", label).strip("_") or "dem"
|
|
236
237
|
|
|
237
238
|
|
|
239
|
+
def _is_newer_than(fn, sources):
|
|
240
|
+
"""True if ``fn`` exists and is at least as new as every existing file in ``sources``."""
|
|
241
|
+
if not os.path.exists(fn):
|
|
242
|
+
return False
|
|
243
|
+
mtime = os.path.getmtime(fn)
|
|
244
|
+
return all(os.path.getmtime(s) <= mtime for s in sources if os.path.exists(s))
|
|
245
|
+
|
|
246
|
+
|
|
238
247
|
def _dh_stats(dh, prefix):
|
|
239
248
|
"""median / NMAD / RMSE of a dh series as ``{prefix}_*_m`` keys (NaN when empty)."""
|
|
240
249
|
values = np.asarray(dh, dtype=float)
|
|
@@ -363,8 +372,10 @@ class DEMBenchmark:
|
|
|
363
372
|
directory : str
|
|
364
373
|
Working folder. pc_align products, the pc_align CSV and the translated
|
|
365
374
|
DEM copy for each candidate go under ``<directory>/dem_benchmark/<label>/``.
|
|
366
|
-
The translated copy is
|
|
367
|
-
|
|
375
|
+
The translated copy is reused while it is newer than its DEM and the
|
|
376
|
+
pc_align output, and otherwise rewritten (under a temporary name, then
|
|
377
|
+
renamed), so two benchmarks sharing a directory and a label must not
|
|
378
|
+
run at the same time.
|
|
368
379
|
dems : dict or list
|
|
369
380
|
``{label: dem_fn}`` (order preserved), or a list of paths /
|
|
370
381
|
``"label=path"`` strings (see :func:`parse_dem_specs`).
|
|
@@ -597,7 +608,8 @@ class DEMBenchmark:
|
|
|
597
608
|
Also run ``pc_align --compute-translation-only`` per DEM and
|
|
598
609
|
re-score against the translated copy. Default True. Existing
|
|
599
610
|
pc_align outputs under the benchmark folder are reused, so a
|
|
600
|
-
re-run makes no pc_align call and works offline
|
|
611
|
+
re-run makes no pc_align call and works offline; so are translated
|
|
612
|
+
DEMs newer than their DEM and pc_align output.
|
|
601
613
|
minimum_points : int, optional
|
|
602
614
|
Skip pc_align for a DEM with fewer valid dh points, default 500
|
|
603
615
|
(the report's threshold).
|
|
@@ -992,10 +1004,23 @@ class DEMBenchmark:
|
|
|
992
1004
|
)
|
|
993
1005
|
return None, None
|
|
994
1006
|
stem = os.path.splitext(os.path.basename(dem_fn))[0]
|
|
995
|
-
aligned_fn =
|
|
996
|
-
|
|
997
|
-
|
|
1007
|
+
aligned_fn = os.path.join(label_dir, f"{stem}_pc_align_translated.tif")
|
|
1008
|
+
if _is_newer_than(
|
|
1009
|
+
aligned_fn,
|
|
1010
|
+
[dem_fn]
|
|
1011
|
+
+ glob.glob(os.path.join(label_dir, "pc_align", f"{prefix_name}-*")),
|
|
1012
|
+
):
|
|
1013
|
+
print(f" Reusing translated DEM {aligned_fn}")
|
|
1014
|
+
return report, aligned_fn
|
|
1015
|
+
# Write under a temporary name and rename, so an interrupted run never
|
|
1016
|
+
# leaves a partial copy that the next run would take as current.
|
|
1017
|
+
partial_fn = os.path.join(label_dir, f"{stem}_pc_align_translated.partial.tif")
|
|
1018
|
+
written = alignment.apply_dem_translation(
|
|
1019
|
+
output_prefix=output_prefix, output_fn=partial_fn
|
|
998
1020
|
)
|
|
1021
|
+
if written != partial_fn:
|
|
1022
|
+
return report, written
|
|
1023
|
+
os.replace(partial_fn, aligned_fn)
|
|
999
1024
|
return report, aligned_fn
|
|
1000
1025
|
|
|
1001
1026
|
def _reference_stats(self, label, dem_fn):
|
|
@@ -14,6 +14,7 @@ Captions live in :mod:`asp_plot.report_captions`.
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
import os
|
|
17
|
+
import re
|
|
17
18
|
import shutil
|
|
18
19
|
from dataclasses import dataclass, field
|
|
19
20
|
from datetime import datetime, timezone
|
|
@@ -74,6 +75,9 @@ class ReportConfig:
|
|
|
74
75
|
plot_altimetry: bool = True
|
|
75
76
|
altimetry_csv: Optional[str] = None
|
|
76
77
|
pc_align: bool = True
|
|
78
|
+
# Fewest altimetry points on the DEM for pc_align to run. None keeps the
|
|
79
|
+
# alignment methods' own defaults: 500 for ICESat-2, 20 for LOLA/MOLA.
|
|
80
|
+
pc_align_min_points: Optional[int] = None
|
|
77
81
|
plot_geometry: bool = True
|
|
78
82
|
subset_km: float = 1.0
|
|
79
83
|
atl06sr_time_range: str = "all"
|
|
@@ -83,6 +87,9 @@ class ReportConfig:
|
|
|
83
87
|
# Resolution ceiling for figures embedded in the PDF, applied at the size
|
|
84
88
|
# each figure is placed on the page. See asp_plot.report.FIGURE_MAX_DPI.
|
|
85
89
|
figure_max_dpi: Optional[int] = FIGURE_MAX_DPI
|
|
90
|
+
# Directory to keep a copy of each report figure in, as NN_<title>.png.
|
|
91
|
+
# None keeps nothing: the figures are deleted with the plots directory.
|
|
92
|
+
figures_directory: Optional[str] = None
|
|
86
93
|
# Reconstructed "asp_report --flag ..." string recorded in the report. The
|
|
87
94
|
# CLI builds this from the Click context; non-CLI callers may leave it None.
|
|
88
95
|
report_command: Optional[str] = None
|
|
@@ -624,10 +631,10 @@ def _build_altimetry_earth(ctx: ReportContext, ctx_kwargs_altimetry: dict):
|
|
|
624
631
|
|
|
625
632
|
# ---- pc_align + ICESat-2 alignment report (Earth only) ----
|
|
626
633
|
if cfg.pc_align:
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
)
|
|
634
|
+
align_kwargs = {"processing_level": "all", "improvement_threshold_pct": 5.0}
|
|
635
|
+
if cfg.pc_align_min_points is not None:
|
|
636
|
+
align_kwargs["minimum_points"] = cfg.pc_align_min_points
|
|
637
|
+
align_result = icesat.align_and_evaluate(**align_kwargs)
|
|
631
638
|
stats_row = _stats_row_from_result(align_result)
|
|
632
639
|
|
|
633
640
|
align_title = "DEM Alignment with ICESat-2"
|
|
@@ -773,7 +780,10 @@ def _build_altimetry_planetary(ctx: ReportContext, body: str):
|
|
|
773
780
|
|
|
774
781
|
# ---- pc_align + planetary alignment report (Moon/Mars) ----
|
|
775
782
|
if cfg.pc_align:
|
|
776
|
-
|
|
783
|
+
align_kwargs = {}
|
|
784
|
+
if cfg.pc_align_min_points is not None:
|
|
785
|
+
align_kwargs["minimum_points"] = cfg.pc_align_min_points
|
|
786
|
+
align_result = alt.align_and_evaluate_planetary(**align_kwargs)
|
|
777
787
|
stats_row = _stats_row_from_result(align_result)
|
|
778
788
|
|
|
779
789
|
align_title = f"DEM Alignment with {instrument}"
|
|
@@ -1052,12 +1062,35 @@ def _write_figure_selections(ctx: ReportContext) -> None:
|
|
|
1052
1062
|
print(f"\nCould not write figure selections: {e}\n")
|
|
1053
1063
|
|
|
1054
1064
|
|
|
1065
|
+
def _keep_figures(sections: List[object], figures_directory: str) -> None:
|
|
1066
|
+
"""Copy each report figure to ``figures_directory`` as ``NN_<title>.png``.
|
|
1067
|
+
|
|
1068
|
+
``NN`` is the figure number printed in the PDF and ``<title>`` the page
|
|
1069
|
+
heading in lowercase with runs of other characters replaced by ``_``
|
|
1070
|
+
(e.g. ``05_dem_results.png``), so a figure can be picked by name even when
|
|
1071
|
+
a section is skipped and the numbers shift. Files of the same name are
|
|
1072
|
+
overwritten; nothing else in the directory is touched.
|
|
1073
|
+
"""
|
|
1074
|
+
figures_directory = os.path.expanduser(figures_directory)
|
|
1075
|
+
os.makedirs(figures_directory, exist_ok=True)
|
|
1076
|
+
kept = 0
|
|
1077
|
+
for section in sections:
|
|
1078
|
+
if not section.image_path or not os.path.exists(section.image_path):
|
|
1079
|
+
continue
|
|
1080
|
+
slug = re.sub(r"[^a-z0-9]+", "_", section.title.lower()).strip("_")
|
|
1081
|
+
fn = f"{section.figure_number:02}_{slug}.png"
|
|
1082
|
+
shutil.copy2(section.image_path, os.path.join(figures_directory, fn))
|
|
1083
|
+
kept += 1
|
|
1084
|
+
print(f"\nKept {kept} report figures in {figures_directory}\n")
|
|
1085
|
+
|
|
1086
|
+
|
|
1055
1087
|
def run_report(config: ReportConfig) -> str:
|
|
1056
1088
|
"""Generate the ASP processing report described by ``config``.
|
|
1057
1089
|
|
|
1058
1090
|
Builds the runtime context, iterates the declarative
|
|
1059
1091
|
:data:`REPORT_SECTIONS` registry to assemble the report body, compiles the
|
|
1060
|
-
PDF, writes the figure-selections sidecar,
|
|
1092
|
+
PDF, writes the figure-selections sidecar, keeps a copy of the figures if
|
|
1093
|
+
``config.figures_directory`` is set, and cleans up. Returns the path
|
|
1061
1094
|
to the written PDF. Importable and callable without any Click context.
|
|
1062
1095
|
"""
|
|
1063
1096
|
ctx = _setup_context(config)
|
|
@@ -1088,6 +1121,9 @@ def run_report(config: ReportConfig) -> str:
|
|
|
1088
1121
|
|
|
1089
1122
|
_write_figure_selections(ctx)
|
|
1090
1123
|
|
|
1124
|
+
if config.figures_directory:
|
|
1125
|
+
_keep_figures(sections, config.figures_directory)
|
|
1126
|
+
|
|
1091
1127
|
shutil.rmtree(ctx.plots_directory)
|
|
1092
1128
|
|
|
1093
1129
|
print(f"\n\nReport saved to {ctx.report_pdf_path}\n\n")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|