Changelog

Contents

Changelog#

Changelog#

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]#

Repository size work, part one (#201). A git clone of this repository is 1.8 GB against a ~275 MB checkout, and the largest single cause is the committed example reports: PDFs do not delta-compress, so every regeneration is a wholly new permanent object, and seven report files had become 116 of them and 846 MB of history. This stops that growing. It does not shrink what is already there — that needs a history rewrite, which is the other half of #201.

Added#

  • A cap on the resolution of figures embedded in PDF reports (asp_plot.report.FIGURE_MAX_DPI, asp_report --figure-max-dpi, 0 disables). Figures are plotted at 220 dpi and then scaled down to fit the page, so the resolution that actually lands in the PDF is set by the placed size, not the save dpi: a 24-inch-wide multi-panel figure squeezed into 186 mm of a Letter page arrives at ~400 dpi. Measuring the Atlanta MVS report found 21.3 of its 21.4 MB was image data and 13.6 MB of it sat at 300–500 dpi — detail no screen shows and no printer reproduces, stored in full. compile_report() now downsamples each figure to a ceiling applied at its placed width, defaulting to 200 dpi. This takes the seven example reports from 79.9 MB to 59.0 MB, and it is a change in output for every user: reports are smaller and figures are capped at 200 dpi unless --figure-max-dpi 0 is passed. 200 dpi rather than the ~150 dpi Ghostscript’s /ebook preset targets, because that preset re-encodes plots as JPEG and rings around axis lines and tick labels; resolution-only downsampling loses nothing the page can show. PIL is already a dependency and fpdf2’s image() takes a PIL.Image, so this adds no dependency and writes no temp files.

  • docs/fetch_example_reports.sh, which downloads the example reports into docs/_static/reports/ at docs-build time. It pins one REPORTS_RELEASE tag, so a docs build of an older commit fetches the reports of its era, and uses curl -f so a missing or renamed asset fails the build loudly instead of shipping a broken iframe.

Changed#

  • The example reports are no longer committed to git. reports/*.pdf is gitignored and git rm --cacheded; the reports are published as assets on a dated reports-<date> GitHub Release in this repository, together with the *_figure_selections.yml sidecars that generated them. A dated tag rather than the per-version releases, because release.yml creates a release per pyproject.toml bump and the reports do not change per version. .readthedocs.yaml’s cp reports/*.pdf step becomes bash docs/fetch_example_reports.sh; the reports page, the report links in the notebooks, and the local docs recipe in AGENTS.md all work as before, because the build still self-hosts the PDFs under _static/reports/ — GitHub serves release assets as content-disposition: attachment, which an <iframe> cannot render. Nothing about generating a report locally changes. Publishing a regenerated set is documented in AGENTS.md; note that the release must be published before the commit that points at it.

Fixed#

  • release.yml no longer picks a reports-<date> tag as the previous version. Its previous-tag step was git describe --tags --abbrev=0, which returns the most recent tag reachable from HEAD — and the reports releases are tagged on main, so the next version release would have linked a changelog comparing reports-2026-09-18...v3.4.0. Now --match 'v*'. The tag-exists check greps for v<version> and was never affected.

  • StereoFiles no longer crashes on a stereo directory with no top-level *-L.tif (#202). The constructor passed the result of glob_file() straight to Raster() to decide whether the run was mapprojected, so a missing left image raised TypeError: expected str, bytes or os.PathLike object, not NoneType instead of falling back to the missing-file placeholders the plots already draw. A missing left image now means not mapprojected. This happens in trimmed example directories (it blocked regenerating the Atlanta MVS report) and in multi-view runs, which keep their aligned images in <prefix>-pairN/; in that layout the lookup is now quiet, like its neighbours.

  • The match-point and disparity figures fall back to *-lMask.tif when *-L.tif and *-D.tif have been deleted (#202). Both figures read the full-resolution file only for its size (or GSD, for mapprojected runs) to rescale onto the sub-sampled images, and showed the missing-files placeholder without it. ASP writes the left mask on the same grid and with the same georeferencing as the left image, and it is much smaller, so it tends to survive when disk space is reclaimed. The mask is also used to decide whether the run is mapprojected. The Atlanta MVS example directory had lost L.tif and D.tif in every pair, and its per-pair figures render again without re-running stereo.

[3.3.0] - 2026-09-18#

The first piece of the scene-combination benchmark (#169): a way to score many DEMs against one altimetry sample. The report assesses a single DEM; the question of which scene combination, which processing flow (joint multi-view triangulation vs. pairwise stereo merged with dem_mosaic), or which parameter setting gives the best DEM needs every candidate scored against exactly the same points, side by side. The Atlanta MVS notebook had been doing that with an ad hoc loop; it is now a class, a figure, and a dem_benchmark command, and the notebook uses them to score six DEMs at once, where the single pairs explain the mosaic’s result (the 5° pair lowers its accuracy) and the five-scene run has the lowest bias but not the lowest spread. No new dependencies; one new entry point, so the conda-forge feedstock recipe needs dem_benchmark added by hand this release.

Added#

  • DEMBenchmark: score any number of DEMs against one ICESat-2 or LOLA/MOLA sample (issue #169). asp_plot/dem_benchmark.py takes {label: dem_fn} plus the ATL06-SR parquet cache a report wrote (or a planetary CSV) and scores every DEM with the report’s own recipe — replayed points, ESA WorldCover water filter, 3σ outlier cut — into a one-row-per-DEM table (stats_df, columns STATS_COLUMNS): coverage (valid_pct, valid_area_km2) inside a common area of interest (by default the intersection of all the DEM footprints, so runs with different crop windows compare fairly), the median and NMAD of the *-IntersectionErr.tif point2dem writes next to each *-DEM.tif (NaN for a mosaic, which has none), altimetry-minus-DEM n / median / NMAD / RMSE before and after a per-DEM pc_align --compute-translation-only with the translation it applied, and optionally each DEM’s difference against one candidate named as the reference. summary_plot() draws the table as one row per DEM sorted best-first by post-alignment NMAD — coverage bars, IntersectionErr bars, and before→after dumbbells for the residual median and NMAD (a translation cannot change NMAD, so that panel separates bias, which alignment removes, from noise, which it cannot); histogram_plot() overlays the residual distributions. One Altimetry per DEM is kept in bench.altimetry[label], so the usual per-DEM figures (histogram_by_landcover(), mapview_plot_atl06sr_to_dem()) can be drawn for any candidate. pc_align products and the translated DEM copies go under <directory>/dem_benchmark/<label>/, never into the candidates’ own folders, and are reused on a re-run, which therefore makes no pc_align call and works offline; a missing pc_align binary degrades to pre-alignment scoring with a warning instead of failing. Large DEMs are read downsampled for the coverage statistics (capped at ~16 M samples per window).

  • A dem_benchmark command wrapping it: positional DEMs as paths or LABEL=PATH (an ASP run-DEM.tif is labelled by its folder), --parquet for Earth or --altimetry-csv for the Moon/Mars, --reference, --no-pc-align, --own-extent, --title, and --directory / --output-directory / --output-filename; writes the summary figure, a _histogram.png twin, and the stats table as CSV, and prints the table. Documented in docs/cli/dem_benchmark.md and added to the CLI index (seven tools). The conda-forge recipe gains the entry point and its --help test command (the autotick bot does not sync those).

  • The Atlanta MVS notebook scores six DEMs on one ICESat-2 sample with DEMBenchmark in place of its ad hoc two-DEM loop: the 3-scene MVS run, the 3-pair dem_mosaic, each of the three single pairs, and the 5-scene MVS run. The single pairs explain the mosaic: the 26.9° and 21.8° pairs score NMAD 0.64 and 0.72 m, the 5.1° pair 1.54 m, and dem_mosaic averages all three to 1.02 m — below either good pair alone — while the 3-scene MVS run’s 0.71 m matches the best pairs and its bias is lower. The 5-scene run has the smallest bias (+0.12 m) but not the smallest spread (0.68 m), and its five-ray IntersectionErr median (0.19 m) reads higher than any pair’s, so the notebook now explains why a narrow pair’s small triangulation error is not a quality ranking. After pc_align every median lands at +0.16–0.27 m and no NMAD moves. The full matrix of scene combinations and flows is the benchmark notebook below.

  • The Atlanta scene-combination benchmark: notebooks/WorldView/worldview_spacenet_benchmark.ipynb (issue #169). The systematic version of the MVS notebook’s comparison: all ten pairs among the five same-pass scenes (5–32° convergence), five dem_mosaic blends of them (the three pairs sharing the reference, all ten with the default average, only the six above 15°, all ten by median, all ten weighted by convergence angle, and all ten weighted by propagated VerticalStdDev — the ASP manual’s own sfm_multiview recipe, which needs every pair re-triangulated with --propagate-errors) and six multi-view runs (3, 3-wide, 4 and 5 scenes, plus the five scenes again with a different reference), twenty-one DEMs scored with DEMBenchmark on one ICESat-2 sample. The runs took about 20 h and ~150 GB on a laptop over two sessions. What it found: single-pair NMAD falls monotonically with convergence angle, 1.5–1.65 m at 5° to 0.60 m at 32°, and IntersectionErr does not reflect it; adding scenes to a joint triangulation changes NMAD little (0.70 → 0.70 → 0.65 → 0.66 m along the nested chain) but does remove bias; and pairwise + mosaic beats multi-view only when the weak pairs are excluded — every pair merged scores 0.99 m against MVS 5’s 0.66 m, weighting by convergence or by propagated uncertainty only reaches 0.80–0.83 m, the median 0.67 m, and the six pairs above 15° 0.62 m. The mechanism, from ASP’s source at the commit that built the DEMs (written up on the issue): a multi-view run is N−1 pairs of the first image with each other image — never scene-to-scene — followed by an unweighted least-squares ray intersection, so its quality is that of the reference’s “star” of pairs. Referenced on the middle scene the star is 5.1°, 10.5°, 16.3° and 21.8°; referenced on the scene at the end of the pass it is 5.5°, 21.8°, 26.9° and 32.3°, and that run scores 0.58 m, the lowest NMAD of the twenty-one. The notebook’s docs card and toctree entry are added.

  • A second site for the benchmark, in the same notebook: SpaceNet UCSD WorldView-3, multi-date, over Mount Soledad (issue #169). Chosen to differ from Atlanta in every respect its caveats list: the archive’s 35 scenes are 35 separate collects, so any N-scene run is multi-date stereo, the regime the ASP manual discourages multi-view for; the 3 × 3 km crop has 244 m of relief, 30 % of its area steeper than 15°, 45 % built-up and 45 % tree cover. Five winter scenes (sun 30–40°, 1.1° to 24° off-nadir, pairs 8–34°), processed as three five-scene multi-view runs (referenced on the nadir, the 24° and the 8° scene), all ten pairs, and four dem_mosaic blends: seventeen DEMs at 1.2 m, 16 h on a laptop. The crop and scene selection are done in the notebook from metadata (Copernicus relief, WorldCover, ICESat-2 track density, pairwise convergence from the XMLs). What it found, with the caveat that a paired track bootstrap (#199) separates only the worst DEM from the other sixteen at this sample size: the point estimates do not reproduce the Atlanta convergence curve — the 8° pair (1.47 m) ties the best 25° pair (1.48 m) and the four pairs with the Nov 9 nadir scene score worst at every convergence, which suggests matching rather than geometry limits a pair in steep multi-date urban terrain; the narrowest reference star scores best rather than the widest (1.48 m nadir against 1.61 m for the 24° reference); and the joint triangulation ties the best pair while every blend of the ten pairs lands at 1.64–1.70 m. Exclusion by convergence angle, which worked at Atlanta, would drop the best-scoring pair here. The notebook ends with the two sites on the same axes and the practical rules revised to those consistent with both sites. Two asp_plot fixes came out of scoring it (below).

  • asp_plot.stereo.read_match_file(fn): read an ASP match file (binary or --matches-as-txt) into a DataFrame without a stereo directory. StereoPlotter.get_match_point_df() delegates to it; it exists because the benchmark notebook derives crop windows from bundle_adjust match files, and a StereoPlotter cannot be built once a run’s L.tif has been cleaned up.

Changed#

  • Alignment.apply_dem_translation() accepts an output_fn for where to write the translated DEM (default unchanged: next to the source DEM), which is how the benchmark keeps candidate folders untouched.

  • The altimetry-minus-DEM outlier cut first drops gross outliers, then applies the 3σ cut as before (AltimetrySource._outlier_mask, used by atl06sr_to_dem_dh(), planetary_to_dem_dh() and filter_outliers(), and so by every report and by DEMBenchmark). Points more than 30 normalized median absolute deviations from the median are removed before the mean/std statistics are computed. Motivation: over the UCSD site one whole ICESat-2 pass is a marine-layer cloud return 150–200 m above the ground — a fifth of the sample — and the mean/std cut, its std inflated to ~70 m by that cluster, removed nothing, leaving every DEM with an RMSE near 85 m and an NMAD a metre too high. A pure median/NMAD cut was tried first and rejected: 3 NMAD is far tighter than 3σ on the heavy-tailed residuals a DSM has against ICESat-2 in a city (it removed a fifth of the Atlanta sample and cut every NMAD there by a third, from 0.68 to 0.48 m for the five-scene run), and those tails are DEM error, not blunders. The gross gate touches only a few dozen of the ~7000 Atlanta points, so existing numbers move by at most 0.02 m, while the cloud pass at 65–75 NMAD is gone. n_sigma keeps its name and default. The benchmark notebook was re-executed with the gate (its Atlanta numbers moved by 0.01–0.02 m, no ranking changed); the earlier MVS notebook keeps its pre-gate numbers.

Fixed#

  • Aligned-DEM residuals are now computed on exactly the points the unaligned DEM was scored on. pc_align’s translation moves a DEM’s holes and edges by a few metres, so a point that sampled NaN on the unaligned DEM — and therefore passed the outlier cut untested — could land on valid data in the translated copy and enter the post-alignment residuals unfiltered. Over UCSD that let six cloud returns sitting on a DEM hole into the aligned statistics as 150–210 m errors, lifting the post-alignment RMSE of some candidates from 2 m to 7–19 m while their NMAD barely moved. atl06sr_to_dem_dh() and planetary_to_dem_dh() now blank the aligned residual wherever the unaligned one is NaN, which also holds the report’s pre/post-alignment plots to one sample.

[3.2.0] - 2026-08-27#

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.

Added#

  • A camera-change figure for bundle_adjust runs, self-contained on the run’s output folder (issues #95 and #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).

[3.1.0] - 2026-08-27#

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); 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). No new dependencies and no entry-point changes.

Added#

  • pc_align_report() parses the error statistics ASP 3.7.0 added to the pc_align log (issue #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.

  • 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.

  • Plain-text ASP match files are discovered and parsed (issue #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.

[3.0.0] - 2026-08-24#

A breaking standardization of the command-line interfaces (#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.

A major version because every existing asp_report/stereo_geom/csm_camera_plot/gallery invocation with a multi-word or boolean option needs editing. Upgrade: re-spell the flags per the entries below — the --help of each command lists the new names, and the committed example reports and notebooks show them in use. No new dependencies and no entry-point changes.

Changed#

  • All CLI options are hyphenated (issue #60). One-for-one renames: --stereo-directory, --dem-filename, --dem-gsd, --map-crs, --reference-dem, --altimetry-csv, --subset-km, --atl06sr-time-range, --reuse-selections, --report-filename, --report-title (asp_report); --output-directory, --output-filename (stereo_geom, gallery); --max-filesize-mb (gallery); --original-cameras, --optimized-cameras, --map-crs, --upper-magnitude-percentile (csm_camera_plot). Single-word options are untouched. The command recorded on the report’s final page is emitted with the new spellings, so it stays re-runnable.

  • Boolean options are now single switches for the non-default behavior, in the Unix idiom: where the default is on, only the negative switch exists — --no-basemap, --no-altimetry, --no-pc-align, --no-geometry (asp_report), --no-basemap (stereo_geom), --no-trim (csm_camera_plot), --no-hillshade (gallery, replacing the --hillshade/--no-hillshade pair) — and where the default is off, only the positive one — --shared-scales, --log-scale-positions, --log-scale-angles, --add-basemap (csm_camera_plot). Where you passed --add_basemap False, pass --no-basemap; the verb-y plot/add prefixes are dropped from the switch names.

  • --bundle_adjust_directory is now --bundle-adjust-prefix, and accepts either the containing directory (ba, the previous behavior) or the same ASP-style output prefix passed to stereo/mapproject (ba/run). A prefix narrows the residuals/log file search to that run’s outputs, so several bundle-adjust runs can share a directory. Internally, ReportConfig.bundle_adjust_directory is renamed to bundle_adjust_prefix.

  • csm_camera_plot options are harmonized with the other CLIs and ASP’s orbit_plot.py: --save_dir → --output-directory, --fig_fn → --output-filename, --figsize → --figure-size. The csm_camera_summary_plot() Python keyword arguments are unchanged.

  • Trailing slashes on directory options are stripped centrally (--directory, --stereo-directory, --bundle-adjust-prefix), closing the last thread of issue #60; the dg_mosaic r100 concern from the same issue was resolved earlier by the sensor readers, which treat *.r100.xml/*.r50.xml as regenerable intermediates.

Removed#

  • The deprecated --plot_icesat alias (deprecated in favor of --plot_altimetry in 1.10.0) is gone; altimetry is on by default and --no-altimetry disables it.

[2.2.0] - 2026-08-18#

A correctness release for the CSM camera comparison, and a new diagnostic layer under the match points.

csm_camera_summary_plot() measured each camera’s roll/pitch/yaw against a satellite body frame estimated separately for that camera — correct when plotting one camera, which is what ASP’s orbit_plot.py does, but wrong for a difference between two (#53). bundle_adjust and jitter_solve both resample the ephemeris finer, and over the ~140 m central-difference baseline that leaves, a 2 m position change tilts the frame by ~0.8° — swamping the orientation change being plotted. Both cameras now share one frame, estimated from the original ephemeris, and the committed Salar de Uyuni pair’s reported pitch change drops from 0.73° (ranging −3.4° to +3.6°) to 2.6e-06°, which finally says what that run did: it moved positions by ±2 m and left the orientations alone. The figure that looked full of outliers was the frame moving, not the data.

The same figure gains a bundle_adjust counterpart to that jitter_solve example, in the UCSD WorldView notebook, so the two solvers’ signatures can be compared directly — and putting a second, much smaller correction next to the first exposed two readability bugs: camera 2 was drawn on camera 1’s colorbar limits, and panel labels overprinted matplotlib’s axis offset text.

Separately, the match point figure now underlays ASP’s raw per-image interest points (.vwip) beneath the matches (#8), so a sparse match set can be traced to poor matching versus nothing detected to match; and reconstructed mapproject commands re-run grid-identically on ASP >= 3.7.0 instead of drifting a pixel east per run (#148).

A minor version: everything is additive, with no API changes and no new dependencies.

Added#

  • A bundle_adjust camera-comparison example in the UCSD WorldView notebook. The only committed csm_camera_summary_plot() example was a jitter_solve run, whose per-segment corrections oscillate along the image. notebooks/WorldView/worldview_spacenet_ucsd_stereo.ipynb now also compares the original and adjusted cameras from its own bundle_adjust run, which solves one rigid translation and rotation per camera and so produces the opposite signature: smooth, nearly flat panels with a sub-metre position offset (0.4 m north on camera 1, 0.9 m up on camera 2, matching ba/run-camera_offsets.txt) and a constant orientation change of ~1e-4°. Having both makes the two solvers’ signatures directly comparable, and the notebook says what it means if either shows up looking like the other. Because bundle_adjust writes CSM state only for the optimized cameras, the notebook also documents how to produce the unadjusted one: re-run with a 4x4 identity --initial-transform and --apply-initial-transform-only.

  • Raw per-image interest points (.vwip) are overlaid on the match point figure (#8). When the .vwip files ASP writes during interest point matching are present in the stereo directory (including per-pair in multi-view runs), the match point figure underlays them in blue beneath the red matches, with per-image counts in the panel titles — so sparse matches can be traced to either poor matching or areas with no detected interest points at all (limited texture, clouds, water). If matching failed outright and there is no match file, the raw interest points are still shown on their own. Either side’s file may be absent (they are intermediates some runs clean up), in which case the figure degrades to exactly what it showed before. Layers denser than 10,000 points are thinned by seeded random sampling for display — dense runs (~100k interest points) would otherwise saturate the panels into solid color and bloat the figure — while the panel titles always report the true counts.

Fixed#

  • Each camera’s map colorbar now covers that camera’s own range. csm_camera_summary_plot() computed colorbar limits from camera 1 and imposed them on camera 2. Two cameras in one solver run routinely differ by more than their own spread — in the new UCSD example bundle_adjust moves one camera 0.432–0.454 m and the other 0.885–0.917 m — so camera 2’s entire track rendered as a single saturated color with no visible spatial pattern. Limits are now per camera; passing shared_scales=True (which already unified the line-panel y-axes) additionally puts both cameras on one common colorbar spanning the union of their ranges, for when comparing magnitudes directly is the point.

  • Panel labels and axis offset text no longer overprint each other. matplotlib parks a y-axis scale/offset label in each top corner of an axes, and the angle panels have two of them — the left axis’s and the twin axis’s — so the right-aligned "Camera N" title rendered on top of the twin’s (+1.669e2), producing unreadable overstruck glyphs. The label is now centered. Separately, over a narrow range of small values a colorbar emits a combined scale-and-offset string (1e-8+1.786e-4) wide enough to overlap the neighbouring map panel’s northing offset (1e6); the colorbars now drop the additive offset, leaving a short multiplier (1e-4) and moving the significant digits into the tick labels, where they are easier to read anyway.

  • CSM camera angle differences no longer report a rotation the camera never underwent (#53). csm_camera_summary_plot() compared roll/pitch/yaw computed against a satellite body frame estimated separately for each camera, as ASP’s orbit_plot.py does — a central difference of that camera’s own ephemeris. That is fine for plotting one camera, but it corrupts a difference between two: bundle_adjust and jitter_solve both perturb the positions and resample the ephemeris to a finer spacing, and at WorldView’s ~7 km/s a 0.01 s spacing leaves only a ~140 m central-difference baseline, so a 2 m radial perturbation tilts the estimated frame by ~0.8° — orders of magnitude more than the orientation change being measured, landing almost entirely in pitch. Both cameras are now expressed in one frame, estimated from the original (unperturbed) ephemeris and resampled onto the optimized camera’s sample grid, so the plotted difference is the true relative rotation between the two camera models. On the committed Salar de Uyuni jitter_solve pair this drops the reported pitch change from 0.73° ± (range −3.4° to +3.6°) to 2.6e-06°, revealing what that run actually did: it moved the camera positions by roughly ±2 m and left the orientations untouched. This is what made the example figure look like it was full of outliers.

  • Angle differences are wrapped at ±180° (#53). Euler angles are recovered on a branch cut, so a camera pointing near ±180° in yaw — a backward-looking sensor such as ASTER’s 3B band — had samples straddling the cut reported as ~360° changes instead of the fraction of a degree they really were. Original angle series are also unwrapped before being resampled onto the optimized camera’s grid, so a series crossing the cut is no longer interpolated through zero.

  • The match point figure no longer raises for raw-image runs whose interest points were found on the aligned images (#8). Older ASP versions name the match file for the aligned images (run-L__R.match) and write the alignment matrices as .exr rather than .txt; those match coordinates are already in aligned space, so plotting now detects this from the match filename (left name == the L image) and just rescales instead of demanding *-align-{L,R}.txt and failing with FileNotFoundError.

  • Reconstructed mapproject commands now re-run grid-identically on ASP >= 3.7.0 (#148). The --t_projwin reconstructed from an output GeoTIFF’s bounds did not round-trip: ASP snaps a given projwin by converting pixel edges to centers and rounding to the nearest grid multiple, and the bounds GDAL reports for an ASP output (after its PixelIsPoint half-pixel shift) land exactly on the rounding tie — re-running the reconstructed command drifted the grid one pixel east per run (and at fractional grid sizes, float noise could grow/shrink the raster by a pixel per edge). The reconstruction now emits ASP’s own pixel-edge box — the bounds shifted half a pixel NW (x − tr/2, y + tr/2) — which survives ASP’s snap unchanged; re-runs were verified bit-identical (grid, extent, and pixel values) at both whole and fractional grid sizes. On pre-3.7.0 ASP, which subtracted one grid size from the projwin maximum, no projwin choice can round-trip; the report’s explanatory note now states the version assumption.

[2.1.0] - 2026-07-30#

asp_plot now reads the same satellite camera metadata the Stereo Pipeline itself does (#168). Where 2.0.0 added Airbus Pléiades, this release finishes the job: the rest of the DIMAP v2 family (Pléiades 1A/1B attitude, SPOT 6/7, PeruSat-1), DIMAP v1 (SPOT 5, ALOS PRISM), ASTER, and RPC-only products (Cartosat-1, Deimos, anything ASP runs with -t rpc). The one gap left is ASP’s pinhole/opticalbar sessions — historical aerial and declassified film, which carry no satellite geometry to plot.

Two of those readers derive their geometry rather than parsing it, because their camera files record none: ASTER writes only look vectors, and an RPC-only product is nothing but a camera model. Both are validated against published or vendor-reported geometry to a tenth of a degree.

Everything is additive — no API changes, and no new dependencies.

Added#

  • RPC-only stereo-geometry support (Cartosat-1, Deimos, anything ASP runs with -t rpc), derived from the camera model itself (#177, #168). These products ship no camera file at all — just rational polynomial coefficients inside the image (or a *_RPC.TXT sidecar, including Cartosat-1’s *_RPC_ORG.TXT variant, which GDAL does not pick up on its own and sensors/rpc.py therefore parses directly). An RPC is still a camera model, so the geometry is derived from it: projecting a pixel to the ground at two heights traces its look ray, which gives the footprint (the image border projected at HEIGHT_OFF), the satellite azimuth/elevation at the ground point, and the GSD; intersecting the look rays from opposite ends of one image line recovers the satellite’s perspective centre, which gives the off-nadir angle and a line-indexed position track. RPC-only runs therefore get a full Stereo Geometry section in the report and a working stereo_geom — pointed at the images rather than at XMLs. The derivation is validated against vendor truth rather than asserted: rewriting the RPC00B coefficients from the committed WorldView camera XMLs into bare image containers reproduces those scenes’ own MEANSATAZ to 0.01°, MEANSATEL and MEANOFFNADIRVIEWANGLE to 0.15°, MEANPRODUCTGSD to 1 cm, their footprints to an IoU above 0.96, and the pair’s convergence angle to 0.02° (42.84° vs 42.82°). What RPCs genuinely do not record stays “not provided”: att_df is None, sun angles and cloud cover are NaN, date comes from the image header (NITF IDATIM, TIFF DateTime) or is absent, and the in-track/cross-track split of the off-nadir angle is left NaN because the only velocity direction available — the drift of the recovered positions over the ~15 km of track one scene spans — is 8–10° off. Since every WorldView and Pléiades delivery also ships images carrying RPCs, this reader is registered as a fallback: it is consulted only after every XML-based reader has declined the input at every search depth.

  • ASTER stereo-geometry support, derived from look vectors (#175, #168). ASP’s gen_aster camera XML records no timestamps, attitude, view/sun angles, or footprint corners — so sensors/aster.py is the first reader that derives its scene dict instead of parsing it. Intersecting each WORLD_SIGHT_VECTOR look ray (from its SAT_POS, one position per lattice line) with the WGS84 ellipsoid yields a ground lattice, from which the footprint (traced around the image border, not the lattice extent, which overshoots it by ~14% in area), the satellite azimuth/elevation at the ground point, the off-nadir/in-track/cross-track angles at the spacecraft, and the ground sample distance all follow. ASTER runs therefore get a full Stereo Geometry section in the report and a working stereo_geom, including the skyplot and convergence angle. The derivation is validated against ASTER’s published geometry: the committed 3N/3B fixtures reproduce the 27.6° backward telescope pointing (as −27.6 in-track, positive being forward), ~15 m VNIR GSD, ~31° ground convergence and a 0.56 base-to-height ratio, with the footprint inside the camera file’s own RPC bounding box. What ASTER genuinely does not record stays “not provided”: att_df is None (the orientation and covariance panels now say so rather than raising), sun angles and cloud cover are NaN, eph_gdf is indexed by image line rather than time, and date is recovered from a neighbouring AST_L1A_* granule name when one is present — which means both bands of a pair report the same acquisition time even though the backward look trails the nadir one by roughly a minute. Both committed ASTER example reports are regenerated with the new section (their --plot_geometry False existed only because there was no reader), each gaining one page.

  • SPOT 5 and ALOS PRISM stereo-geometry support (#179, #168). A new sensors/dimap_v1.py adds the two DIMAP v1-family readers ASP supports — Spot5Metadata (mirroring SPOT_XML.cc, the spot5 session) and PrismMetadata (mirroring PRISM_XML.cc, gated on METADATA_PROFILE == "ALOS" exactly as ASP is) — sharing a base for what the formats have in common: the Metadata_Id header, Dataset_Frame corner footprints, and Ephemeris/Points/Point trajectories. Neither format reports quaternions, so att_df grows a second shape: time-indexed roll/pitch/yaw in degrees (converted from radians for SPOT 5) alongside the existing scalar-last q1..q4, with attrs["rpy_frame"] naming the frame the angles are defined in. StereoGeometryPlotter._orientation_series() dispatches on which columns are present — quaternions are still converted to roll/pitch/yaw against the orbital frame, vendor angles are plotted as delivered — and the panel title names the frame either way, because the two are not interchangeable: PRISM’s angles share this package’s (along, across, down) orbital frame and Rz Ry Rx convention, while SPOT 5’s are in the SPOT Geometry Handbook navigation frame. DIMAP v1 carries no satellite azimuth, so the convergence angle and skyplot markers degrade to NaN and the skyplot now says why instead of rendering empty. Both readers are written from ASP’s reader spec with no real delivery available to validate against: parsing warns once per reader, the docs matrix marks them 🧪, and the synthetic fixtures in tests/test_data/dimap_v1_synthetic/ ship with the generator that wrote them.

  • The DIMAP reader covers the full Airbus family ASP supports: Pléiades 1A/1B attitude, SPOT 6/7, and PeruSat-1 (#161, #168). Pléiades 1A/1B products don’t tabulate attitude samples — each quaternion component is a degree-3 polynomial in scaled time (Polynomial_Quaternions, argument (t − (midnight + OFFSET)) / SCALE), which previously made the reader fail on 1A/1B attitude. The reader now evaluates the polynomials at the ephemeris timestamps — mirroring ASP’s own read_attitudes_1A1B (PleiadesXML.cc) and get_camera_pose_at_time (LinescanPleiadesModel.cc) — and normalizes the result, so 1A/1B scenes yield the same tabulated scalar-last att_df as every other sensor and the roll/pitch/yaw plots work unchanged. SPOT 6/7 (S6_SENSOR/S7_SENSOR) and PeruSat-1 (PER1_SENSOR, single Located_Geometric_Values block instead of nine) share the DIMAP v2 layout and are now accepted by the profile gate. Because these three profiles are implemented from the ASP reader spec rather than validated against real deliveries, parsing one emits a one-time warning asking for issue reports, and the docs support matrix records the distinction (🧪 vs ✅).

Changed#

  • Scene dicts may now omit eph_gdf entirely, and the plots handle it (#177). ASTER established att_df = None for sensors that record no attitude; RPC-only products go one step further, since a camera model whose look rays do not converge yields no satellite position at all. The pair map, the multi-view overview map and the position/orientation panel now read eph_gdf with .get() and fall back to drawing the footprint alone (with a legend proxy, because matplotlib’s legend cannot use the polygon collection geopandas draws), instead of raising. camera_files_from_stereo_run() likewise learned to fall back to the image tokens of a stereo command that names no camera model of any kind — an RPC-only run — while a CSM run’s .json cameras still mean “nothing to scope to”.

  • Sensor detection is content-based (#162). The WorldView reader previously claimed any XML that wasn’t named *ortho*/README and then failed deep inside parsing (ValueError: Tag 'SATID' not found ...) when handed unrelated files. Each reader now implements a cheap _is_camera_file() content check (iterparse, stopping at the first identifying tags) that discovery, filtering, and detection are built on in sensors/base.py — deduplicating the shallow-then-recursive discovery pattern the two readers previously repeated. WorldView requires the <isd> root plus the IMD/EPH/ATT blocks (mirroring ASP’s own RPC_XML.cc requirements; the root alone would still claim ASP’s gen_aster ASTER XMLs, which share it), and dg_mosaic outputs still pass. The DIMAP reader additionally requires a supported METADATA_PROFILE: products from unsupported DIMAP profiles are skipped with a one-time warning naming the profile instead of a wrong parse. Unrecognized inputs now produce the clean “No supported sensor metadata files found” error, and a new sensor support matrix in the docs records what’s validated, planned (#168), and out of scope.

  • WorldView scene dicts degrade gracefully when optional tags are missing (#163). dg_mosaic can strip image tags and Multi (multispectral) products carry per-band TDI rather than a single TDILEVEL, but the reader previously crashed on any missing summary tag. The scene-dict schema is now formalized in sensors/base.py as a required identity core (xml_fn, catid, sensor, date, geom — still read strictly) plus optional fields (OPTIONAL_SCENE_FIELDS) that land as None (scandir, tdi — omitted from scene strings) or NaN (mean view/sun angles, GSD, cloud cover — rendered as “nan”), matching the “not provided” convention the Pléiades reader established. Pair-level consumers are hardened to match: get_pair_utm_epsg() and get_intersection_bounds() fall back to the footprint union for non-overlapping pairs (previously AttributeError/TypeError on None), and pair_dict()/get_title() tolerate scenes without timestamps (cdate/dt become None, rendered “N/A”).

  • asp_plot/sensors.py is now the asp_plot/sensors/ package (#168). Pure reorganization as groundwork for broader sensor support: the SensorMetadata ABC and shared helpers move to sensors/base.py, the WorldView reader to sensors/worldview.py, the Airbus DIMAP reader to sensors/dimap.py, and the SENSORS registry plus the sensor_for_directory()/sensor_for_inputs()/resolve_xml_inputs() entry points to sensors/__init__.py, which re-exports every public name — from asp_plot.sensors import ... is unchanged, and no behavior changes. Also fixes the two WorldView scene-selection notebooks, which still called parser.get_id_dict()/parser.xml2poly() from before those methods moved from StereopairMetadataParser to the sensor readers (the correct call is parser.reader.get_id_dict()).

Fixed#

  • “Please report this” warnings now point at a new issue rather than a closed one. The spec-only reader warnings and the docs support matrix directed users to the issue that implemented each reader — but those close on merge, so SPOT 5 and ALOS PRISM reports were already being sent to closed #179. All of them now link to the new-issue form.

  • worldview_spacenet_ucsd_stereo.ipynb runs again (#182). It called get_pair_utm_epsg(), get_scene_bounds() and get_intersection_bounds() on StereoGeometryPlotter; all three live on StereopairMetadataParser, which the plotter has composed rather than inherited since #25, so the notebook raised AttributeError on its first geometry cell. Its committed outputs predated the refactor, so nothing surfaced the break until a re-run.

[2.0.0] - 2026-07-29#

A major version because two names changed. Neither has a back-compat alias, and both are one-line fixes at the call site:

  • The CLI: asp_plot → asp_report (#165). Flags are unchanged, so replacing the command name in your scripts and notebook cells is the whole upgrade.

  • A method: StereoGeometryPlotter.dg_geom_plot() → stereo_geom_plot() (#155).

Everything else here is additive: Airbus Pléiades/Pléiades Neo (DIMAP) support and ASP multi-view stereo handling, including per-pair scenes/match-points/disparity rendering. The package is still asp_plot (pip install asp-plot, import asp_plot), and the other four CLIs — stereo_geom, csm_camera_plot, request_planetary_altimetry, gallery — keep their names.

Added#

  • Multi-view runs render per-pair scenes, match points, and disparity (#160). An ASP multi-view run keeps its per-pair intermediate products in run-pair*/ subdirectories (only the joint PC/DEM/IntersectionErr are at the top level), so the Input Scenes, Match Points, and Disparity report sections were “missing files” placeholders after #155. SceneFiles / StereoFiles now detect the layout (find_pair_directories()) and resolve each pair’s N-L_sub.tif/N-R_sub.tif, match file, alignment matrices, and N-D_sub.tif/N-D.tif; plot_scenes(), plot_match_points(), and plot_disparity() render one figure per pair, labeled Pair N: <reference> ↔ <image> (image names recovered from the pair’s N-stereo.default config copy), and return the saved filename list the way stereo_geom_plot() does for N-scene runs. The report registry emits one section per figure (”… (continued)”), and the placeholder behavior remains the fallback for genuinely missing files. The Marseille multi-view example report and notebook are regenerated with the per-pair figures. Validated against 3-scene (Pléiades Neo tri-stereo) and 3/5-scene WorldView-2 same-pass multi-view runs (SpaceNet Atlanta, #159).

  • The Stereo Geometry report section is scoped to the run’s scenes (#160). The section previously plotted every camera metadata file in the processing directory, which draws unrelated scenes (and their N-choose-2 pairwise figures) when a directory holds cameras for several runs — common with multi-view subsets. camera_files_from_stereo_run() now recovers the camera files named in the stereo command (parsed from the run’s newest *log-stereo*.txt) and feeds them to StereoGeometryPlotter(inputs=...); directory-based discovery remains the fallback when the command names fewer than two metadata files (e.g. CSM .json cameras) or they cannot be found on disk.

  • Airbus Pléiades / Pléiades Neo (DIMAP) support (#155). A new PleiadesMetadata sensor reader parses DIMAP v2 product XMLs (DIM_*.XML) into the sensor-agnostic scene dicts used by the stereo-geometry tooling — footprints, view/sun angles and GSD from the Located_Geometric_Values grid, ECEF ephemeris with per-point times, and attitude quaternions reordered from the Airbus scalar-first Q0 layout to the scalar-last convention shared with WorldView. RPC_*.XML sidecars are filtered out by METADATA_SUBPROFILE, and sensor detection is now two-pass (shallow matches beat recursive ones) so WorldView XMLs at a directory top level win over DIMAP files nested in a raw delivery, and vice versa. Pléiades panels carry “© Airbus DS” attribution via the new detect_satellite_attribution() — which returns the rights-holder name ("Vantor" or "Airbus DS") and replaces the is_vantor bool (detect_vantor_satellite() remains as a backward-compatible wrapper) — and get_acquisition_dates() falls back to the DIMAP refined-model start time when FIRSTLINETIME is absent. Validated end-to-end on the free Airbus Pléiades Neo tri-stereo sample over Marseille: executed example notebook (3-scene multi-view stereo) and committed PDF report, wired into the docs.

  • ASP multi-view stereo runs no longer crash the report (#155). A multi-view run keeps its per-pair match/scene/disparity files in run-pair*/ subdirectories, which previously crashed StereoFiles discovery. Initially patched to draw “missing files” placeholders for those sections; superseded in this same release by full per-pair rendering (see the #160 entry above).

Changed#

  • Breaking: the main CLI is renamed asp_plot → asp_report (#165). asp_plot is the package; the command should be named after what it does with the package — generate a report. The console script, its module (asp_plot/cli/asp_plot.py → asp_plot/cli/asp_report.py), and the command recorded on each report’s “Report Generation Command” page are all asp_report now; the Python API (import asp_plot, run_report(), every class) is untouched, as are the other four CLIs (stereo_geom, csm_camera_plot, request_planetary_altimetry, gallery). No asp_plot alias is kept — a shim was rejected in favor of a clean break while the user base is small. The default auto-generated report filename follows: asp_plot_report_<title>_<timestamp>.pdf → asp_report_<title>_<timestamp>.pdf, and the committed example reports are renamed *-asp-plot-report.pdf → *-asp-report.pdf (docs and notebook links updated to match). Upgrade: replace asp_plot with asp_report in scripts and notebook cells; all flags are unchanged.

  • The Atlanta example is now a 3-scene multi-view run, and the committed examples are trimmed (#159). A new worldview_spacenet_atlanta_mvs.ipynb notebook processes three same-pass SpaceNet Atlanta WorldView-2 scenes (chosen with the retained scene-selection notebook) through wv_correct → 5-scene bundle_adjust → 3-scene multi-view parallel_stereo → point2dem, and compares the multi-view DEM against the ASP-docs-recommended alternative — the three pairwise stereo runs merged with dem_mosaic — on coverage, DEM difference, and ICESat-2 residuals. A matching WorldView_Atlanta_MVS report exercises the per-pair multi-view rendering from #160. To keep the committed examples few and meaningful (UCSD remains the classic two-scene pair example), the two old Atlanta pair notebooks and reports (mapprojected and no-mapprojection variants) and the ~18 MB Pléiades Marseille report are removed; the Pléiades notebook remains the multi-view Airbus example. Net effect: three PDFs (~47 MB) out, one in.

  • Breaking: StereoGeometryPlotter.dg_geom_plot() renamed to stereo_geom_plot() (#155). The dg_ (DigitalGlobe) prefix predated multi-sensor support and was wrong for Pléiades; the new name matches the stereo_geom CLI and its *_stereo_geom.png outputs. No back-compat alias.

Fixed#

  • The Pléiades notebook now reaches the docs build (#159). .readthedocs.yaml never copied notebooks/Pleiades/*.ipynb into docs/examples/notebooks/, so the Marseille tri-stereo page added in #155 was missing from the rendered docs.

  • Report robustness for N-scene and covariance-free sensors (#155). The stereo-geometry report section emits one PDF section per saved figure (N-scene runs produce an overview plus per-pair figures); long DEM-summary values (e.g. three joined acquisition dates) wrap inside the title-page table instead of overflowing the page; satellite_position_orientation_plot generalizes from the two-scene layout to one column per scene; scene labels omit scan direction and TDI when the sensor does not provide them (DIMAP has neither); and the covariance panels annotate “not provided” instead of crashing when a sensor carries no ephemeris/attitude covariance (as DIMAP does not).

[1.19.0] - 2026-06-30#

Added#

  • stereo_geom makes pre-ASP stereo geometry plots straight from satellite XMLs (#73). You can now assess candidate stereo geometry (skyplot + footprint/ephemeris map + convergence/B:H/BIE/asymmetry stats) before running ASP, directly from the delivered camera XMLs, without a tidy directory layout. Delivered across three pieces:

    • Robust, structure-agnostic XML discovery (#150). Camera XMLs are found recursively (they routinely sit three or four directories deep), decoys like README.XML and ortho sidecars are ignored, and scenes are grouped by the CATID read from the file contents — never assumed to be in the filename. Tiled deliveries are mosaicked per CATID via dg_mosaic.

    • Flexible CLI inputs (#152). stereo_geom takes positional INPUTS that may be any mix of XML files, directories, and glob patterns — stereo_geom *.XML, stereo_geom a.xml b.xml, stereo_geom delivery_dir/ — searched recursively. The original --directory flag is retained as the fallback, so existing usage is unchanged. New resolve_xml_inputs() / sensor_for_inputs() / SensorMetadata.detect_files() plumbing and a WorldViewMetadata(image_list=...) constructor back this, with inputs= threaded through StereopairMetadataParser and StereoGeometryPlotter.

    • N-scene multi-view assessment. stereo_geom is no longer limited to a pair. Given more than two scenes it writes one color-coded overview figure (all satellite positions on the skyplot, all footprints/ephemeris on the map) plus one figure per pair for every N-choose-2 combination, each titled with that pair’s full stereo stats — the multi-view use case from the issue (e.g. the Utqiagvik scenes). Two-scene output is unchanged. Pairs whose footprints do not overlap are still plotted, with intersection-dependent stats shown as N/A. The parser gains get_pair_dicts() (all combinations), get_scenes_centroid_projection(), and get_pair_map_projection(); get_pair_dict() is now the exact-two-scene entry point.

Fixed#

  • Stereo-geometry map basemap no longer errors/hangs on wide extents (#73). The map intentionally shows the full satellite ephemeris tracks, which (for off-nadir scenes) sit tens to hundreds of kilometers from the ground footprints, so the extent is necessarily wide. At that extent contextily’s auto-zoom resolves to an invalid (negative) level, which previously produced a slow, failing tile request that could appear to hang. The basemap fetch now falls back to a coarse continental zoom (_add_basemap_safe) when the auto-zoom fails, so the map keeps its full track-revealing extent and still gets a basemap. The satellite tracks are also drawn on top of the (semi-transparent) footprints so they are not buried.

  • Non-overlapping footprints no longer crash intersection handling. get_pair_intersection() computed the local-projection area before checking whether the two footprints actually intersect, raising on a None intersection. This was unreachable in the two-scene path (always overlapping) but is common among the pairs of an N-scene set; the area calculation is now skipped when there is no overlap.

[1.18.1] - 2026-06-26#

Fixed#

  • © Vantor attribution now covers all Vantor-owned satellites, not just WorldView (#137). The copyright-overlay check (detect_vantor_satellite) matched only SATID values starting with WV, so GeoEye-1 (GE01), QuickBird (QB02), and IKONOS scenes — all owned by the same rights-holder (DigitalGlobe → Maxar → Vantor) — were silently left un-attributed. Detection now matches a VANTOR_SATID_PREFIXES whitelist (WV incl. WorldView Legion WVLG, GE, QB, IK). This clarifies that is_vantor / detect_vantor_satellite are an attribution concern (named for the company), intentionally distinct from sensor/reader identity (the WorldView-named abstraction in sensors.py); the two names are documented as deliberately different so they aren’t reconciled into one.

[1.18.0] - 2026-06-25#

Added#

  • Reconstruct mapproject commands in the PDF report (#96). ASP’s mapproject does not write a log file the way bundle_adjust / stereo / point2dem do, so the processing-parameters page never documented the mapprojection step. Rather than depend on a new ASP --log flag, the new asp_plot/mapproject.py reconstructs the command from the output GeoTIFF metadata alone: ASP stamps INPUT_IMAGE_FILE / CAMERA_FILE / DEM_FILE / CAMERA_MODEL_TYPE / BUNDLE_ADJUST_PREFIX into each mapprojected output, and combined with the raster’s CRS (--t_srs), resolution (--tr), and bounds (--t_projwin) that is enough to rebuild the invocation. ProcessingParameters.from_log_files() now adds a mapproject key (a list — one command per mapprojected input scene found), and the report’s “Processing Parameters” page renders them under a “Mapproject Command(s)” heading with a note that the values are reconstructed (resolved session/grid). Works across ASTER, WorldView/RPC, and CSM (jitter) sessions, including custom projections without an EPSG code (falls back to the PROJ string). The command(s) are rendered in ASP pipeline order (between bundle_adjust and stereo), and scoped to the run being reported via the stereo command — a non-mapprojected run that shares a parent directory with mapprojected scenes (the stereo/ + stereo_no_mapproj/ layout) does not spuriously list a mapproject step.

[1.17.0] - 2026-06-24#

Changed#

  • Structural rewrite of the codebase (#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).

    • Body abstraction (#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.

    • Collapsed duplicated pc_align + alignment-evaluation paths (#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.

    • Declarative report pipeline (#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 ReportSpecs (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.

    • Sensor-flexible metadata parsing (#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.

    • Plotter scaffold + file-discovery separation (#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.

    • Altimetry god-class split (#130, #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.

    • csm_camera.py split (#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.

    • Versioned ASP-log adapter (#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.

[1.16.0] - 2026-06-11#

Added#

  • Reusable “figure selections” for run-to-run comparison (#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.

    • 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.

    • 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.

    • 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.

    • 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.)

    • 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.

[1.15.2] - 2026-06-11#

Fixed#

  • ESA WorldCover sampling crashed on machines configured with AWS SSO/login. _sample_worldcover_into_gdf() opened the public ESA WorldCover S3 COGs with rasterio’s default AWS session, which eagerly resolves credentials. On a machine whose ~/.aws/config uses an SSO/login provider, botocore raised MissingDependencyException: Using the login credential provider requires an additional dependency ... botocore[crt], aborting the entire asp_plot report even though the bucket is public and needs no credentials. The reads now use an explicit unsigned session (rasterio.session.AWSSession(aws_unsigned=True)), so anonymous access is used regardless of the user’s AWS configuration.

[1.15.1] - 2026-06-10#

Fixed#

  • Altimetry.to_csv_for_pc_align() wrote its CSV to the current working directory instead of the run directory. Running the asp_plot CLI (or Altimetry.align_and_evaluate()) from a directory other than the dataset directory left a stray atl06sr_for_pc_align_<key>.csv in the cwd. The output path is now rooted at self.directory via os.path.join(), matching every other output in the class (_save_to_parquet, the pc_align outputs, and the planetary twin to_csv_for_pc_align_planetary()). No consumer changes were needed — the single internal caller (align_and_evaluate()) uses the return value directly, and Alignment.pc_align_dem_to_atl06sr() handles the directory-prefixed path unchanged.

[1.15.0] - 2026-06-09#

Added#

  • Gallery plotting for many DEM outputs (#11). New GalleryPlotter class (asp_plot/gallery.py) and gallery CLI tool that lay out a stack of DEMs as a grid of thumbnails sharing a single global percentile color stretch and one shared colorbar — useful for QA’ing multi-date / multi-pair ASP output at a glance. Brings the legacy original_code/gallery.py into the modular package, dropping its pygeotools / imview dependencies in favor of the existing Raster, Plotter, and ColorBar utilities.

    • DEMs are rendered with the package’s standard convention (gray hillshade underlay + semi-transparent viridis DEM); the hillshade underlay is on by default and can be disabled with --no-hillshade.

    • The layout sizes each panel to the rasters’ aspect ratio and places panels with absolute positioning, so galleries of 1 to N rasters (including non-square ones) pack tightly without stray whitespace. Per-panel titles use the full filename, auto-shrunk (by measuring rendered text width) to fit the panel.

    • Output resolution is matched to the rendered detail for crisp zooming, with an automatic dpi cap that keeps the PNG under --max_filesize_mb (default 10) regardless of the number of rasters.

    • GalleryPlotter.from_directory(directory, pattern="*-DEM.tif") resolves a directory + glob into the raster list; the CLI also accepts an explicit list of files.

[1.14.1] - 2026-06-05#

Fixed#

  • Raster.get_epsg_code() returned None for compound / 3D-promoted CRSs (e.g. "EPSG:32610+EPSG:4979", as written by stereopipeline-quickstart’s fetch_cop_dem.py to assert ellipsoid heights on the COP30 DEM). PROJ represents such a CRS as a UTM CRS “promoted to 3D” with no exact EPSG match, so rasterio’s to_epsg() yields None and downstream f"EPSG:{epsg}" strings crash (e.g. passing the DEM as dem_fn to Altimetry, or Raster.get_bounds(latlon=True)). Now falls back to the EPSG code of the horizontal (2D) component via pyproj’s CRS.to_2d().

[1.14.0] - 2026-04-28#

Added#

  • Automatic pc_align step in the planetary altimetry block (#119). The existing --pc_align CLI flag now also runs against MOLA (Mars) and LOLA (Moon) — previously Earth/ICESat-2 only. Mirrors the Earth pipeline: a single alignment-report page on insufficient_points / no_improvement, plus a pre/post mapview and pre/post histogram on success.

  • Altimetry.align_and_evaluate_planetary(...): planetary sibling of align_and_evaluate. Returns the same AlignmentResult dataclass; defaults max_displacement=500 m (per ASAP-Stereo’s CTX cookbook) and minimum_points=20 (planetary tracks are sparse).

  • Alignment.pc_align_dem_to_planetary_csv(...): invokes ASP pc_align with --csv-format '1:lon 2:lat 3:radius_m' and --datum D_MARS/D_MOON (aligned with the ASP next_steps documentation on MOLA alignment).

  • Altimetry.to_csv_for_pc_align_planetary(): writes lon, lat, radius_m from self.planetary_points to drive pc_align.

  • plot_aligned kwarg on Altimetry.mapview_plot_planetary_to_dem and Altimetry.histogram_planetary_to_dem: pre/post panels share color/bin scales when an aligned DEM is available.

  • Module-level constants MARS_IAU_SPHERE_RADIUS = 3_396_190.0 and MOON_IAU_SPHERE_RADIUS = 1_737_400.0 so callers can reconstruct ASP-style “height above sphere” without magic numbers.

Changed#

  • MOLA loader switched to PLANET_RAD. _load_mola_csv() now reads the absolute planetary radius from the ODE GDS *_pts_csv.csv and computes height = PLANET_RAD - 3,396,190 (IAU 2000 Mars sphere). 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, so dh from TOPOGRAPHY carries a latitude-dependent offset of up to ~10 km that pc_align cannot remove. Verified on the MOC NA tutorial scene at lat 34°N: signed median dh dropped from +6,000 m (TOPOGRAPHY path) → +99.74 m (PLANET_RAD path) → +3.13 m (after pc_align). Reference: MOLA PEDR Software Interface Specification (PDS Geosciences).

  • LOLA loader prefers Pt_Radius (km) when available. The Point per Row LOLA RDR CSV (results=p) carries Pt_Radius in kilometers; the simple Topography CSV (results=u) carries Topography in meters. _load_lola_csv() auto-detects km by magnitude (< 10 000) and converts to meters, then writes both height (m above the IAU 1737.4 km lunar sphere) and radius_m to self.planetary_points. The Moon is essentially spherical (~1.4 km equatorial-vs-polar variation), so either CSV gives the same dh to ~1 m. Reference: ODE GDS REST V2.0 manual.

  • Alignment.apply_dem_translation() is body-aware. Picks a body-centered geocentric “ECEF-equivalent” CRS from a new 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. Without this fix, applying a pc_align translation to a Mars/Moon DEM raised RuntimeError: Source and target ellipsoid do not belong to the same celestial body.

  • planetary_to_dem_dh() also samples the aligned DEM when self.aligned_dem_fn is set, populating aligned_dem_height and altimetry_minus_aligned_dem so pre/post plots share a single sample. Refactored shared interpolation into _sample_dem_at_planetary_points().

Documentation#

  • MOC NA notebook consolidated into notebooks/Mars_MGS/mars_mgs_orbital_camera.ipynb covering both stereo variants of the M0100115 / E0201461 pair (the mars_mgs_orbital_camera_narrow_angle.ipynb notebook for a different scene pair was removed). Mirrors the ASTER mapproj/non-mapproj layout. Stereo commands match this repo’s WorldView convention (parallel_stereo --stereo-algorithm asp_mgm --subpixel-mode 9 --processes 2 --threads 4, --alignment-method affineepipolar for non-mapprojected, --alignment-method none for mapprojected via cam2map4stereo.py). The notebook intro includes a callout explaining the spherical-vs-oblate elevation-range surprise. Reports: MOC-asp-plot-report.pdf and MOC_mapproj-asp-plot-report.pdf.

  • LRO NAC notebook reprocessed on the full 5000×5000 cubes in LRONAC_example.tar instead of the 900×973 sub-window the ASP “lightning fast” tutorial uses. Resulting DEM is 4720×4510 at 1.04 m GSD (~4.7 km × 4.7 km, vs the old ~1 km × 1 km), 95.88% valid pixels. LOLA query expanded to match: 1539 of 2044 LOLA points overlap the DEM (vs 12 of 19 on the old crop), enough for a meaningful pc_align and to bring out spacecraft jitter in the disparity panels.

[1.13.0] - 2026-04-20#

Added#

  • Automatic pc_align step in the Earth altimetry block, gated by a new --pc_align CLI flag (default True; disabled automatically when --plot_altimetry / --plot_icesat is False). Runs pc_align against ICESat-2 ATL06-SR, evaluates whether the aligned DEM is worth keeping, and appends the outcome as one or more report pages:

    • Always: an alignment report page with the parameters table, a single-row horizontal stats table (p16/p50/p84 beg/end, north/east/down shifts, translation magnitude, values to 2 sig figs), a description explaining what pc_align does and the meaning of every column in the tables above, and a bold status line for the outcome of this run.

    • On success (p50 drops toward 0 by more than improvement_threshold_pct, default 5%, and pc_align actually wrote an aligned DEM): three additional full-page diagnostic figures against the aligned DEM — a pre-/post-alignment landcover histogram, the full profile, and the best/worst 1 km segments.

    • On insufficient ATL06-SR coverage or no meaningful improvement: the aligned DEM on disk is cleaned up so its presence is a truthy signal that the alignment is worth using.

  • Altimetry.align_and_evaluate(...) (new method) returning a plain AlignmentResult dataclass (status ∈ {"insufficient_points", "no_improvement", "success"}, alignment_report_df, aligned_dem_fn, improvement_pct, message, parameters_used). Does not import any fpdf / report dependencies, so it is safe to call from notebooks.

  • plot_aligned kwargs on Altimetry.histogram_by_landcover and Altimetry.plot_best_worst_segments:

    • histogram_by_landcover(plot_aligned=True) overlays the pre- and post-alignment distributions using shared bin edges and renders two vertically stacked per-landcover stats text boxes whose outline colors match the bar colors (color = legend).

    • plot_best_worst_segments(plot_aligned=True) keeps segment selection fixed (based on the unaligned dh so segments are comparable), overlays aligned DEM heights on each segment, and appends aligned Median/NMAD to the segment titles.

  • AlignmentReportPage dataclass in asp_plot.report: a report-section type that renders a kwargs table + single-row stats table + description + bold status line + optional figure with caption. Body text blocks render left-aligned to avoid justified word-spacing gaps.

Changed#

  • Processing Parameters is now page 2 of the PDF, immediately after the DEM Summary on the title page, instead of the trailing appendix. Page order is now: title + DEM summary → processing parameters → diagnostic figures → (if any) alignment results.

  • plot_atl06sr_dem_profile(plot_aligned=True): the lower dh panel now plots the post-alignment residuals (icesat_minus_aligned_dem) with Med/NMAD recomputed against the aligned DEM, with the legend entry tagged "(Aligned DEM)". The upper elevation panel still overlays both the unaligned and aligned DEM for comparison. plot_aligned=False behavior is unchanged.

[1.12.1] - 2026-04-14#

Changed#

  • Report panel order: Disparity maps now follow the Bundle Adjust panels, and DEM Results precedes Detailed Hillshade (was: Hillshade → DEM Results → Disparity).

  • Input Scenes caption clarifies that mapprojected scenes are RPC-orthorectified against a reference DEM to roughly pre-align the stereo pair prior to correlation (reducing disparity search range), which addresses confusion for readers coming from non-ASP photogrammetry workflows.

  • Match Points caption notes these come from stereo_corr’s initial interest point matching step (used to set search windows), not dense correlation.

Added#

  • Acquisition Date(s) row on the DEM Summary title-page table, populated when recoverable from scene metadata.

  • get_acquisition_dates() helper in utils.py: reads FIRSTLINETIME from WorldView/Maxar XMLs and parses the capture timestamp from AST_L1A_... file/directory names. Deduplicates and sorts; returns an empty list if no date can be found, in which case the summary-table row is omitted.

  • Unit tests for get_acquisition_dates() covering WorldView XMLs, ASTER filenames (top-level and in subdirectories), dedupe, and sorting of multi-date pairs.

Fixed#

  • --report_filename accepts absolute and relative paths (not just bare filenames), and ~ in CLI path arguments is expanded (#113).

[1.12.0] - 2026-04-10#

Changed#

  • ICESat-2 time filter default changed to "all" (full mission range) instead of auto-detect ±1 year. The --atl06sr_time_range CLI option now accepts "all" (default), "auto" (XML metadata ±time_buffer_days), "START,END", or a single date (buffered). Programmatic API: _resolve_time_range() and request_atl06sr_multi_processing() take a new time_range parameter ("all" or "buffered") with cascade: t0/t1 > scene_date > XML metadata > fall back to "all". t1 is truncated to midnight UTC for stable parquet caching.

  • ESA WorldCover sampled locally from AWS S3 COGs via rasterio vsicurl instead of through the slow SlideRule samples parameter. WorldCover is now sampled inside request_atl06sr_multi_processing before the parquet save, so the column is persisted in the cache and doesn’t have to be re-sampled on subsequent runs. COP30 also sampled alongside (asset name corrected to esa-copernicus-30meter).

  • 3σ outlier filter applied by default in atl06sr_to_dem_dh (and planetary_to_dem_dh) using the true mean ± 3·standard deviation (not NMAD). Pass n_sigma=None to skip. Dh colorbars and histograms use symmetric ±|filtered min/max| (≈ ±3σ) centered on 0 as display limits; all data is still plotted. Displayed stats remain Median / NMAD.

  • Profile plot (plot_atl06sr_dem_profile) restructured: stacked elevation/dh plots on the left (shared x-axis, no vertical gap, grid lines), map view on the right spanning both rows. Figure reshaped to 16×8. Dh points colored gray instead of salmon.

  • Best/worst segments (plot_best_worst_segments) simplified to a 1×2 figure (removed the context map). Scoring formula changed from |median(dh)| + NMAD(dh) to 3·|median(dh)| + NMAD(dh) so a large median bias can’t be hidden by a small NMAD. Labels “Better agreement” / “Worse agreement” instead of “Best” / “Worst”. CLI report caption documents the formula.

  • Parquet cache saved next to the ASP processing directory (self.directory) instead of the current working directory.

  • SlideRule logging silenced (verbose=False, WARNING level, explicit filter on sliderule.session).

Added#

  • filter_outliers() method: removes dh points beyond n_sigma × standard deviation from the mean.

  • sample_esa_worldcover() method: samples ESA WorldCover 10m values from AWS S3 COGs for manually-loaded data (auto-called inside request_atl06sr_multi_processing).

  • plot_best_worst_segments() method: 1×2 figure showing 1 km segments with better and worse ICESat-2 vs DEM agreement.

  • ICESat-2 time filtering documentation section in docs/cli/asp_plot.md explaining the three modes.

Fixed#

  • Parquet cache regeneration bug: SlideRule mutates the parms dict by injecting a random temp file path at output.path during run(), causing the string comparison to fail on every subsequent run. output is now stripped from both sides of the comparison and from stored parameters.

  • Parquet cache error swallowing: the broad try/except around the cache comparison also wrapped the SlideRule API call; narrowed it so API errors propagate instead of being silently eaten.

  • Histograms no longer cut data: replaced range= (which excludes data outside the range from the bins) with ax.set_xlim(), so all data is plotted and used in stats.

  • Single-date CLI argument now uses scene_date buffering instead of being treated as a start date.

  • COP30 SlideRule asset name corrected (esa-copernicus-30meter, not cop30-dem).

Dependencies#

  • Pinned sliderule>=5.3.0 to pick up temp file handling fixes.

[1.11.1] - 2026-03-30#

Fixed#

  • Asymmetry angle calculation: ECEF ground point z-coordinate was incorrectly set to 0 (equatorial plane) instead of using the proper WGS84 ellipsoid position from pyproj, producing wrong values at non-equatorial latitudes

Changed#

  • Stereo geometry functions (get_convergence_angle, get_bh_ratio, get_bie_angle, get_asymmetry_angle) extracted to module-level in stereopair_metadata_parser.py for reuse and testability

Added#

  • Unit tests for convergence angle, B/H ratio, BIE, and asymmetry angle calculations, including a regression test for the ECEF z=0 bug

[1.11.0] - 2026-03-26#

Added#

  • New --atl06sr_time_range CLI option for controlling ICESat-2 ATL06-SR time filtering: use "all" for full mission range, or "START,END" for a custom date range (e.g. "2020-01-01,2024-12-31")

  • Corresponding t0/t1 parameters on Altimetry.request_atl06sr_multi_processing() and Altimetry._resolve_time_range() for programmatic use

  • New WorldView-3 UCSD example notebook (worldview_spacenet_ucsd_stereo.ipynb) using publicly available IARPA CORE3D data, with comprehensive stereopair selection analysis

  • Example report: WorldView_UCSD-asp-plot-report.pdf

Fixed#

  • Alignment.pc_align_report() and Alignment.apply_dem_translation() now return None gracefully when pc_align log files are not found, instead of crashing with TypeError

  • Altimetry.alignment_report() handles missing pc_align results with a warning instead of crashing

  • key_for_aligned_dem parameter in Altimetry.alignment_report() now defaults to the processing_level value instead of being hardcoded to "ground"

[1.10.0] - 2026-03-21#

Added#

  • Planetary altimetry validation: LOLA (Moon) and MOLA (Mars) DEM comparison via the ODE Granular Data System (GDS) REST API, analogous to the existing ICESat-2 workflow for Earth DEMs

  • New request_planetary_altimetry CLI tool to submit async LOLA/MOLA data requests with email notification, saving request metadata to altimetry_request_info.yml

  • New --plot_altimetry flag on the asp_plot CLI with automatic body detection (Earth → ICESat-2, Moon → LOLA, Mars → MOLA)

  • New --altimetry_csv flag to pass a pre-downloaded LOLA/MOLA *_topo_csv.csv file for planetary altimetry plots

  • detect_planetary_body() utility function: detects Earth/Moon/Mars from DEM CRS WKT

  • get_planetary_bounds() utility function: converts DEM bounds to planetocentric 0-360 lon/lat for GDS queries

  • Altimetry.load_planetary_csv(): loads LOLA or MOLA CSV with column validation and helpful error messages

  • Altimetry.planetary_to_dem_dh(): computes altimetry-minus-DEM differences using WKT-based CRS (supports planetary DEMs without EPSG codes)

  • Altimetry.mapview_plot_planetary_to_dem(): DEM hillshade with dh point overlay

  • Altimetry.histogram_planetary_to_dem(): dh histogram with n/median/NMAD statistics

  • Lazy SlideRule initialization: Altimetry.__init__ no longer requires an internet connection; SlideRule is initialized on first ICESat-2 method call

  • LOLA/MOLA altimetry sections added to LRO NAC, Mars MGS MOC, Mars MGS MOC NA, and Mars MRO HiRISE example notebooks

  • Unit tests for body detection, planetary bounds, lazy init, CSV loading/validation, and planetary dh computation

Changed#

  • --plot_icesat is now a deprecated alias for --plot_altimetry (prints deprecation warning if used)

  • Basemaps are automatically skipped for non-Earth DEMs

  • pyyaml added as an explicit dependency

[1.9.0] - 2026-03-10#

Added#

  • Match points now overlay on non-mapprojected images using alignment transform matrices (run-align-{L,R}.txt), replacing the previous blank-right-panel behavior

  • Report command string recorded in PDF report via new report_command parameter in compile_report()

  • Pixel-unit scalebar for non-mapprojected disparity plots (mapprojected scenes continue to use GSD-based scalebar)

  • Guard with FileNotFoundError when alignment matrix files are missing for non-mapprojected match point overlay

  • Warning when unit="meters" is passed for non-mapprojected disparity (unsupported, falls back to pixels)

  • Test coverage for non-mapprojected stereo code paths (9 new tests with resampled ASTER test data)

Changed#

  • Report figures are now fitted to page dimensions, preventing overflow and cutoff for large/wide figures

  • Report caption reserve is now dynamically calculated from actual caption length instead of a hardcoded 20mm

  • Input Scenes caption updated to explain alignment rotation applied to non-mapprojected imagery

  • Match points right subplot title simplified from “Right (scenes shown only if mapprojected)” to “Right”

  • save_figure() default DPI changed from hardcoded 150 to None (uses figure’s own creation DPI), fixing pixelated ICESat-2 report figures

  • ICESat-2 altimetry figures created at 220 DPI for high-quality PDF embedding

  • CLI parameter values are now quoted with shlex.quote() for proper reconstruction of commands with spaces

  • Cleaned up example notebook report links and removed stale PDF files

  • Removed unnecessary read_align_matrix() method; alignment matrices are loaded inline via np.loadtxt()

Fixed#

  • Disparity plot scale for non-mapprojected scenes: GSD-based rescale was producing near-zero values from the identity transform; now skips rescaling and uses pixel-unit scalebar instead

  • Match point plot whitespace for non-mapprojected scenes caused by a 1x1 dummy image plotted underneath scatter points

  • Pixelated ICESat-2 ATL06-SR figures in PDF reports caused by save_figure() overriding figure DPI with 150

[1.8.0] - 2026-03-03#

Added#

  • New _select_best_track() method to find the RGT/cycle/spot combination with the most valid ATL06-SR points for profile plotting

  • New histogram_by_landcover() method producing a histogram of ICESat-2 vs DEM differences with per-landcover-class statistics (count, median, NMAD) using ESA WorldCover

  • New plot_atl06sr_dem_profile() method with a three-row figure: combined elevation + dh profile with dual y-axes, two 1 km zoom segments (best/worst agreement scored by |median(dh)| + NMAD), and DEM hillshade map with track overlay

  • Server-side time filtering for SlideRule API requests via new _resolve_time_range() method with three-tier cascade: explicit scene_date parameter, auto-detect from stereopair XML metadata, or 2-year fallback

  • scene_date and time_buffer_days parameters added to request_atl06sr_multi_processing()

  • Module-level ICESAT2_MISSION_START constant and WORLDCOVER_NAMES dictionary for reuse across methods

  • Module-level _nmad() helper function (Normalized Median Absolute Deviation)

  • Time range labels displayed on ICESat-2 plot titles

  • Tests for _select_best_track, histogram_by_landcover, plot_atl06sr_dem_profile, and _resolve_time_range

Changed#

  • Migrated SlideRule API from legacy icesat2.atl06p() to x-series sliderule_api.run("atl03x") with automatic index and column normalization

  • Simplified ICESat-2 report section: single "all" processing level with landcover histogram and profile plot, replacing the previous multi-level (all + ground) workflow with temporal filtering and plain histograms

  • Report section ordering: bundle adjustment plots now appear after match points and before DEM hillshade

  • Profile plot legend now includes axis labels (left/right) for all entries and embeds Med/NMAD statistics in the dh legend item

Removed#

  • --icesat_filter_date CLI option (time filtering is now automatic via _resolve_time_range())

  • Commented-out plot_atl06sr_dem_profiles() stub and ATL03 request stub (replaced by implemented methods)

  • Duplicated WorldCover classification table from filter_esa_worldcover() docstring (now references WORLDCOVER_NAMES)

Fixed#

  • TypeError: Cannot subtract tz-naive and tz-aware datetime-like objects in predefined_temporal_filter_atl06sr when scene date is UTC-aware but DataFrame index is tz-naive

  • KeyError: 'translation_magnitude' in alignment_report() when requested processing level has no data (now returns early with a warning)

  • TypeError: unhashable type: 'numpy.ndarray' in histogram_by_landcover caused by parquet round-trip deserializing arrays as Python lists

  • TypeError: 'int' object is not callable when builtin len() was shadowed by the len=40 parameter inside request_atl06sr_multi_processing

  • OverflowError: cannot convert float infinity to integer in profile segment selection when median point spacing is zero

[1.7.0] - 2026-02-24#

Added#

  • ASP version and asp_plot version displayed on report title page

  • Copyright overlay (”© Vantor {year}”) on WorldView satellite imagery in scene, match point, and detailed hillshade plots

  • detect_vantor_satellite() utility to identify WorldView imagery from XML SATID tags

  • add_copyright_overlay() utility for matplotlib axes

  • ProcessingParameters.get_asp_version() method to extract ASP version from log files

  • Raster._mask_nodata() private helper to consolidate nodata/invalid value masking

  • Raster._load_and_diff_rasters_da() private static method returning xarray DataArray for raster differencing

Changed#

  • Raster.get_bounds() now uses self.ds.bounds (rasterio) instead of opening a redundant rioxarray dataset

  • Raster.compute_difference() uses rio.to_raster() for saving when save=True, avoiding manual profile construction

  • StereoPlotter.plot_detailed_hillshade() reuses existing raster.ds.transform instead of reopening the DEM file

  • Consolidated duplicated nodata masking logic into Raster._mask_nodata()

  • Updated ASTER and WorldView example notebooks

[1.6.4] - 2026-02-17#

Changed#

  • Updated README installation instructions with conda-forge as recommended install method

  • Updated README release process documentation for automated pipeline

[1.6.3] - 2026-02-16#

Fixed#

  • Added missing runtime dependencies to pyproject.toml: geopandas, matplotlib-scalebar, sliderule

[1.6.2] - 2026-02-16#

Added#

  • Automated PyPI publishing via OIDC trusted publishing on GitHub Release

  • conda-forge reference recipe for staged-recipes submission

  • Runtime dependencies declared in pyproject.toml (pip install asp-plot now installs all deps)

Changed#

  • Replaced deprecated actions/create-release@v1 with softprops/action-gh-release@v2 in release workflow

  • Added missing dependencies to environment.yml: pyproj, scipy, shapely, xarray

[1.6.0] - 2026-02-16#

Added#

  • Structured PDF report generation with title page, section headings, figure captions, DEM metadata summary table, and runtime summary table

  • New report.py module containing ReportSection and ReportMetadata dataclasses, ASPReportPDF class, and compile_report() function

  • DEM metadata (dimensions, GSD, CRS, nodata %, elevation range) automatically collected and displayed on the report title page

  • Figure captions describing each plot in the generated PDF report

  • Page headers (report title) and footers (page numbers) throughout the report

  • Tests for report dataclasses and PDF compilation (8 new tests)

Changed#

  • Replaced markdown-pdf dependency with fpdf2 (available on conda-forge, enabling conda-only installation)

  • Reordered report sections: Input Scenes and Stereo Geometry now appear before DEM results, matching the logical processing flow

  • Report generation moved from utils.py to dedicated report.py module

  • PNG images are now embedded directly in the PDF (eliminated intermediate PNG-to-JPEG conversion step)

Removed#

  • Dependency on markdown-pdf (pip-only package that blocked conda-forge packaging)

[1.5.0] - 2026-02-13#

Added#

  • Satellite attitude (ATT) parsing from DigitalGlobe/Maxar XML files: new getAtt() and getAtt_df() methods on StereopairMetadataParser, mirroring the existing ephemeris parsing

  • New satellite_position_orientation_plot() method on StereoGeometryPlotter producing a 3x2 figure showing position covariance, roll/pitch/yaw orientation, and attitude covariance for each scene

  • Attitude data (att_df) now included in catalog ID dictionaries returned by get_catid_dicts()

Changed#

  • Ephemeris covariance columns in getEphem_gdf() renamed from x_cov, y_cov, ... to cov_11, cov_12, cov_13, cov_22, cov_23, cov_33 for clarity

[1.4.0] - 2025-12-12#

Added#

  • New WorldView SpaceNet Atlanta stereo processing example notebook using publicly available data

  • New utility function get_utm_epsg() for determining UTM EPSG code from longitude/latitude

  • New Raster.get_utm_epsg_code() method for estimating UTM zone from raster location

  • New StereopairMetadataParser methods: get_pair_utm_epsg(), get_intersection_bounds(), get_scene_bounds()

Changed#

  • Alignment.get_alignment_report() now returns North-East-Down shift keys (north_shift, east_shift, down_shift) instead of ECEF Cartesian keys (x_shift, y_shift, z_shift).

  • Renamed worldview_comprehensive.ipynb to worldview_utqiagvik_stereo.ipynb.

Fixed#

  • Fixed geodiff command in bundle adjustment processing: corrected argument order (DEM must come before CSV) and csv-format syntax (spaces instead of commas between column specs)

  • Fixed graceful handling when --mapproj-dem flag was not used in bundle_adjust: geodiff plots are now skipped with a warning instead of causing the entire bundle adjustment section to fail

  • Fixed relative reference DEM paths read from log files not being resolved to absolute paths, causing “file not found” errors

[1.3.1] - 2025-11-17#

Added#

  • Jitter solved ASTER example processing notebook

Fixed#

  • Currently the GSD for bundle adjustment calculations is pulled from the metadata for WorldView scenes. ASTER does not contain this metadata, so a fallback value is used (1 m GSD), which effectively renders the bundle adjustment calculations always in pixels. We will eventually want to support an argument or other parser, but this is not important at the moment and instead this approach gracefully allows plotting to continue without erroring out.

[1.3.0] - 2025-11-14#

Added#

  • Several new example processing notebooks in notebooks/

  • A new argument date to Altimetry.predefined_temporal_filter_atl06sr, which can be used to pass the capture date of the scene for filtering. Previously, the date was read from metadata, but that only works for WorldView right now.

  • A new flag to the asp_plot CLI: --icesat_filter_date, which passes the YYYY-MM-DD formatted date to the icesat filtering method

Fixed#

  • Previously, when void pixels were contained in the detailed mapprojected subset images in the detailed hillshade plots, the entire subset plot would appear blank. This is fixed by masking no data values and calculating the color ranges excluding them.

  • Similarly, the disparity maps were also improperly showing data void areas. This is fixed by better handling of void areas during the disparity map calculations and plotting.

[1.2.1] - 2025-10-19#

Added#

  • New notebooks/Mars_MOC example

Fixed#

  • Added a regular hillshade fallback to StereoPlotter.plot_detailed_hillshade() for the case where *-IntersectionErr.tif was not produced and is not available for detailed hillshade plots.

Internal#

  • Extracted common hillshade plotting logic in StereoPlotter to utility function.

[1.2.0] - 2025-10-12#

Added#

  • Support for non-terrestrial (planetary) ASP processing, tested with Lunar Reconnaissance Orbiter (LRO) Narrow Angle Camera (NAC) data

  • New --plot_geometry CLI flag to optionally skip stereo geometry plots (default: True)

  • New --subset_km CLI flag to configure hillshade subset size in kilometers (default: 1.0 km)

  • Example notebook for LRO NAC processing in notebooks/LRO_NAC/

  • Detection and handling of non-georeferenced (raw, non-map-projected) raster data

Changed#

  • API change: ScenePlotter.plot_orthos() renamed to ScenePlotter.plot_scenes() for sensor-agnostic naming

  • ScenePlotter no longer depends on StereopairMetadataParser, making it compatible with non-Earth sensors

  • Scene plots now automatically detect and display whether images are map-projected or raw

  • Scene plot titles now show filenames instead of Earth-specific metadata (catalog ID, GSD)

  • Raster.transform property now returns None for non-georeferenced images (identity transform) instead of identity Affine

  • Suppressed NotGeoreferencedWarning when opening non-georeferenced rasters

  • Match points plot clarification text updated: “scenes shown only if mapprojected”

Removed#

  • StereoPlotter.is_mapprojected() method - replaced with simpler Raster.transform check

Internal#

  • Simplified map-projection detection logic using Raster.transform is None check

[1.1.1] - 2025-10-10#

Changed#

  • Moved existing example notebooks into WorldView sub-directory, since we plan to introduce other sensors and we’d like to keep things separated in our examples.

Fixed#

  • While moving and re-running notebooks, it was noted that Altimetry.plot_atl06 had a bug when plot_dem=True. The rasterio.plot.show was improperly imported. This is properly imported now.

[1.1.0] - 2025-10-03#

Added#

  • downsample parameter to Raster class for efficient downsampled reading

  • Lazy-loaded data property on Raster class using @property decorator

  • save_raster() static method for flexible raster saving with reference metadata

  • Optional save parameter (default False) to compute_difference() method

  • _calculate_downsampled_shape() private method for modular downsampling logic

  • Comprehensive test suite for Raster and ColorBar classes (21 new tests in test_utils.py)

  • Explicit rioxarray dependency to environment.yml (was previously an implicit dependency via geoutils)

Changed#

  • Refactored Raster class to remove dependency on geoutils

  • load_and_diff_rasters() now uses rioxarray for efficient reprojection and cropping (matching geoutils behavior with simpler implementation)

  • compute_difference() no longer saves by default (use save=True to enable)

  • Difference rasters are now cropped to the intersection of both input rasters (matching geoutils behavior)

  • Updated altimetry.py to use native rasterio plotting instead of geoutils

Removed#

  • Dependency on geoutils (>=0.1.9)

  • Dependency on xdem (was unused)

Internal#

  • Extracted downsampling logic into reusable private method

  • Added properties for data and transform with lazy loading

  • Improved separation of concerns between data loading and file I/O

[1.0.2] - 2025-08-09#

Fixed#

  • Small typo csm_camera CLI help text

  • Improper passing of map_crs into csm_camera CLI tool fixed

  • Sometimes while trimming linescan cameras to only the rows of image capture in the csm_camera utilities, the indices of first and last collection line are reversed. I think this has to do with ascending versus descending orbits, but I didn’t investigate deeply. I did add a conditional check to the responsible function to switch the index slicing in this case.

[1.0.1] - 2025-04-27#

Improved#

  • Added comprehensive docstrings throughout the codebase for better code documentation

[1.0.0] - 2025-04-27#

This is the first stable release of asp_plot. While it was previously available as a pre-1.0 package, this release marks a commitment to proper versioning and documentation.

Added#

  • New stereo_geom command-line tool for visualizing stereo geometry

  • Added comprehensive docstrings to CLI tools

  • Created this CHANGELOG file for better tracking of changes

  • Added support for multiple XML files with automatic mosaicking via dg_mosaic

Fixed#

  • Fixed subprocess handling in stereopair_metadata_parser.py for multiple XML file processing

[0.5.10] - 2024-XX-XX#

Combined beta release of asp_plot since version 0.0.1, before a proper change log was established.

Added#

  • Initial public release of asp_plot

  • Support for bundle adjust visualization

  • Support for stereo visualization

  • Report generation capabilities

  • CSM camera plot tool