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.
- {asp_plot-1.15.2 → asp_plot-1.17.0}/.gitignore +5 -0
- asp_plot-1.17.0/AGENTS.md +444 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/CHANGELOG.md +23 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/PKG-INFO +3 -1
- {asp_plot-1.15.2 → asp_plot-1.17.0}/README.md +2 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/alignment.py +70 -43
- asp_plot-1.17.0/asp_plot/altimetry.py +1042 -0
- asp_plot-1.17.0/asp_plot/altimetry_plots.py +1369 -0
- asp_plot-1.17.0/asp_plot/altimetry_source.py +102 -0
- asp_plot-1.17.0/asp_plot/asp_log.py +276 -0
- asp_plot-1.17.0/asp_plot/bodies.py +124 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/bundle_adjust.py +2 -10
- asp_plot-1.17.0/asp_plot/cli/asp_plot.py +186 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/cli/stereo_geom.py +1 -1
- asp_plot-1.17.0/asp_plot/csm_analysis.py +242 -0
- asp_plot-1.17.0/asp_plot/csm_camera.py +511 -0
- asp_plot-1.17.0/asp_plot/csm_io.py +634 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/gallery.py +3 -33
- asp_plot-1.17.0/asp_plot/icesat2_source.py +1330 -0
- asp_plot-1.17.0/asp_plot/planetary_source.py +470 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/processing_parameters.py +97 -135
- asp_plot-1.17.0/asp_plot/report_captions.py +181 -0
- asp_plot-1.17.0/asp_plot/report_pipeline.py +986 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/scenes.py +84 -25
- asp_plot-1.17.0/asp_plot/selections.py +231 -0
- asp_plot-1.17.0/asp_plot/sensors.py +587 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/stereo.py +382 -182
- {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/stereo_geometry.py +20 -12
- {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/stereopair_metadata_parser.py +32 -390
- {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/utils.py +118 -29
- {asp_plot-1.15.2 → asp_plot-1.17.0}/pyproject.toml +1 -1
- asp_plot-1.15.2/asp_plot/altimetry.py +0 -3493
- asp_plot-1.15.2/asp_plot/cli/asp_plot.py +0 -917
- asp_plot-1.15.2/asp_plot/csm_camera.py +0 -1541
- {asp_plot-1.15.2 → asp_plot-1.17.0}/.flake8 +0 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/.github/workflows/release.yml +0 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/.github/workflows/run-tests.yml +0 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/.pre-commit-config.yaml +0 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/.readthedocs.yaml +0 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/LICENSE +0 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/__init__.py +0 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/cli/__init__.py +0 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/cli/csm_camera_plot.py +0 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/cli/gallery.py +0 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/cli/request_planetary_altimetry.py +0 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/asp_plot/report.py +0 -0
- {asp_plot-1.15.2 → asp_plot-1.17.0}/conda-forge-recipe/meta.yaml +0 -0
- {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 "<t0> to <t1>" 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.
|
|
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).
|