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,0disables). 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 0is passed. 200 dpi rather than the ~150 dpi Ghostscript’s/ebookpreset 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’simage()takes aPIL.Image, so this adds no dependency and writes no temp files.docs/fetch_example_reports.sh, which downloads the example reports intodocs/_static/reports/at docs-build time. It pins oneREPORTS_RELEASEtag, so a docs build of an older commit fetches the reports of its era, and usescurl -fso 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/*.pdfis gitignored andgit rm --cacheded; the reports are published as assets on a datedreports-<date>GitHub Release in this repository, together with the*_figure_selections.ymlsidecars that generated them. A dated tag rather than the per-version releases, becauserelease.ymlcreates a release perpyproject.tomlbump and the reports do not change per version..readthedocs.yaml’scp reports/*.pdfstep becomesbash 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 ascontent-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.ymlno longer picks areports-<date>tag as the previous version. Its previous-tag step wasgit describe --tags --abbrev=0, which returns the most recent tag reachable fromHEAD— and the reports releases are tagged onmain, so the next version release would have linked a changelog comparingreports-2026-09-18...v3.4.0. Now--match 'v*'. The tag-exists check greps forv<version>and was never affected.StereoFilesno longer crashes on a stereo directory with no top-level*-L.tif(#202). The constructor passed the result ofglob_file()straight toRaster()to decide whether the run was mapprojected, so a missing left image raisedTypeError: expected str, bytes or os.PathLike object, not NoneTypeinstead 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.tifwhen*-L.tifand*-D.tifhave 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 lostL.tifandD.tifin 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.pytakes{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, columnsSTATS_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.tifpoint2demwrites next to each*-DEM.tif(NaN for a mosaic, which has none), altimetry-minus-DEM n / median / NMAD / RMSE before and after a per-DEMpc_align --compute-translation-onlywith 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. OneAltimetryper DEM is kept inbench.altimetry[label], so the usual per-DEM figures (histogram_by_landcover(),mapview_plot_atl06sr_to_dem()) can be drawn for any candidate.pc_alignproducts 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 nopc_aligncall and works offline; a missingpc_alignbinary 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_benchmarkcommand wrapping it: positional DEMs as paths orLABEL=PATH(an ASPrun-DEM.tifis labelled by its folder),--parquetfor Earth or--altimetry-csvfor the Moon/Mars,--reference,--no-pc-align,--own-extent,--title, and--directory/--output-directory/--output-filename; writes the summary figure, a_histogram.pngtwin, and the stats table as CSV, and prints the table. Documented indocs/cli/dem_benchmark.mdand added to the CLI index (seven tools). The conda-forge recipe gains the entry point and its--helptest command (the autotick bot does not sync those).The Atlanta MVS notebook scores six DEMs on one ICESat-2 sample with
DEMBenchmarkin place of its ad hoc two-DEM loop: the 3-scene MVS run, the 3-pairdem_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, anddem_mosaicaverages 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. Afterpc_alignevery 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), fivedem_mosaicblends 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 propagatedVerticalStdDev— 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 withDEMBenchmarkon 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_mosaicblends: 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. Twoasp_plotfixes 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 frombundle_adjustmatch files, and aStereoPlottercannot be built once a run’sL.tifhas been cleaned up.
Changed#
Alignment.apply_dem_translation()accepts anoutput_fnfor 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 byatl06sr_to_dem_dh(),planetary_to_dem_dh()andfilter_outliers(), and so by every report and byDEMBenchmark). 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_sigmakeeps 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()andplanetary_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_adjustruns, 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 wrotetriangulation_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 thebundle_adjustfolder itself: the per-camera*.adjusttranslation + rotation,camera_offsets.txt(ASP 3.7.0’s “change in camera positions” report) andtriangulation_offsets.txtassociated to cameras positionally throughcamera_list.txt, and each camera’s absolute position from its*.adjusted_state.jsonor, for DigitalGlobe runs that write only.adjustdeltas, from the original.xmlephemeris. No original camera files are needed, unlikecsm_camera_plot. NewReadBundleAdjustCamerasandPlotBundleAdjustCamerasinbundle_adjust.py, a newbundle_adjust_camerascommand (--directory= the BA folder;--map-crs,--original-cameras-directory,--title,--output-directory,--output-filename), and a new “Camera Changes from Bundle Adjustment” page in theasp_reportPDF right after the residual pages whenever--bundle-adjust-prefixis 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 indocs/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 thepc_alignlog (issue #146): theInput stats (meters):/Output stats (meters):lines becomemean_beg/end,stddev_beg/end,rmse_beg/end,median_beg/end,nmad_beg/endalongside the existing percentiles and translation, and flow intoAltimetry.alignment_report_df. The alignment report page (ICESat-2 and LOLA/MOLA) now showsMedian,NMADandRMSEbefore/after alignment in place of the 16/50/84 percentiles, with the column description updated;mean/stddevstay 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 inreports/are regenerated with the new page.The
pc_alignlog 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, onex1 y1 unc1 x2 y2 unc2line per match), andparallel_stereo/bundle_adjust --matches-as-txtwrite it instead of.match— so a stereo directory produced with that switch previously got the “missing match file” placeholder on the match-point page.StereoFilesnow 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 likemy__run),StereoPlotter.get_match_point_df()reads either format into the samex1/y1/x2/y2DataFrame, and the.vwipinterest-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.csvcache a binary conversion leaves behind. A text twin of the raw-image fixture, converted withipmatch --binary-to-txt(ASP 3.8.0-alpha), is committed so the two readers are checked against each other..vwipfiles 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-hillshadepair) — 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-yplot/addprefixes are dropped from the switch names.--bundle_adjust_directoryis now--bundle-adjust-prefix, and accepts either the containing directory (ba, the previous behavior) or the same ASP-style output prefix passed tostereo/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_directoryis renamed tobundle_adjust_prefix.csm_camera_plotoptions are harmonized with the other CLIs and ASP’sorbit_plot.py:--save_dir→--output-directory,--fig_fn→--output-filename,--figsize→--figure-size. Thecsm_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_mosaicr100concern from the same issue was resolved earlier by the sensor readers, which treat*.r100.xml/*.r50.xmlas regenerable intermediates.
Removed#
The deprecated
--plot_icesatalias (deprecated in favor of--plot_altimetryin 1.10.0) is gone; altimetry is on by default and--no-altimetrydisables 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_adjustcamera-comparison example in the UCSD WorldView notebook. The only committedcsm_camera_summary_plot()example was ajitter_solverun, whose per-segment corrections oscillate along the image.notebooks/WorldView/worldview_spacenet_ucsd_stereo.ipynbnow also compares the original and adjusted cameras from its ownbundle_adjustrun, 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, matchingba/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. Becausebundle_adjustwrites CSM state only for the optimized cameras, the notebook also documents how to produce the unadjusted one: re-run with a 4x4 identity--initial-transformand--apply-initial-transform-only.Raw per-image interest points (
.vwip) are overlaid on the match point figure (#8). When the.vwipfiles 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 examplebundle_adjustmoves 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; passingshared_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’sorbit_plot.pydoes — a central difference of that camera’s own ephemeris. That is fine for plotting one camera, but it corrupts a difference between two:bundle_adjustandjitter_solveboth 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 Uyunijitter_solvepair 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.exrrather 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}.txtand failing withFileNotFoundError.Reconstructed
mapprojectcommands now re-run grid-identically on ASP >= 3.7.0 (#148). The--t_projwinreconstructed 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 itsPixelIsPointhalf-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.TXTsidecar, including Cartosat-1’s*_RPC_ORG.TXTvariant, which GDAL does not pick up on its own andsensors/rpc.pytherefore 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 atHEIGHT_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 workingstereo_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’ ownMEANSATAZto 0.01°,MEANSATELandMEANOFFNADIRVIEWANGLEto 0.15°,MEANPRODUCTGSDto 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_dfis None, sun angles and cloud cover are NaN,datecomes from the image header (NITFIDATIM, TIFFDateTime) 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 afallback: 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_astercamera XML records no timestamps, attitude, view/sun angles, or footprint corners — sosensors/aster.pyis the first reader that derives its scene dict instead of parsing it. Intersecting eachWORLD_SIGHT_VECTORlook ray (from itsSAT_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 workingstereo_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_dfis None (the orientation and covariance panels now say so rather than raising), sun angles and cloud cover are NaN,eph_gdfis indexed by image line rather than time, anddateis recovered from a neighbouringAST_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 Falseexisted 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.pyadds the two DIMAP v1-family readers ASP supports —Spot5Metadata(mirroringSPOT_XML.cc, thespot5session) andPrismMetadata(mirroringPRISM_XML.cc, gated onMETADATA_PROFILE == "ALOS"exactly as ASP is) — sharing a base for what the formats have in common: theMetadata_Idheader,Dataset_Framecorner footprints, andEphemeris/Points/Pointtrajectories. Neither format reports quaternions, soatt_dfgrows a second shape: time-indexedroll/pitch/yawin degrees (converted from radians for SPOT 5) alongside the existing scalar-lastq1..q4, withattrs["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 andRz Ry Rxconvention, 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 intests/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 ownread_attitudes_1A1B(PleiadesXML.cc) andget_camera_pose_at_time(LinescanPleiadesModel.cc) — and normalizes the result, so 1A/1B scenes yield the same tabulated scalar-lastatt_dfas every other sensor and the roll/pitch/yaw plots work unchanged. SPOT 6/7 (S6_SENSOR/S7_SENSOR) and PeruSat-1 (PER1_SENSOR, singleLocated_Geometric_Valuesblock 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_gdfentirely, and the plots handle it (#177). ASTER establishedatt_df = Nonefor 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 readeph_gdfwith.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.jsoncameras still mean “nothing to scope to”.Sensor detection is content-based (#162). The WorldView reader previously claimed any XML that wasn’t named
*ortho*/READMEand 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 insensors/base.py— deduplicating the shallow-then-recursive discovery pattern the two readers previously repeated. WorldView requires the<isd>root plus theIMD/EPH/ATTblocks (mirroring ASP’s ownRPC_XML.ccrequirements; the root alone would still claim ASP’sgen_asterASTER XMLs, which share it), anddg_mosaicoutputs still pass. The DIMAP reader additionally requires a supportedMETADATA_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_mosaiccan strip image tags and Multi (multispectral) products carry per-band TDI rather than a singleTDILEVEL, but the reader previously crashed on any missing summary tag. The scene-dict schema is now formalized insensors/base.pyas a required identity core (xml_fn,catid,sensor,date,geom— still read strictly) plus optional fields (OPTIONAL_SCENE_FIELDS) that land asNone(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()andget_intersection_bounds()fall back to the footprint union for non-overlapping pairs (previouslyAttributeError/TypeErroronNone), andpair_dict()/get_title()tolerate scenes without timestamps (cdate/dtbecome None, rendered “N/A”).asp_plot/sensors.pyis now theasp_plot/sensors/package (#168). Pure reorganization as groundwork for broader sensor support: theSensorMetadataABC and shared helpers move tosensors/base.py, the WorldView reader tosensors/worldview.py, the Airbus DIMAP reader tosensors/dimap.py, and theSENSORSregistry plus thesensor_for_directory()/sensor_for_inputs()/resolve_xml_inputs()entry points tosensors/__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 calledparser.get_id_dict()/parser.xml2poly()from before those methods moved fromStereopairMetadataParserto the sensor readers (the correct call isparser.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.ipynbruns again (#182). It calledget_pair_utm_epsg(),get_scene_bounds()andget_intersection_bounds()onStereoGeometryPlotter; all three live onStereopairMetadataParser, which the plotter has composed rather than inherited since #25, so the notebook raisedAttributeErroron 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/StereoFilesnow detect the layout (find_pair_directories()) and resolve each pair’sN-L_sub.tif/N-R_sub.tif, match file, alignment matrices, andN-D_sub.tif/N-D.tif;plot_scenes(),plot_match_points(), andplot_disparity()render one figure per pair, labeledPair N: <reference> ↔ <image>(image names recovered from the pair’sN-stereo.defaultconfig copy), and return the saved filename list the waystereo_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 toStereoGeometryPlotter(inputs=...); directory-based discovery remains the fallback when the command names fewer than two metadata files (e.g. CSM.jsoncameras) or they cannot be found on disk.Airbus Pléiades / Pléiades Neo (DIMAP) support (#155). A new
PleiadesMetadatasensor 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 theLocated_Geometric_Valuesgrid, ECEF ephemeris with per-point times, and attitude quaternions reordered from the Airbus scalar-firstQ0layout to the scalar-last convention shared with WorldView.RPC_*.XMLsidecars are filtered out byMETADATA_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 newdetect_satellite_attribution()— which returns the rights-holder name ("Vantor"or"Airbus DS") and replaces theis_vantorbool (detect_vantor_satellite()remains as a backward-compatible wrapper) — andget_acquisition_dates()falls back to the DIMAP refined-model start time whenFIRSTLINETIMEis 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 crashedStereoFilesdiscovery. 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_plotis 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 allasp_reportnow; 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). Noasp_plotalias 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: replaceasp_plotwithasp_reportin 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.ipynbnotebook processes three same-pass SpaceNet Atlanta WorldView-2 scenes (chosen with the retained scene-selection notebook) throughwv_correct→ 5-scenebundle_adjust→ 3-scene multi-viewparallel_stereo→point2dem, and compares the multi-view DEM against the ASP-docs-recommended alternative — the three pairwise stereo runs merged withdem_mosaic— on coverage, DEM difference, and ICESat-2 residuals. A matchingWorldView_Atlanta_MVSreport 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 tostereo_geom_plot()(#155). Thedg_(DigitalGlobe) prefix predated multi-sensor support and was wrong for Pléiades; the new name matches thestereo_geomCLI and its*_stereo_geom.pngoutputs. No back-compat alias.
Fixed#
The Pléiades notebook now reaches the docs build (#159).
.readthedocs.yamlnever copiednotebooks/Pleiades/*.ipynbintodocs/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_plotgeneralizes 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_geommakes 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.XMLand 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 viadg_mosaic.Flexible CLI inputs (#152).
stereo_geomtakes positionalINPUTSthat 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--directoryflag is retained as the fallback, so existing usage is unchanged. Newresolve_xml_inputs()/sensor_for_inputs()/SensorMetadata.detect_files()plumbing and aWorldViewMetadata(image_list=...)constructor back this, withinputs=threaded throughStereopairMetadataParserandStereoGeometryPlotter.N-scene multi-view assessment.
stereo_geomis 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 asN/A. The parser gainsget_pair_dicts()(all combinations),get_scenes_centroid_projection(), andget_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 aNoneintersection. 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#
© Vantorattribution now covers all Vantor-owned satellites, not just WorldView (#137). The copyright-overlay check (detect_vantor_satellite) matched onlySATIDvalues starting withWV, 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 aVANTOR_SATID_PREFIXESwhitelist (WVincl. WorldView LegionWVLG,GE,QB,IK). This clarifies thatis_vantor/detect_vantor_satelliteare an attribution concern (named for the company), intentionally distinct from sensor/reader identity (the WorldView-named abstraction insensors.py); the two names are documented as deliberately different so they aren’t reconciled into one.
[1.18.0] - 2026-06-25#
Added#
Reconstruct
mapprojectcommands in the PDF report (#96). ASP’smapprojectdoes not write a log file the waybundle_adjust/stereo/point2demdo, so the processing-parameters page never documented the mapprojection step. Rather than depend on a new ASP--logflag, the newasp_plot/mapproject.pyreconstructs the command from the output GeoTIFF metadata alone: ASP stampsINPUT_IMAGE_FILE/CAMERA_FILE/DEM_FILE/CAMERA_MODEL_TYPE/BUNDLE_ADJUST_PREFIXinto 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 amapprojectkey (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 (thestereo/+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 frozenBodydataclass +BODIESregistry in a newasp_plot/bodies.py(altimetry instrument, IAU sphere radius,pc_aligndatum, geocentric PROJ string, geographic CRS WKT, ellipsoid fallback).alignment.py, the altimetry sources, the CLI, andutils.pynow readbody.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-freerun_report(config): aReportConfigdataclass packs the options, aREPORT_SECTIONSregistry ofReportSpecs (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(SensorMetadataABC +WorldViewMetadatareader +SENSORSregistry) separates sensor-specific scene discovery/extraction from the sensor-agnostic pair geometry;StereoGeometryPlotternow composes aStereopairMetadataParserinstead of inheriting it.Plotterscaffold + file-discovery separation (#129). ThePlotterbase gainssave()/plot_missing()/ copyright-awareplot_array(), and newStereoFiles/SceneFilesdiscovery classes own theglob_filelogic that was duplicated across plotters.Altimetry god-class split (#130, #140). The 3800-line
Altimetryclass splits into a thin coordinator (altimetry.py) composingIcesat2Source(icesat2_source.py), planetary sources (planetary_source.py), andAltimetryPlotter(altimetry_plots.py), with shared DEM-sampling / outlier-mask / CSV-writer machinery in anAltimetrySourcebase (altimetry_source.py). Planetary loading graduates to per-bodyLolaSource/MolaSourcesubclasses dispatched from the DEM body at construction. The publicasp_plot.altimetryAPI and re-exports are preserved by delegation.csm_camera.pysplit (#131). The 1541-line module splits intocsm_io.py(ASP-mirrored camera-model readers),csm_analysis.py(the asp_plot-specific analysis), andcsm_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(AspLogFormatadapter keyed by ASP version +AspLogreader) replaces the hardcoded string surgery inprocessing_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_plotCLI now writes a<report_stem>_figure_selections.ymlsidecar next to the report recording every non-deterministic selection, and a new--reuse_selections PATHflag replays a prior run’s choices so figures are directly comparable.New
asp_plot/selections.pymodule (FigureSelectionsdataclass + YAML read/write + clip-box ↔ pixel-window + CRS-reprojection helpers), deliberately free ofreport.py/fpdfimports so it is safe to use from notebooks.StereoPlotter.plot_detailed_hillshade()gains aclip_windows(+clip_windows_crs) kwarg and records the boxes it drew onself.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.Altimetryreuses the exact prior ICESat-2 points viaload_atl06sr_from_parquet(), pins the profile track (rgt/cycle/spot) and best/worst segments (segments=) throughplot_atl06sr_dem_profile()/plot_best_worst_segments(), and reports its choices viaget_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/configuses an SSO/login provider, botocore raisedMissingDependencyException: Using the login credential provider requires an additional dependency ... botocore[crt], aborting the entireasp_plotreport 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 theasp_plotCLI (orAltimetry.align_and_evaluate()) from a directory other than the dataset directory left a strayatl06sr_for_pc_align_<key>.csvin the cwd. The output path is now rooted atself.directoryviaos.path.join(), matching every other output in the class (_save_to_parquet, thepc_alignoutputs, and the planetary twinto_csv_for_pc_align_planetary()). No consumer changes were needed — the single internal caller (align_and_evaluate()) uses the return value directly, andAlignment.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
GalleryPlotterclass (asp_plot/gallery.py) andgalleryCLI 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 legacyoriginal_code/gallery.pyinto the modular package, dropping itspygeotools/imviewdependencies in favor of the existingRaster,Plotter, andColorBarutilities.DEMs are rendered with the package’s standard convention (gray hillshade underlay + semi-transparent
viridisDEM); 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()returnedNonefor compound / 3D-promoted CRSs (e.g."EPSG:32610+EPSG:4979", as written by stereopipeline-quickstart’sfetch_cop_dem.pyto assert ellipsoid heights on the COP30 DEM). PROJ represents such a CRS as a UTM CRS “promoted to 3D” with no exact EPSG match, sorasterio’sto_epsg()yieldsNoneand downstreamf"EPSG:{epsg}"strings crash (e.g. passing the DEM asdem_fntoAltimetry, orRaster.get_bounds(latlon=True)). Now falls back to the EPSG code of the horizontal (2D) component viapyproj’sCRS.to_2d().
[1.14.0] - 2026-04-28#
Added#
Automatic
pc_alignstep in the planetary altimetry block (#119). The existing--pc_alignCLI flag now also runs against MOLA (Mars) and LOLA (Moon) — previously Earth/ICESat-2 only. Mirrors the Earth pipeline: a single alignment-report page oninsufficient_points/no_improvement, plus a pre/post mapview and pre/post histogram onsuccess.Altimetry.align_and_evaluate_planetary(...): planetary sibling ofalign_and_evaluate. Returns the sameAlignmentResultdataclass; defaultsmax_displacement=500m (per ASAP-Stereo’s CTX cookbook) andminimum_points=20(planetary tracks are sparse).Alignment.pc_align_dem_to_planetary_csv(...): invokes ASPpc_alignwith--csv-format '1:lon 2:lat 3:radius_m'and--datum D_MARS/D_MOON(aligned with the ASPnext_stepsdocumentation on MOLA alignment).Altimetry.to_csv_for_pc_align_planetary(): writeslon, lat, radius_mfromself.planetary_pointsto drivepc_align.plot_alignedkwarg onAltimetry.mapview_plot_planetary_to_demandAltimetry.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.0andMOON_IAU_SPHERE_RADIUS = 1_737_400.0so 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.csvand computesheight = 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 thatpc_aligncannot 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 (afterpc_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) carriesPt_Radiusin 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 bothheight(m above the IAU 1737.4 km lunar sphere) andradius_mtoself.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_PROJdict — Earth usesEPSG:4978; Mars/Moon use PROJ strings (+proj=geocent +R=...) because PROJ refuses to convert across celestial bodies. Without this fix, applying apc_aligntranslation to a Mars/Moon DEM raisedRuntimeError: Source and target ellipsoid do not belong to the same celestial body.planetary_to_dem_dh()also samples the aligned DEM whenself.aligned_dem_fnis set, populatingaligned_dem_heightandaltimetry_minus_aligned_demso 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.ipynbcovering both stereo variants of the M0100115 / E0201461 pair (themars_mgs_orbital_camera_narrow_angle.ipynbnotebook 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 affineepipolarfor non-mapprojected,--alignment-method nonefor mapprojected viacam2map4stereo.py). The notebook intro includes a callout explaining the spherical-vs-oblate elevation-range surprise. Reports:MOC-asp-plot-report.pdfandMOC_mapproj-asp-plot-report.pdf.LRO NAC notebook reprocessed on the full 5000×5000 cubes in
LRONAC_example.tarinstead 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 meaningfulpc_alignand to bring out spacecraft jitter in the disparity panels.
[1.13.0] - 2026-04-20#
Added#
Automatic
pc_alignstep in the Earth altimetry block, gated by a new--pc_alignCLI flag (defaultTrue; disabled automatically when--plot_altimetry/--plot_icesatisFalse). Runspc_alignagainst 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_aligndoes 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%, andpc_alignactually 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 plainAlignmentResultdataclass (status ∈ {"insufficient_points", "no_improvement", "success"},alignment_report_df,aligned_dem_fn,improvement_pct,message,parameters_used). Does not import anyfpdf/ report dependencies, so it is safe to call from notebooks.plot_alignedkwargs onAltimetry.histogram_by_landcoverandAltimetry.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 unaligneddhso segments are comparable), overlays aligned DEM heights on each segment, and appends aligned Median/NMAD to the segment titles.
AlignmentReportPagedataclass inasp_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 lowerdhpanel 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=Falsebehavior 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 inutils.py: readsFIRSTLINETIMEfrom WorldView/Maxar XMLs and parses the capture timestamp fromAST_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_filenameaccepts 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_rangeCLI option now accepts"all"(default),"auto"(XML metadata ±time_buffer_days),"START,END", or a single date (buffered). Programmatic API:_resolve_time_range()andrequest_atl06sr_multi_processing()take a newtime_rangeparameter ("all"or"buffered") with cascade:t0/t1>scene_date> XML metadata > fall back to"all".t1is truncated to midnight UTC for stable parquet caching.ESA WorldCover sampled locally from AWS S3 COGs via
rasteriovsicurl instead of through the slow SlideRulesamplesparameter. WorldCover is now sampled insiderequest_atl06sr_multi_processingbefore 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 toesa-copernicus-30meter).3σ outlier filter applied by default in
atl06sr_to_dem_dh(andplanetary_to_dem_dh) using the true mean ± 3·standard deviation (not NMAD). Passn_sigma=Noneto 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)to3·|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 onsliderule.session).
Added#
filter_outliers()method: removes dh points beyondn_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 insiderequest_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.mdexplaining the three modes.
Fixed#
Parquet cache regeneration bug: SlideRule mutates the
parmsdict by injecting a random temp file path atoutput.pathduringrun(), causing the string comparison to fail on every subsequent run.outputis now stripped from both sides of the comparison and from stored parameters.Parquet cache error swallowing: the broad
try/exceptaround 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) withax.set_xlim(), so all data is plotted and used in stats.Single-date CLI argument now uses
scene_datebuffering instead of being treated as a start date.COP30 SlideRule asset name corrected (
esa-copernicus-30meter, notcop30-dem).
Dependencies#
Pinned
sliderule>=5.3.0to 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 instereopair_metadata_parser.pyfor 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_rangeCLI 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/t1parameters onAltimetry.request_atl06sr_multi_processing()andAltimetry._resolve_time_range()for programmatic useNew WorldView-3 UCSD example notebook (
worldview_spacenet_ucsd_stereo.ipynb) using publicly available IARPA CORE3D data, with comprehensive stereopair selection analysisExample report:
WorldView_UCSD-asp-plot-report.pdf
Fixed#
Alignment.pc_align_report()andAlignment.apply_dem_translation()now returnNonegracefully when pc_align log files are not found, instead of crashing withTypeErrorAltimetry.alignment_report()handles missing pc_align results with a warning instead of crashingkey_for_aligned_demparameter inAltimetry.alignment_report()now defaults to theprocessing_levelvalue 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_altimetryCLI tool to submit async LOLA/MOLA data requests with email notification, saving request metadata toaltimetry_request_info.ymlNew
--plot_altimetryflag on theasp_plotCLI with automatic body detection (Earth → ICESat-2, Moon → LOLA, Mars → MOLA)New
--altimetry_csvflag to pass a pre-downloaded LOLA/MOLA*_topo_csv.csvfile for planetary altimetry plotsdetect_planetary_body()utility function: detects Earth/Moon/Mars from DEM CRS WKTget_planetary_bounds()utility function: converts DEM bounds to planetocentric 0-360 lon/lat for GDS queriesAltimetry.load_planetary_csv(): loads LOLA or MOLA CSV with column validation and helpful error messagesAltimetry.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 overlayAltimetry.histogram_planetary_to_dem(): dh histogram with n/median/NMAD statisticsLazy SlideRule initialization:
Altimetry.__init__no longer requires an internet connection; SlideRule is initialized on first ICESat-2 method callLOLA/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_icesatis now a deprecated alias for--plot_altimetry(prints deprecation warning if used)Basemaps are automatically skipped for non-Earth DEMs
pyyamladded 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 behaviorReport command string recorded in PDF report via new
report_commandparameter incompile_report()Pixel-unit scalebar for non-mapprojected disparity plots (mapprojected scenes continue to use GSD-based scalebar)
Guard with
FileNotFoundErrorwhen alignment matrix files are missing for non-mapprojected match point overlayWarning 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 toNone(uses figure’s own creation DPI), fixing pixelated ICESat-2 report figuresICESat-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 spacesCleaned up example notebook report links and removed stale PDF files
Removed unnecessary
read_align_matrix()method; alignment matrices are loaded inline vianp.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 plottingNew
histogram_by_landcover()method producing a histogram of ICESat-2 vs DEM differences with per-landcover-class statistics (count, median, NMAD) using ESA WorldCoverNew
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 overlayServer-side time filtering for SlideRule API requests via new
_resolve_time_range()method with three-tier cascade: explicitscene_dateparameter, auto-detect from stereopair XML metadata, or 2-year fallbackscene_dateandtime_buffer_daysparameters added torequest_atl06sr_multi_processing()Module-level
ICESAT2_MISSION_STARTconstant andWORLDCOVER_NAMESdictionary for reuse across methodsModule-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-seriessliderule_api.run("atl03x")with automatic index and column normalizationSimplified 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 histogramsReport 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_dateCLI 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 referencesWORLDCOVER_NAMES)
Fixed#
TypeError: Cannot subtract tz-naive and tz-aware datetime-like objectsinpredefined_temporal_filter_atl06srwhen scene date is UTC-aware but DataFrame index is tz-naiveKeyError: 'translation_magnitude'inalignment_report()when requested processing level has no data (now returns early with a warning)TypeError: unhashable type: 'numpy.ndarray'inhistogram_by_landcovercaused by parquet round-trip deserializing arrays as Python listsTypeError: 'int' object is not callablewhen builtinlen()was shadowed by thelen=40parameter insiderequest_atl06sr_multi_processingOverflowError: cannot convert float infinity to integerin 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 tagsadd_copyright_overlay()utility for matplotlib axesProcessingParameters.get_asp_version()method to extract ASP version from log filesRaster._mask_nodata()private helper to consolidate nodata/invalid value maskingRaster._load_and_diff_rasters_da()private static method returning xarray DataArray for raster differencing
Changed#
Raster.get_bounds()now usesself.ds.bounds(rasterio) instead of opening a redundant rioxarray datasetRaster.compute_difference()usesrio.to_raster()for saving whensave=True, avoiding manual profile constructionStereoPlotter.plot_detailed_hillshade()reuses existingraster.ds.transforminstead of reopening the DEM fileConsolidated 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-plotnow installs all deps)
Changed#
Replaced deprecated
actions/create-release@v1withsoftprops/action-gh-release@v2in release workflowAdded 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.pymodule containingReportSectionandReportMetadatadataclasses,ASPReportPDFclass, andcompile_report()functionDEM 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-pdfdependency withfpdf2(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.pyto dedicatedreport.pymodulePNG 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()andgetAtt_df()methods onStereopairMetadataParser, mirroring the existing ephemeris parsingNew
satellite_position_orientation_plot()method onStereoGeometryPlotterproducing a 3x2 figure showing position covariance, roll/pitch/yaw orientation, and attitude covariance for each sceneAttitude data (
att_df) now included in catalog ID dictionaries returned byget_catid_dicts()
Changed#
Ephemeris covariance columns in
getEphem_gdf()renamed fromx_cov, y_cov, ...tocov_11, cov_12, cov_13, cov_22, cov_23, cov_33for 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/latitudeNew
Raster.get_utm_epsg_code()method for estimating UTM zone from raster locationNew
StereopairMetadataParsermethods: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.ipynbtoworldview_utqiagvik_stereo.ipynb.
Fixed#
Fixed
geodiffcommand 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-demflag was not used in bundle_adjust: geodiff plots are now skipped with a warning instead of causing the entire bundle adjustment section to failFixed 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
datetoAltimetry.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_plotCLI:--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_MOCexample
Fixed#
Added a regular hillshade fallback to
StereoPlotter.plot_detailed_hillshade()for the case where*-IntersectionErr.tifwas not produced and is not available for detailed hillshade plots.
Internal#
Extracted common hillshade plotting logic in
StereoPlotterto 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_geometryCLI flag to optionally skip stereo geometry plots (default: True)New
--subset_kmCLI 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 toScenePlotter.plot_scenes()for sensor-agnostic namingScenePlotterno longer depends onStereopairMetadataParser, making it compatible with non-Earth sensorsScene 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.transformproperty now returnsNonefor non-georeferenced images (identity transform) instead of identity AffineSuppressed
NotGeoreferencedWarningwhen opening non-georeferenced rastersMatch points plot clarification text updated: “scenes shown only if mapprojected”
Removed#
StereoPlotter.is_mapprojected()method - replaced with simplerRaster.transformcheck
Internal#
Simplified map-projection detection logic using
Raster.transform is Nonecheck
[1.1.1] - 2025-10-10#
Changed#
Moved existing example notebooks into
WorldViewsub-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_atl06had a bug whenplot_dem=True. Therasterio.plot.showwas improperly imported. This is properly imported now.
[1.1.0] - 2025-10-03#
Added#
downsampleparameter toRasterclass for efficient downsampled readingLazy-loaded
dataproperty onRasterclass using@propertydecoratorsave_raster()static method for flexible raster saving with reference metadataOptional
saveparameter (defaultFalse) tocompute_difference()method_calculate_downsampled_shape()private method for modular downsampling logicComprehensive test suite for
RasterandColorBarclasses (21 new tests intest_utils.py)Explicit
rioxarraydependency toenvironment.yml(was previously an implicit dependency via geoutils)
Changed#
Refactored
Rasterclass to remove dependency ongeoutilsload_and_diff_rasters()now usesrioxarrayfor efficient reprojection and cropping (matching geoutils behavior with simpler implementation)compute_difference()no longer saves by default (usesave=Trueto enable)Difference rasters are now cropped to the intersection of both input rasters (matching geoutils behavior)
Updated
altimetry.pyto 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
dataandtransformwith lazy loadingImproved 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_geomcommand-line tool for visualizing stereo geometryAdded 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.pyfor 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_plotSupport for bundle adjust visualization
Support for stereo visualization
Report generation capabilities
CSM camera plot tool