asp-plot 1.15.2__tar.gz → 1.17.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 (48) hide show
  1. {asp_plot-1.15.2 → asp_plot-1.17.0}/.gitignore +5 -0
  2. asp_plot-1.17.0/AGENTS.md +444 -0
  3. {asp_plot-1.15.2 → asp_plot-1.17.0}/CHANGELOG.md +23 -0
  4. {asp_plot-1.15.2 → asp_plot-1.17.0}/PKG-INFO +3 -1
  5. {asp_plot-1.15.2 → asp_plot-1.17.0}/README.md +2 -0
  6. {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/alignment.py +70 -43
  7. asp_plot-1.17.0/asp_plot/altimetry.py +1042 -0
  8. asp_plot-1.17.0/asp_plot/altimetry_plots.py +1369 -0
  9. asp_plot-1.17.0/asp_plot/altimetry_source.py +102 -0
  10. asp_plot-1.17.0/asp_plot/asp_log.py +276 -0
  11. asp_plot-1.17.0/asp_plot/bodies.py +124 -0
  12. {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/bundle_adjust.py +2 -10
  13. asp_plot-1.17.0/asp_plot/cli/asp_plot.py +186 -0
  14. {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/cli/stereo_geom.py +1 -1
  15. asp_plot-1.17.0/asp_plot/csm_analysis.py +242 -0
  16. asp_plot-1.17.0/asp_plot/csm_camera.py +511 -0
  17. asp_plot-1.17.0/asp_plot/csm_io.py +634 -0
  18. {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/gallery.py +3 -33
  19. asp_plot-1.17.0/asp_plot/icesat2_source.py +1330 -0
  20. asp_plot-1.17.0/asp_plot/planetary_source.py +470 -0
  21. {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/processing_parameters.py +97 -135
  22. asp_plot-1.17.0/asp_plot/report_captions.py +181 -0
  23. asp_plot-1.17.0/asp_plot/report_pipeline.py +986 -0
  24. {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/scenes.py +84 -25
  25. asp_plot-1.17.0/asp_plot/selections.py +231 -0
  26. asp_plot-1.17.0/asp_plot/sensors.py +587 -0
  27. {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/stereo.py +382 -182
  28. {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/stereo_geometry.py +20 -12
  29. {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/stereopair_metadata_parser.py +32 -390
  30. {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/utils.py +118 -29
  31. {asp_plot-1.15.2 → asp_plot-1.17.0}/pyproject.toml +1 -1
  32. asp_plot-1.15.2/asp_plot/altimetry.py +0 -3493
  33. asp_plot-1.15.2/asp_plot/cli/asp_plot.py +0 -917
  34. asp_plot-1.15.2/asp_plot/csm_camera.py +0 -1541
  35. {asp_plot-1.15.2 → asp_plot-1.17.0}/.flake8 +0 -0
  36. {asp_plot-1.15.2 → asp_plot-1.17.0}/.github/workflows/release.yml +0 -0
  37. {asp_plot-1.15.2 → asp_plot-1.17.0}/.github/workflows/run-tests.yml +0 -0
  38. {asp_plot-1.15.2 → asp_plot-1.17.0}/.pre-commit-config.yaml +0 -0
  39. {asp_plot-1.15.2 → asp_plot-1.17.0}/.readthedocs.yaml +0 -0
  40. {asp_plot-1.15.2 → asp_plot-1.17.0}/LICENSE +0 -0
  41. {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/__init__.py +0 -0
  42. {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/cli/__init__.py +0 -0
  43. {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/cli/csm_camera_plot.py +0 -0
  44. {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/cli/gallery.py +0 -0
  45. {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/cli/request_planetary_altimetry.py +0 -0
  46. {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/report.py +0 -0
  47. {asp_plot-1.15.2 → asp_plot-1.17.0}/conda-forge-recipe/meta.yaml +0 -0
  48. {asp_plot-1.15.2 → asp_plot-1.17.0}/environment.yml +0 -0
@@ -143,4 +143,9 @@ CLAUDE*
143
143
  scripts/
144
144
  /*.parquet
145
145
  reports/regenerate_reports.sh
146
+ # Figure-selections sidecars written next to each report by `asp_plot`. These are
147
+ # regeneration byproducts that hardcode absolute local paths (dem_filename,
148
+ # parquet_cache), so they are not committed; a sanitized example is documented in
149
+ # docs/cli/asp_plot.md instead.
150
+ reports/*_figure_selections.yml
146
151
  *.csv
@@ -0,0 +1,444 @@
1
+ # AGENTS.md
2
+
3
+ This file provides guidance to AI coding agents (Claude Code and others) when working with code in this repository. It is the git-tracked onboarding reference for the package's architecture; keep it in sync with the code.
4
+
5
+ ## Project Overview
6
+
7
+ `asp_plot` (current version: 1.17.0) is a Python package for visualizing output from the NASA Ames Stereo Pipeline (ASP). It processes stereo satellite imagery results (both terrestrial and planetary), generates diagnostic plots, and creates comprehensive PDF reports similar to those from commercial SfM software like Agisoft Metashape. Requires Python >= 3.11. Published on PyPI and conda-forge.
8
+
9
+ The package supports:
10
+ - Stereo DEM processing visualization
11
+ - Bundle adjustment analysis
12
+ - CSM camera model comparisons (for tools like bundle_adjust and jitter_solve)
13
+ - Altimetry comparisons: ICESat-2 ATL06-SR (Earth), LOLA (Moon), MOLA (Mars)
14
+ - Stereo geometry visualization from satellite XML metadata (Earth-based sensors)
15
+ - Non-terrestrial/planetary processing (tested with LRO NAC, Mars MRO CTX/HiRISE, Mars MGS MOC, and ASTER)
16
+ - Gallery plotting of many DEMs as a grid of thumbnails sharing one color scale
17
+
18
+ ## Development Commands
19
+
20
+ ### Environment Setup
21
+ ```bash
22
+ # Create and activate conda environment (installs package with dev+docs extras)
23
+ conda env create -f environment.yml
24
+ conda activate asp_plot
25
+
26
+ # Install pre-commit hooks (REQUIRED for development)
27
+ pre-commit install
28
+ ```
29
+
30
+ ### Testing
31
+ ```bash
32
+ # Run all tests
33
+ pytest
34
+
35
+ # Run tests with output visible (useful for debugging)
36
+ pytest -s
37
+
38
+ # Run a specific test file
39
+ pytest tests/test_stereo.py
40
+
41
+ # Run a specific test function
42
+ pytest tests/test_stereo.py::test_function_name
43
+ ```
44
+
45
+ ### Linting and Formatting
46
+ Pre-commit hooks automatically run on commit. To run manually:
47
+ ```bash
48
+ # Run all pre-commit hooks
49
+ pre-commit run --all-files
50
+
51
+ # Format code with black
52
+ black asp_plot/ tests/
53
+
54
+ # Lint with flake8
55
+ flake8 asp_plot/ tests/
56
+
57
+ # Sort imports with isort (profile: black)
58
+ isort --profile black asp_plot/ tests/
59
+ ```
60
+
61
+ Flake8 configuration is in `.flake8` (extends ignore: E203, E701). Pre-commit further extends ignore to: E501, E722, E203, E207.
62
+
63
+ ### Rebuilding the Package
64
+ When making changes to CLI tools or entry points:
65
+ ```bash
66
+ pip install -e ".[dev]"
67
+ ```
68
+
69
+ ### Building Docs Locally
70
+ ```bash
71
+ # One-time: copy notebooks, reports, and figures for local preview
72
+ mkdir -p docs/examples/notebooks && cp notebooks/**/*.ipynb docs/examples/notebooks/
73
+ mkdir -p docs/_static/reports && cp reports/*.pdf docs/_static/reports/
74
+ mkdir -p docs/_extra/examples/figures && cp notebooks/figures/* docs/_extra/examples/figures/
75
+
76
+ # Build once
77
+ sphinx-build docs docs/_build/html
78
+
79
+ # Or live-reloading preview
80
+ sphinx-autobuild docs docs/_build/html --open-browser
81
+ ```
82
+
83
+ ## Code Architecture
84
+
85
+ ### Core Module Structure
86
+
87
+ **`__init__.py`** exports `__version__` via `importlib.metadata` (reads from `pyproject.toml` at install time, falls back to `"unknown"`).
88
+
89
+ The package is organized by functionality, with each module focused on a specific aspect of ASP output visualization. A structural rewrite (issue #122, v1.17.0) split several monoliths into single-concern modules — most of the registry/source/adapter modules below date from that work.
90
+
91
+ **`bodies.py`** - Single source of truth for per-body planetary facts (issue #126)
92
+ - `Body`: frozen dataclass bundling one body's facts — `name`, `altimetry_instrument` (ICESat-2 / LOLA / MOLA), `iau_sphere_radius_m`, `datum` (pc_align `D_MARS`/`D_MOON`), `geocentric_proj` (PROJ string for `apply_dem_translation`), `geographic_crs_wkt`, and ellipsoid fallback (`semi_major_axis_m`, `inverse_flattening`)
93
+ - `BODIES`: registry dict keyed `"earth"`/`"moon"`/`"mars"`. Replaced the ad-hoc `{"moon": ..., "mars": ...}` literals that were duplicated across ~40 sites in `alignment.py`, the altimetry sources, `utils.py`, and the CLI
94
+ - `body_for_dem(dem_fn, body=None)`: resolves a `Body` from an explicit name or by auto-detecting via `detect_planetary_body()`
95
+ - Module constants `MARS_IAU_SPHERE_RADIUS` / `MOON_IAU_SPHERE_RADIUS` (in `planetary_source.py`) are sourced from this registry
96
+
97
+ **`utils.py`** - Shared utilities for the entire package
98
+ - `Raster`: Wrapper class for raster operations using GDAL/rasterio/rioxarray
99
+ - Supports both georeferenced and non-georeferenced (raw) imagery
100
+ - `transform` property returns `None` for non-georeferenced data (identity transform)
101
+ - `get_epsg_code()`: Returns the EPSG code of the raster's CRS. Falls back to the horizontal (2D) component's EPSG via `pyproj` `CRS.to_2d()` when there is no exact match (compound / 3D-promoted CRSs like `"EPSG:32610+EPSG:4979"`, e.g. COP30 DEMs with ellipsoid heights asserted)
102
+ - `get_utm_epsg_code()`: Estimates UTM EPSG code from raster center coordinates
103
+ - `get_gsd()`: Returns ground sample distance in meters
104
+ - `get_bounds()`: Returns bounds using `self.ds.bounds` (rasterio), with optional lat/lon transform
105
+ - `_mask_nodata()`: Private helper that handles both undeclared nodata (inferred from pixel data) and invalid values (inf/nan) — needed because rasterio's `masked=True` alone doesn't catch undeclared nodata in ASP outputs
106
+ - `_load_and_diff_rasters_da()`: Private static method returning xarray DataArray for raster differencing; used internally by both `load_and_diff_rasters()` (converts to numpy) and `compute_difference()` (uses `.rio.to_raster()` for saving)
107
+ - `Plotter`: Base class for all plotting classes with common matplotlib setup. The scaffold added in #129 centralizes the boilerplate that was copy-pasted across plot methods: `save(fig, save_dir, fig_fn, ...)` (the `tight_layout()` + conditional-save tail), `plot_missing(ax, message)` (the "required files are missing" placeholder), and `plot_array(..., copyright=True)` which draws the Vantor/WorldView overlay from `self.is_vantor` so the check no longer threads through every call site
108
+ - `ColorBar`: Handles colorbar creation and formatting. `get_norm(clim=...)` takes an explicit clim so callers (e.g. `plot_geodataframe`) keep clim local instead of mutating `self.cb.clim`
109
+ - File utilities: `glob_file()`, `save_figure()`, `show_existing_figure()`
110
+ - Coordinate utilities: `get_utm_epsg()` for determining UTM EPSG from lon/lat, `get_planetary_bounds()` for DEM bounds in planetocentric 0-360 lon/lat
111
+ - Planetary body detection: `detect_planetary_body(dem_fn)` returns `"earth"`, `"moon"`, or `"mars"` by inspecting CRS WKT DATUM/ELLIPSOID fields
112
+ - Subprocess utilities: `run_subprocess_command()`
113
+ - Vantor/copyright utilities:
114
+ - `detect_vantor_satellite(directory)`: Checks XML files for WorldView SATIDs (WV01/WV02/WV03)
115
+ - `add_copyright_overlay(ax)`: Adds "© Vantor {year}" text overlay to bottom-right of matplotlib axes
116
+ - Scene metadata: `get_acquisition_dates(directory, extra_dirs=None)` reads `FIRSTLINETIME` from WorldView/Maxar XMLs and parses the capture timestamp from `AST_L1A_...` file/directory names. Returns a sorted, deduplicated list of `"YYYY-MM-DD HH:MM:SS UTC"` strings; empty if nothing is found. Used by the CLI to populate `ReportMetadata.acquisition_dates`.
117
+
118
+ **`selections.py`** - Reproducible "figure selections" for run-to-run comparison (issue #121)
119
+ - `FigureSelections` dataclass mirroring the YAML sidecar schema (`detailed_hillshade` clips + `icesat2` track/segments/parquet/request); `to_dict()`/`from_dict()`
120
+ - `write_selections_yaml()` / `read_selections_yaml()` round-trip (uses `pyyaml`)
121
+ - `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
122
+ - `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
123
+ - Deliberately imports nothing from `report.py` / `fpdf`, so it is safe to use from notebooks
124
+ - The `asp_plot` CLI always writes `<report_stem>_figure_selections.yml` next to the report; `--reuse_selections PATH` replays a prior run's choices
125
+
126
+ **`report.py`** - PDF report generation using fpdf2
127
+ - `ReportSection`: Dataclass representing a report figure (title, image path, caption)
128
+ - `AlignmentReportPage`: Dataclass for the pc_align + ICESat-2 alignment workflow (title, parameters dict, 1-row stats dict, description paragraph, status message, optional figure + caption). Rendered alongside `ReportSection` by `compile_report()`. Body text blocks are left-aligned (not justified) to avoid word-spacing gaps on long lines. Long pc_align column names (`north_shift`, `east_shift`, `down_shift`, `translation_magnitude`) are displayed as `N_shift`, `E_shift`, `D_shift`, `|T|` via `_ALIGNMENT_STATS_DISPLAY_LABELS` so the 10-column horizontal stats row fits in the page width.
129
+ - `ReportMetadata`: Dataclass for DEM metadata displayed on the title page (dimensions, GSD, CRS, nodata %, elevation range, DEM filename, reference DEM, acquisition dates). The "Acquisition Date(s)" row is added to the summary table only when `acquisition_dates` is non-empty.
130
+ - `ASPReportPDF`: FPDF subclass with custom header/footer and page numbers
131
+ - `compile_report()`: Assembles title page, Processing Parameters (page 2), figure sections with captions, and any trailing alignment pages into a PDF. Accepts optional `report_command` string to record the CLI invocation. Figures are automatically scaled to fit page dimensions, preventing overflow/cutoff. `sections` is a mixed list of `ReportSection | AlignmentReportPage`; dispatch is by `isinstance` check.
132
+ - `_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).
133
+ - `_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.
134
+ - Title page displays: processing date, ASP version (from logs), asp_plot version (from package metadata)
135
+ - Page order: title + DEM summary → Processing Parameters → diagnostic figures → (if `--pc_align` ran) alignment report page + aligned-DEM figures.
136
+ - `report.py` is fed declaratively by `report_pipeline.py`; it was **not** rewritten in #128.
137
+
138
+ **`report_pipeline.py`** - Declarative report pipeline behind the `asp_plot` CLI (issue #128)
139
+ - `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)
140
+ - `run_report(config)`: importable/callable from notebooks and tests with no Click context; returns the written PDF path
141
+ - 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
142
+ - 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`)
143
+
144
+ **`report_captions.py`** - Caption + description text for the report sections (issue #128)
145
+ - 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)
146
+
147
+ **`asp_log.py`** - Versioned adapter for parsing ASP log files (issue #132)
148
+ - Replaces the hardcoded string surgery that used to live inline in `processing_parameters.py`. `AspLogFormat` describes how to read one ASP log layout (version banner, timestamps, command line, reference DEM) via documented patterns; tool invocations are located by executable basename (against `ASP_TOOL_NAMES`) rather than arbitrary substring matches
149
+ - `AspLog`: a single parsed log file, exposing `asp_version`, `command` / `canonical_command` / `tool`, `timestamps` / `first_timestamp` / `last_timestamp`, `reference_dem`
150
+ - `select_format()` picks the adapter by version banner; unrecognized banners fall back to `DEFAULT_FORMAT` with a warning; unparseable fields return `None` and log (instead of being swallowed by a bare `except`). `register_format()` is the extension point for future ASP format drift. `STEREO_STEP_ORDER` generalizes stereo step ordering (earliest/latest stage present) instead of hardcoded pprc/tri
151
+
152
+ **`processing_parameters.py`** - `ProcessingParameters` class
153
+ - Delegates all ASP-log parsing to `asp_log.AspLog` (no more inline string surgery; the bare `except:` clauses became directory guards)
154
+ - Extracts command lines, run times, and processing parameters; `from_log_files()` returns a dict including the `asp_version` key
155
+ - Used by report generation to document processing settings
156
+
157
+ **`sensors.py`** - Sensor-specific scene metadata readers (issue #25)
158
+ - `SensorMetadata` ABC defining the reader interface (`detect` + `get_scene_dicts`) and the sensor-agnostic scene-dict schema, mirroring the `bodies.py` registry pattern so adding ASTER/HiRISE/etc. is a new subclass with no change to the geometry code
159
+ - `WorldViewMetadata(SensorMetadata)`: the WorldView/Maxar XML logic (file discovery, `dg_mosaic` tiling, per-scene extraction, ephemeris/attitude/footprint) moved verbatim out of the parser. Named after the satellite family rather than the (twice-renamed: DigitalGlobe → Maxar → Vantor) company; the same format also covers GeoEye-1/QuickBird/IKONOS
160
+ - `SENSORS` registry + `sensor_for_directory()` detection
161
+
162
+ **`stereopair_metadata_parser.py`** - `StereopairMetadataParser` class
163
+ - Now a **sensor-agnostic orchestrator** (issue #25): detects a reader via `sensor_for_directory()`, delegates scene discovery/extraction to it, and keeps only the pair-level geometry
164
+ - Computes stereo geometry metrics: convergence angle, base-to-height ratio, bisector elevation, asymmetry
165
+ - Provides spatial utilities: `get_pair_utm_epsg()`, `get_intersection_bounds()`, `get_scene_bounds()`
166
+ - `get_catid_dicts()` returns dictionaries per catalog ID with ephemeris, attitude, and geometry data (sourced from the sensor reader)
167
+ - Currently supports pairs; N-scene support is planned
168
+
169
+ **`stereo.py`** - `StereoPlotter` class (inherits from `Plotter`)
170
+ - Visualizes ASP stereo processing results
171
+ - Plots DEMs, hillshades, disparity maps, match points
172
+ - Creates difference maps with reference DEMs
173
+ - Supports both map-projected and raw (non-georeferenced) imagery
174
+ - Detects map-projection status via `Raster.transform` check
175
+ - 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
176
+ - Detects Vantor (WorldView) satellite via `is_vantor` attribute; adds copyright overlay to optical imagery in `plot_match_points()` and `plot_detailed_hillshade()`
177
+ - `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.
178
+ - Key methods: `plot_dem_results()`, `plot_disparity()`, `plot_match_points()`, `plot_detailed_hillshade()`
179
+
180
+ **`bundle_adjust.py`** - Two main classes
181
+ - `ReadBundleAdjustFiles`: Reads bundle adjustment CSV outputs (residual pointmaps)
182
+ - `get_initial_final_residuals_gdfs()`: Returns initial and final residual GeoDataFrames
183
+ - `get_initial_final_geodiff_gdfs()`: Returns geodiff comparison GeoDataFrames (requires `--mapproj-dem` flag in bundle_adjust)
184
+ - `get_mapproj_residuals_gdf()`: Returns map-projected residual GeoDataFrame
185
+ - `PlotBundleAdjustFiles` (inherits from `Plotter`): Visualizes bundle adjustment residuals before/after optimization
186
+ - Plots include map views of residuals, histograms, and geodiff comparisons
187
+
188
+ **CSM camera model comparison** — split into three layers by issue #131 (was one 1541-LOC `csm_camera.py`):
189
+
190
+ **`csm_io.py`** - Camera-model I/O readers mirrored from ASP's `orbit_plot.py`
191
+ - Function-based, with a provenance header noting they are synced from upstream ASP. `read_csm_cam()`, `read_tsai_cam()`, `read_frame_csm_cam()`, `read_linescan_pos_rot()`, `read_angles()`, `toCsmPixel()`, `isLinescan()`, `roll_pitch_yaw()`, `estim_satellite_orientation()`, etc.
192
+ - `stereo_geometry.py` imports `estim_satellite_orientation` from here directly
193
+
194
+ **`csm_analysis.py`** - asp_plot-specific analysis built on `csm_io`
195
+ - `get_orbit_plot_gdf()`: turns an original/optimized camera pair into the position- and orientation-difference GeoDataFrame consumed by the plotting layer
196
+ - `reproject_ecef()`, `poly_fit()`
197
+
198
+ **`csm_camera.py`** - Plotting layer for CSM camera optimization / jitter results
199
+ - 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)
200
+ - `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
201
+ - Re-exports the moved `csm_io`/`csm_analysis` symbols for backward compatibility with notebooks and downstream imports
202
+
203
+ **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.
204
+
205
+ **`altimetry.py`** - `Altimetry` coordinator + `AlignmentResult`
206
+ - Composes three collaborators — `self.icesat2` (`Icesat2Source`), `self.planetary` (a `PlanetarySource` subclass), and `self.plotter` (`AltimetryPlotter`) — each holding a back-reference to the coordinator, which owns the cross-cutting `directory` / `dem_fn` / `aligned_dem_fn`. The planetary source class is chosen **once, at construction**, from the DEM's body: `{"moon": LolaSource, "mars": MolaSource}.get(detect_planetary_body(dem_fn), PlanetarySource)`
207
+ - The full public/notebook API (`request_atl06sr_multi_processing`, `load_planetary_csv`, `atl06sr_to_dem_dh`, `planetary_to_dem_dh`, the `plot_*`/`histogram*`/`mapview_*` methods, `to_csv_for_pc_align*`, the `atl06sr_processing_levels*` / `planetary_points` properties, …) is preserved as **delegating wrappers** over the collaborators; plotting receives already-prepared dataframes (the coordinator computes the dh columns and resolves the track before delegating, so render no longer triggers heavy I/O mid-figure)
208
+ - Keeps the `pc_align` orchestration and the shared keep/discard decision: `alignment_report()`, `align_and_evaluate()` (Earth) / `align_and_evaluate_planetary()` (planetary) and the helpers `_improvement_pct` / `_evaluate_improvement` / `_success_result` (the shared decision factored out in #127). Both `align_and_evaluate*` return an `AlignmentResult` dataclass with `status ∈ {"insufficient_points", "no_improvement", "success"}`; the aligned DEM is removed on the non-success branches so its on-disk existence is a truthy "alignment worth using" signal. On success the Earth path re-calls `atl06sr_to_dem_dh(n_sigma=None)` to populate `icesat_minus_aligned_dem` without re-filtering. `Altimetry`, `AlignmentResult`, `ICESAT2_MISSION_START` stay importable from `asp_plot.altimetry`; none of this imports `report.py` / `fpdf`, so it is notebook-safe
209
+ - New dependency from this subsystem: `pyyaml` (request-metadata YAML); `sliderule>=5.3.0` pinned
210
+
211
+ **`altimetry_source.py`** - `AltimetrySource` base (issue #140)
212
+ - Shared machinery lifted out of the ICESat-2 and planetary sources so neither re-implements it: `_interp_dem_at_points(dem, points)` (bilinear DEM sampling; returns sampled values **and** the points reprojected into the DEM/working CRS), `_open_dem(dem_fn)`, `_std_outlier_mask(dh, n_sigma)` (n-σ std-from-mean mask, returns `None` on empty/all-NaN or zero spread to signal "do not filter"), and `_write_csv_to_directory(df, filename)` (roots pc_align CSVs at `self.alt.directory`)
213
+
214
+ **`icesat2_source.py`** - `Icesat2Source(AltimetrySource)` (Earth / ICESat-2)
215
+ - Requests/processes ICESat-2 ATL06-SR via the SlideRule x-series API (`sliderule_api.run("atl03x")`) with lazy init (`_ensure_sliderule()` — connects only when ICESat-2 methods run). SlideRule logging silenced (`verbose=False`, WARNING, explicit filter on `sliderule.session`)
216
+ - `request_atl06sr_multi_processing()` decomposed (#140) into a short request/cache/ingest loop over helpers: `_print_time_filter_summary`, `_build_level_parms`, `_load_cached_atl06sr`, `_params_match_cache`, `_request_atl06sr_level`, `_ingest_atl06sr`. Parquet cache saved to `self.alt.directory` (not CWD); the param comparison strips the `output` key (SlideRule injects a random temp path into `parms` during `run()`) and only the comparison is wrapped in `try/except` so SlideRule API errors propagate
217
+ - Server-side time filtering via `_resolve_time_range(...)`: default `"all"` = full mission (2018-10-14 → today UTC midnight); `"buffered"` cascades explicit `t0`/`t1` > `scene_date` ± `time_buffer_days` > XML metadata ± buffer > `"all"`. `t1` truncated to midnight UTC for stable caching
218
+ - ESA WorldCover sampling: `sample_esa_worldcover()` / `_sample_worldcover_into_gdf()` read 10m values directly from public AWS S3 COGs (`esa-worldcover.s3.amazonaws.com/v200/2021/map/`) via `rasterio` vsicurl, batched per tile, persisted into the parquet cache (much faster than SlideRule's server-side `samples`). `_worldcover_tile_url(lat, lon)` maps a coord to the 3×3° tile URL (flat layout). Anonymous reads (unsigned AWS session) so SSO configs don't crash
219
+ - `filter_outliers(n_sigma=3)`: drops dh beyond `n_sigma × std` from the mean (true std, not NMAD); called automatically by `atl06sr_to_dem_dh()`; pass `None` to skip
220
+ - `atl06sr_to_dem_dh()`: opens the DEM via `self._open_dem`, samples via `self._interp_dem_at_points`, computes the dh column. Track/segment selection: `_select_best_track()` (most valid dh points), `_find_best_worst_segments()` / `_segment_dict()` / `_resolve_best_track()`
221
+ - **Run-to-run reuse (issue #121)**: `load_atl06sr_from_parquet()` replays the *exact* prior points (bypassing SlideRule), `_restore_request_metadata_from_parquet()` restores `t0`/`t1` for plot titles, `get_altimetry_selections()` returns request params / parquet paths / track / segments. Segments pinned by **absolute `x_atc`** (not km-from-track-start) so they survive a track-start shift when 3σ filtering a different DEM drops a different first point
222
+ - `_extract_scalar()` handles array-valued cells from the x-series API / parquet round-trip; module constant `ICESAT2_MISSION_START`
223
+
224
+ **`planetary_source.py`** - `PlanetarySource(AltimetrySource)` + `LolaSource` / `MolaSource` (issue #140)
225
+ - Two-step workflow: (1) submit async query via `request_planetary_altimetry` CLI → email with download link, (2) load the CSV. The body subclass is selected by the coordinator at construction, so loading no longer re-detects the body per call
226
+ - `PlanetarySource` (body-agnostic base): the dh + export half — `_load_planetary_csv_common()`, `_build_planetary_gdf()`, `_find_csv_column()`, `_sample_dem_at_planetary_points()` (uses `self._open_dem` / `self._interp_dem_at_points`), `planetary_to_dem_dh()` (uses `self._std_outlier_mask`; when `aligned_dem_fn` is set also samples the aligned DEM so pre/post plots share one sample), `to_csv_for_pc_align_planetary()` (writes `lon, lat, radius_m`). Its `load_planetary_csv()` raises and redirects to ICESat-2 (the Earth case)
227
+ - `LolaSource` (Moon, `instrument = "LOLA"`): prefers `Pt_Radius` (LOLA RDR Point-per-Row CSV, in **km** — auto-detected by magnitude < 10 000, converted to m), falls back to `Topography` (simple-topo CSV, meters). Moon ≈ spherical (~1.4 km variation) so the fallback is fine
228
+ - `MolaSource` (Mars, `instrument = "MOLA"`): reads `PLANET_RAD` from the ODE GDS `*_pts_csv.csv`, `height = PLANET_RAD - 3_396_190`. The `*_topo_csv.csv` (TOPOGRAPHY only) is **rejected** with an explanatory error — TOPOGRAPHY is referenced to the **oblate** MOLA areoid while ASP DEMs use the **spherical** IAU 2000 datum, a latitude-dependent offset up to ~10 km that pc_align can't remove
229
+ - Both loaders store `height` (above the IAU sphere; plot labels + dh stats) and `radius_m` (absolute planetary radius; pc_align) on `planetary_points`
230
+ - Module-level `gds_query_async()` + `GDS_BASE_URL` (`https://oderest.rsl.wustl.edu/livegds`), re-exported from `asp_plot.altimetry` so the CLI import is unchanged; constants `MARS_IAU_SPHERE_RADIUS` / `MOON_IAU_SPHERE_RADIUS` (sourced from `BODIES`). `LolaSource` / `MolaSource` / `PlanetarySource` are also re-exported from `asp_plot.altimetry`
231
+
232
+ **`altimetry_plots.py`** - `AltimetryPlotter` (issue #130)
233
+ - All figure rendering, operating on already-prepared dataframes. `histogram_by_landcover()`, `plot_atl06sr_dem_profile()` (2×2: stacked elevation + dh on the left, DEM hillshade map spanning both rows on the right), `plot_best_worst_segments()` (scoring `3·|median(dh)| + NMAD(dh)`, weighting bias 3× over dispersion; requires >75% DEM coverage per segment), `mapview_plot_planetary_to_dem()` / `histogram_planetary_to_dem()`, plus the over-long profile/segment methods decomposed into panel helpers (`_profile_elevation_panel`, `_profile_dh_panel`, `_draw_segment_panel`, `_plot_hillshade_map`, `_add_segment_spans`, `_build_landcover_stats_text`)
234
+ - `plot_aligned=True` overlays pre/post-alignment data on shared color/bin scales; dh maps + histograms use symmetric ±|3σ min/max| clim centered on 0 (labeled `[±3σ]`); all data is still plotted and used for Median/NMAD stats. Module constant `WORLDCOVER_NAMES`; the shared `nmad` helper now lives in `utils.py`
235
+ - Requires internet for data requests (SlideRule + AWS S3 WorldCover COGs)
236
+
237
+ **`alignment.py`** - `Alignment` class
238
+ - Performs DEM alignment using ASP's `pc_align` tool
239
+ - `pc_align_dem_to_atl06sr()`: ICESat-2 path, csv-format `1:lon 2:lat 3:height_above_datum`
240
+ - `pc_align_dem_to_planetary_csv(planetary_csv, body, ...)`: MOLA/LOLA path. Uses csv-format `1:lon 2:lat 3:radius_m` and `--datum D_MARS`/`D_MOON` (per ASAP-Stereo's CTX cookbook). Default `max_displacement=500` m
241
+ - Both public methods keep their signatures/validation/errors and delegate to a shared `_run_pc_align(csv, csv_format, max_displacement, datum=...)` (#127); generated argv is byte-identical to before, with `--datum` emitted only on the planetary path
242
+ - `pc_align_report()`: Extracts begin/end percentiles + N-E-D translation from the pc_align log
243
+ - `apply_dem_translation()`: Applies pc_align's Cartesian translation to the DEM (geotransform shift + scalar add to pixel values, no resampling). Picks the right body-centered geocentric source CRS via the module-level `_GEOCENTRIC_PROJ` dict — Earth uses EPSG:4978, Mars/Moon use PROJ strings (`+proj=geocent +R=...`) because PROJ refuses to convert across celestial bodies
244
+ - Used by `Altimetry` class for DEM-to-altimetry alignment on Earth, Mars, and Moon
245
+
246
+ **`stereo_geometry.py`** - `StereoGeometryPlotter` class
247
+ - **Composes** a `StereopairMetadataParser` via `self.parser` rather than subclassing it (issue #25); imports `estim_satellite_orientation` from `csm_io` directly
248
+ - Visualizes stereo acquisition geometry from XML metadata
249
+ - `dg_geom_plot()`: Creates skyplot (satellite viewing angles) and map view (footprints)
250
+ - `satellite_position_orientation_plot()`: Creates 3x2 figure showing position covariance, roll/pitch/yaw orientation, and attitude covariance for each scene
251
+
252
+ **`scenes.py`** - `ScenePlotter` class (inherits from `Plotter`)
253
+ - Plots individual input scenes (satellite/spacecraft images)
254
+ - Works with any sensor (terrestrial or planetary)
255
+ - Automatically detects and indicates whether scenes are map-projected or raw
256
+ - Displays filenames rather than sensor-specific metadata
257
+ - Detects Vantor (WorldView) satellite via `is_vantor` attribute; adds copyright overlay to scene images in `plot_scenes()`
258
+ - Key method: `plot_scenes()` (formerly `plot_orthos()` prior to v1.2.0)
259
+ - Used in comprehensive reports to show source imagery
260
+
261
+ **`gallery.py`** - `GalleryPlotter` class (inherits from `Plotter`)
262
+ - Lays out a *stack* of DEMs as a grid of thumbnails sharing one global percentile color stretch and a single colorbar (for QA'ing multi-date / multi-pair ASP output at a glance). Replaces the legacy `original_code/gallery.py` and its `pygeotools`/`imview` deps; reuses `Raster` (downsampled reads), `ColorBar.find_common_clim`, and `save_figure`
263
+ - Renders DEMs with the package's standard convention: gray hillshade underlay + semi-transparent `viridis` DEM, `"Elevation (m HAE)"` colorbar (matches `stereo.py` `_plot_dem_with_hillshade`). Hillshade underlay on by default
264
+ - `from_directory(directory, pattern="*-DEM.tif")` resolves a directory + glob into the raster list; globs with `recursive=True` so `**` descends into subdirectories (the per-pair ASP layout). Also accepts an explicit file list via `__init__`
265
+ - Layout sizes each panel to the rasters' median aspect ratio and places panels with absolute positioning, so 1 to N rasters (incl. non-square) pack tightly without internal or trailing-cell whitespace
266
+ - Per-panel titles use the full filename, auto-shrunk to fit the panel by *measuring* rendered text width with an Agg renderer (`_fit_titles()`); falls back to a character-count heuristic (`_fit_title_fontsize()`) if measurement fails
267
+ - Output detail is read at ~`GALLERY_TARGET_PX` (1200) px/panel and save dpi is matched to it for crisp zoom, then dpi is capped to a pixel budget so the PNG stays under `max_filesize_mb` (default 10) regardless of raster count
268
+ - Key method: `plot_gallery()`; static helper `_grid_shape(n, aspect)` picks the most square-in-display grid
269
+ - Not wired into the main `asp_plot` PDF report (standalone class + `gallery` CLI)
270
+
271
+ ### CLI Tools
272
+
273
+ All CLI tools are in `asp_plot/cli/` and use Click for argument parsing:
274
+
275
+ **`asp_plot.py`** - Main CLI tool (`asp_plot` command)
276
+ - Generates comprehensive PDF reports of ASP processing
277
+ - 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
278
+ - Accepts directories for stereo and bundle_adjust outputs
279
+ - Options for reference DEMs, ICESat-2 comparisons, basemaps
280
+ - 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.
281
+ - 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.
282
+ - `--directory`: Root ASP processing directory (default: `./`)
283
+ - `--stereo_directory`: Stereo output subdirectory (default: `stereo`)
284
+ - `--bundle_adjust_directory`: Optional BA directory
285
+ - `--dem_filename`: Custom DEM filename (default: auto-detect `*-DEM.tif`)
286
+ - `--dem_gsd`: Custom DEM ground sample distance
287
+ - `--map_crs`: Projection as `EPSG:XXXX` (default: auto-detect from DEM, fallback `EPSG:4326`)
288
+ - `--reference_dem`: Reference DEM path (auto-detected from logs if not supplied)
289
+ - `--add_basemap`: Add Esri WorldImagery basemaps (default: True, requires internet)
290
+ - `--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.
291
+ - `--plot_icesat`: Deprecated alias for `--plot_altimetry`. Prints deprecation warning if used.
292
+ - `--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.
293
+ - `--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.
294
+ - `--plot_geometry`: Plot stereo geometry (default: True; disable for planetary missions)
295
+ - `--subset_km`: Hillshade subset size in km (default: 1.0)
296
+ - `--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).
297
+ - `--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_plot.md`.
298
+ - `--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
299
+ - `--report_title`: Custom report title (default: directory name)
300
+
301
+ **`csm_camera_plot.py`** - CSM camera comparison tool (`csm_camera_plot` command)
302
+ - Wrapper for `csm_camera.py` functions
303
+ - Compares original and optimized CSM camera models
304
+ - Visualizes position/angle differences and camera footprints
305
+
306
+ **`request_planetary_altimetry.py`** - Planetary altimetry data request tool (`request_planetary_altimetry` command)
307
+ - Submits async LOLA (Moon) or MOLA (Mars) queries to the ODE GDS REST API
308
+ - Auto-detects planetary body from DEM CRS via `detect_planetary_body()`
309
+ - `--dem`: Path to ASP DEM (required)
310
+ - `--email`: Email for notification when query finishes (required)
311
+ - `--channels`: LOLA detector channels (Moon only, default `tffff` = channel 1 only)
312
+ - Saves request metadata as `altimetry_request_info.yml` alongside the DEM
313
+ - Workflow: submit query → receive email → download/unzip → pass `*_pts_csv.csv` (Mars) or `*_topo_simple_csv.csv`/`*_pts_csv.csv` (Moon) to `asp_plot --altimetry_csv`
314
+
315
+ **`stereo_geom.py`** - Stereo geometry visualization tool (`stereo_geom` command)
316
+ - Wrapper for `StereoGeometryPlotter`
317
+ - Creates skyplot and map view from XML camera files
318
+ - Supports multiple XMLs with automatic mosaicking
319
+
320
+ **`gallery.py`** - DEM gallery tool (`gallery` command)
321
+ - Wrapper for `GalleryPlotter`; lays out many DEMs as a grid sharing one color scale
322
+ - `--directory` + `--pattern` (supports recursive `**` for subdirectories) or an explicit list of `FILES` (files take precedence)
323
+ - `--hillshade/--no-hillshade`, `--cmap`, `--downsample`, `--max_filesize_mb`, `--title`, `--output_directory/--output_filename`
324
+ - Saves `<dirname>_gallery.png` into the input directory by default
325
+
326
+ ## Documentation Website
327
+
328
+ Documentation is hosted at **https://asp-plot.readthedocs.io** and built with Sphinx + MyST Markdown + sphinx-book-theme. ReadTheDocs builds automatically on push to `main`.
329
+
330
+ ### Architecture
331
+
332
+ - **Stack**: Sphinx, myst-nb (Markdown + notebook rendering), sphinx-autoapi (API docs via static analysis, no package import needed), sphinx-design (cards/grids), sphinx-book-theme (collapsible sidebar)
333
+ - **Config**: `docs/conf.py` — Sphinx configuration; `.readthedocs.yaml` — RTD build config
334
+ - **RTD build deps**: `docs/requirements.txt` — installed instead of the full package (avoids GDAL on RTD)
335
+ - **Version fallback**: `conf.py` uses `try/except` for `importlib.metadata.version()` since the package isn't installed on RTD
336
+
337
+ ### Content Structure
338
+
339
+ ```
340
+ docs/
341
+ conf.py # Sphinx configuration
342
+ index.md # Landing page with sphinx-design cards
343
+ installation.md # conda/pip/source install
344
+ cli/ # CLI tool docs (asp_plot, stereo_geom, csm_camera_plot, request_planetary_altimetry, gallery)
345
+ examples/
346
+ index.md # Notebook gallery with cards by sensor
347
+ reports.md # PDF reports embedded as iframes
348
+ notebooks/ # .gitignored — copied from notebooks/ during build
349
+ figures/ # Committed doc figures (e.g. example_gallery.png); referenced from .md via ../figures/
350
+ contributing.md # Dev setup, testing, release process
351
+ changelog.md # Includes top-level CHANGELOG.md via MyST include
352
+ _static/reports/ # .gitignored — copied from reports/ during build
353
+ _extra/examples/figures/ # .gitignored — copied from notebooks/figures/ during build
354
+ _templates/footer.html # RTD ethical ads placement in footer
355
+ requirements.txt # Docs-only pip dependencies for RTD
356
+ ```
357
+
358
+ ### Key Design Decisions
359
+
360
+ - **Notebooks and reports stay at top level** (`notebooks/`, `reports/`). RTD's `pre_build` jobs in `.readthedocs.yaml` copy them into the docs tree. The `docs/examples/notebooks/`, `docs/_static/reports/`, and `docs/_extra/` directories are `.gitignore`d.
361
+ - **sphinx-autoapi** generates API reference from static analysis — no GDAL/rasterio needed on RTD. This is why RTD installs only `docs/requirements.txt` instead of the full package.
362
+ - **`html_extra_path`** is used to serve `notebooks/figures/` at the correct relative path for notebook `<img src>` references.
363
+ - **`docs/` is excluded from sdist** in `pyproject.toml` so docs never ship in the PyPI/conda package.
364
+ - **Changelog** uses `{include} ../CHANGELOG.md` so there's one source of truth.
365
+ - **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_plot --report_filename` writes (so the URL on RTD actually resolves).
366
+
367
+ ### Dependencies
368
+
369
+ Docs dependencies are in `pyproject.toml` under `[project.optional-dependencies] docs` and mirrored in `docs/requirements.txt` (for RTD). The `environment.yml` installs with `pip install -e ".[dev,docs]"`.
370
+
371
+ ## Key Design Patterns
372
+
373
+ **Inheritance for shared scaffolding, composition for collaborators**: Plotting classes inherit from the `Plotter` base for consistent matplotlib setup and helpers (`save`, `plot_missing`, `plot_array`); the altimetry sources inherit `AltimetrySource` for shared DEM-sampling/outlier/CSV helpers. But cross-concern wiring is *composed*, not inherited (the #122 rewrite moved several god-classes this way): `Altimetry` composes its sources + plotter, `StereoGeometryPlotter` composes a `StereopairMetadataParser`, `StereopairMetadataParser` composes a `SensorMetadata` reader, and `StereoPlotter`/`ScenePlotter` compose a `*Files` discovery object.
374
+
375
+ **Registries over conditionals**: per-target facts live in one registry rather than scattered `if body == ...` / `if sensor == ...` branches — `BODIES` (`bodies.py`), `SENSORS` (`sensors.py`), `ASP_LOG_FORMATS` (`asp_log.py`), `REPORT_SECTIONS` (`report_pipeline.py`). Adding a 4th body / 3rd sensor / new ASP-log format / reordered report section is a new registry entry, not edits across many files.
376
+
377
+ **Lazy Loading**: Data is loaded on-demand (e.g., DEMs, hillshades) rather than in `__init__` to avoid unnecessary I/O.
378
+
379
+ **Internal rioxarray, Public numpy**: rioxarray is used internally where it simplifies implementations (e.g., raster reprojection/alignment in `_load_and_diff_rasters_da()`, saving via `.rio.to_raster()`). The public API always returns `numpy.ma.MaskedArray`. Avoid opening files redundantly — prefer reusing the existing `self.ds` (rasterio dataset) for metadata like bounds and transforms.
380
+
381
+ **External Tool Integration**: The package wraps ASP command-line tools (pc_align, geodiff, dg_mosaic) via `run_subprocess_command()`.
382
+
383
+ **Report Generation**: The main `asp_plot` CLI uses `compile_report()` to combine individual plots into a single PDF with metadata tables.
384
+
385
+ ## ASP Tool Dependencies
386
+
387
+ This package is designed to work with outputs from the NASA Ames Stereo Pipeline. Key ASP tools used:
388
+ - `stereo` / `parallel_stereo`: Main stereo processing (generates DEMs, disparity maps, match files)
389
+ - `bundle_adjust`: Camera optimization (generates residual pointmaps)
390
+ - `pc_align`: Point cloud alignment (used for DEM-to-altimetry alignment)
391
+ - `geodiff`: Generates difference statistics between DEMs
392
+ - `dg_mosaic`: Mosaics multiple XML files for tiled imagery
393
+ - `point2dem`: Converts point clouds to DEMs
394
+
395
+ ## External Data Sources
396
+
397
+ **ICESat-2 ATL06-SR** (Earth): Fetched via SlideRule x-series API (`sliderule_api.run("atl03x")`, requires internet). The `Altimetry` class handles requests with automatic server-side time filtering, data normalization, land cover filtering, and comparison with ASP DEMs.
398
+
399
+ **LOLA RDR** (Moon): Queried via `request_planetary_altimetry` CLI (`query=lolardr`). User receives an email with a download link. Either the simple-topography CSV (`results=u`, columns `Pt_Longitude, Pt_Latitude, Topography`) or the Point per Row CSV (`results=p`, includes `Pt_Radius` in **kilometers**) works. The Moon is essentially spherical (~1.4 km equatorial-vs-polar variation), so LOLA topography ≈ height above the IAU 1737.4 km lunar sphere to ~1 m. Loaded via `Altimetry.load_planetary_csv()`.
400
+
401
+ **MOLA PEDR** (Mars): Queried via `request_planetary_altimetry` CLI (`query=molapedr`). User receives an email with a download link containing both `*_topo_csv.csv` (TOPOGRAPHY only, areoid-referenced) and `*_pts_csv.csv` (full-fidelity record including `PLANET_RAD`). **Use the `*_pts_csv.csv`.** The loader computes `height = PLANET_RAD - 3,396,190` (IAU 2000 Mars sphere) and rejects the topo-only file with an explanatory error. Reason: MOLA TOPOGRAPHY is referenced to the **oblate** MOLA areoid (~20 km equatorial-vs-polar variation) while ASP DEMs use the **spherical** IAU sphere; dh from TOPOGRAPHY carries a latitude-dependent offset of up to ~10 km that pc_align cannot remove (verified at lat 34°N: MOC NA dh dropped from +6000 m to ~+25 m simply by switching to PLANET_RAD).
402
+
403
+ **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.
404
+
405
+ **Basemaps**: Uses `contextily` to fetch Esri WorldImagery tiles (requires internet). Can be disabled with `--add_basemap False`. Automatically skipped for planetary (non-Earth) DEMs.
406
+
407
+ ## Testing
408
+
409
+ Tests are in `tests/` with sample data in `tests/test_data/`. Most modules have corresponding test files (e.g., `test_stereo.py` for `stereo.py`). Test data includes synthetic rasters, XML camera files, bundle adjustment CSVs, ICESat-2 parquet files, pc_align outputs, and jitter correction data. There is also a `test_imports.py` that verifies all modules can be imported.
410
+
411
+ Example notebooks demonstrating modular usage are organized by sensor type:
412
+ - `notebooks/WorldView/` - DigitalGlobe/Maxar WorldView examples (Earth-based). Includes a no-mapprojection variant (`worldview_spacenet_atlanta_stereo_without_mapprojection.ipynb`) that runs `parallel_stereo --alignment-method affineepipolar` on the raw `*_corr.tif` images instead of `*_corr_map.tif`. To keep the comparison ROI matched to the mapprojected example, that notebook documents an inverse-RPC trick: open the original `.tif` (which carries RPC metadata; the `wv_correct` `_corr.tif` does not) with GDAL, build a `gdal.Transformer(["METHOD=RPC", "RPC_HEIGHT=<m>"])`, call `TransformPoint(1, lon, lat, h)` (direction `1` = inverse: ground → pixel) on the four corners of the mapprojected notebook's UTM `t_projwin`, take the bounding box, and pass the result as `--left-image-crop-win`. This reuses the existing `ba/` outputs and writes to `stereo_no_mapproj/` so both runs coexist.
413
+ - `notebooks/ASTER/` - ASTER examples with map-projection and jitter correction (Earth-based)
414
+ - `notebooks/LRO_NAC/` - Lunar Reconnaissance Orbiter Narrow Angle Camera examples (Lunar)
415
+ - `notebooks/Mars_MGS/` - Mars Global Surveyor MOC NA examples (Mars). Single notebook covers both mapprojected (`cam2map4stereo.py` first) and non-mapprojected (`--alignment-method affineepipolar`) variants of the M0100115 / E0201461 pair, mirroring the ASTER mapproj/non-mapproj layout
416
+ - `notebooks/Mars_MRO/` - Mars Reconnaissance Orbiter CTX and HiRISE examples (Mars)
417
+
418
+ ## Versioning and Release Process
419
+
420
+ Follow semantic versioning (MAJOR.MINOR.PATCH). To release:
421
+ 1. Update version in `pyproject.toml`
422
+ 2. Update `CHANGELOG.md` with version and date
423
+ 3. Merge to `main`
424
+
425
+ The rest is automated:
426
+ - `release.yml` detects the version bump, creates a GitHub Release + tag, then builds and publishes to PyPI via OIDC trusted publishing (uses `softprops/action-gh-release@v2`)
427
+ - conda-forge's `regro-cf-autotick-bot` detects the new PyPI version and opens a feedstock PR automatically
428
+
429
+ **One-time setup** (already completed):
430
+ - PyPI trusted publisher configured at pypi.org for `release.yml` (environment name left blank)
431
+ - conda-forge feedstock created via `staged-recipes` PR (reference recipe in `conda-forge-recipe/meta.yaml`)
432
+ - All runtime dependencies declared in `pyproject.toml` (`pip install asp-plot` installs all deps)
433
+ - Documentation hosted on ReadTheDocs (auto-builds on push to `main`)
434
+
435
+ ## Common File Patterns
436
+
437
+ ASP output files follow specific naming patterns:
438
+ - DEMs: `*-DEM.tif` or `*_dem.tif`
439
+ - Disparity: `*-F.tif` (disparity map)
440
+ - Match files: `*.match`
441
+ - Bundle adjust residuals: `*-initial_residuals_pointmap.csv`, `*-final_residuals_pointmap.csv`
442
+ - Log files: `*log-bundle_adjust*.txt`, `*log-stereo*.txt`, `*log-point2dem*.txt`
443
+
444
+ Use `glob_file()` utility to find files matching these patterns.
@@ -5,6 +5,29 @@ 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
+ ## [1.17.0] - 2026-06-24
9
+
10
+ ### Changed
11
+ - **Structural rewrite of the codebase** ([#122](https://github.com/uw-cryo/asp_plot/issues/122)). A top-down refactor of the package into smaller, single-concern modules, shipped as nine independent sub-issues behind the existing test suite. No user-visible behavior change — the CLIs, the public class APIs, and the generated reports are unchanged (verified by golden/characterization tests on each step); this is an internal re-organization that pays off the next feature (a 4th body, a 3rd sensor, a reordered report).
12
+ - **Body abstraction** ([#126](https://github.com/uw-cryo/asp_plot/issues/126)). The Earth/Moon/Mars facts that were re-typed as ad-hoc `{"moon": ..., "mars": ...}` literals across ~40 sites collapse into one frozen `Body` dataclass + `BODIES` registry in a new `asp_plot/bodies.py` (altimetry instrument, IAU sphere radius, `pc_align` datum, geocentric PROJ string, geographic CRS WKT, ellipsoid fallback). `alignment.py`, the altimetry sources, the CLI, and `utils.py` now read `body.attr`.
13
+ - **Collapsed duplicated `pc_align` + alignment-evaluation paths** ([#127](https://github.com/uw-cryo/asp_plot/issues/127)). `Alignment._run_pc_align()` backs both the ICESat-2 and LOLA/MOLA public methods (byte-identical argv), and three shared helpers (`_improvement_pct`, `_evaluate_improvement`, `_success_result`) unify the Earth/planetary keep-or-discard decision.
14
+ - **Declarative report pipeline** ([#128](https://github.com/uw-cryo/asp_plot/issues/128)). The ~880-line `cli/asp_plot.py::main()` becomes a thin Click wrapper over a Click-free `run_report(config)`: a `ReportConfig` dataclass packs the options, a `REPORT_SECTIONS` registry of `ReportSpec`s (enabled-predicate + build-function) replaces the inline plot-and-append wall, and captions move to a data module (`report_captions.py`). `run_report()` is now importable from notebooks.
15
+ - **Sensor-flexible metadata parsing** ([#25](https://github.com/uw-cryo/asp_plot/issues/25)). A new `sensors.py` (`SensorMetadata` ABC + `WorldViewMetadata` reader + `SENSORS` registry) separates sensor-specific scene discovery/extraction from the sensor-agnostic pair geometry; `StereoGeometryPlotter` now *composes* a `StereopairMetadataParser` instead of inheriting it.
16
+ - **`Plotter` scaffold + file-discovery separation** ([#129](https://github.com/uw-cryo/asp_plot/issues/129)). The `Plotter` base gains `save()` / `plot_missing()` / copyright-aware `plot_array()`, and new `StereoFiles` / `SceneFiles` discovery classes own the `glob_file` logic that was duplicated across plotters.
17
+ - **Altimetry god-class split** ([#130](https://github.com/uw-cryo/asp_plot/issues/130), [#140](https://github.com/uw-cryo/asp_plot/issues/140)). The 3800-line `Altimetry` class splits into a thin coordinator (`altimetry.py`) composing `Icesat2Source` (`icesat2_source.py`), planetary sources (`planetary_source.py`), and `AltimetryPlotter` (`altimetry_plots.py`), with shared DEM-sampling / outlier-mask / CSV-writer machinery in an `AltimetrySource` base (`altimetry_source.py`). Planetary loading graduates to per-body `LolaSource` / `MolaSource` subclasses dispatched from the DEM body at construction. The public `asp_plot.altimetry` API and re-exports are preserved by delegation.
18
+ - **`csm_camera.py` split** ([#131](https://github.com/uw-cryo/asp_plot/issues/131)). The 1541-line module splits into `csm_io.py` (ASP-mirrored camera-model readers), `csm_analysis.py` (the asp_plot-specific analysis), and `csm_camera.py` (plotting). The near-verbatim cam1/cam2 plotting halves collapse into a single `_plot_camera()`; moved symbols are re-exported for backward compatibility.
19
+ - **Versioned ASP-log adapter** ([#132](https://github.com/uw-cryo/asp_plot/issues/132)). A new `asp_log.py` (`AspLogFormat` adapter keyed by ASP version + `AspLog` reader) replaces the hardcoded string surgery in `processing_parameters.py`; `register_format()` is the extension point for future ASP format drift.
20
+
21
+ ## [1.16.0] - 2026-06-11
22
+
23
+ ### Added
24
+ - **Reusable "figure selections" for run-to-run comparison** ([#121](https://github.com/uw-cryo/asp_plot/issues/121)). When re-processing the same scene with different ASP parameters, the diagnostic figures previously changed *what they showed* between runs — a fresh ICESat-2 request returned a slightly different point set, the "best" profile track flipped, the best/worst agreement segments moved, and the detailed-hillshade clip boxes were re-selected from the re-processed intersection-error raster — making before/after comparison impossible. The `asp_plot` CLI now writes a `<report_stem>_figure_selections.yml` sidecar next to the report recording every non-deterministic selection, and a new `--reuse_selections PATH` flag replays a prior run's choices so figures are directly comparable.
25
+ - New `asp_plot/selections.py` module (`FigureSelections` dataclass + YAML read/write + clip-box ↔ pixel-window + CRS-reprojection helpers), deliberately free of `report.py` / `fpdf` imports so it is safe to use from notebooks.
26
+ - `StereoPlotter.plot_detailed_hillshade()` gains a `clip_windows` (+ `clip_windows_crs`) kwarg and records the boxes it drew on `self.detailed_hillshade_clips`. Clip boxes are stored in map coordinates and reprojected to the current DEM's CRS on reuse, so the same ground area is clipped even across stereo variants in different projections (e.g. mapprojected vs. non-mapprojected, which can land in different CRSs); boxes that fall outside the current DEM warn and fall back to automatic selection.
27
+ - `Altimetry` reuses the *exact* prior ICESat-2 points via `load_atl06sr_from_parquet()`, pins the profile track (`rgt`/`cycle`/`spot`) and best/worst segments (`segments=`) through `plot_atl06sr_dem_profile()` / `plot_best_worst_segments()`, and reports its choices via `get_altimetry_selections()`. A single run now also resolves the best track once and shares it across the profile and segment figures for self-consistency.
28
+ - Best/worst segments are pinned by **absolute along-track distance (`x_atc`)** rather than km-from-track-start, so a reused segment lands on the same ground even when outlier (3σ) filtering against a different DEM drops a different first point and shifts the track start. (Manifests keep the km extents for readability and still accept the legacy km-only form.)
29
+ - The reuse path restores the request's date range (`t0`/`t1`) from the parquet's stored SlideRule parameters, so plot titles keep their "&lt;t0&gt; to &lt;t1&gt;" line when points are loaded from cache instead of re-requested.
30
+
8
31
  ## [1.15.2] - 2026-06-11
9
32
 
10
33
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asp_plot
3
- Version: 1.15.2
3
+ Version: 1.17.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
@@ -87,3 +87,5 @@ See the [CLI documentation](https://asp-plot.readthedocs.io/en/latest/cli/index.
87
87
  ## Contributing
88
88
 
89
89
  See the [contributing guide](https://asp-plot.readthedocs.io/en/latest/contributing.html) for development setup, testing, and release process.
90
+
91
+ [`AGENTS.md`](AGENTS.md) is a module-by-module map of the codebase and its design patterns — a useful orientation for contributors, and the onboarding reference auto-discovered by AI coding agents (Claude Code and others).
@@ -43,3 +43,5 @@ See the [CLI documentation](https://asp-plot.readthedocs.io/en/latest/cli/index.
43
43
  ## Contributing
44
44
 
45
45
  See the [contributing guide](https://asp-plot.readthedocs.io/en/latest/contributing.html) for development setup, testing, and release process.
46
+
47
+ [`AGENTS.md`](AGENTS.md) is a module-by-module map of the codebase and its design patterns — a useful orientation for contributors, and the onboarding reference auto-discovered by AI coding agents (Claude Code and others).