asp-plot 3.0.0__tar.gz → 3.2.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 (60) hide show
  1. {asp_plot-3.0.0 → asp_plot-3.2.0}/AGENTS.md +1 -1
  2. {asp_plot-3.0.0 → asp_plot-3.2.0}/ARCHITECTURE.md +13 -3
  3. {asp_plot-3.0.0 → asp_plot-3.2.0}/CHANGELOG.md +16 -0
  4. {asp_plot-3.0.0 → asp_plot-3.2.0}/PKG-INFO +2 -2
  5. {asp_plot-3.0.0 → asp_plot-3.2.0}/README.md +1 -1
  6. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/alignment.py +53 -0
  7. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/altimetry.py +8 -8
  8. asp_plot-3.2.0/asp_plot/bundle_adjust.py +1716 -0
  9. asp_plot-3.2.0/asp_plot/cli/bundle_adjust_cameras.py +93 -0
  10. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/report.py +125 -33
  11. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/report_captions.py +56 -20
  12. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/report_pipeline.py +84 -3
  13. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/stereo.py +114 -22
  14. {asp_plot-3.0.0 → asp_plot-3.2.0}/conda-forge-recipe/meta.yaml +2 -0
  15. {asp_plot-3.0.0 → asp_plot-3.2.0}/pyproject.toml +2 -1
  16. asp_plot-3.0.0/asp_plot/bundle_adjust.py +0 -643
  17. {asp_plot-3.0.0 → asp_plot-3.2.0}/.flake8 +0 -0
  18. {asp_plot-3.0.0 → asp_plot-3.2.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  19. {asp_plot-3.0.0 → asp_plot-3.2.0}/.github/ISSUE_TEMPLATE/problem-report.yml +0 -0
  20. {asp_plot-3.0.0 → asp_plot-3.2.0}/.github/workflows/claude-code-review.yml +0 -0
  21. {asp_plot-3.0.0 → asp_plot-3.2.0}/.github/workflows/claude.yml +0 -0
  22. {asp_plot-3.0.0 → asp_plot-3.2.0}/.github/workflows/release.yml +0 -0
  23. {asp_plot-3.0.0 → asp_plot-3.2.0}/.github/workflows/run-tests.yml +0 -0
  24. {asp_plot-3.0.0 → asp_plot-3.2.0}/.gitignore +0 -0
  25. {asp_plot-3.0.0 → asp_plot-3.2.0}/.pre-commit-config.yaml +0 -0
  26. {asp_plot-3.0.0 → asp_plot-3.2.0}/.readthedocs.yaml +0 -0
  27. {asp_plot-3.0.0 → asp_plot-3.2.0}/CLAUDE.md +0 -0
  28. {asp_plot-3.0.0 → asp_plot-3.2.0}/LICENSE +0 -0
  29. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/__init__.py +0 -0
  30. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/altimetry_plots.py +0 -0
  31. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/altimetry_source.py +0 -0
  32. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/asp_log.py +0 -0
  33. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/bodies.py +0 -0
  34. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/cli/__init__.py +0 -0
  35. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/cli/asp_report.py +0 -0
  36. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/cli/csm_camera_plot.py +0 -0
  37. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/cli/gallery.py +0 -0
  38. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/cli/request_planetary_altimetry.py +0 -0
  39. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/cli/stereo_geom.py +0 -0
  40. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/csm_analysis.py +0 -0
  41. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/csm_camera.py +0 -0
  42. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/csm_io.py +0 -0
  43. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/gallery.py +0 -0
  44. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/icesat2_source.py +0 -0
  45. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/mapproject.py +0 -0
  46. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/planetary_source.py +0 -0
  47. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/processing_parameters.py +0 -0
  48. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/scenes.py +0 -0
  49. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/selections.py +0 -0
  50. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/sensors/__init__.py +0 -0
  51. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/sensors/aster.py +0 -0
  52. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/sensors/base.py +0 -0
  53. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/sensors/dimap.py +0 -0
  54. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/sensors/dimap_v1.py +0 -0
  55. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/sensors/rpc.py +0 -0
  56. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/sensors/worldview.py +0 -0
  57. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/stereo_geometry.py +0 -0
  58. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/stereopair_metadata_parser.py +0 -0
  59. {asp_plot-3.0.0 → asp_plot-3.2.0}/asp_plot/utils.py +0 -0
  60. {asp_plot-3.0.0 → asp_plot-3.2.0}/environment.yml +0 -0
@@ -76,7 +76,7 @@ The rest is automated: `release.yml` detects the version bump, creates a GitHub
76
76
  ASP output files follow specific naming patterns (find them with the `glob_file()` utility):
77
77
  - DEMs: `*-DEM.tif` or `*_dem.tif`
78
78
  - Disparity: `*-F.tif`
79
- - Match files: `*.match`
79
+ - Match files: `*.match` (binary) or `*__*.txt` (plain text, from ASP >= 3.7.0 `--matches-as-txt`; binary preferred when both exist)
80
80
  - Bundle adjust residuals: `*-initial_residuals_pointmap.csv`, `*-final_residuals_pointmap.csv`
81
81
  - Log files: `*log-bundle_adjust*.txt`, `*log-stereo*.txt`, `*log-point2dem*.txt`
82
82
 
@@ -46,7 +46,7 @@ The package is organized by functionality, with each module focused on a specifi
46
46
 
47
47
  **`report.py`** - PDF report generation using fpdf2
48
48
  - `ReportSection`: Dataclass representing a report figure (title, image path, caption)
49
- - `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.
49
+ - `AlignmentReportPage`: Dataclass for the pc_align + altimetry alignment workflow (title, parameters dict, flat stats dict from `pc_align_report()`, 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. `_add_alignment_stats_tables()` splits the flat stats dict (`_split_alignment_stats()`) into two side-by-side tables: "Error Statistics (m)" with one row per `<stat>_beg`/`<stat>_end` pair and Before / After / Change columns (row labels via `_ALIGNMENT_STAT_LABELS`, Change from `_fmt_pct_change()`), and "Translation (m)" with one row per remaining key (`north_shift` `North`, `translation_magnitude` → `Magnitude |T|` via `_ALIGNMENT_TRANSLATION_LABELS`). Either table takes the full width when the other is empty.
50
50
  - `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.
51
51
  - `ASPReportPDF`: FPDF subclass with custom header/footer and page numbers
52
52
  - `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.
@@ -109,18 +109,22 @@ The package is organized by functionality, with each module focused on a specifi
109
109
  - Detects map-projection status via `Raster.transform` check
110
110
  - For non-mapprojected scenes: match points are overlaid on images using alignment transform matrices (`run-align-{L,R}.txt` loaded via `np.loadtxt`), and disparity plots use pixel-unit scalebar instead of GSD-based. Exception: when the match file is named for the aligned images themselves (`run-L__R.match`, older ASP raw-image runs that write `.exr` alignment matrices), the coordinates are already aligned — detected via `_ip_on_aligned_images()` (match stem's left name == L image stem) — and are only rescaled
111
111
  - **Raw interest point overlay** (issue #8): `StereoFiles` also discovers the per-image `.vwip` files (named after the images the match file pairs — `<A>__<B>.match` ↔ `<A>.vwip`/`<prefix>-<B>.vwip` — derived from the match stem, with a `*-L.vwip`/`*-R.vwip` glob fallback when no match file exists; either side may be absent). `get_vwip_df()` parses the binary format (a uint64 count then the same interest point records as `.match`); `plot_match_points()` underlays them in blue beneath the red matches through the same per-side coordinate transform (layers denser than 10,000 points are thinned by seeded random sampling for display; titles report true counts), renders interest-points-only when the match file is missing, and falls back to the previous matches-only figure when the `.vwip` files are gone
112
+ - **Plain-text match files** (issue #147): ASP >= 3.7.0 writes `<A>__<B>.txt` (one `x1 y1 unc1 x2 y2 unc2` line per match) *instead of* `.match` when run with `--matches-as-txt`, so `StereoFiles._find_match_file()` globs both `*.match` and `*__*.txt` (the `__` anchor plus a first-row check — six numeric fields, `_opens_with_match_row()` — keeps logs and alignment matrices out even when the run prefix itself contains `__`), preferring binary when both coexist. `get_match_point_df()` detects the format from the bytes (`_is_text_match_file()`: the binary header's uint64 counts contain NULs, text never does — so a renamed file still parses) and routes text through `_read_text_match_file()` — read directly, uncertainties dropped, never via the `.csv` cache a binary conversion leaves behind — while the binary path keeps its CSV conversion. `.vwip` files stay binary-only: stereo never writes text ones (`ipfind --binary-to-txt` is a manual conversion)
112
113
  - **Multi-view aware** (issue #160): `StereoFiles` resolves each `<prefix>-pairN/` subdirectory into a `PairStereoFiles` (field names mirror the top-level attributes so the per-figure helpers consume either, duck-typed); `plot_match_points()` / `plot_disparity()` render one figure per pair (`<stem>_pairN.png`) and return the saved filename list — `[fig_fn]` for a standard run — mirroring the `stereo_geom_plot()` N-scene contract
113
114
  - Detects the imagery rights-holder via the `attribution` attribute (`detect_satellite_attribution`); adds the copyright overlay to optical imagery in `plot_match_points()` and `plot_detailed_hillshade()`
114
115
  - `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.
115
116
  - Key methods: `plot_dem_results()`, `plot_disparity()`, `plot_match_points()`, `plot_detailed_hillshade()`
116
117
 
117
- **`bundle_adjust.py`** - Two main classes
118
+ **`bundle_adjust.py`** - Four main classes
118
119
  - `ReadBundleAdjustFiles`: Reads bundle adjustment CSV outputs (residual pointmaps)
119
120
  - `get_initial_final_residuals_gdfs()`: Returns initial and final residual GeoDataFrames
120
121
  - `get_initial_final_geodiff_gdfs()`: Returns geodiff comparison GeoDataFrames (requires `--mapproj-dem` flag in bundle_adjust)
121
122
  - `get_mapproj_residuals_gdf()`: Returns map-projected residual GeoDataFrame
122
123
  - `PlotBundleAdjustFiles` (inherits from `Plotter`): Visualizes bundle adjustment residuals before/after optimization
123
124
  - Plots include map views of residuals, histograms, and geodiff comparisons
125
+ - **Camera before/after position and orientation (issues #95, #43)** — shows where each camera *moved* in the solve, not just the ground residuals. Unlike `csm_camera.py` (which needs the user to pass original + optimized cameras), this is **self-contained on a bundle_adjust output folder**: the pre-BA cameras are not co-located there, and ASP writes no "unadjusted" state (see the AGENTS.md gotcha).
126
+ - `ReadBundleAdjustCameras`: discovery is driven by the `*.adjust` files (rigid ECEF translation `T` + rotation quaternion; matched as both `<base>.adjust` and `<base>.adjusted_state.adjust`, the ASTER jitter naming). Per ASP's `.adjust` convention (a world point projects the same in the original camera as `R*(P−C)+C+T` in the adjusted one, `C` = camera center for pixel (0,0)), `T` is the exact bulk camera-center shift at the anchor pixel; the rotation only adds a lever-arm shift for other lines. Each camera's absolute center is anchored at the **center image line** (sub-satellite point at mid-acquisition, more meaningful than the trajectory mean when the ephemeris is padded beyond the image; falls back to the mean if the timing can't be computed): from `*.adjusted_state.json` (CSM runs; `getTimeAtLine` + ephemeris interpolation) or, for DigitalGlobe runs that write only `.adjust` deltas, from the original camera `.xml` ephemeris (`<EPHEMLIST>` interpolated at `FIRSTLINETIME + (NUMROWS/2)/AVGLINERATE`), auto-found in the BA dir and its parent or via `original_cameras_directory`. The two paths agree to <1 m on the same scenes. `get_camera_optimization_gdf(map_crs, original_cameras_directory)` returns one row per camera: the translation in local ENU (`t_east/t_north/t_up`, `t_horizontal`), the adjustment `adj_roll/adj_pitch/adj_yaw`, and `horizontal_offset_m/vertical_offset_m` — from `camera_offsets.txt` when present (authoritative: folds in the rotation lever-arm; associated to cameras positionally by zipping with `camera_list.txt`, both written per input image in the same order), else derived from `T`; `offsets_from_asp` flags which. Cameras whose center cannot be located, or whose `.adjust`/state file fails to parse, are warned and skipped. `triangulation_offsets.txt` (ASP >= 3.6; per-image mean/median/count of the initial-vs-final triangulated-point distance) is read the same positional way into `tri_mean_m/tri_median_m/tri_count`, NaN when absent — both reports go through `_rows_by_camera_basename()`.
127
+ - `PlotBundleAdjustCameras` (inherits from `Plotter`): per-camera bar rows sharing the camera order — `plot_center_offset_bars()` (horizontal and vertical center change, m), `plot_orientation_bars()` (signed roll/pitch/yaw in degrees with the value printed on every bar; `_draw_satellite()` draws one satellite cartoon beside the row as the legend for the body axes and rotation sense — deliberately not scaled, the numbers carry the magnitude), and `plot_triangulation_offset_bars()` (median/mean triangulated-point change with the point count), which `summary_plot()` adds as a third row only when `has_triangulation_offsets`. An all-zero run (identity `--initial-transform`) still draws the panels, with a "no camera change (identity adjustment)" note overlaid; signed zeros print as `+0` (`_fmt_deg`). Earlier map-view/orientation quivers and the per-camera cartoon grid were dropped: sub-meter shifts can't be drawn to scale on a ~400 km map, and fixed-size arcs looked identical at 1e-7° and 0.016°.
124
128
 
125
129
  **CSM camera model comparison** — split into three layers by issue #131 (was one 1541-LOC `csm_camera.py`):
126
130
 
@@ -180,7 +184,7 @@ The package is organized by functionality, with each module focused on a specifi
180
184
  - `pc_align_dem_to_atl06sr()`: ICESat-2 path, csv-format `1:lon 2:lat 3:height_above_datum`
181
185
  - `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
182
186
  - 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
183
- - `pc_align_report()`: Extracts begin/end percentiles + N-E-D translation from the pc_align log
187
+ - `pc_align_report()`: Extracts begin/end percentiles, the ASP >= 3.7.0 Mean/StdDev/RMSE/Median/NMAD summary (keys absent for older logs), and the N-E-D translation from the pc_align log; the report page shows Median/NMAD/RMSE + translation when the log has them, else the percentiles (`report_pipeline._stats_row_from_result()`)
184
188
  - `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
185
189
  - Used by `Altimetry` class for DEM-to-altimetry alignment on Earth, Mars, and Moon
186
190
 
@@ -245,6 +249,12 @@ All CLI tools are in `asp_plot/cli/` and use Click for argument parsing:
245
249
  - Compares original and optimized CSM camera models
246
250
  - Visualizes position/angle differences and camera footprints
247
251
 
252
+ **`bundle_adjust_cameras.py`** - Self-contained camera before/after tool (`bundle_adjust_cameras` command)
253
+ - Wrapper for `ReadBundleAdjustCameras` + `PlotBundleAdjustCameras` in `bundle_adjust.py`
254
+ - `--directory` is the bundle_adjust output folder itself (split internally into the reader's root + subdirectory); no original cameras needed, except `--original-cameras-directory` for DigitalGlobe runs whose XMLs are in neither the BA folder nor its parent
255
+ - `--map-crs`, `--title`, `--output-directory` (defaults to the BA folder, so a bare call always writes a figure) / `--output-filename`
256
+ - Renders `summary_plot()`: center-offset bars, orientation bars (with the legend cartoon), and the triangulated-point row when the run wrote `triangulation_offsets.txt`
257
+
248
258
  **`request_planetary_altimetry.py`** - Planetary altimetry data request tool (`request_planetary_altimetry` command)
249
259
  - Submits async LOLA (Moon) or MOLA (Mars) queries to the ODE GDS REST API
250
260
  - Auto-detects planetary body from DEM CRS via `detect_planetary_body()`
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.2.0] - 2026-08-27
9
+
10
+ A new view of what `bundle_adjust` did to the cameras — read from the run's own output folder, so no original camera files are needed — as a `bundle_adjust_cameras` command, a page of the `asp_report` PDF, and four notebook examples. The figure was reviewed on ten local runs (two to fourteen cameras, ASP 3.4 through 3.8); the row that earned its place is the ground effect from `triangulation_offsets.txt`, which is what tells a real camera error apart from a position/orientation trade in the solve. No new dependencies; one new entry point, so the conda-forge feedstock recipe needs the `bundle_adjust_cameras` entry point and test command added by hand this release.
11
+
12
+ ### Added
13
+ - **A camera-change figure for `bundle_adjust` runs, self-contained on the run's output folder** (issues [#95](https://github.com/uw-cryo/asp_plot/issues/95) and [#43](https://github.com/uw-cryo/asp_plot/issues/43)). The bundle adjustment residual pages show how well the *tie points* reproject; the new figure shows what the solve did to the *cameras*: per-camera bars of the horizontal and vertical camera-center change (meters), per-camera bars of the roll / pitch / yaw orientation change (degrees, the value printed on every bar, with one unscaled satellite cartoon as the legend for the body axes), and — when ASP >= 3.6 wrote `triangulation_offsets.txt` — a third row of the median and mean change of each image's triangulated points, the effect of the camera change on the ground. Everything is read from the `bundle_adjust` folder itself: the per-camera `*.adjust` translation + rotation, `camera_offsets.txt` (ASP 3.7.0's "change in camera positions" report) and `triangulation_offsets.txt` associated to cameras positionally through `camera_list.txt`, and each camera's absolute position from its `*.adjusted_state.json` or, for DigitalGlobe runs that write only `.adjust` deltas, from the original `.xml` ephemeris. No original camera files are needed, unlike `csm_camera_plot`. New `ReadBundleAdjustCameras` and `PlotBundleAdjustCameras` in `bundle_adjust.py`, a new **`bundle_adjust_cameras` command** (`--directory` = the BA folder; `--map-crs`, `--original-cameras-directory`, `--title`, `--output-directory`, `--output-filename`), and a new **"Camera Changes from Bundle Adjustment" page in the `asp_report` PDF** right after the residual pages whenever `--bundle-adjust-prefix` is given (a prefix with a run stem narrows the files to that run, as for the residuals). A run that applied only an identity transform draws the panels with a "no camera change" note. Reviewed on ten local runs — two to fourteen cameras, ASP 3.4 through 3.8, CSM and DigitalGlobe cameras, ASTER, WorldView and Pléiades — which is also where the ground-change row came from: in the five-scene Atlanta solve the cameras moved 24–70 m while the ground points moved under 1 m, a position/orientation trade that the camera bars alone would misrepresent. The WorldView UCSD and Atlanta, Pléiades Marseille, and ASTER notebooks gain the figure with a write-up, the two committed WorldView reports gain the page, and the CLI is documented in `docs/cli/bundle_adjust_cameras.md`. The conda-forge recipe entry points and test commands are updated for the new command (the autotick bot does not sync those).
14
+
15
+ ## [3.1.0] - 2026-08-27
16
+
17
+ A compatibility release for two things ASP 3.7.0 changed in the files the report reads. `pc_align` now writes Mean/StdDev/RMSE/Median/NMAD error statistics to its log, and the alignment page shows Median, NMAD and RMSE before and after alignment in place of the 16/50/84 percentiles ([#146](https://github.com/uw-cryo/asp_plot/issues/146)); older logs keep the percentiles, with no version sniffing. And a `parallel_stereo`/`bundle_adjust` run made with `--matches-as-txt` writes plain-text match files instead of binary `.match`, which previously left the match-point page with a "missing match file" placeholder — both formats are now discovered and parsed into the same DataFrame ([#147](https://github.com/uw-cryo/asp_plot/issues/147)). No new dependencies and no entry-point changes.
18
+
19
+ ### Added
20
+ - **`pc_align_report()` parses the error statistics ASP 3.7.0 added to the `pc_align` log** (issue [#146](https://github.com/uw-cryo/asp_plot/issues/146)): the `Input stats (meters):` / `Output stats (meters):` lines become `mean_beg/end`, `stddev_beg/end`, `rmse_beg/end`, `median_beg/end`, `nmad_beg/end` alongside the existing percentiles and translation, and flow into `Altimetry.alignment_report_df`. The alignment report page (ICESat-2 and LOLA/MOLA) now shows `Median`, `NMAD` and `RMSE` before/after alignment in place of the 16/50/84 percentiles, with the column description updated; `mean`/`stddev` stay in the dataframe only. Logs from ASP < 3.7.0 parse exactly as before, without the new keys, and the page keeps showing the percentiles for them — no version sniffing, the absence of the new stats is the signal. The seven committed example reports in `reports/` are regenerated with the new page.
21
+ - **The `pc_align` log parser is now regression-tested** against real logs from both generations — the existing 2024-11 fixtures and a new ASP 3.8.0-alpha LOLA log (`tests/test_data/pc_align/pc_align_lola-log-pc_align.txt`) — which also confirmed the percentile and translation lines we key off are unchanged in 3.8.0.
22
+ - **Plain-text ASP match files are discovered and parsed** (issue [#147](https://github.com/uw-cryo/asp_plot/issues/147)). ASP 3.7.0 added a text match-file format (`<prefix>-<A>__<B>.txt`, one `x1 y1 unc1 x2 y2 unc2` line per match), and `parallel_stereo`/`bundle_adjust --matches-as-txt` write it *instead of* `.match` — so a stereo directory produced with that switch previously got the "missing match file" placeholder on the match-point page. `StereoFiles` now also looks for `*__*.txt` (anchored on the `__` image-name separator and required to open with a six-field match row, so logs and alignment matrices are never mistaken for it — even with a run prefix like `my__run`), `StereoPlotter.get_match_point_df()` reads either format into the same `x1/y1/x2/y2` DataFrame, and the `.vwip` interest-point pairing works off the text stem too. The format is detected from the file's bytes (the binary header has NUL bytes; text never does), not the extension, so a renamed file still parses; when both forms coexist the binary file is preferred, as before; text files are read directly, never through the `.csv` cache a binary conversion leaves behind. A text twin of the raw-image fixture, converted with `ipmatch --binary-to-txt` (ASP 3.8.0-alpha), is committed so the two readers are checked against each other. `.vwip` files remain binary-only — stereo never writes text ones.
23
+
8
24
  ## [3.0.0] - 2026-08-24
9
25
 
10
26
  A breaking standardization of the command-line interfaces ([#60](https://github.com/uw-cryo/asp_plot/issues/60)), done deliberately as a clean break — no aliases, no deprecation period — while the user base is small. Every multi-word option across the five CLIs (`asp_report`, `stereo_geom`, `csm_camera_plot`, `gallery`, `request_planetary_altimetry`) moves from underscores to the hyphenated style ASP itself uses (`--stereo_directory` → `--stereo-directory`), booleans become single switches for the non-default behavior (`--add_basemap False` → `--no-basemap`), and `--bundle_adjust_directory` becomes `--bundle-adjust-prefix`, matching both the name and the semantics of ASP's own option. The Python API is unchanged.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: asp_plot
3
- Version: 3.0.0
3
+ Version: 3.2.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
@@ -77,7 +77,7 @@ Generate a PDF report from an ASP processing directory:
77
77
  asp_report --directory ./ --stereo-directory stereo
78
78
  ```
79
79
 
80
- See the [CLI documentation](https://asp-plot.readthedocs.io/en/latest/cli/index.html) for all options and additional tools (`stereo_geom`, `csm_camera_plot`).
80
+ See the [CLI documentation](https://asp-plot.readthedocs.io/en/latest/cli/index.html) for all options and additional tools (`stereo_geom`, `csm_camera_plot`, `bundle_adjust_cameras`, `request_planetary_altimetry`, `gallery`).
81
81
 
82
82
  ## Examples
83
83
 
@@ -33,7 +33,7 @@ Generate a PDF report from an ASP processing directory:
33
33
  asp_report --directory ./ --stereo-directory stereo
34
34
  ```
35
35
 
36
- See the [CLI documentation](https://asp-plot.readthedocs.io/en/latest/cli/index.html) for all options and additional tools (`stereo_geom`, `csm_camera_plot`).
36
+ See the [CLI documentation](https://asp-plot.readthedocs.io/en/latest/cli/index.html) for all options and additional tools (`stereo_geom`, `csm_camera_plot`, `bundle_adjust_cameras`, `request_planetary_altimetry`, `gallery`).
37
37
 
38
38
  ## Examples
39
39
 
@@ -17,6 +17,43 @@ logging.basicConfig(level=logging.WARNING)
17
17
  logger = logging.getLogger(__name__)
18
18
 
19
19
 
20
+ # Labels ASP >= 3.7.0 prints on the "Input stats (meters):" / "Output stats
21
+ # (meters):" pc_align log lines, mapped to the keys pc_align_report() uses.
22
+ _PC_ALIGN_STATS_FIELDS = {
23
+ "Mean": "mean",
24
+ "StdDev": "stddev",
25
+ "RMSE": "rmse",
26
+ "Median": "median",
27
+ "NMAD": "nmad",
28
+ }
29
+
30
+
31
+ def _parse_pc_align_stats_line(line, suffix):
32
+ """Parse one ``Input/Output stats (meters):`` pc_align log line.
33
+
34
+ Parameters
35
+ ----------
36
+ line : str
37
+ e.g. ``"... Input stats (meters): Mean: 71.4632, StdDev: 134.758,
38
+ RMSE: 152.535, Median: 5.71217, NMAD: 3.20963"``
39
+ suffix : str
40
+ ``"beg"`` for the Input line, ``"end"`` for the Output line.
41
+
42
+ Returns
43
+ -------
44
+ dict
45
+ ``{"mean_<suffix>": float, "stddev_<suffix>": float, ...}`` for every
46
+ label found on the line; labels that are missing are left out rather
47
+ than raising, so a future change to the line degrades gracefully.
48
+ """
49
+ parsed = {}
50
+ for label, key in _PC_ALIGN_STATS_FIELDS.items():
51
+ match = re.search(rf"{label}: (-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)", line)
52
+ if match:
53
+ parsed[f"{key}_{suffix}"] = float(match.group(1))
54
+ return parsed
55
+
56
+
20
57
  class Alignment:
21
58
  """
22
59
  Perform DEM alignment using point cloud alignment techniques.
@@ -274,11 +311,22 @@ class Alignment:
274
311
  dict
275
312
  Dictionary containing alignment metrics:
276
313
  - p16_beg, p50_beg, p84_beg: Error percentiles before alignment
314
+ - mean_beg, stddev_beg, rmse_beg, median_beg, nmad_beg: Error
315
+ statistics before alignment (ASP >= 3.7.0 only; absent from
316
+ the dict when the log predates them)
277
317
  - p16_end, p50_end, p84_end: Error percentiles after alignment
318
+ - mean_end, stddev_end, rmse_end, median_end, nmad_end: Error
319
+ statistics after alignment (ASP >= 3.7.0 only)
278
320
  - north_shift, east_shift, down_shift: Translation vector components
279
321
  in North-East-Down (NED) coordinate frame, in meters
280
322
  - translation_magnitude: Magnitude of translation vector
281
323
 
324
+ All errors are the absolute point-to-point (or point-to-plane)
325
+ distances pc_align reports, in meters. ``median_*`` duplicates
326
+ ``p50_*``; ``nmad_*`` is the robust spread and ``rmse_*`` the
327
+ outlier-sensitive one, so a large RMSE alongside a small NMAD flags
328
+ a tail of gross errors rather than a broad misfit.
329
+
282
330
  Notes
283
331
  -----
284
332
  This method expects the log file to contain specific keyword patterns
@@ -317,6 +365,11 @@ class Alignment:
317
365
  "p84_end": float(values[2]),
318
366
  }
319
367
  report = report | percentile_dict
368
+ # ASP >= 3.7.0 adds a one-line summary after each percentile line
369
+ if "Input stats (meters):" in line:
370
+ report = report | _parse_pc_align_stats_line(line, "beg")
371
+ if "Output stats (meters):" in line:
372
+ report = report | _parse_pc_align_stats_line(line, "end")
320
373
  if "Translation vector (North-East-Down, meters):" in line:
321
374
  ned_shift = np.genfromtxt(
322
375
  [line.split("Vector3")[1][1:-2]], delimiter=","
@@ -59,10 +59,10 @@ class AlignmentResult:
59
59
  One of:
60
60
  - ``"insufficient_points"``: not enough ATL06-SR points for
61
61
  pc_align to run (the aligned DEM is removed if one was written).
62
- - ``"no_improvement"``: pc_align ran but p50 did not improve
62
+ - ``"no_improvement"``: pc_align ran but the median (p50) did not improve
63
63
  toward 0 by more than the ``improvement_threshold_pct``; the
64
64
  aligned DEM has been removed.
65
- - ``"success"``: p50 improved by more than the threshold; the
65
+ - ``"success"``: the median (p50) improved by more than the threshold; the
66
66
  aligned DEM is retained and ``Altimetry.aligned_dem_fn`` points
67
67
  to it.
68
68
  alignment_report_df : pandas.DataFrame
@@ -729,11 +729,11 @@ class Altimetry:
729
729
  reason = (
730
730
  f"Translation magnitude is below {min_translation_threshold*100:.0f}% "
731
731
  "of the DEM GSD, so no aligned DEM was written despite a "
732
- f"{improvement_repr} p50 reduction."
732
+ f"{improvement_repr} median reduction."
733
733
  )
734
734
  else:
735
735
  reason = (
736
- f"p50 {p50_beg:.2f} m -> {p50_end:.2f} m, "
736
+ f"median {p50_beg:.2f} m -> {p50_end:.2f} m, "
737
737
  f"{improvement_repr} <= {improvement_threshold_pct:.1f}% "
738
738
  "threshold. Aligned DEM removed."
739
739
  )
@@ -795,7 +795,7 @@ class Altimetry:
795
795
  ``--max-displacement`` for pc_align, in meters. Default 500
796
796
  (ASAP-Stereo's CTX cookbook recommendation).
797
797
  improvement_threshold_pct : float, optional
798
- Minimum p50 reduction (%) required to keep the aligned DEM.
798
+ Minimum median (p50) reduction (%) required to keep the aligned DEM.
799
799
  min_translation_threshold : float, optional
800
800
  Minimum translation magnitude as a fraction of the DEM GSD.
801
801
  minimum_points : int, optional
@@ -897,11 +897,11 @@ class Altimetry:
897
897
  reason = (
898
898
  f"Translation magnitude is below {min_translation_threshold*100:.0f}% "
899
899
  f"of the DEM GSD ({gsd:.2f} m), so no aligned DEM was "
900
- f"written despite a {improvement_repr} p50 reduction."
900
+ f"written despite a {improvement_repr} median reduction."
901
901
  )
902
902
  else:
903
903
  reason = (
904
- f"p50 {p50_beg:.2f} m -> {p50_end:.2f} m, "
904
+ f"median {p50_beg:.2f} m -> {p50_end:.2f} m, "
905
905
  f"{improvement_repr} <= {improvement_threshold_pct:.1f}% "
906
906
  "threshold."
907
907
  )
@@ -1034,7 +1034,7 @@ class Altimetry:
1034
1034
  aligned_dem_fn=self.aligned_dem_fn,
1035
1035
  improvement_pct=improvement_pct,
1036
1036
  message=(
1037
- f"p50 improved from {p50_beg:.2f} m -> {p50_end:.2f} m "
1037
+ f"Median improved from {p50_beg:.2f} m -> {p50_end:.2f} m "
1038
1038
  f"({improvement_pct:.1f}% reduction). Aligned DEM written to "
1039
1039
  f"{self.aligned_dem_fn}."
1040
1040
  ),