asp-plot 2.1.0__tar.gz → 3.0.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 (58) hide show
  1. {asp_plot-2.1.0 → asp_plot-3.0.0}/.github/workflows/claude-code-review.yml +1 -1
  2. {asp_plot-2.1.0 → asp_plot-3.0.0}/AGENTS.md +3 -1
  3. {asp_plot-2.1.0 → asp_plot-3.0.0}/ARCHITECTURE.md +32 -28
  4. {asp_plot-2.1.0 → asp_plot-3.0.0}/CHANGELOG.md +40 -0
  5. {asp_plot-2.1.0 → asp_plot-3.0.0}/PKG-INFO +3 -3
  6. {asp_plot-2.1.0 → asp_plot-3.0.0}/README.md +1 -1
  7. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/bundle_adjust.py +14 -5
  8. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/cli/asp_report.py +43 -48
  9. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/cli/csm_camera_plot.py +31 -31
  10. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/cli/gallery.py +9 -8
  11. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/cli/request_planetary_altimetry.py +4 -4
  12. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/cli/stereo_geom.py +9 -9
  13. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/csm_analysis.py +146 -27
  14. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/csm_camera.py +103 -26
  15. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/mapproject.py +38 -6
  16. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/processing_parameters.py +15 -2
  17. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/report.py +2 -1
  18. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/report_captions.py +1 -1
  19. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/report_pipeline.py +37 -34
  20. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/stereo.py +266 -73
  21. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/utils.py +36 -0
  22. {asp_plot-2.1.0 → asp_plot-3.0.0}/pyproject.toml +1 -1
  23. {asp_plot-2.1.0 → asp_plot-3.0.0}/.flake8 +0 -0
  24. {asp_plot-2.1.0 → asp_plot-3.0.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  25. {asp_plot-2.1.0 → asp_plot-3.0.0}/.github/ISSUE_TEMPLATE/problem-report.yml +0 -0
  26. {asp_plot-2.1.0 → asp_plot-3.0.0}/.github/workflows/claude.yml +0 -0
  27. {asp_plot-2.1.0 → asp_plot-3.0.0}/.github/workflows/release.yml +0 -0
  28. {asp_plot-2.1.0 → asp_plot-3.0.0}/.github/workflows/run-tests.yml +0 -0
  29. {asp_plot-2.1.0 → asp_plot-3.0.0}/.gitignore +0 -0
  30. {asp_plot-2.1.0 → asp_plot-3.0.0}/.pre-commit-config.yaml +0 -0
  31. {asp_plot-2.1.0 → asp_plot-3.0.0}/.readthedocs.yaml +0 -0
  32. {asp_plot-2.1.0 → asp_plot-3.0.0}/CLAUDE.md +0 -0
  33. {asp_plot-2.1.0 → asp_plot-3.0.0}/LICENSE +0 -0
  34. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/__init__.py +0 -0
  35. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/alignment.py +0 -0
  36. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/altimetry.py +0 -0
  37. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/altimetry_plots.py +0 -0
  38. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/altimetry_source.py +0 -0
  39. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/asp_log.py +0 -0
  40. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/bodies.py +0 -0
  41. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/cli/__init__.py +0 -0
  42. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/csm_io.py +0 -0
  43. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/gallery.py +0 -0
  44. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/icesat2_source.py +0 -0
  45. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/planetary_source.py +0 -0
  46. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/scenes.py +0 -0
  47. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/selections.py +0 -0
  48. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/sensors/__init__.py +0 -0
  49. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/sensors/aster.py +0 -0
  50. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/sensors/base.py +0 -0
  51. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/sensors/dimap.py +0 -0
  52. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/sensors/dimap_v1.py +0 -0
  53. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/sensors/rpc.py +0 -0
  54. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/sensors/worldview.py +0 -0
  55. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/stereo_geometry.py +0 -0
  56. {asp_plot-2.1.0 → asp_plot-3.0.0}/asp_plot/stereopair_metadata_parser.py +0 -0
  57. {asp_plot-2.1.0 → asp_plot-3.0.0}/conda-forge-recipe/meta.yaml +0 -0
  58. {asp_plot-2.1.0 → asp_plot-3.0.0}/environment.yml +0 -0
@@ -34,6 +34,6 @@ jobs:
34
34
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
35
35
  plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
36
36
  plugins: 'code-review@claude-code-plugins'
37
- prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
37
+ prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }} --comment'
38
38
  # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
39
39
  # or https://code.claude.com/docs/en/cli-reference for available options
@@ -46,13 +46,15 @@ sphinx-autobuild docs docs/_build/html --open-browser # or sphinx-build for a
46
46
  - **ASP's `mapproject` writes no log file**; its command is reconstructed from output GeoTIFF metadata (`mapproject.py`), not parsed from logs like the other tools.
47
47
  - **Attribution vs sensor naming is deliberate**: copyright/attribution names the rights-holder (`detect_satellite_attribution()` → `"Vantor"` or `"Airbus DS"`), while readers in the `asp_plot/sensors/` package are named for the satellite family (WorldView, Pleiades). Don't reconcile them into one name (#137).
48
48
  - **Package vs CLI naming is a deliberate split** (v2.0.0, #165): the *package* is `asp_plot` (`import asp_plot`, `pip install asp-plot`), the report *command* is `asp_report` (`asp_plot/cli/asp_report.py`). There is no `asp_plot` console script and no alias — don't "fix" either name to match the other. The other four CLIs (`stereo_geom`, `csm_camera_plot`, `request_planetary_altimetry`, `gallery`) are unprefixed and unchanged.
49
+ - **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
+ - **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.
49
51
  - **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.
50
52
  - **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.
51
53
 
52
54
  ## External Data Sources
53
55
 
54
56
  - **ICESat-2 ATL06-SR** (Earth): requested through the SlideRule API; results cached as parquet next to the report.
55
- - **LOLA (Moon) / MOLA (Mars)**: async queries to the ODE GDS REST API via the `request_planetary_altimetry` CLI — the user gets a download link by email, then passes the CSV to `asp_report --altimetry_csv`. Coordinates are east-positive 0–360 longitude, planetocentric latitude.
57
+ - **LOLA (Moon) / MOLA (Mars)**: async queries to the ODE GDS REST API via the `request_planetary_altimetry` CLI — the user gets a download link by email, then passes the CSV to `asp_report --altimetry-csv`. Coordinates are east-positive 0–360 longitude, planetocentric latitude.
56
58
 
57
59
  ## Testing
58
60
 
@@ -42,7 +42,7 @@ The package is organized by functionality, with each module focused on a specifi
42
42
  - `pixel_window_to_bbox()` / `bbox_to_pixel_offset()`: convert a detailed-hillshade clip between a DEM-CRS map bbox and a top-left pixel offset. `bbox_to_pixel_offset` uses `rowcol(..., op=round)` (round-to-nearest, **not** floor) so a replayed clip doesn't drift by a pixel from floating-point error
43
43
  - `reproject_bbox(bbox, src_crs, dst_crs)`: reprojects a clip bbox between CRSs (via `rasterio.warp.transform_bounds`) so clip reuse works across stereo variants in different projections (e.g. MOC non-mapproj Stereographic vs mapproj Sinusoidal). `plot_detailed_hillshade(clip_windows_crs=...)` passes the manifest's `dem_crs`; reprojection is a no-op when CRSs match
44
44
  - Deliberately imports nothing from `report.py` / `fpdf`, so it is safe to use from notebooks
45
- - The `asp_report` CLI always writes `<report_stem>_figure_selections.yml` next to the report; `--reuse_selections PATH` replays a prior run's choices
45
+ - The `asp_report` CLI always writes `<report_stem>_figure_selections.yml` next to the report; `--reuse-selections PATH` replays a prior run's choices
46
46
 
47
47
  **`report.py`** - PDF report generation using fpdf2
48
48
  - `ReportSection`: Dataclass representing a report figure (title, image path, caption)
@@ -53,13 +53,13 @@ The package is organized by functionality, with each module focused on a specifi
53
53
  - `_add_processing_parameters_page()`: helper that renders the runtime summary table plus the bundle_adjust / stereo / point2dem / report commands on page 2 (moved from the trailing page as of v1.13.0). Also renders the reconstructed `mapproject` command(s) (from the optional `mapproject` list key; see `mapproject.py`) with a "reconstructed from output metadata" note.
54
54
  - `_fmt_sig()`: formats a number compactly — 2 decimals for |x| < 10, 1 decimal for 10 ≤ |x| < 100, 0 decimals above, "n/a" for non-finite. Used for alignment stats.
55
55
  - Title page displays: processing date, ASP version (from logs), asp_plot version (from package metadata)
56
- - Page order: title + DEM summary → Processing Parameters → diagnostic figures → (if `--pc_align` ran) alignment report page + aligned-DEM figures.
56
+ - Page order: title + DEM summary → Processing Parameters → diagnostic figures → (if the `pc_align` step ran) alignment report page + aligned-DEM figures.
57
57
  - `report.py` is fed declaratively by `report_pipeline.py`; it was **not** rewritten in #128.
58
58
 
59
59
  **`report_pipeline.py`** - Declarative report pipeline behind the `asp_report` CLI (issue #128)
60
60
  - `ReportConfig`: dataclass packing the ~18 CLI options into one Click-free object (field names/defaults mirror the options one-for-one, guarded by a test)
61
61
  - `run_report(config)`: importable/callable from notebooks and tests with no Click context; returns the written PDF path
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. `--plot_geometry` / `--plot_altimetry` / `--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
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
65
 
@@ -77,8 +77,8 @@ The package is organized by functionality, with each module focused on a specifi
77
77
  - Used by report generation to document processing settings
78
78
 
79
79
  **`mapproject.py`** - Reconstruct `mapproject` commands from output GeoTIFF metadata (issue #96)
80
- - ASP's `mapproject` writes **no log file** (unlike `bundle_adjust`/`stereo`/`point2dem`), so `asp_log.py` has nothing to parse for the mapprojection step. Instead of requiring a new ASP `--log` flag, the command is reconstructed **from the output data alone**: ASP stamps `INPUT_IMAGE_FILE` / `CAMERA_FILE` / `DEM_FILE` / `CAMERA_MODEL_TYPE` / `BUNDLE_ADJUST_PREFIX` into each mapprojected GeoTIFF header, and the raster's own CRS / resolution / bounds give `--t_srs` / `--tr` / `--t_projwin`
81
- - `reconstruct_mapproject_command(raster_path)`: returns the `mapproject ...` string, or `None` if the ASP mapproject tag signature (`REQUIRED_TAGS` = `INPUT_IMAGE_FILE` + `CAMERA_FILE` + `DEM_FILE`, all read back during reconstruction) is absent. Reuses the `utils.Raster` wrapper (free `NotGeoreferencedWarning` suppression + `get_epsg_code()` with the compound-CRS 2D fallback + `get_gsd()`/bounds) rather than re-opening with raw rasterio. `--t_srs` is `EPSG:XXXX` when an EPSG code exists (incl. the 2D fallback), else the quoted PROJ string (custom planetary/local frames, e.g. jitter stereographic); a malformed CRS returns `None` and logs instead of crashing the report. `_format_coord` renders coordinates to 12 significant figures, positionally — clean for large UTM northings *and* full-precision for degree-scale geographic `--tr`, with no scientific notation or float-repr noise. The reconstruction is faithful but **not byte-for-byte re-runnable** (session is the resolved `-t`, an input `--mpp` shows as the resolved `--tr`, output reads the actual filename) — the report flags this with a one-line note
80
+ - ASP's `mapproject` writes **no log file** (unlike `bundle_adjust`/`stereo`/`point2dem`), so `asp_log.py` has nothing to parse for the mapprojection step. Instead of requiring a new ASP `--log` flag, the command is reconstructed **from the output data alone**: ASP stamps `INPUT_IMAGE_FILE` / `CAMERA_FILE` / `DEM_FILE` / `CAMERA_MODEL_TYPE` / `BUNDLE_ADJUST_PREFIX` into each mapprojected GeoTIFF header, and the raster's own CRS / resolution / bounds give `--t_srs` / `--tr` / `--t_projwin` (the bounds shifted half a pixel NW to ASP's pixel-edge convention, #148)
81
+ - `reconstruct_mapproject_command(raster_path)`: returns the `mapproject ...` string, or `None` if the ASP mapproject tag signature (`REQUIRED_TAGS` = `INPUT_IMAGE_FILE` + `CAMERA_FILE` + `DEM_FILE`, all read back during reconstruction) is absent. Reuses the `utils.Raster` wrapper (free `NotGeoreferencedWarning` suppression + `get_epsg_code()` with the compound-CRS 2D fallback + `get_gsd()`/bounds) rather than re-opening with raw rasterio. `--t_srs` is `EPSG:XXXX` when an EPSG code exists (incl. the 2D fallback), else the quoted PROJ string (custom planetary/local frames, e.g. jitter stereographic); a malformed CRS returns `None` and logs instead of crashing the report. `_format_coord` renders coordinates to 12 significant figures, positionally — clean for large UTM northings *and* full-precision for degree-scale geographic `--tr`, with no scientific notation or float-repr noise. The reconstruction is faithful but **not byte-for-byte re-runnable** (session is the resolved `-t`, an input `--mpp` shows as the resolved `--tr`, output reads the actual filename) — the report flags this with a one-line note. It **is grid-exact on ASP >= 3.7.0** (#148): `--t_projwin` is emitted as ASP's pixel-edge box — the GDAL bounds shifted half a pixel NW (`x − tr/2`, `y + tr/2`) — because ASP snaps a projwin by an edge→center conversion plus round-to-nearest, and the raw GDAL bounds of an ASP output (post `PixelIsPoint` shift) land exactly on the rounding tie, drifting the re-run grid one pixel east. The shifted box survives the snap unchanged; re-runs were verified bit-identical. Pre-3.7.0 ASP subtracts one grid size from the projwin maximum, so nothing round-trips there (and the GeoTIFF records no ASP version to adapt to)
82
82
  - `find_mapproject_commands(directories, stereo_command=None)`: scans dirs (processing root, BA dir, stereo dir) for all `*.tif`/`*.tiff` and keeps those carrying the tag signature — **identity is decided by the file's own metadata, never by filename**, so there is no naming-convention dependency (reading a GeoTIFF header is cheap; the `NotGeoreferencedWarning` from raw non-georef inputs is silenced). Dedupes by the reconstructed command string (identical scene reached via two dirs collapses; distinct left/right both show). When `stereo_command` is given, a discovered output is kept only if its filename appears in that command — this scopes the result to the run being reported, so a non-mapprojected run sharing a parent dir with mapprojected scenes (the `stereo/` + `stereo_no_mapproj/` layout) does **not** spuriously list a mapproject step. It's a whole-token basename membership test (the output basename must equal one of the stereo command's argument basenames — not a raw substring, so `run.tif` can't match `prun.tif`), not positional parsing. `ProcessingParameters.get_mapproject_commands(stereo_command)` passes the parsed stereo command; `report.py` renders the results under "Mapproject Command(s)" on the Processing Parameters page (via the module-level `_render_command_block` helper, shared with the bundle_adjust/stereo/point2dem commands)
83
83
 
84
84
  **`sensors/`** - Sensor-specific scene metadata readers (issue #25; split from a single `sensors.py` into a package as groundwork for the sensor expansion in issue #168)
@@ -107,7 +107,8 @@ The package is organized by functionality, with each module focused on a specifi
107
107
  - Creates difference maps with reference DEMs
108
108
  - Supports both map-projected and raw (non-georeferenced) imagery
109
109
  - Detects map-projection status via `Raster.transform` check
110
- - For non-mapprojected scenes: match points are overlaid on images using alignment transform matrices (`run-align-{L,R}.txt` loaded via `np.loadtxt`), and disparity plots use pixel-unit scalebar instead of GSD-based
110
+ - For non-mapprojected scenes: match points are overlaid on images using alignment transform matrices (`run-align-{L,R}.txt` loaded via `np.loadtxt`), and disparity plots use pixel-unit scalebar instead of GSD-based. Exception: when the match file is named for the aligned images themselves (`run-L__R.match`, older ASP raw-image runs that write `.exr` alignment matrices), the coordinates are already aligned — detected via `_ip_on_aligned_images()` (match stem's left name == L image stem) — and are only rescaled
111
+ - **Raw interest point overlay** (issue #8): `StereoFiles` also discovers the per-image `.vwip` files (named after the images the match file pairs — `<A>__<B>.match` ↔ `<A>.vwip`/`<prefix>-<B>.vwip` — derived from the match stem, with a `*-L.vwip`/`*-R.vwip` glob fallback when no match file exists; either side may be absent). `get_vwip_df()` parses the binary format (a uint64 count then the same interest point records as `.match`); `plot_match_points()` underlays them in blue beneath the red matches through the same per-side coordinate transform (layers denser than 10,000 points are thinned by seeded random sampling for display; titles report true counts), renders interest-points-only when the match file is missing, and falls back to the previous matches-only figure when the `.vwip` files are gone
111
112
  - **Multi-view aware** (issue #160): `StereoFiles` resolves each `<prefix>-pairN/` subdirectory into a `PairStereoFiles` (field names mirror the top-level attributes so the per-figure helpers consume either, duck-typed); `plot_match_points()` / `plot_disparity()` render one figure per pair (`<stem>_pairN.png`) and return the saved filename list — `[fig_fn]` for a standard run — mirroring the `stereo_geom_plot()` N-scene contract
112
113
  - Detects the imagery rights-holder via the `attribution` attribute (`detect_satellite_attribution`); adds the copyright overlay to optical imagery in `plot_match_points()` and `plot_detailed_hillshade()`
113
114
  - `plot_detailed_hillshade()` auto-selects three subset clips from intersection-error variance (low/medium/high) via `_auto_hillshade_clip_offsets()`. Accepts `clip_windows` (DEM-CRS bboxes) + `clip_windows_crs` to pin/replay clips for run-to-run comparison (issue #121); records the boxes it drew on `self.detailed_hillshade_clips`. Out-of-bounds pinned boxes warn and fall back to auto.
@@ -129,11 +130,15 @@ The package is organized by functionality, with each module focused on a specifi
129
130
 
130
131
  **`csm_analysis.py`** - asp_plot-specific analysis built on `csm_io`
131
132
  - `get_orbit_plot_gdf()`: turns an original/optimized camera pair into the position- and orientation-difference GeoDataFrame consumed by the plotting layer
133
+ - `read_angles_common_frame()`: the asp_plot replacement for `csm_io.read_angles()` when *differencing* two cameras (#53). ASP's `orbit_plot.py` estimates the satellite body frame per camera, from a central difference of that camera's own ephemeris; a solver both perturbs the positions and resamples the ephemeris finer, and over a ~140 m central-difference baseline a 2 m perturbation tilts that frame by ~0.8° (mostly into pitch), which swamps the orientation change being measured. Both cameras are instead put in one frame, estimated from the original ephemeris and resampled onto the optimized camera's grid. Falls back to `read_angles()` for single-sample (frame) cameras, which give no baseline to estimate a frame from
134
+ - `wrap_angle_diff()`: wraps angle differences into [-180, 180), so a camera pointing near ±180° in yaw (ASTER's backward-looking 3B band, say) does not report a ~360° change across the Euler branch cut
132
135
  - `reproject_ecef()`, `poly_fit()`
133
136
 
134
137
  **`csm_camera.py`** - Plotting layer for CSM camera optimization / jitter results
135
138
  - Compares original vs optimized CSM camera models (from bundle_adjust/jitter_solve); analyzes position/orientation differences along the trajectory. Currently supports linescan cameras (e.g., WorldView)
136
139
  - `csm_camera_summary_plot()`: the near-verbatim cam1/cam2 halves (~210 duplicated lines) collapsed into a single `_plot_camera()` called once per camera (`_apply_frame_xaxis()` for the shared linescan tick logic). Figure output verified unchanged by golden line-content characterization tests
140
+ - Map colorbar limits come from `_magnitude_limits()` **per camera**; `shared_scales=True` widens them to the union across both cameras. Camera 1's scale is not imposed on camera 2 — two cameras in one solver run often differ by more than their own spread, which flattened camera 2's track to one saturated color
141
+ - Two layout rules exist because matplotlib puts a y-axis scale/offset label in each top corner: the `"Camera N"` panel label is **centered** (right-aligned collided with the twin axis's offset), and `_no_offset_colorbar_ticks()` strips the colorbars' *additive* offset (a `1e-8+1.786e-4` string overlapped the adjacent map panel's `1e6` northing offset). Both are pinned by tests in `TestColorbarScaling` / `TestPanelTitlePlacement`
137
142
  - Re-exports the moved `csm_io`/`csm_analysis` symbols for backward compatibility with notebooks and downstream imports
138
143
 
139
144
  **Altimetry** — the 3800-line `Altimetry` god-class was split (issues #130, #140) into a thin coordinator plus a source/plotter/base layer. The public API and the `asp_plot.altimetry` re-exports are preserved by delegation, so `report_pipeline.py`, the CLI, and notebooks are unchanged.
@@ -213,28 +218,27 @@ All CLI tools are in `asp_plot/cli/` and use Click for argument parsing:
213
218
  **`asp_report.py`** - Main CLI tool (`asp_report` command)
214
219
  - Generates comprehensive PDF reports of ASP processing
215
220
  - A **thin Click wrapper** (issue #128): parses the ~18 options into a `ReportConfig` and calls `report_pipeline.run_report(config)`. All orchestration lives in `report_pipeline.py` (see the module structure section); the CLI file itself is now just option definitions + the `ReportConfig` build
216
- - Accepts directories for stereo and bundle_adjust outputs
221
+ - Accepts a directory for stereo outputs and a directory or ASP-style prefix for bundle_adjust outputs; trailing slashes are tolerated (issue #60)
217
222
  - Options for reference DEMs, ICESat-2 comparisons, basemaps
218
223
  - Report section order: Input Scenes → Stereo Geometry → Match Points → Bundle Adjust panels (Log/Linear Residuals, Map-Projected Residuals, Geodiff) → Disparity → DEM Results → Detailed Hillshade → Altimetry panels. Disparity follows Bundle Adjust (not Hillshade) and DEM Results precedes Hillshade, so the title page → inputs → per-step diagnostics → final DEM products narrative reads top-down.
219
224
  - Title-page `ReportMetadata` is populated here; `get_acquisition_dates()` is called with the main directory plus stereo/BA subdirs as `extra_dirs` so the Acquisition Date(s) row appears on both raw-input layouts (WV XMLs at top level) and layouts where XMLs live one level deep.
220
225
  - `--directory`: Root ASP processing directory (default: `./`)
221
- - `--stereo_directory`: Stereo output subdirectory (default: `stereo`)
222
- - `--bundle_adjust_directory`: Optional BA directory
223
- - `--dem_filename`: Custom DEM filename (default: auto-detect `*-DEM.tif`)
224
- - `--dem_gsd`: Custom DEM ground sample distance
225
- - `--map_crs`: Projection as `EPSG:XXXX` (default: auto-detect from DEM, fallback `EPSG:4326`)
226
- - `--reference_dem`: Reference DEM path (auto-detected from logs if not supplied)
227
- - `--add_basemap`: Add Esri WorldImagery basemaps (default: True, requires internet)
228
- - `--plot_altimetry`: Plot altimetry comparisons (default: True). Auto-detects planetary body from DEM CRS: Earth → ICESat-2 (requires internet), Moon → LOLA, Mars → MOLA. For planetary DEMs, requires `--altimetry_csv`. Replaces the deprecated `--plot_icesat` flag.
229
- - `--plot_icesat`: Deprecated alias for `--plot_altimetry`. Prints deprecation warning if used.
230
- - `--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.
231
- - `--pc_align`: If True (default) and `--plot_altimetry` is True, 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. Disabled automatically when `--plot_altimetry` / `--plot_icesat` is False.
232
- - `--plot_geometry`: Plot stereo geometry (default: True; disable for planetary missions)
233
- - `--subset_km`: Hillshade subset size in km (default: 1.0)
234
- - `--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).
235
- - `--reuse_selections`: Path to a `*_figure_selections.yml` from a prior run. Replays that run's ICESat-2 points (parquet), profile track, best/worst segments, and detailed-hillshade clips so re-processing runs (e.g. mapproj vs non-mapproj) are directly comparable (issue #121). Every run always writes `<report_stem>_figure_selections.yml` next to the report (the `regenerate_reports.sh` paired variants use this to reuse each other). Generated sidecars are gitignored (they hardcode absolute local paths); a sanitized example is in `docs/cli/asp_report.md`.
236
- - `--report_filename`: PDF report filename or path. A bare filename saves in the stereo directory; a path (e.g. `reports/report.pdf`) is used as-is. Default is auto-generated as `asp_report_<report_title>_<UTC timestamp>.pdf`
237
- - `--report_title`: Custom report title (default: directory name)
226
+ - `--stereo-directory`: Stereo output subdirectory (default: `stereo`)
227
+ - `--bundle-adjust-prefix`: Optional BA output location — either a directory (`ba`) or an ASP-style output prefix (`ba/run`, as passed to stereo/mapproject); a prefix narrows file globs to that run's outputs
228
+ - `--dem-filename`: Custom DEM filename (default: auto-detect `*-DEM.tif`)
229
+ - `--dem-gsd`: Custom DEM ground sample distance
230
+ - `--map-crs`: Projection as `EPSG:XXXX` (default: auto-detect from DEM, fallback `EPSG:4326`)
231
+ - `--reference-dem`: Reference DEM path (auto-detected from logs if not supplied)
232
+ - `--no-basemap`: Skip the Esri WorldImagery basemaps added by default (which require internet)
233
+ - `--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`.
234
+ - `--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.
235
+ - `--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.
236
+ - `--no-geometry`: Skip the stereo geometry plots drawn by default (used for planetary missions)
237
+ - `--subset-km`: Hillshade subset size in km (default: 1.0)
238
+ - `--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).
239
+ - `--reuse-selections`: Path to a `*_figure_selections.yml` from a prior run. Replays that run's ICESat-2 points (parquet), profile track, best/worst segments, and detailed-hillshade clips so re-processing runs (e.g. mapproj vs non-mapproj) are directly comparable (issue #121). Every run always writes `<report_stem>_figure_selections.yml` next to the report (the `regenerate_reports.sh` paired variants use this to reuse each other). Generated sidecars are gitignored (they hardcode absolute local paths); a sanitized example is in `docs/cli/asp_report.md`.
240
+ - `--report-filename`: PDF report filename or path. A bare filename saves in the stereo directory; a path (e.g. `reports/report.pdf`) is used as-is. Default is auto-generated as `asp_report_<report_title>_<UTC timestamp>.pdf`
241
+ - `--report-title`: Custom report title (default: directory name)
238
242
 
239
243
  **`csm_camera_plot.py`** - CSM camera comparison tool (`csm_camera_plot` command)
240
244
  - Wrapper for `csm_camera.py` functions
@@ -248,7 +252,7 @@ All CLI tools are in `asp_plot/cli/` and use Click for argument parsing:
248
252
  - `--email`: Email for notification when query finishes (required)
249
253
  - `--channels`: LOLA detector channels (Moon only, default `tffff` = channel 1 only)
250
254
  - Saves request metadata as `altimetry_request_info.yml` alongside the DEM
251
- - Workflow: submit query → receive email → download/unzip → pass `*_pts_csv.csv` (Mars) or `*_topo_simple_csv.csv`/`*_pts_csv.csv` (Moon) to `asp_report --altimetry_csv`
255
+ - Workflow: submit query → receive email → download/unzip → pass `*_pts_csv.csv` (Mars) or `*_topo_simple_csv.csv`/`*_pts_csv.csv` (Moon) to `asp_report --altimetry-csv`
252
256
 
253
257
  **`stereo_geom.py`** - Stereo geometry visualization tool (`stereo_geom` command)
254
258
  - Wrapper for `StereoGeometryPlotter`
@@ -260,7 +264,7 @@ All CLI tools are in `asp_plot/cli/` and use Click for argument parsing:
260
264
  **`gallery.py`** - DEM gallery tool (`gallery` command)
261
265
  - Wrapper for `GalleryPlotter`; lays out many DEMs as a grid sharing one color scale
262
266
  - `--directory` + `--pattern` (supports recursive `**` for subdirectories) or an explicit list of `FILES` (files take precedence)
263
- - `--hillshade/--no-hillshade`, `--cmap`, `--downsample`, `--max_filesize_mb`, `--title`, `--output_directory/--output_filename`
267
+ - `--no-hillshade`, `--cmap`, `--downsample`, `--max-filesize-mb`, `--title`, `--output-directory/--output-filename`
264
268
  - Saves `<dirname>_gallery.png` into the input directory by default
265
269
 
266
270
  ## Documentation Website
@@ -302,7 +306,7 @@ docs/
302
306
  - **`html_extra_path`** is used to serve `notebooks/figures/` at the correct relative path for notebook `<img src>` references.
303
307
  - **`docs/` is excluded from sdist** in `pyproject.toml` so docs never ship in the PyPI/conda package.
304
308
  - **Changelog** uses `{include} ../CHANGELOG.md` so there's one source of truth.
305
- - **Selective notebook exclusion**: `.readthedocs.yaml`'s `pre_build` copies every `notebooks/**/*.ipynb` into `docs/examples/notebooks/`, but specific notebooks can be dropped from the build by listing them in `exclude_patterns` in `docs/conf.py` (e.g., `worldview_utqiagvik_stereo.ipynb`). Inter-notebook links in the WorldView examples use fully-qualified `https://asp-plot.readthedocs.io/en/latest/...` URLs so they resolve both on RTD and in raw notebook previews. Report-link convention: each notebook's "Full Report Generation" section ends with a `#### See the resulting [report](https://asp-plot.readthedocs.io/en/latest/_static/reports/<filename>.pdf).` line that links directly to the PDF served from `_static/reports/`. The `<filename>` must match what the cell's `!asp_report --report_filename` writes (so the URL on RTD actually resolves).
309
+ - **Selective notebook exclusion**: `.readthedocs.yaml`'s `pre_build` copies every `notebooks/**/*.ipynb` into `docs/examples/notebooks/`, but specific notebooks can be dropped from the build by listing them in `exclude_patterns` in `docs/conf.py` (e.g., `worldview_utqiagvik_stereo.ipynb`). Inter-notebook links in the WorldView examples use fully-qualified `https://asp-plot.readthedocs.io/en/latest/...` URLs so they resolve both on RTD and in raw notebook previews. Report-link convention: each notebook's "Full Report Generation" section ends with a `#### See the resulting [report](https://asp-plot.readthedocs.io/en/latest/_static/reports/<filename>.pdf).` line that links directly to the PDF served from `_static/reports/`. The `<filename>` must match what the cell's `!asp_report --report-filename` writes (so the URL on RTD actually resolves).
306
310
 
307
311
  ### Dependencies
308
312
 
@@ -332,7 +336,7 @@ Docs dependencies are in `pyproject.toml` under `[project.optional-dependencies]
332
336
 
333
337
  **ODE GDS REST API**: Base URL `https://oderest.rsl.wustl.edu/livegds`. Queries are submitted via `gds_query_async()` in async mode. The `request_planetary_altimetry` CLI submits the query and the user downloads results via email link. Coordinates use east-positive 0-360 longitude and planetocentric latitude.
334
338
 
335
- **Basemaps**: Uses `contextily` to fetch Esri WorldImagery tiles (requires internet). Can be disabled with `--add_basemap False`. Automatically skipped for planetary (non-Earth) DEMs.
339
+ **Basemaps**: Uses `contextily` to fetch Esri WorldImagery tiles (requires internet). Can be disabled with `--no-basemap`. Automatically skipped for planetary (non-Earth) DEMs.
336
340
 
337
341
  ## Example Notebooks
338
342
 
@@ -5,6 +5,46 @@ 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.0.0] - 2026-08-24
9
+
10
+ A breaking standardization of the command-line interfaces ([#60](https://github.com/uw-cryo/asp_plot/issues/60)), done deliberately as a clean break — no aliases, no deprecation period — while the user base is small. Every multi-word option across the five CLIs (`asp_report`, `stereo_geom`, `csm_camera_plot`, `gallery`, `request_planetary_altimetry`) moves from underscores to the hyphenated style ASP itself uses (`--stereo_directory` → `--stereo-directory`), booleans become single switches for the non-default behavior (`--add_basemap False` → `--no-basemap`), and `--bundle_adjust_directory` becomes `--bundle-adjust-prefix`, matching both the name and the semantics of ASP's own option. The Python API is unchanged.
11
+
12
+ A major version because every existing `asp_report`/`stereo_geom`/`csm_camera_plot`/`gallery` invocation with a multi-word or boolean option needs editing. **Upgrade:** re-spell the flags per the entries below — the `--help` of each command lists the new names, and the committed example reports and notebooks show them in use. No new dependencies and no entry-point changes.
13
+
14
+ ### Changed
15
+ - **All CLI options are hyphenated** (issue [#60](https://github.com/uw-cryo/asp_plot/issues/60)). One-for-one renames: `--stereo-directory`, `--dem-filename`, `--dem-gsd`, `--map-crs`, `--reference-dem`, `--altimetry-csv`, `--subset-km`, `--atl06sr-time-range`, `--reuse-selections`, `--report-filename`, `--report-title` (asp_report); `--output-directory`, `--output-filename` (stereo_geom, gallery); `--max-filesize-mb` (gallery); `--original-cameras`, `--optimized-cameras`, `--map-crs`, `--upper-magnitude-percentile` (csm_camera_plot). Single-word options are untouched. The command recorded on the report's final page is emitted with the new spellings, so it stays re-runnable.
16
+ - **Boolean options are now single switches for the non-default behavior**, in the Unix idiom: where the default is on, only the negative switch exists — `--no-basemap`, `--no-altimetry`, `--no-pc-align`, `--no-geometry` (asp_report), `--no-basemap` (stereo_geom), `--no-trim` (csm_camera_plot), `--no-hillshade` (gallery, replacing the `--hillshade/--no-hillshade` pair) — and where the default is off, only the positive one — `--shared-scales`, `--log-scale-positions`, `--log-scale-angles`, `--add-basemap` (csm_camera_plot). Where you passed `--add_basemap False`, pass `--no-basemap`; the verb-y `plot`/`add` prefixes are dropped from the switch names.
17
+ - **`--bundle_adjust_directory` is now `--bundle-adjust-prefix`**, and accepts either the containing directory (`ba`, the previous behavior) or the same ASP-style output prefix passed to `stereo`/`mapproject` (`ba/run`). A prefix narrows the residuals/log file search to that run's outputs, so several bundle-adjust runs can share a directory. Internally, `ReportConfig.bundle_adjust_directory` is renamed to `bundle_adjust_prefix`.
18
+ - **`csm_camera_plot` options are harmonized with the other CLIs and ASP's `orbit_plot.py`**: `--save_dir` → `--output-directory`, `--fig_fn` → `--output-filename`, `--figsize` → `--figure-size`. The `csm_camera_summary_plot()` Python keyword arguments are unchanged.
19
+ - **Trailing slashes on directory options are stripped centrally** (`--directory`, `--stereo-directory`, `--bundle-adjust-prefix`), closing the last thread of issue [#60](https://github.com/uw-cryo/asp_plot/issues/60); the dg_mosaic `r100` concern from the same issue was resolved earlier by the sensor readers, which treat `*.r100.xml`/`*.r50.xml` as regenerable intermediates.
20
+
21
+ ### Removed
22
+ - **The deprecated `--plot_icesat` alias** (deprecated in favor of `--plot_altimetry` in 1.10.0) is gone; altimetry is on by default and `--no-altimetry` disables it.
23
+
24
+ ## [2.2.0] - 2026-08-18
25
+
26
+ A correctness release for the CSM camera comparison, and a new diagnostic layer under the match points.
27
+
28
+ `csm_camera_summary_plot()` measured each camera's roll/pitch/yaw against a satellite body frame estimated separately for that camera — correct when plotting one camera, which is what ASP's `orbit_plot.py` does, but wrong for a *difference* between two ([#53](https://github.com/uw-cryo/asp_plot/issues/53)). `bundle_adjust` and `jitter_solve` both resample the ephemeris finer, and over the ~140 m central-difference baseline that leaves, a 2 m position change tilts the frame by ~0.8° — swamping the orientation change being plotted. Both cameras now share one frame, estimated from the original ephemeris, and the committed Salar de Uyuni pair's reported pitch change drops from 0.73° (ranging −3.4° to +3.6°) to 2.6e-06°, which finally says what that run did: it moved positions by ±2 m and left the orientations alone. The figure that looked full of outliers was the frame moving, not the data.
29
+
30
+ The same figure gains a `bundle_adjust` counterpart to that `jitter_solve` example, in the UCSD WorldView notebook, so the two solvers' signatures can be compared directly — and putting a second, much smaller correction next to the first exposed two readability bugs: camera 2 was drawn on camera 1's colorbar limits, and panel labels overprinted matplotlib's axis offset text.
31
+
32
+ Separately, the match point figure now underlays ASP's raw per-image interest points (`.vwip`) beneath the matches ([#8](https://github.com/uw-cryo/asp_plot/issues/8)), so a sparse match set can be traced to poor matching versus nothing detected to match; and reconstructed `mapproject` commands re-run grid-identically on ASP >= 3.7.0 instead of drifting a pixel east per run ([#148](https://github.com/uw-cryo/asp_plot/issues/148)).
33
+
34
+ A minor version: everything is additive, with no API changes and no new dependencies.
35
+
36
+ ### Added
37
+ - **A `bundle_adjust` camera-comparison example in the UCSD WorldView notebook.** The only committed `csm_camera_summary_plot()` example was a `jitter_solve` run, whose per-segment corrections oscillate along the image. `notebooks/WorldView/worldview_spacenet_ucsd_stereo.ipynb` now also compares the original and adjusted cameras from its own `bundle_adjust` run, which solves one rigid translation and rotation per camera and so produces the opposite signature: smooth, nearly flat panels with a sub-metre position offset (0.4 m north on camera 1, 0.9 m up on camera 2, matching `ba/run-camera_offsets.txt`) and a constant orientation change of ~1e-4°. Having both makes the two solvers' signatures directly comparable, and the notebook says what it means if either shows up looking like the other. Because `bundle_adjust` writes CSM state only for the *optimized* cameras, the notebook also documents how to produce the unadjusted one: re-run with a 4x4 identity `--initial-transform` and `--apply-initial-transform-only`.
38
+ - **Raw per-image interest points (`.vwip`) are overlaid on the match point figure** ([#8](https://github.com/uw-cryo/asp_plot/issues/8)). When the `.vwip` files ASP writes during interest point matching are present in the stereo directory (including per-pair in multi-view runs), the match point figure underlays them in blue beneath the red matches, with per-image counts in the panel titles — so sparse matches can be traced to either poor matching or areas with no detected interest points at all (limited texture, clouds, water). If matching failed outright and there is no match file, the raw interest points are still shown on their own. Either side's file may be absent (they are intermediates some runs clean up), in which case the figure degrades to exactly what it showed before. Layers denser than 10,000 points are thinned by seeded random sampling for display — dense runs (~100k interest points) would otherwise saturate the panels into solid color and bloat the figure — while the panel titles always report the true counts.
39
+
40
+ ### Fixed
41
+ - **Each camera's map colorbar now covers that camera's own range.** `csm_camera_summary_plot()` computed colorbar limits from camera 1 and imposed them on camera 2. Two cameras in one solver run routinely differ by more than their own spread — in the new UCSD example `bundle_adjust` moves one camera 0.432–0.454 m and the other 0.885–0.917 m — so camera 2's entire track rendered as a single saturated color with no visible spatial pattern. Limits are now per camera; passing `shared_scales=True` (which already unified the line-panel y-axes) additionally puts both cameras on one common colorbar spanning the union of their ranges, for when comparing magnitudes directly is the point.
42
+ - **Panel labels and axis offset text no longer overprint each other.** matplotlib parks a y-axis scale/offset label in each top corner of an axes, and the angle panels have two of them — the left axis's and the twin axis's — so the right-aligned `"Camera N"` title rendered on top of the twin's (`+1.669e2`), producing unreadable overstruck glyphs. The label is now centered. Separately, over a narrow range of small values a colorbar emits a combined scale-and-offset string (`1e-8+1.786e-4`) wide enough to overlap the neighbouring map panel's northing offset (`1e6`); the colorbars now drop the additive offset, leaving a short multiplier (`1e-4`) and moving the significant digits into the tick labels, where they are easier to read anyway.
43
+ - **CSM camera angle differences no longer report a rotation the camera never underwent** ([#53](https://github.com/uw-cryo/asp_plot/issues/53)). `csm_camera_summary_plot()` compared roll/pitch/yaw computed against a satellite body frame estimated *separately for each camera*, as ASP's `orbit_plot.py` does — a central difference of that camera's own ephemeris. That is fine for plotting one camera, but it corrupts a difference between two: `bundle_adjust` and `jitter_solve` both perturb the positions and resample the ephemeris to a finer spacing, and at WorldView's ~7 km/s a 0.01 s spacing leaves only a ~140 m central-difference baseline, so a 2 m radial perturbation tilts the estimated frame by ~0.8° — orders of magnitude more than the orientation change being measured, landing almost entirely in pitch. Both cameras are now expressed in one frame, estimated from the original (unperturbed) ephemeris and resampled onto the optimized camera's sample grid, so the plotted difference is the true relative rotation between the two camera models. On the committed Salar de Uyuni `jitter_solve` pair this drops the reported pitch change from 0.73° ± (range −3.4° to +3.6°) to 2.6e-06°, revealing what that run actually did: it moved the camera positions by roughly ±2 m and left the orientations untouched. This is what made the example figure look like it was full of outliers.
44
+ - **Angle differences are wrapped at ±180°** ([#53](https://github.com/uw-cryo/asp_plot/issues/53)). Euler angles are recovered on a branch cut, so a camera pointing near ±180° in yaw — a backward-looking sensor such as ASTER's 3B band — had samples straddling the cut reported as ~360° changes instead of the fraction of a degree they really were. Original angle series are also unwrapped before being resampled onto the optimized camera's grid, so a series crossing the cut is no longer interpolated through zero.
45
+ - **The match point figure no longer raises for raw-image runs whose interest points were found on the aligned images** ([#8](https://github.com/uw-cryo/asp_plot/issues/8)). Older ASP versions name the match file for the aligned images (`run-L__R.match`) and write the alignment matrices as `.exr` rather than `.txt`; those match coordinates are already in aligned space, so plotting now detects this from the match filename (left name == the L image) and just rescales instead of demanding `*-align-{L,R}.txt` and failing with `FileNotFoundError`.
46
+ - **Reconstructed `mapproject` commands now re-run grid-identically on ASP >= 3.7.0** ([#148](https://github.com/uw-cryo/asp_plot/issues/148)). The `--t_projwin` reconstructed from an output GeoTIFF's bounds did not round-trip: ASP snaps a given projwin by converting pixel edges to centers and rounding to the nearest grid multiple, and the bounds GDAL reports for an ASP output (after its `PixelIsPoint` half-pixel shift) land exactly on the rounding tie — re-running the reconstructed command drifted the grid one pixel east per run (and at fractional grid sizes, float noise could grow/shrink the raster by a pixel per edge). The reconstruction now emits ASP's own pixel-edge box — the bounds shifted half a pixel NW (`x − tr/2`, `y + tr/2`) — which survives ASP's snap unchanged; re-runs were verified bit-identical (grid, extent, and pixel values) at both whole and fractional grid sizes. On pre-3.7.0 ASP, which subtracted one grid size from the projwin maximum, no projwin choice can round-trip; the report's explanatory note now states the version assumption.
47
+
8
48
  ## [2.1.0] - 2026-07-30
9
49
 
10
50
  `asp_plot` now reads the same satellite camera metadata the Stereo Pipeline itself does ([#168](https://github.com/uw-cryo/asp_plot/issues/168)). Where 2.0.0 added Airbus Pléiades, this release finishes the job: the rest of the DIMAP v2 family (Pléiades 1A/1B attitude, SPOT 6/7, PeruSat-1), DIMAP v1 (SPOT 5, ALOS PRISM), ASTER, and RPC-only products (Cartosat-1, Deimos, anything ASP runs with `-t rpc`). The one gap left is ASP's `pinhole`/`opticalbar` sessions — historical aerial and declassified film, which carry no satellite geometry to plot.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: asp_plot
3
- Version: 2.1.0
3
+ Version: 3.0.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
@@ -74,7 +74,7 @@ See the [installation guide](https://asp-plot.readthedocs.io/en/latest/installat
74
74
  Generate a PDF report from an ASP processing directory:
75
75
 
76
76
  ```
77
- asp_report --directory ./ --stereo_directory stereo
77
+ asp_report --directory ./ --stereo-directory stereo
78
78
  ```
79
79
 
80
80
  See the [CLI documentation](https://asp-plot.readthedocs.io/en/latest/cli/index.html) for all options and additional tools (`stereo_geom`, `csm_camera_plot`).
@@ -30,7 +30,7 @@ See the [installation guide](https://asp-plot.readthedocs.io/en/latest/installat
30
30
  Generate a PDF report from an ASP processing directory:
31
31
 
32
32
  ```
33
- asp_report --directory ./ --stereo_directory stereo
33
+ asp_report --directory ./ --stereo-directory stereo
34
34
  ```
35
35
 
36
36
  See the [CLI documentation](https://asp-plot.readthedocs.io/en/latest/cli/index.html) for all options and additional tools (`stereo_geom`, `csm_camera_plot`).
@@ -41,7 +41,7 @@ class ReadBundleAdjustFiles:
41
41
  >>> mapproj_gdf = ba_reader.get_mapproj_residuals_gdf()
42
42
  """
43
43
 
44
- def __init__(self, directory, bundle_adjust_directory):
44
+ def __init__(self, directory, bundle_adjust_directory, stem=None):
45
45
  """
46
46
  Initialize the ReadBundleAdjustFiles object.
47
47
 
@@ -51,10 +51,15 @@ class ReadBundleAdjustFiles:
51
51
  Root directory of ASP processing
52
52
  bundle_adjust_directory : str
53
53
  Subdirectory containing bundle adjustment outputs
54
+ stem : str, optional
55
+ Run stem of an ASP output prefix (the ``run`` in ``ba/run``).
56
+ When given, file globs are narrowed to that run's outputs;
57
+ otherwise any run in the directory matches.
54
58
  """
55
59
  self.directory = os.path.expanduser(directory)
56
60
  self.bundle_adjust_directory = bundle_adjust_directory
57
61
  self.full_directory = os.path.join(self.directory, bundle_adjust_directory)
62
+ self.stem_glob = stem if stem else "*"
58
63
 
59
64
  def get_csv_paths(self, geodiff_files=False):
60
65
  """
@@ -86,8 +91,8 @@ class ReadBundleAdjustFiles:
86
91
  will attempt to generate them using the geodiff ASP tool.
87
92
  """
88
93
  filenames = [
89
- "*-initial_residuals_pointmap.csv",
90
- "*-final_residuals_pointmap.csv",
94
+ f"{self.stem_glob}-initial_residuals_pointmap.csv",
95
+ f"{self.stem_glob}-final_residuals_pointmap.csv",
91
96
  ]
92
97
 
93
98
  if geodiff_files:
@@ -371,7 +376,9 @@ class ReadBundleAdjustFiles:
371
376
  the distance between matched interest points in the map-projected
372
377
  images after bundle adjustment.
373
378
  """
374
- path = glob_file(self.full_directory, "*-mapproj_match_offsets.txt")
379
+ path = glob_file(
380
+ self.full_directory, f"{self.stem_glob}-mapproj_match_offsets.txt"
381
+ )
375
382
  if path is None:
376
383
  raise ValueError("\n\nMapProj Residuals TXT file not found.\n\n")
377
384
 
@@ -411,7 +418,9 @@ class ReadBundleAdjustFiles:
411
418
  the bundle adjustment residuals propagate to uncertainty in
412
419
  triangulation. This is an estimate of the final DEM error.
413
420
  """
414
- path = glob_file(self.full_directory, "*-triangulation_uncertainty.txt")
421
+ path = glob_file(
422
+ self.full_directory, f"{self.stem_glob}-triangulation_uncertainty.txt"
423
+ )
415
424
  if path is None:
416
425
  raise ValueError("\n\nTriangulation Uncertainty TXT file not found.\n\n")
417
426
 
@@ -16,7 +16,10 @@ def _reconstruct_command():
16
16
  for param in click_ctx.command.params:
17
17
  val = click_ctx.params.get(param.name)
18
18
  if val is not None and val != param.default:
19
- cmd_parts.append(f"--{param.name} {shlex.quote(str(val))}")
19
+ if param.is_bool_flag:
20
+ cmd_parts.append(param.opts[0])
21
+ else:
22
+ cmd_parts.append(f"{param.opts[0]} {shlex.quote(str(val))}")
20
23
  return " ".join(cmd_parts)
21
24
 
22
25
 
@@ -28,123 +31,116 @@ def _reconstruct_command():
28
31
  help="Required directory of ASP processing with scenes and sub-directories for stereo and optionally bundle adjustment. Default: current directory.",
29
32
  )
30
33
  @click.option(
31
- "--bundle_adjust_directory",
34
+ "--bundle-adjust-prefix",
32
35
  prompt=False,
33
36
  default=None,
34
- help="Optional directory of bundle adjustment files. If expected *residuals_pointmap.csv files are not found in the supplied directory, no bundle adjustment plots will be generated. Default: None.",
37
+ help="Optional bundle adjustment output location, relative to --directory. Accepts either a directory (e.g. 'ba') or an ASP-style output prefix as passed to --bundle-adjust-prefix in stereo/mapproject (e.g. 'ba/run'). If expected *residuals_pointmap.csv files are not found there, no bundle adjustment plots will be generated. Default: None.",
35
38
  )
36
39
  @click.option(
37
- "--stereo_directory",
40
+ "--stereo-directory",
38
41
  prompt=True,
39
42
  default="stereo",
40
43
  help="Required directory of stereo files. Default: stereo.",
41
44
  )
42
45
  @click.option(
43
- "--dem_filename",
46
+ "--dem-filename",
44
47
  prompt=False,
45
48
  default=None,
46
49
  help="Optional DEM filename in the stereo directory. Default: None, which will search for the *-DEM.tif file in the stereo directory. Specify it as the basename with extension, e.g. my-custom-dem-name.tif.",
47
50
  )
48
51
  @click.option(
49
- "--dem_gsd",
52
+ "--dem-gsd",
50
53
  prompt=False,
51
54
  default=None,
52
55
  help="Optional ground sample distance of the DEM. Default: None, which will search for the *-DEM.tif file in the stereo directory. If there is a GSD in the name of the file, specify it here as a float or integer, e.g. 1, 1.5, etc.",
53
56
  )
54
57
  @click.option(
55
- "--map_crs",
58
+ "--map-crs",
56
59
  prompt=False,
57
60
  default=None,
58
61
  help="Projection for altimetry and bundle adjustment plots. As EPSG:XXXX. Default: None, which will use the projection of the ASP DEM, and fall back on EPSG:4326 if not found.",
59
62
  )
60
63
  @click.option(
61
- "--reference_dem",
64
+ "--reference-dem",
62
65
  prompt=False,
63
66
  default=None,
64
67
  help="Optional reference DEM used in ASP processing. No default. If not supplied, the logs will be examined to find it. If not found, no difference plots will be generated.",
65
68
  )
66
69
  @click.option(
67
- "--add_basemap",
68
- prompt=False,
69
- default=True,
70
- help="If True, add a basemaps to the figures, which requires internet connection. Default: True.",
71
- )
72
- @click.option(
73
- "--plot_altimetry",
74
- prompt=False,
75
- default=True,
76
- help="If True, plot altimetry comparisons (ICESat-2 for Earth, LOLA for Moon, MOLA for Mars). For planetary DEMs, requires --altimetry_csv. Default: True.",
70
+ "--no-basemap",
71
+ is_flag=True,
72
+ default=False,
73
+ help="Skip the figure basemaps (basemaps are added by default, which requires an internet connection).",
77
74
  )
78
75
  @click.option(
79
- "--plot_icesat",
80
- prompt=False,
81
- default=None,
82
- help="Deprecated: use --plot_altimetry instead. Kept for backward compatibility.",
76
+ "--no-altimetry",
77
+ is_flag=True,
78
+ default=False,
79
+ help="Skip the altimetry comparisons (plotted by default: ICESat-2 for Earth, LOLA for Moon, MOLA for Mars; planetary DEMs require --altimetry-csv).",
83
80
  )
84
81
  @click.option(
85
- "--altimetry_csv",
82
+ "--altimetry-csv",
86
83
  prompt=False,
87
84
  default=None,
88
85
  type=click.Path(exists=True),
89
86
  help="Path to a LOLA/MOLA *_topo_csv.csv file from the ODE GDS API. Required for planetary altimetry plots. Obtain via: request_planetary_altimetry --dem <dem> --email <email>, then download and unzip the result.",
90
87
  )
91
88
  @click.option(
92
- "--pc_align",
93
- prompt=False,
94
- default=True,
95
- help="If True and --plot_altimetry is True, run pc_align against the reference altimetry (ICESat-2 for Earth, MOLA for Mars, LOLA for Moon) and append the alignment-report pages. Disabled automatically when --plot_altimetry / --plot_icesat is False. Default: True.",
89
+ "--no-pc-align",
90
+ is_flag=True,
91
+ default=False,
92
+ 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).",
96
93
  )
97
94
  @click.option(
98
- "--plot_geometry",
99
- prompt=False,
100
- default=True,
101
- help="If True, plot the stereo geometry. Default: True.",
95
+ "--no-geometry",
96
+ is_flag=True,
97
+ default=False,
98
+ help="Skip the stereo geometry plots (plotted by default).",
102
99
  )
103
100
  @click.option(
104
- "--subset_km",
101
+ "--subset-km",
105
102
  prompt=False,
106
103
  default=1.0,
107
104
  help="Size in km of the subset to plot for the detailed hillshade. Default: 1 km.",
108
105
  )
109
106
  @click.option(
110
- "--atl06sr_time_range",
107
+ "--atl06sr-time-range",
111
108
  prompt=False,
112
109
  default="all",
113
110
  help='Time range for ICESat-2 ATL06-SR data requests. "all" for all available data (mission start to present), or "START,END" for a custom range (e.g. "2020-01-01,2024-12-31"), or "auto" for scene metadata +/- 1 year. Default: all.',
114
111
  )
115
112
  @click.option(
116
- "--reuse_selections",
113
+ "--reuse-selections",
117
114
  prompt=False,
118
115
  default=None,
119
116
  type=click.Path(exists=True),
120
117
  help="Path to a *_figure_selections.yml written by a previous run. When supplied, replays that run's ICESat-2 points (parquet), profile track, best/worst segments, and detailed-hillshade clip boxes so figures are directly comparable across re-processing runs. Default: None.",
121
118
  )
122
119
  @click.option(
123
- "--report_filename",
120
+ "--report-filename",
124
121
  prompt=False,
125
122
  default=None,
126
123
  help="PDF report filename or path. A bare filename (e.g. 'report.pdf') is saved in the stereo directory. A path (e.g. 'reports/report.pdf' or '/tmp/report.pdf') is used as-is. Default: auto-generated from directory name.",
127
124
  )
128
125
  @click.option(
129
- "--report_title",
126
+ "--report-title",
130
127
  prompt=False,
131
128
  default=None,
132
129
  help="Title for the report. Default: Directory name of ASP processing.",
133
130
  )
134
131
  def main(
135
132
  directory,
136
- bundle_adjust_directory,
133
+ bundle_adjust_prefix,
137
134
  stereo_directory,
138
135
  dem_filename,
139
136
  dem_gsd,
140
137
  map_crs,
141
138
  reference_dem,
142
- add_basemap,
143
- plot_altimetry,
144
- plot_icesat,
139
+ no_basemap,
140
+ no_altimetry,
145
141
  altimetry_csv,
146
- pc_align,
147
- plot_geometry,
142
+ no_pc_align,
143
+ no_geometry,
148
144
  subset_km,
149
145
  atl06sr_time_range,
150
146
  reuse_selections,
@@ -160,18 +156,17 @@ def main(
160
156
  """
161
157
  config = ReportConfig(
162
158
  directory=directory,
163
- bundle_adjust_directory=bundle_adjust_directory,
159
+ bundle_adjust_prefix=bundle_adjust_prefix,
164
160
  stereo_directory=stereo_directory,
165
161
  dem_filename=dem_filename,
166
162
  dem_gsd=dem_gsd,
167
163
  map_crs=map_crs,
168
164
  reference_dem=reference_dem,
169
- add_basemap=add_basemap,
170
- plot_altimetry=plot_altimetry,
171
- plot_icesat=plot_icesat,
165
+ add_basemap=not no_basemap,
166
+ plot_altimetry=not no_altimetry,
172
167
  altimetry_csv=altimetry_csv,
173
- pc_align=pc_align,
174
- plot_geometry=plot_geometry,
168
+ pc_align=not no_pc_align,
169
+ plot_geometry=not no_geometry,
175
170
  subset_km=subset_km,
176
171
  atl06sr_time_range=atl06sr_time_range,
177
172
  reuse_selections=reuse_selections,