asp-plot 1.13.0__tar.gz → 1.14.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {asp_plot-1.13.0 → asp_plot-1.14.0}/CHANGELOG.md +20 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/PKG-INFO +1 -1
- {asp_plot-1.13.0 → asp_plot-1.14.0}/asp_plot/alignment.py +108 -2
- {asp_plot-1.13.0 → asp_plot-1.14.0}/asp_plot/altimetry.py +503 -127
- {asp_plot-1.13.0 → asp_plot-1.14.0}/asp_plot/cli/asp_plot.py +116 -2
- {asp_plot-1.13.0 → asp_plot-1.14.0}/pyproject.toml +1 -1
- {asp_plot-1.13.0 → asp_plot-1.14.0}/.flake8 +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/.github/workflows/release.yml +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/.github/workflows/run-tests.yml +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/.gitignore +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/.pre-commit-config.yaml +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/.readthedocs.yaml +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/LICENSE +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/README.md +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/asp_plot/__init__.py +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/asp_plot/bundle_adjust.py +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/asp_plot/cli/__init__.py +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/asp_plot/cli/csm_camera_plot.py +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/asp_plot/cli/request_planetary_altimetry.py +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/asp_plot/cli/stereo_geom.py +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/asp_plot/csm_camera.py +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/asp_plot/processing_parameters.py +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/asp_plot/report.py +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/asp_plot/scenes.py +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/asp_plot/stereo.py +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/asp_plot/stereo_geometry.py +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/asp_plot/stereopair_metadata_parser.py +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/asp_plot/utils.py +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/conda-forge-recipe/meta.yaml +0 -0
- {asp_plot-1.13.0 → asp_plot-1.14.0}/environment.yml +0 -0
|
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.14.0] - 2026-04-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Automatic `pc_align` step in the planetary altimetry block** ([#119](https://github.com/uw-cryo/asp_plot/pull/119)). The existing `--pc_align` CLI flag now also runs against MOLA (Mars) and LOLA (Moon) — previously Earth/ICESat-2 only. Mirrors the Earth pipeline: a single alignment-report page on `insufficient_points` / `no_improvement`, plus a pre/post mapview and pre/post histogram on `success`.
|
|
12
|
+
- **`Altimetry.align_and_evaluate_planetary(...)`**: planetary sibling of `align_and_evaluate`. Returns the same `AlignmentResult` dataclass; defaults `max_displacement=500` m (per ASAP-Stereo's CTX cookbook) and `minimum_points=20` (planetary tracks are sparse).
|
|
13
|
+
- **`Alignment.pc_align_dem_to_planetary_csv(...)`**: invokes ASP `pc_align` with `--csv-format '1:lon 2:lat 3:radius_m'` and `--datum D_MARS`/`D_MOON` (aligned with the [ASP `next_steps` documentation on MOLA alignment](https://stereopipeline.readthedocs.io/en/latest/next_steps.html)).
|
|
14
|
+
- **`Altimetry.to_csv_for_pc_align_planetary()`**: writes `lon, lat, radius_m` from `self.planetary_points` to drive `pc_align`.
|
|
15
|
+
- **`plot_aligned` kwarg** on `Altimetry.mapview_plot_planetary_to_dem` and `Altimetry.histogram_planetary_to_dem`: pre/post panels share color/bin scales when an aligned DEM is available.
|
|
16
|
+
- **Module-level constants** `MARS_IAU_SPHERE_RADIUS = 3_396_190.0` and `MOON_IAU_SPHERE_RADIUS = 1_737_400.0` so callers can reconstruct ASP-style "height above sphere" without magic numbers.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **MOLA loader switched to `PLANET_RAD`**. `_load_mola_csv()` now reads the absolute planetary radius from the ODE GDS `*_pts_csv.csv` and computes `height = PLANET_RAD - 3,396,190` (IAU 2000 Mars sphere). The `*_topo_csv.csv` (TOPOGRAPHY only) is **rejected** with an explanatory error: TOPOGRAPHY is referenced to the **oblate** MOLA areoid while ASP DEMs use the **spherical** IAU 2000 datum, so dh from TOPOGRAPHY carries a latitude-dependent offset of up to ~10 km that `pc_align` cannot remove. Verified on the MOC NA tutorial scene at lat 34°N: signed median dh dropped from +6,000 m (TOPOGRAPHY path) → +99.74 m (PLANET_RAD path) → +3.13 m (after `pc_align`). Reference: [MOLA PEDR Software Interface Specification (PDS Geosciences)](https://pds-geosciences.wustl.edu/mgs/mgs-m-mola-3-pedr-l1a-v1/mgsl_21xx/document/pedrsis.pdf).
|
|
20
|
+
- **LOLA loader prefers `Pt_Radius` (km) when available**. The Point per Row LOLA RDR CSV (`results=p`) carries `Pt_Radius` in **kilometers**; the simple Topography CSV (`results=u`) carries Topography in meters. `_load_lola_csv()` auto-detects km by magnitude (< 10 000) and converts to meters, then writes both `height` (m above the IAU 1737.4 km lunar sphere) and `radius_m` to `self.planetary_points`. The Moon is essentially spherical (~1.4 km equatorial-vs-polar variation), so either CSV gives the same dh to ~1 m. Reference: [ODE GDS REST V2.0 manual](https://oderest.rsl.wustl.edu/GDS_REST_V2.0.pdf).
|
|
21
|
+
- **`Alignment.apply_dem_translation()` is body-aware**. Picks a body-centered geocentric "ECEF-equivalent" CRS from a new module-level `_GEOCENTRIC_PROJ` dict — Earth uses `EPSG:4978`; Mars/Moon use PROJ strings (`+proj=geocent +R=...`) because PROJ refuses to convert across celestial bodies. Without this fix, applying a `pc_align` translation to a Mars/Moon DEM raised `RuntimeError: Source and target ellipsoid do not belong to the same celestial body`.
|
|
22
|
+
- **`planetary_to_dem_dh()` also samples the aligned DEM** when `self.aligned_dem_fn` is set, populating `aligned_dem_height` and `altimetry_minus_aligned_dem` so pre/post plots share a single sample. Refactored shared interpolation into `_sample_dem_at_planetary_points()`.
|
|
23
|
+
|
|
24
|
+
### Documentation
|
|
25
|
+
- **MOC NA notebook consolidated** into `notebooks/Mars_MGS/mars_mgs_orbital_camera.ipynb` covering both stereo variants of the M0100115 / E0201461 pair (the `mars_mgs_orbital_camera_narrow_angle.ipynb` notebook for a different scene pair was removed). Mirrors the ASTER mapproj/non-mapproj layout. Stereo commands match this repo's WorldView convention (`parallel_stereo --stereo-algorithm asp_mgm --subpixel-mode 9 --processes 2 --threads 4`, `--alignment-method affineepipolar` for non-mapprojected, `--alignment-method none` for mapprojected via `cam2map4stereo.py`). The notebook intro includes a callout explaining the spherical-vs-oblate elevation-range surprise. Reports: `MOC-asp-plot-report.pdf` and `MOC_mapproj-asp-plot-report.pdf`.
|
|
26
|
+
- **LRO NAC notebook reprocessed on the full 5000×5000 cubes** in [`LRONAC_example.tar`](https://github.com/NeoGeographyToolkit/StereoPipelineSolvedExamples/releases/download/LRONAC/LRONAC_example.tar) instead of the 900×973 sub-window the ASP "lightning fast" tutorial uses. Resulting DEM is 4720×4510 at 1.04 m GSD (~4.7 km × 4.7 km, vs the old ~1 km × 1 km), 95.88% valid pixels. LOLA query expanded to match: 1539 of 2044 LOLA points overlap the DEM (vs 12 of 19 on the old crop), enough for a meaningful `pc_align` and to bring out spacecraft jitter in the disparity panels.
|
|
27
|
+
|
|
8
28
|
## [1.13.0] - 2026-04-20
|
|
9
29
|
|
|
10
30
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asp_plot
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.14.0
|
|
4
4
|
Summary: Package for plotting outputs Ames Stereo Pipeline processing
|
|
5
5
|
Project-URL: Homepage, https://github.com/uw-cryo/asp_plot
|
|
6
6
|
Project-URL: Documentation, https://asp-plot.readthedocs.io
|
|
@@ -5,7 +5,23 @@ import re
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
from osgeo import gdal, osr
|
|
7
7
|
|
|
8
|
-
from asp_plot.utils import
|
|
8
|
+
from asp_plot.utils import (
|
|
9
|
+
Raster,
|
|
10
|
+
detect_planetary_body,
|
|
11
|
+
glob_file,
|
|
12
|
+
run_subprocess_command,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Body-centered geocentric ("ECEF-equivalent") CRS for each supported
|
|
16
|
+
# planet. Used by apply_dem_translation to convert pc_align's Cartesian
|
|
17
|
+
# translation vector into the DEM's projected coordinate system. The
|
|
18
|
+
# Earth case is keyed via EPSG; planets need a PROJ string because PROJ
|
|
19
|
+
# refuses to operate across celestial bodies.
|
|
20
|
+
_GEOCENTRIC_PROJ = {
|
|
21
|
+
"earth": None, # use EPSG:4978
|
|
22
|
+
"moon": "+proj=geocent +R=1737400 +units=m +no_defs",
|
|
23
|
+
"mars": "+proj=geocent +R=3396190 +units=m +no_defs",
|
|
24
|
+
}
|
|
9
25
|
|
|
10
26
|
logging.basicConfig(level=logging.WARNING)
|
|
11
27
|
logger = logging.getLogger(__name__)
|
|
@@ -134,6 +150,86 @@ class Alignment:
|
|
|
134
150
|
|
|
135
151
|
run_subprocess_command(command)
|
|
136
152
|
|
|
153
|
+
def pc_align_dem_to_planetary_csv(
|
|
154
|
+
self,
|
|
155
|
+
planetary_csv,
|
|
156
|
+
body,
|
|
157
|
+
max_displacement=500,
|
|
158
|
+
max_source_points=10000000,
|
|
159
|
+
alignment_method="point-to-point",
|
|
160
|
+
output_prefix="pc_align/pc_align",
|
|
161
|
+
):
|
|
162
|
+
"""
|
|
163
|
+
Align DEM to MOLA/LOLA point cloud using pc_align.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
planetary_csv : str
|
|
168
|
+
Path to a CSV with ``lon``, ``lat``, ``radius_m`` columns
|
|
169
|
+
(planetary radius in meters from the body center). Produced by
|
|
170
|
+
:meth:`asp_plot.altimetry.Altimetry.to_csv_for_pc_align_planetary`.
|
|
171
|
+
body : str
|
|
172
|
+
``"moon"`` or ``"mars"`` — selects the ASP ``--datum`` flag.
|
|
173
|
+
max_displacement : float, optional
|
|
174
|
+
Maximum expected displacement in meters. ASAP-Stereo's CTX
|
|
175
|
+
cookbook recommends ~500 m as a generic default for planetary
|
|
176
|
+
stereo where absolute pointing is uncertain.
|
|
177
|
+
max_source_points : int, optional
|
|
178
|
+
Maximum number of source DEM points pc_align will sample.
|
|
179
|
+
alignment_method : str, optional
|
|
180
|
+
ASP alignment method. Default ``point-to-point``.
|
|
181
|
+
output_prefix : str, optional
|
|
182
|
+
Prefix for pc_align output files, relative to ``self.directory``.
|
|
183
|
+
|
|
184
|
+
Notes
|
|
185
|
+
-----
|
|
186
|
+
Uses ``--csv-format '1:lon 2:lat 3:radius_m'`` so pc_align reads
|
|
187
|
+
the absolute planetary radius and avoids any datum/areoid
|
|
188
|
+
ambiguity. ``--datum`` is set to ``D_MARS`` (3,396,190 m) or
|
|
189
|
+
``D_MOON`` (1,737,400 m) to match ASP DEM heights, which are
|
|
190
|
+
stored as ``radius - sphere``.
|
|
191
|
+
"""
|
|
192
|
+
if not os.path.exists(planetary_csv):
|
|
193
|
+
raise ValueError(
|
|
194
|
+
f"\nPlanetary altimetry CSV not found: {planetary_csv}\n"
|
|
195
|
+
"Use Altimetry.to_csv_for_pc_align_planetary() to create it.\n"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
datum = {"moon": "D_MOON", "mars": "D_MARS"}.get(body)
|
|
199
|
+
if datum is None:
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"Unsupported body for pc_align_dem_to_planetary_csv: {body}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
pc_align_folder = os.path.join(self.directory, output_prefix)
|
|
205
|
+
|
|
206
|
+
print(
|
|
207
|
+
f"Running pc_align on {self.dem_fn} and {planetary_csv}\n"
|
|
208
|
+
f" --datum {datum}, --max-displacement {max_displacement}\n"
|
|
209
|
+
f"Writing to {pc_align_folder}*"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
command = [
|
|
213
|
+
"pc_align",
|
|
214
|
+
"--max-displacement",
|
|
215
|
+
str(max_displacement),
|
|
216
|
+
"--max-num-source-points",
|
|
217
|
+
str(max_source_points),
|
|
218
|
+
"--alignment-method",
|
|
219
|
+
alignment_method,
|
|
220
|
+
"--csv-format",
|
|
221
|
+
"1:lon 2:lat 3:radius_m",
|
|
222
|
+
"--datum",
|
|
223
|
+
datum,
|
|
224
|
+
"--compute-translation-only",
|
|
225
|
+
"--output-prefix",
|
|
226
|
+
pc_align_folder,
|
|
227
|
+
self.dem_fn,
|
|
228
|
+
planetary_csv,
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
run_subprocess_command(command)
|
|
232
|
+
|
|
137
233
|
def pc_align_report(self, output_prefix="pc_align/pc_align"):
|
|
138
234
|
"""
|
|
139
235
|
Extract alignment statistics from pc_align log files.
|
|
@@ -281,8 +377,18 @@ class Alignment:
|
|
|
281
377
|
llz_c = llz_c[i]
|
|
282
378
|
llz_shift = llz_shift[i]
|
|
283
379
|
|
|
380
|
+
# pc_align's Cartesian translation lives in the body-centered
|
|
381
|
+
# frame defined by --datum, so the source CRS must match the
|
|
382
|
+
# body of the DEM. PROJ refuses to convert between celestial
|
|
383
|
+
# bodies, so use a body-specific geocentric PROJ string for
|
|
384
|
+
# Mars/Moon and EPSG:4978 for Earth.
|
|
385
|
+
body = detect_planetary_body(self.dem_fn)
|
|
284
386
|
ecef_srs = osr.SpatialReference()
|
|
285
|
-
|
|
387
|
+
proj_string = _GEOCENTRIC_PROJ.get(body)
|
|
388
|
+
if proj_string is None:
|
|
389
|
+
ecef_srs.ImportFromEPSG(4978)
|
|
390
|
+
else:
|
|
391
|
+
ecef_srs.ImportFromProj4(proj_string)
|
|
286
392
|
|
|
287
393
|
s_srs = ecef_srs
|
|
288
394
|
src_c = ecef_c
|
|
@@ -25,6 +25,11 @@ logger = logging.getLogger(__name__)
|
|
|
25
25
|
|
|
26
26
|
ICESAT2_MISSION_START = datetime(2018, 10, 14, tzinfo=timezone.utc)
|
|
27
27
|
|
|
28
|
+
# IAU 2000 mean equatorial radii used by ASP DEMs and pc_align.
|
|
29
|
+
# ASP DEM heights are stored as (planetary_radius - sphere_radius).
|
|
30
|
+
MARS_IAU_SPHERE_RADIUS = 3_396_190.0 # meters
|
|
31
|
+
MOON_IAU_SPHERE_RADIUS = 1_737_400.0 # meters
|
|
32
|
+
|
|
28
33
|
WORLDCOVER_NAMES = {
|
|
29
34
|
10: "Tree cover",
|
|
30
35
|
20: "Shrubland",
|
|
@@ -1379,7 +1384,16 @@ class Altimetry:
|
|
|
1379
1384
|
]
|
|
1380
1385
|
_LAT_CANDIDATES = ["pt_latitude", "lat_north", "latitude", "areocentric_latitude"]
|
|
1381
1386
|
_TOPO_CANDIDATES = ["topography", "topo"]
|
|
1382
|
-
|
|
1387
|
+
# PLANET_RAD column names from ODE GDS PEDR (Mars) and LOLA RDR
|
|
1388
|
+
# "Point Per Row" CSV (Moon, has Pt_Radius). Order matters — names
|
|
1389
|
+
# earlier in the list are preferred.
|
|
1390
|
+
_RADIUS_CANDIDATES = [
|
|
1391
|
+
"planet_rad",
|
|
1392
|
+
"planet_rad (shot_planetary_radius)",
|
|
1393
|
+
"pt_radius",
|
|
1394
|
+
"planetary_radius",
|
|
1395
|
+
"radius",
|
|
1396
|
+
]
|
|
1383
1397
|
|
|
1384
1398
|
# Geographic CRS WKT strings for building GeoDataFrames
|
|
1385
1399
|
_MOON_GEO_CRS = 'GEOGCRS["Moon",DATUM["D_MOON",ELLIPSOID["MOON",1737400,0]],PRIMEM["Reference_Meridian",0],CS[ellipsoidal,2],AXIS["latitude",north,ORDER[1],ANGLEUNIT["degree",0.0174532925199433]],AXIS["longitude",east,ORDER[2],ANGLEUNIT["degree",0.0174532925199433]]]'
|
|
@@ -1406,10 +1420,13 @@ class Altimetry:
|
|
|
1406
1420
|
return cols_lower[c]
|
|
1407
1421
|
return None
|
|
1408
1422
|
|
|
1409
|
-
def _load_planetary_csv_common(self, csv_path, instrument):
|
|
1423
|
+
def _load_planetary_csv_common(self, csv_path, instrument, prefer="radius"):
|
|
1410
1424
|
"""Shared CSV loading logic for LOLA and MOLA.
|
|
1411
1425
|
|
|
1412
1426
|
Reads the CSV, validates columns, converts longitude to -180/180.
|
|
1427
|
+
Picks PLANET_RAD over TOPOGRAPHY by default (``prefer="radius"``)
|
|
1428
|
+
because TOPOGRAPHY is referenced to the oblate areoid and produces
|
|
1429
|
+
a latitude-dependent offset against ASP's spherical-IAU DEMs.
|
|
1413
1430
|
|
|
1414
1431
|
Parameters
|
|
1415
1432
|
----------
|
|
@@ -1417,12 +1434,14 @@ class Altimetry:
|
|
|
1417
1434
|
Path to the CSV file.
|
|
1418
1435
|
instrument : str
|
|
1419
1436
|
``"LOLA"`` or ``"MOLA"`` (for error messages).
|
|
1437
|
+
prefer : {"radius", "topo"}
|
|
1438
|
+
Which column family to try first.
|
|
1420
1439
|
|
|
1421
1440
|
Returns
|
|
1422
1441
|
-------
|
|
1423
|
-
tuple of (pandas.DataFrame,
|
|
1442
|
+
tuple of (pandas.DataFrame, bool)
|
|
1424
1443
|
(df with ``lon``, ``lat``, ``height_raw`` columns,
|
|
1425
|
-
|
|
1444
|
+
``is_radius`` flag — True if the chosen column is a planetary radius)
|
|
1426
1445
|
"""
|
|
1427
1446
|
df = pd.read_csv(csv_path)
|
|
1428
1447
|
|
|
@@ -1436,21 +1455,24 @@ class Altimetry:
|
|
|
1436
1455
|
|
|
1437
1456
|
lon_col = self._find_csv_column(cols_lower, self._LON_CANDIDATES)
|
|
1438
1457
|
lat_col = self._find_csv_column(cols_lower, self._LAT_CANDIDATES)
|
|
1439
|
-
topo_col = self._find_csv_column(cols_lower, self._TOPO_CANDIDATES)
|
|
1440
1458
|
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1459
|
+
if prefer == "radius":
|
|
1460
|
+
primary = self._find_csv_column(cols_lower, self._RADIUS_CANDIDATES)
|
|
1461
|
+
fallback = self._find_csv_column(cols_lower, self._TOPO_CANDIDATES)
|
|
1462
|
+
is_radius = primary is not None
|
|
1463
|
+
else:
|
|
1464
|
+
primary = self._find_csv_column(cols_lower, self._TOPO_CANDIDATES)
|
|
1465
|
+
fallback = self._find_csv_column(cols_lower, self._RADIUS_CANDIDATES)
|
|
1466
|
+
is_radius = primary is None and fallback is not None
|
|
1467
|
+
|
|
1468
|
+
height_col = primary if primary is not None else fallback
|
|
1446
1469
|
|
|
1447
1470
|
if lon_col is None or lat_col is None or height_col is None:
|
|
1448
1471
|
raise ValueError(
|
|
1449
1472
|
f"{instrument} CSV does not have expected columns.\n"
|
|
1450
1473
|
f" Found: {list(df.columns)}\n"
|
|
1451
|
-
f" Expected longitude, latitude, and
|
|
1452
|
-
f"
|
|
1453
|
-
f"ODE GDS download, not the '*_pts_csv.csv' or label file."
|
|
1474
|
+
f" Expected longitude, latitude, and either a planetary "
|
|
1475
|
+
f"radius (PLANET_RAD / Pt_Radius) or topography column."
|
|
1454
1476
|
)
|
|
1455
1477
|
|
|
1456
1478
|
df = df.rename(
|
|
@@ -1463,15 +1485,44 @@ class Altimetry:
|
|
|
1463
1485
|
return df, is_radius
|
|
1464
1486
|
|
|
1465
1487
|
def _load_lola_csv(self, csv_path):
|
|
1466
|
-
"""Parse a LOLA
|
|
1488
|
+
"""Parse a LOLA topo CSV into a GeoDataFrame.
|
|
1489
|
+
|
|
1490
|
+
Stores height above the IAU 1737.4 km lunar sphere on
|
|
1491
|
+
``self.planetary_points["height"]`` and the planetary radius on
|
|
1492
|
+
``self.planetary_points["radius_m"]`` (used by pc_align).
|
|
1493
|
+
|
|
1494
|
+
The Moon is nearly spherical (1.4 km equatorial-vs-polar variation),
|
|
1495
|
+
so LOLA TOPOGRAPHY ≈ Pt_Radius − 1737.4 km. Either column gives the
|
|
1496
|
+
same dh against an ASP lunar DEM to <1 m.
|
|
1467
1497
|
|
|
1468
1498
|
Parameters
|
|
1469
1499
|
----------
|
|
1470
1500
|
csv_path : str
|
|
1471
|
-
Path to
|
|
1501
|
+
Path to a LOLA RDR CSV from the ODE GDS API.
|
|
1472
1502
|
"""
|
|
1473
|
-
df,
|
|
1474
|
-
|
|
1503
|
+
df, is_radius = self._load_planetary_csv_common(
|
|
1504
|
+
csv_path, "LOLA", prefer="radius"
|
|
1505
|
+
)
|
|
1506
|
+
|
|
1507
|
+
if is_radius:
|
|
1508
|
+
# LOLA RDR Pt_Radius is in KILOMETERS (per the ODE GDS Point
|
|
1509
|
+
# per Row CSV header), unlike MOLA PEDR PLANET_RAD which is
|
|
1510
|
+
# in meters. Detect units by magnitude (~1737 vs ~1737000)
|
|
1511
|
+
# and normalize to meters.
|
|
1512
|
+
if df["height_raw"].median() < 10000:
|
|
1513
|
+
df["height_raw"] = df["height_raw"] * 1000.0
|
|
1514
|
+
unit_note = " (km → m)"
|
|
1515
|
+
else:
|
|
1516
|
+
unit_note = ""
|
|
1517
|
+
df["height"] = df["height_raw"] - MOON_IAU_SPHERE_RADIUS
|
|
1518
|
+
df["radius_m"] = df["height_raw"]
|
|
1519
|
+
source = f"Pt_Radius{unit_note}"
|
|
1520
|
+
else:
|
|
1521
|
+
# Topography path: the Moon is essentially spherical so LOLA
|
|
1522
|
+
# topography ≈ height above the IAU 1737.4 km sphere.
|
|
1523
|
+
df["height"] = df["height_raw"]
|
|
1524
|
+
df["radius_m"] = df["height_raw"] + MOON_IAU_SPHERE_RADIUS
|
|
1525
|
+
source = "Topography"
|
|
1475
1526
|
|
|
1476
1527
|
gdf = gpd.GeoDataFrame(
|
|
1477
1528
|
df,
|
|
@@ -1479,62 +1530,89 @@ class Altimetry:
|
|
|
1479
1530
|
crs=self._MOON_GEO_CRS,
|
|
1480
1531
|
)
|
|
1481
1532
|
self.planetary_points = gdf
|
|
1482
|
-
print(f"Loaded {len(gdf)} LOLA points")
|
|
1533
|
+
print(f"Loaded {len(gdf)} LOLA points (using {source} column)")
|
|
1483
1534
|
|
|
1484
1535
|
def _load_mola_csv(self, csv_path):
|
|
1485
1536
|
"""Parse a MOLA PEDR CSV into a GeoDataFrame.
|
|
1486
1537
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1538
|
+
Uses the PLANET_RAD column and converts to height above the IAU
|
|
1539
|
+
Mars sphere (3,396,190 m): ``height = PLANET_RAD - 3,396,190``.
|
|
1540
|
+
This is the same reference as ASP DEMs, so dh = MOLA − DEM is
|
|
1541
|
+
directly meaningful at all latitudes.
|
|
1542
|
+
|
|
1543
|
+
TOPOGRAPHY (in ``*_topo_csv.csv``) is referenced to the MOLA
|
|
1544
|
+
oblate areoid and produces a latitude-dependent offset of up to
|
|
1545
|
+
~10 km against an ASP DEM, so it is rejected here. Pass the
|
|
1546
|
+
``*_pts_csv.csv`` from the ODE GDS download instead.
|
|
1494
1547
|
|
|
1495
1548
|
Parameters
|
|
1496
1549
|
----------
|
|
1497
1550
|
csv_path : str
|
|
1498
|
-
Path to
|
|
1551
|
+
Path to a MOLA PEDR CSV from the ODE GDS API. Must include
|
|
1552
|
+
a ``PLANET_RAD`` column (use the ``*_pts_csv.csv`` file).
|
|
1499
1553
|
"""
|
|
1500
|
-
df, is_radius = self._load_planetary_csv_common(
|
|
1554
|
+
df, is_radius = self._load_planetary_csv_common(
|
|
1555
|
+
csv_path, "MOLA", prefer="radius"
|
|
1556
|
+
)
|
|
1501
1557
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
"(
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
# These are different vertical datums, so a systematic offset
|
|
1513
|
-
# (equal to the local areoid height) will be present in the
|
|
1514
|
-
# dh values. This is a known limitation — correcting it
|
|
1515
|
-
# requires the MOLA areoid grid or ASP's `dem_geoid --geoid MOLA`.
|
|
1516
|
-
df["height"] = df["height_raw"]
|
|
1517
|
-
print(
|
|
1518
|
-
"Note: MOLA topography is referenced to the MOLA areoid. "
|
|
1519
|
-
"ASP DEMs use the IAU sphere. A systematic vertical offset "
|
|
1520
|
-
"may be present in dh values."
|
|
1558
|
+
if not is_radius:
|
|
1559
|
+
raise ValueError(
|
|
1560
|
+
f"MOLA CSV is missing the PLANET_RAD column: {csv_path}\n"
|
|
1561
|
+
"The ODE GDS '*_topo_csv.csv' only has TOPOGRAPHY, which is "
|
|
1562
|
+
"height above the oblate MOLA areoid. ASP DEMs use the "
|
|
1563
|
+
"spherical IAU 2000 datum (3,396,190 m), so dh from "
|
|
1564
|
+
"TOPOGRAPHY carries a latitude-dependent offset of up to "
|
|
1565
|
+
"~10 km that pc_align cannot remove.\n\n"
|
|
1566
|
+
"Pass the '*_pts_csv.csv' file from the same ODE GDS "
|
|
1567
|
+
"download instead — it contains PLANET_RAD."
|
|
1521
1568
|
)
|
|
1522
1569
|
|
|
1570
|
+
df["height"] = df["height_raw"] - MARS_IAU_SPHERE_RADIUS
|
|
1571
|
+
df["radius_m"] = df["height_raw"]
|
|
1572
|
+
|
|
1523
1573
|
gdf = gpd.GeoDataFrame(
|
|
1524
1574
|
df,
|
|
1525
1575
|
geometry=gpd.points_from_xy(df["lon"], df["lat"]),
|
|
1526
1576
|
crs=self._MARS_GEO_CRS,
|
|
1527
1577
|
)
|
|
1528
1578
|
self.planetary_points = gdf
|
|
1529
|
-
print(
|
|
1579
|
+
print(
|
|
1580
|
+
f"Loaded {len(gdf)} MOLA points "
|
|
1581
|
+
f"(PLANET_RAD - {MARS_IAU_SPHERE_RADIUS:.0f} m → height above IAU sphere)"
|
|
1582
|
+
)
|
|
1583
|
+
|
|
1584
|
+
def _sample_dem_at_planetary_points(self, dem_fn, height_col, dh_col):
|
|
1585
|
+
"""Interpolate DEM heights at the loaded altimetry points.
|
|
1586
|
+
|
|
1587
|
+
Adds ``height_col`` and ``dh_col`` (= altimetry height − DEM height)
|
|
1588
|
+
to ``self.planetary_points`` in-place. Used by
|
|
1589
|
+
:meth:`planetary_to_dem_dh` for both the raw and aligned DEMs.
|
|
1590
|
+
"""
|
|
1591
|
+
if self.planetary_points is None or self.planetary_points.empty:
|
|
1592
|
+
return
|
|
1593
|
+
|
|
1594
|
+
dem = rioxarray.open_rasterio(dem_fn, masked=True).squeeze()
|
|
1595
|
+
dem_crs = dem.rio.crs
|
|
1596
|
+
|
|
1597
|
+
pts = self.planetary_points.to_crs(dem_crs)
|
|
1598
|
+
x = xr.DataArray(pts.geometry.x.values, dims="z")
|
|
1599
|
+
y = xr.DataArray(pts.geometry.y.values, dims="z")
|
|
1600
|
+
sample = dem.interp(x=x, y=y).values
|
|
1601
|
+
|
|
1602
|
+
self.planetary_points[height_col] = sample
|
|
1603
|
+
self.planetary_points[dh_col] = self.planetary_points["height"] - sample
|
|
1530
1604
|
|
|
1531
1605
|
def planetary_to_dem_dh(self, n_sigma=3):
|
|
1532
1606
|
"""Compute height differences between planetary altimetry and DEM.
|
|
1533
1607
|
|
|
1534
1608
|
Reprojects ``self.planetary_points`` to the DEM CRS, interpolates
|
|
1535
1609
|
DEM heights at altimetry locations, and computes the difference
|
|
1536
|
-
``altimetry_minus_dem = height - dem_height``.
|
|
1537
|
-
``
|
|
1610
|
+
``altimetry_minus_dem = height - dem_height``. When
|
|
1611
|
+
``self.aligned_dem_fn`` is set, also populates
|
|
1612
|
+
``aligned_dem_height`` and ``altimetry_minus_aligned_dem`` so
|
|
1613
|
+
pre/post-alignment plots can share a single sample. Outliers
|
|
1614
|
+
beyond ``n_sigma`` × std from the mean (computed on the
|
|
1615
|
+
unaligned dh) are removed by default.
|
|
1538
1616
|
|
|
1539
1617
|
Parameters
|
|
1540
1618
|
----------
|
|
@@ -1548,23 +1626,15 @@ class Altimetry:
|
|
|
1548
1626
|
logger.warning("No planetary altimetry points loaded.")
|
|
1549
1627
|
return
|
|
1550
1628
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
pts["dem_height"] = sample.values
|
|
1562
|
-
pts["altimetry_minus_dem"] = pts["height"] - pts["dem_height"]
|
|
1563
|
-
|
|
1564
|
-
# Update geometry back to geographic CRS for storage
|
|
1565
|
-
self.planetary_points = pts.to_crs(self.planetary_points.crs)
|
|
1566
|
-
self.planetary_points["dem_height"] = pts["dem_height"].values
|
|
1567
|
-
self.planetary_points["altimetry_minus_dem"] = pts["altimetry_minus_dem"].values
|
|
1629
|
+
self._sample_dem_at_planetary_points(
|
|
1630
|
+
self.dem_fn, "dem_height", "altimetry_minus_dem"
|
|
1631
|
+
)
|
|
1632
|
+
if self.aligned_dem_fn:
|
|
1633
|
+
self._sample_dem_at_planetary_points(
|
|
1634
|
+
self.aligned_dem_fn,
|
|
1635
|
+
"aligned_dem_height",
|
|
1636
|
+
"altimetry_minus_aligned_dem",
|
|
1637
|
+
)
|
|
1568
1638
|
|
|
1569
1639
|
valid = self.planetary_points["altimetry_minus_dem"].dropna()
|
|
1570
1640
|
print(f"Computed dh for {len(valid)} of {len(self.planetary_points)} points")
|
|
@@ -1587,28 +1657,272 @@ class Altimetry:
|
|
|
1587
1657
|
f"(removed {n_before - n_after})"
|
|
1588
1658
|
)
|
|
1589
1659
|
|
|
1660
|
+
def to_csv_for_pc_align_planetary(self, filename_prefix="planetary_for_pc_align"):
|
|
1661
|
+
"""Export ``self.planetary_points`` to a CSV for pc_align.
|
|
1662
|
+
|
|
1663
|
+
Writes columns ``lon, lat, radius_m`` (planetary radius from the
|
|
1664
|
+
body center, in meters). Used as the ``planetary_csv`` argument
|
|
1665
|
+
to :meth:`asp_plot.alignment.Alignment.pc_align_dem_to_planetary_csv`.
|
|
1666
|
+
|
|
1667
|
+
Parameters
|
|
1668
|
+
----------
|
|
1669
|
+
filename_prefix : str, optional
|
|
1670
|
+
Prefix for the output CSV filename. Saved in
|
|
1671
|
+
``self.directory``.
|
|
1672
|
+
|
|
1673
|
+
Returns
|
|
1674
|
+
-------
|
|
1675
|
+
str
|
|
1676
|
+
Absolute path to the created CSV file.
|
|
1677
|
+
"""
|
|
1678
|
+
if self.planetary_points is None or self.planetary_points.empty:
|
|
1679
|
+
raise ValueError("No planetary altimetry points loaded.")
|
|
1680
|
+
if "radius_m" not in self.planetary_points.columns:
|
|
1681
|
+
raise ValueError(
|
|
1682
|
+
"planetary_points has no radius_m column. Call "
|
|
1683
|
+
"load_planetary_csv() first to populate it."
|
|
1684
|
+
)
|
|
1685
|
+
|
|
1686
|
+
# Drop dh-NaN rows so pc_align doesn't reject the file. We also
|
|
1687
|
+
# restrict to points that fall on the DEM (have a finite
|
|
1688
|
+
# dem_height) when available — pc_align can use them as the
|
|
1689
|
+
# reference cloud only if they overlap the source.
|
|
1690
|
+
df = self.planetary_points.copy()
|
|
1691
|
+
df["lon"] = df.geometry.x
|
|
1692
|
+
df["lat"] = df.geometry.y
|
|
1693
|
+
df = df[["lon", "lat", "radius_m"]].dropna()
|
|
1694
|
+
if df.empty:
|
|
1695
|
+
raise ValueError("No valid planetary altimetry points after dropping NaNs.")
|
|
1696
|
+
|
|
1697
|
+
csv_fn = os.path.join(self.directory, f"{filename_prefix}.csv")
|
|
1698
|
+
df.to_csv(csv_fn, header=True, index=False)
|
|
1699
|
+
return csv_fn
|
|
1700
|
+
|
|
1701
|
+
def align_and_evaluate_planetary(
|
|
1702
|
+
self,
|
|
1703
|
+
max_displacement=500,
|
|
1704
|
+
improvement_threshold_pct=5.0,
|
|
1705
|
+
min_translation_threshold=0.1,
|
|
1706
|
+
minimum_points=20,
|
|
1707
|
+
):
|
|
1708
|
+
"""Run pc_align against MOLA/LOLA and evaluate whether to keep it.
|
|
1709
|
+
|
|
1710
|
+
Mirrors :meth:`align_and_evaluate` (the ICESat-2 path) but for
|
|
1711
|
+
planetary altimetry. Requires :meth:`load_planetary_csv` to have
|
|
1712
|
+
been called so ``self.planetary_points`` is populated.
|
|
1713
|
+
|
|
1714
|
+
Decision logic:
|
|
1715
|
+
|
|
1716
|
+
1. If fewer than ``minimum_points`` valid planetary points are
|
|
1717
|
+
available, return ``status="insufficient_points"``.
|
|
1718
|
+
2. Otherwise compute
|
|
1719
|
+
``improvement_pct = (p50_beg - p50_end) / p50_beg * 100``. If
|
|
1720
|
+
``p50_end >= p50_beg`` or
|
|
1721
|
+
``improvement_pct <= improvement_threshold_pct`` or the
|
|
1722
|
+
translation magnitude is below ``min_translation_threshold ×
|
|
1723
|
+
DEM GSD``, delete the aligned DEM and return
|
|
1724
|
+
``status="no_improvement"``.
|
|
1725
|
+
3. Otherwise re-run :meth:`planetary_to_dem_dh` to populate
|
|
1726
|
+
``altimetry_minus_aligned_dem`` and return ``status="success"``.
|
|
1727
|
+
|
|
1728
|
+
Parameters
|
|
1729
|
+
----------
|
|
1730
|
+
max_displacement : float, optional
|
|
1731
|
+
``--max-displacement`` for pc_align, in meters. Default 500
|
|
1732
|
+
(ASAP-Stereo's CTX cookbook recommendation).
|
|
1733
|
+
improvement_threshold_pct : float, optional
|
|
1734
|
+
Minimum p50 reduction (%) required to keep the aligned DEM.
|
|
1735
|
+
min_translation_threshold : float, optional
|
|
1736
|
+
Minimum translation magnitude as a fraction of the DEM GSD.
|
|
1737
|
+
minimum_points : int, optional
|
|
1738
|
+
Minimum number of valid altimetry points to attempt
|
|
1739
|
+
alignment. Planetary tracks are sparser than ICESat-2, so
|
|
1740
|
+
this defaults to a much smaller number than the Earth path.
|
|
1741
|
+
|
|
1742
|
+
Returns
|
|
1743
|
+
-------
|
|
1744
|
+
AlignmentResult
|
|
1745
|
+
"""
|
|
1746
|
+
from asp_plot.utils import detect_planetary_body
|
|
1747
|
+
|
|
1748
|
+
body = detect_planetary_body(self.dem_fn)
|
|
1749
|
+
instrument = {"moon": "LOLA", "mars": "MOLA"}.get(body, "Altimetry")
|
|
1750
|
+
|
|
1751
|
+
parameters_used = {
|
|
1752
|
+
"max_displacement": max_displacement,
|
|
1753
|
+
"minimum_points": minimum_points,
|
|
1754
|
+
"min_translation_threshold": min_translation_threshold,
|
|
1755
|
+
"improvement_threshold_pct": improvement_threshold_pct,
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
if self.planetary_points is None or self.planetary_points.empty:
|
|
1759
|
+
return AlignmentResult(
|
|
1760
|
+
status="insufficient_points",
|
|
1761
|
+
alignment_report_df=pd.DataFrame(),
|
|
1762
|
+
aligned_dem_fn=None,
|
|
1763
|
+
improvement_pct=None,
|
|
1764
|
+
message=(
|
|
1765
|
+
f"Alignment skipped: no {instrument} points loaded. "
|
|
1766
|
+
"Call load_planetary_csv() first."
|
|
1767
|
+
),
|
|
1768
|
+
parameters_used=parameters_used,
|
|
1769
|
+
)
|
|
1770
|
+
|
|
1771
|
+
# planetary_to_dem_dh both samples and 3σ-filters; require enough
|
|
1772
|
+
# points whose dh is finite (i.e. fall inside the DEM extent).
|
|
1773
|
+
if "altimetry_minus_dem" not in self.planetary_points.columns:
|
|
1774
|
+
self.planetary_to_dem_dh()
|
|
1775
|
+
n_valid = int(self.planetary_points["altimetry_minus_dem"].notna().sum())
|
|
1776
|
+
if n_valid < minimum_points:
|
|
1777
|
+
self._remove_aligned_dem_if_present()
|
|
1778
|
+
return AlignmentResult(
|
|
1779
|
+
status="insufficient_points",
|
|
1780
|
+
alignment_report_df=pd.DataFrame(),
|
|
1781
|
+
aligned_dem_fn=None,
|
|
1782
|
+
improvement_pct=None,
|
|
1783
|
+
message=(
|
|
1784
|
+
f"Alignment skipped: only {n_valid} {instrument} points "
|
|
1785
|
+
f"overlap the DEM (need >= {minimum_points})."
|
|
1786
|
+
),
|
|
1787
|
+
parameters_used=parameters_used,
|
|
1788
|
+
)
|
|
1789
|
+
|
|
1790
|
+
csv_fn = self.to_csv_for_pc_align_planetary(
|
|
1791
|
+
filename_prefix=f"{instrument.lower()}_for_pc_align"
|
|
1792
|
+
)
|
|
1793
|
+
|
|
1794
|
+
alignment = Alignment(self.directory, self.dem_fn)
|
|
1795
|
+
output_prefix = f"pc_align/pc_align_{instrument.lower()}"
|
|
1796
|
+
alignment.pc_align_dem_to_planetary_csv(
|
|
1797
|
+
planetary_csv=csv_fn,
|
|
1798
|
+
body=body,
|
|
1799
|
+
max_displacement=max_displacement,
|
|
1800
|
+
output_prefix=output_prefix,
|
|
1801
|
+
)
|
|
1802
|
+
|
|
1803
|
+
report = alignment.pc_align_report(output_prefix=output_prefix)
|
|
1804
|
+
if not report:
|
|
1805
|
+
self._remove_aligned_dem_if_present()
|
|
1806
|
+
return AlignmentResult(
|
|
1807
|
+
status="insufficient_points",
|
|
1808
|
+
alignment_report_df=pd.DataFrame(),
|
|
1809
|
+
aligned_dem_fn=None,
|
|
1810
|
+
improvement_pct=None,
|
|
1811
|
+
message=(
|
|
1812
|
+
"pc_align ran but produced no parseable log. Check "
|
|
1813
|
+
f"{output_prefix}-log-pc_align*.txt."
|
|
1814
|
+
),
|
|
1815
|
+
parameters_used=parameters_used,
|
|
1816
|
+
)
|
|
1817
|
+
|
|
1818
|
+
df = pd.DataFrame([{"key": instrument.lower(), **report}])
|
|
1819
|
+
|
|
1820
|
+
gsd = Raster(self.dem_fn).get_gsd()
|
|
1821
|
+
translation_too_small = (
|
|
1822
|
+
df["translation_magnitude"].iloc[0] < min_translation_threshold * gsd
|
|
1823
|
+
)
|
|
1824
|
+
|
|
1825
|
+
p50_beg = float(report.get("p50_beg", float("nan")))
|
|
1826
|
+
p50_end = float(report.get("p50_end", float("nan")))
|
|
1827
|
+
if not np.isfinite(p50_beg) or not np.isfinite(p50_end) or p50_beg == 0:
|
|
1828
|
+
improvement_pct = None
|
|
1829
|
+
else:
|
|
1830
|
+
improvement_pct = (p50_beg - p50_end) / p50_beg * 100.0
|
|
1831
|
+
|
|
1832
|
+
if (
|
|
1833
|
+
improvement_pct is None
|
|
1834
|
+
or p50_end >= p50_beg
|
|
1835
|
+
or improvement_pct <= improvement_threshold_pct
|
|
1836
|
+
or translation_too_small
|
|
1837
|
+
):
|
|
1838
|
+
self._remove_aligned_dem_if_present()
|
|
1839
|
+
improvement_repr = (
|
|
1840
|
+
f"{improvement_pct:.1f}%" if improvement_pct is not None else "n/a"
|
|
1841
|
+
)
|
|
1842
|
+
if translation_too_small:
|
|
1843
|
+
reason = (
|
|
1844
|
+
f"Translation magnitude is below {min_translation_threshold*100:.0f}% "
|
|
1845
|
+
f"of the DEM GSD ({gsd:.2f} m), so no aligned DEM was "
|
|
1846
|
+
f"written despite a {improvement_repr} p50 reduction."
|
|
1847
|
+
)
|
|
1848
|
+
else:
|
|
1849
|
+
reason = (
|
|
1850
|
+
f"p50 {p50_beg:.2f} m -> {p50_end:.2f} m, "
|
|
1851
|
+
f"{improvement_repr} <= {improvement_threshold_pct:.1f}% "
|
|
1852
|
+
"threshold."
|
|
1853
|
+
)
|
|
1854
|
+
return AlignmentResult(
|
|
1855
|
+
status="no_improvement",
|
|
1856
|
+
alignment_report_df=df,
|
|
1857
|
+
aligned_dem_fn=None,
|
|
1858
|
+
improvement_pct=improvement_pct,
|
|
1859
|
+
message=f"No significant improvement: {reason}",
|
|
1860
|
+
parameters_used=parameters_used,
|
|
1861
|
+
)
|
|
1862
|
+
|
|
1863
|
+
# Apply the translation and persist the aligned DEM
|
|
1864
|
+
aligned = alignment.apply_dem_translation(output_prefix=output_prefix)
|
|
1865
|
+
if aligned is None:
|
|
1866
|
+
self._remove_aligned_dem_if_present()
|
|
1867
|
+
return AlignmentResult(
|
|
1868
|
+
status="no_improvement",
|
|
1869
|
+
alignment_report_df=df,
|
|
1870
|
+
aligned_dem_fn=None,
|
|
1871
|
+
improvement_pct=improvement_pct,
|
|
1872
|
+
message=(
|
|
1873
|
+
"pc_align reported an improvement but the translation "
|
|
1874
|
+
"could not be applied. Aligned DEM not written."
|
|
1875
|
+
),
|
|
1876
|
+
parameters_used=parameters_used,
|
|
1877
|
+
)
|
|
1878
|
+
|
|
1879
|
+
self.aligned_dem_fn = aligned
|
|
1880
|
+
# Re-run planetary_to_dem_dh with n_sigma=None so we don't filter
|
|
1881
|
+
# the already-clean sample again.
|
|
1882
|
+
self.planetary_to_dem_dh(n_sigma=None)
|
|
1883
|
+
|
|
1884
|
+
return AlignmentResult(
|
|
1885
|
+
status="success",
|
|
1886
|
+
alignment_report_df=df,
|
|
1887
|
+
aligned_dem_fn=self.aligned_dem_fn,
|
|
1888
|
+
improvement_pct=improvement_pct,
|
|
1889
|
+
message=(
|
|
1890
|
+
f"p50 improved from {p50_beg:.2f} m -> {p50_end:.2f} m "
|
|
1891
|
+
f"({improvement_pct:.1f}% reduction). Aligned DEM written to "
|
|
1892
|
+
f"{self.aligned_dem_fn}."
|
|
1893
|
+
),
|
|
1894
|
+
parameters_used=parameters_used,
|
|
1895
|
+
)
|
|
1896
|
+
|
|
1590
1897
|
def mapview_plot_planetary_to_dem(
|
|
1591
1898
|
self,
|
|
1592
1899
|
clim=None,
|
|
1593
1900
|
save_dir=None,
|
|
1594
1901
|
fig_fn=None,
|
|
1595
1902
|
title=None,
|
|
1903
|
+
plot_aligned=False,
|
|
1596
1904
|
):
|
|
1597
1905
|
"""Map view of planetary altimetry vs DEM height differences.
|
|
1598
1906
|
|
|
1599
1907
|
Plots the DEM hillshade as background with altimetry dh points
|
|
1600
|
-
overlaid using a divergent colourmap.
|
|
1908
|
+
overlaid using a divergent colourmap. When ``plot_aligned=True``
|
|
1909
|
+
and ``self.aligned_dem_fn`` is set, renders pre/post panels side
|
|
1910
|
+
by side.
|
|
1601
1911
|
|
|
1602
1912
|
Parameters
|
|
1603
1913
|
----------
|
|
1604
1914
|
clim : tuple or None, optional
|
|
1605
|
-
Colour limits ``(min, max)`` for dh. Default auto
|
|
1915
|
+
Colour limits ``(min, max)`` for dh. Default auto (symmetric
|
|
1916
|
+
±|max| around zero).
|
|
1606
1917
|
save_dir : str or None, optional
|
|
1607
1918
|
Directory to save figure.
|
|
1608
1919
|
fig_fn : str or None, optional
|
|
1609
1920
|
Filename for saved figure.
|
|
1610
1921
|
title : str or None, optional
|
|
1611
1922
|
Custom plot title. Auto-detected if None.
|
|
1923
|
+
plot_aligned : bool, optional
|
|
1924
|
+
Add a second panel showing dh against the aligned DEM.
|
|
1925
|
+
Requires that pc_align has been run successfully.
|
|
1612
1926
|
"""
|
|
1613
1927
|
from asp_plot.utils import Raster, detect_planetary_body
|
|
1614
1928
|
|
|
@@ -1619,68 +1933,86 @@ class Altimetry:
|
|
|
1619
1933
|
if "altimetry_minus_dem" not in self.planetary_points.columns:
|
|
1620
1934
|
self.planetary_to_dem_dh()
|
|
1621
1935
|
|
|
1622
|
-
gdf = self.planetary_points.dropna(subset=["altimetry_minus_dem"])
|
|
1623
|
-
if gdf.empty:
|
|
1624
|
-
logger.warning("No valid dh values for map view.")
|
|
1625
|
-
return
|
|
1626
|
-
|
|
1627
|
-
dh = gdf["altimetry_minus_dem"]
|
|
1628
|
-
n = len(dh)
|
|
1629
|
-
med = np.nanmedian(dh.values)
|
|
1630
|
-
nmad = _nmad(dh.values)
|
|
1631
|
-
|
|
1632
1936
|
body = detect_planetary_body(self.dem_fn)
|
|
1633
1937
|
instrument = {"moon": "LOLA", "mars": "MOLA"}.get(body, "Altimetry")
|
|
1634
1938
|
if title is None:
|
|
1635
1939
|
title = f"{instrument} vs DEM"
|
|
1636
1940
|
|
|
1637
|
-
|
|
1638
|
-
|
|
1941
|
+
show_aligned = (
|
|
1942
|
+
plot_aligned
|
|
1943
|
+
and self.aligned_dem_fn
|
|
1944
|
+
and "altimetry_minus_aligned_dem" in self.planetary_points.columns
|
|
1945
|
+
)
|
|
1946
|
+
|
|
1947
|
+
# Generate hillshade — use the aligned DEM as the backdrop when
|
|
1948
|
+
# available so the post-alignment panel matches the dh sample.
|
|
1949
|
+
backdrop_dem = self.aligned_dem_fn if show_aligned else self.dem_fn
|
|
1950
|
+
dem_raster = Raster(backdrop_dem, downsample=4)
|
|
1639
1951
|
hs = dem_raster.hillshade()
|
|
1640
1952
|
extent = rioplot.plotting_extent(dem_raster.ds, transform=dem_raster.transform)
|
|
1641
|
-
|
|
1642
|
-
# Reproject points to DEM CRS for plotting
|
|
1643
1953
|
dem_crs = dem_raster.ds.crs
|
|
1644
|
-
gdf_proj = gdf.to_crs(dem_crs)
|
|
1645
1954
|
|
|
1646
|
-
|
|
1647
|
-
|
|
1955
|
+
# Build the symmetric ±|max| color limits across all visible
|
|
1956
|
+
# panels so they are directly comparable.
|
|
1957
|
+
gdf_unaligned = self.planetary_points.dropna(subset=["altimetry_minus_dem"])
|
|
1958
|
+
if gdf_unaligned.empty:
|
|
1959
|
+
logger.warning("No valid dh values for map view.")
|
|
1960
|
+
return
|
|
1961
|
+
dh_arrays = [gdf_unaligned["altimetry_minus_dem"].values]
|
|
1962
|
+
if show_aligned:
|
|
1963
|
+
dh_arrays.append(
|
|
1964
|
+
self.planetary_points["altimetry_minus_aligned_dem"].dropna().values
|
|
1965
|
+
)
|
|
1648
1966
|
|
|
1649
|
-
# Symmetric ±3σ centered on 0 (data is already 3σ-filtered in
|
|
1650
|
-
# planetary_to_dem_dh, so min/max ≈ ±3σ from the mean)
|
|
1651
1967
|
if clim is None:
|
|
1652
|
-
abs_max = max(
|
|
1968
|
+
abs_max = max(np.nanmax(np.abs(a)) for a in dh_arrays if a.size)
|
|
1653
1969
|
clim = (-abs_max, abs_max)
|
|
1654
|
-
cbar_label = f"{instrument} - DEM (m)\n[±3σ]"
|
|
1655
|
-
else:
|
|
1656
|
-
cbar_label = f"{instrument} - DEM (m)"
|
|
1657
|
-
|
|
1658
|
-
gdf_proj.plot(
|
|
1659
|
-
ax=ax,
|
|
1660
|
-
column="altimetry_minus_dem",
|
|
1661
|
-
cmap="RdBu",
|
|
1662
|
-
vmin=clim[0],
|
|
1663
|
-
vmax=clim[1],
|
|
1664
|
-
markersize=2,
|
|
1665
|
-
legend=True,
|
|
1666
|
-
legend_kwds={"label": cbar_label},
|
|
1667
|
-
)
|
|
1668
1970
|
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
0.98,
|
|
1673
|
-
stats_text,
|
|
1674
|
-
transform=ax.transAxes,
|
|
1675
|
-
verticalalignment="top",
|
|
1676
|
-
fontsize=8,
|
|
1677
|
-
fontfamily="monospace",
|
|
1678
|
-
bbox=dict(boxstyle="round,pad=0.4", facecolor="white", alpha=0.9),
|
|
1971
|
+
ncols = 2 if show_aligned else 1
|
|
1972
|
+
fig, axes = plt.subplots(
|
|
1973
|
+
1, ncols, figsize=(8 * ncols, 6), dpi=220, squeeze=False
|
|
1679
1974
|
)
|
|
1975
|
+
axes = axes[0]
|
|
1976
|
+
|
|
1977
|
+
panels = [("altimetry_minus_dem", "ASP DEM")]
|
|
1978
|
+
if show_aligned:
|
|
1979
|
+
panels.append(("altimetry_minus_aligned_dem", "Aligned DEM"))
|
|
1980
|
+
|
|
1981
|
+
for ax, (column, label) in zip(axes, panels):
|
|
1982
|
+
gdf = self.planetary_points.dropna(subset=[column])
|
|
1983
|
+
dh = gdf[column]
|
|
1984
|
+
n = len(dh)
|
|
1985
|
+
med = np.nanmedian(dh.values)
|
|
1986
|
+
nmad = _nmad(dh.values)
|
|
1987
|
+
|
|
1988
|
+
ax.imshow(hs, cmap="gray", extent=extent, alpha=0.7, interpolation="none")
|
|
1989
|
+
cbar_label = f"{instrument} - {label} (m)\n[±|max|]"
|
|
1990
|
+
gdf.to_crs(dem_crs).plot(
|
|
1991
|
+
ax=ax,
|
|
1992
|
+
column=column,
|
|
1993
|
+
cmap="RdBu",
|
|
1994
|
+
vmin=clim[0],
|
|
1995
|
+
vmax=clim[1],
|
|
1996
|
+
markersize=2,
|
|
1997
|
+
legend=True,
|
|
1998
|
+
legend_kwds={"label": cbar_label},
|
|
1999
|
+
)
|
|
2000
|
+
stats_text = f"n={n}\nMedian={med:+.2f} m\nNMAD={nmad:.2f} m"
|
|
2001
|
+
ax.text(
|
|
2002
|
+
0.02,
|
|
2003
|
+
0.98,
|
|
2004
|
+
stats_text,
|
|
2005
|
+
transform=ax.transAxes,
|
|
2006
|
+
verticalalignment="top",
|
|
2007
|
+
fontsize=8,
|
|
2008
|
+
fontfamily="monospace",
|
|
2009
|
+
bbox=dict(boxstyle="round,pad=0.4", facecolor="white", alpha=0.9),
|
|
2010
|
+
)
|
|
2011
|
+
ax.set_title(label, size=9)
|
|
2012
|
+
ax.set_xticks([])
|
|
2013
|
+
ax.set_yticks([])
|
|
1680
2014
|
|
|
1681
|
-
|
|
1682
|
-
ax.set_yticks([])
|
|
1683
|
-
fig.suptitle(f"{title}\n(n={n})", size=10)
|
|
2015
|
+
fig.suptitle(title, size=10)
|
|
1684
2016
|
fig.tight_layout()
|
|
1685
2017
|
if save_dir and fig_fn:
|
|
1686
2018
|
save_figure(fig, save_dir, fig_fn)
|
|
@@ -1690,6 +2022,7 @@ class Altimetry:
|
|
|
1690
2022
|
save_dir=None,
|
|
1691
2023
|
fig_fn=None,
|
|
1692
2024
|
title=None,
|
|
2025
|
+
plot_aligned=False,
|
|
1693
2026
|
):
|
|
1694
2027
|
"""Histogram of planetary altimetry vs DEM height differences.
|
|
1695
2028
|
|
|
@@ -1701,6 +2034,9 @@ class Altimetry:
|
|
|
1701
2034
|
Filename for saved figure.
|
|
1702
2035
|
title : str or None, optional
|
|
1703
2036
|
Custom plot title. Auto-detected if None.
|
|
2037
|
+
plot_aligned : bool, optional
|
|
2038
|
+
Overlay the post-alignment dh distribution on the same axes.
|
|
2039
|
+
Requires that pc_align has been run successfully.
|
|
1704
2040
|
"""
|
|
1705
2041
|
from asp_plot.utils import detect_planetary_body
|
|
1706
2042
|
|
|
@@ -1716,25 +2052,64 @@ class Altimetry:
|
|
|
1716
2052
|
logger.warning("No valid dh values for histogram.")
|
|
1717
2053
|
return
|
|
1718
2054
|
|
|
1719
|
-
n = len(dh)
|
|
1720
|
-
med = np.nanmedian(dh.values)
|
|
1721
|
-
nmad = _nmad(dh.values)
|
|
1722
|
-
|
|
1723
2055
|
body = detect_planetary_body(self.dem_fn)
|
|
1724
2056
|
instrument = {"moon": "LOLA", "mars": "MOLA"}.get(body, "Altimetry")
|
|
1725
2057
|
if title is None:
|
|
1726
2058
|
title = f"{instrument} vs ASP DEM"
|
|
1727
2059
|
|
|
1728
|
-
|
|
2060
|
+
show_aligned = (
|
|
2061
|
+
plot_aligned
|
|
2062
|
+
and self.aligned_dem_fn
|
|
2063
|
+
and "altimetry_minus_aligned_dem" in self.planetary_points.columns
|
|
2064
|
+
)
|
|
2065
|
+
if show_aligned:
|
|
2066
|
+
dh_aligned = self.planetary_points["altimetry_minus_aligned_dem"].dropna()
|
|
2067
|
+
|
|
2068
|
+
# Shared bin edges and xlim across both distributions
|
|
2069
|
+
if show_aligned:
|
|
2070
|
+
abs_max = max(
|
|
2071
|
+
abs(dh.min()),
|
|
2072
|
+
abs(dh.max()),
|
|
2073
|
+
abs(dh_aligned.min()),
|
|
2074
|
+
abs(dh_aligned.max()),
|
|
2075
|
+
)
|
|
2076
|
+
else:
|
|
2077
|
+
abs_max = max(abs(dh.min()), abs(dh.max()))
|
|
2078
|
+
bins = np.linspace(-abs_max, abs_max, 129)
|
|
1729
2079
|
|
|
1730
|
-
ax.
|
|
2080
|
+
fig, ax = plt.subplots(1, 1, figsize=(8, 5), dpi=220)
|
|
2081
|
+
ax.hist(
|
|
2082
|
+
dh.values,
|
|
2083
|
+
bins=bins,
|
|
2084
|
+
alpha=0.6,
|
|
2085
|
+
color="steelblue",
|
|
2086
|
+
label=f"DEM (n={len(dh)})",
|
|
2087
|
+
)
|
|
2088
|
+
if show_aligned:
|
|
2089
|
+
ax.hist(
|
|
2090
|
+
dh_aligned.values,
|
|
2091
|
+
bins=bins,
|
|
2092
|
+
alpha=0.6,
|
|
2093
|
+
color="orange",
|
|
2094
|
+
label=f"Aligned DEM (n={len(dh_aligned)})",
|
|
2095
|
+
)
|
|
1731
2096
|
|
|
1732
|
-
# Symmetric ±3σ centered on 0 (data is already 3σ-filtered in
|
|
1733
|
-
# planetary_to_dem_dh; stats above are on the filtered data)
|
|
1734
|
-
abs_max = max(abs(dh.min()), abs(dh.max()))
|
|
1735
2097
|
ax.set_xlim(-abs_max, abs_max)
|
|
1736
2098
|
|
|
1737
|
-
|
|
2099
|
+
if show_aligned:
|
|
2100
|
+
med0 = np.nanmedian(dh.values)
|
|
2101
|
+
nmad0 = _nmad(dh.values)
|
|
2102
|
+
med1 = np.nanmedian(dh_aligned.values)
|
|
2103
|
+
nmad1 = _nmad(dh_aligned.values)
|
|
2104
|
+
stats_text = (
|
|
2105
|
+
f"DEM: Med={med0:+.2f} NMAD={nmad0:.2f} m\n"
|
|
2106
|
+
f"Aligned DEM: Med={med1:+.2f} NMAD={nmad1:.2f} m"
|
|
2107
|
+
)
|
|
2108
|
+
else:
|
|
2109
|
+
n = len(dh)
|
|
2110
|
+
med = np.nanmedian(dh.values)
|
|
2111
|
+
nmad = _nmad(dh.values)
|
|
2112
|
+
stats_text = f"n={n}\nMedian={med:+.2f} m\nNMAD={nmad:.2f} m"
|
|
1738
2113
|
ax.text(
|
|
1739
2114
|
0.02,
|
|
1740
2115
|
0.98,
|
|
@@ -1746,9 +2121,10 @@ class Altimetry:
|
|
|
1746
2121
|
bbox=dict(boxstyle="round,pad=0.4", facecolor="white", alpha=0.9),
|
|
1747
2122
|
)
|
|
1748
2123
|
|
|
1749
|
-
ax.set_xlabel(f"{instrument} - DEM (m) [
|
|
2124
|
+
ax.set_xlabel(f"{instrument} - DEM (m) [±|max|]")
|
|
1750
2125
|
ax.set_ylabel("Count")
|
|
1751
|
-
|
|
2126
|
+
ax.legend(loc="upper right", fontsize=8)
|
|
2127
|
+
fig.suptitle(title, size=10)
|
|
1752
2128
|
fig.tight_layout()
|
|
1753
2129
|
if save_dir and fig_fn:
|
|
1754
2130
|
save_figure(fig, save_dir, fig_fn)
|
|
@@ -93,7 +93,7 @@ from asp_plot.utils import Raster, detect_planetary_body, get_acquisition_dates
|
|
|
93
93
|
"--pc_align",
|
|
94
94
|
prompt=False,
|
|
95
95
|
default=True,
|
|
96
|
-
help="If True and --plot_altimetry is True, run pc_align against ICESat-2
|
|
96
|
+
help="If True and --plot_altimetry is True, run pc_align against the reference altimetry (ICESat-2 for Earth, MOLA for Mars, LOLA for Moon) and append the alignment-report pages. Disabled automatically when --plot_altimetry / --plot_icesat is False. Default: True.",
|
|
97
97
|
)
|
|
98
98
|
@click.option(
|
|
99
99
|
"--plot_geometry",
|
|
@@ -742,7 +742,7 @@ def main(
|
|
|
742
742
|
f" 1. Run: request_planetary_altimetry --dem {asp_dem} --email <your_email>\n"
|
|
743
743
|
f" 2. Wait for the email with a download link\n"
|
|
744
744
|
f" 3. Download and unzip the result\n"
|
|
745
|
-
f" 4. Re-run asp_plot with: --altimetry_csv <
|
|
745
|
+
f" 4. Re-run asp_plot with: --altimetry_csv <path_to_pts_csv.csv>\n"
|
|
746
746
|
f"\nSkipping {instrument} altimetry plots.\n"
|
|
747
747
|
f"{'='*60}\n"
|
|
748
748
|
)
|
|
@@ -777,6 +777,120 @@ def main(
|
|
|
777
777
|
)
|
|
778
778
|
)
|
|
779
779
|
|
|
780
|
+
# ---- pc_align + planetary alignment report (Moon/Mars) ----
|
|
781
|
+
if pc_align:
|
|
782
|
+
align_result = alt.align_and_evaluate_planetary()
|
|
783
|
+
stats_row = {}
|
|
784
|
+
if (
|
|
785
|
+
align_result.alignment_report_df is not None
|
|
786
|
+
and not align_result.alignment_report_df.empty
|
|
787
|
+
):
|
|
788
|
+
row = align_result.alignment_report_df.iloc[0].to_dict()
|
|
789
|
+
row.pop("key", None)
|
|
790
|
+
stats_row = row
|
|
791
|
+
|
|
792
|
+
align_title = f"DEM Alignment with {instrument}"
|
|
793
|
+
alignment_description = (
|
|
794
|
+
f"ASP's pc_align estimates a rigid 3D translation that "
|
|
795
|
+
f"minimizes the height residuals between the ASP DEM "
|
|
796
|
+
f"and the {instrument} planetary radii. The CSV is "
|
|
797
|
+
f"passed as the reference cloud with --csv-format "
|
|
798
|
+
f"'1:lon 2:lat 3:radius_m', and --datum is set to "
|
|
799
|
+
f"D_MARS or D_MOON to match the ASP DEM. The "
|
|
800
|
+
f"resulting translation is applied to the DEM "
|
|
801
|
+
f"directly (geotransform + pixel-value shift, no "
|
|
802
|
+
f"resampling) to produce the aligned DEM.\n\n"
|
|
803
|
+
f"Alignment Parameters (above):\n"
|
|
804
|
+
f" - max_displacement: pc_align upper bound on the "
|
|
805
|
+
f"translation magnitude (m).\n"
|
|
806
|
+
f" - minimum_points: minimum {instrument} points "
|
|
807
|
+
f"that overlap the DEM; below this the alignment is "
|
|
808
|
+
f"skipped.\n"
|
|
809
|
+
f" - min_translation_threshold: minimum translation "
|
|
810
|
+
f"magnitude (as a fraction of the DEM GSD) required "
|
|
811
|
+
f"to write out an aligned DEM.\n"
|
|
812
|
+
f" - improvement_threshold_pct: minimum percentage "
|
|
813
|
+
f"reduction in p50 required to keep the aligned DEM "
|
|
814
|
+
f"on disk; below this, the aligned DEM is removed.\n\n"
|
|
815
|
+
f"Alignment Statistics (above, in meters):\n"
|
|
816
|
+
f" - p16_beg / p50_beg / p84_beg: 16th / 50th / 84th "
|
|
817
|
+
f"percentile of the DEM-vs-{instrument} absolute "
|
|
818
|
+
f"height residuals before alignment.\n"
|
|
819
|
+
f" - p16_end / p50_end / p84_end: same percentiles "
|
|
820
|
+
f"after alignment.\n"
|
|
821
|
+
f" - N_shift / E_shift / D_shift: north / east / down "
|
|
822
|
+
f"components of the applied translation vector.\n"
|
|
823
|
+
f" - |T|: magnitude of the translation vector."
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
if align_result.status == "insufficient_points":
|
|
827
|
+
sections.append(
|
|
828
|
+
AlignmentReportPage(
|
|
829
|
+
title=align_title,
|
|
830
|
+
parameters=align_result.parameters_used,
|
|
831
|
+
description=alignment_description,
|
|
832
|
+
status_message=align_result.message,
|
|
833
|
+
)
|
|
834
|
+
)
|
|
835
|
+
elif align_result.status == "no_improvement":
|
|
836
|
+
sections.append(
|
|
837
|
+
AlignmentReportPage(
|
|
838
|
+
title=align_title,
|
|
839
|
+
parameters=align_result.parameters_used,
|
|
840
|
+
stats_row=stats_row,
|
|
841
|
+
description=alignment_description,
|
|
842
|
+
status_message=align_result.message,
|
|
843
|
+
)
|
|
844
|
+
)
|
|
845
|
+
elif align_result.status == "success":
|
|
846
|
+
sections.append(
|
|
847
|
+
AlignmentReportPage(
|
|
848
|
+
title=align_title,
|
|
849
|
+
parameters=align_result.parameters_used,
|
|
850
|
+
stats_row=stats_row,
|
|
851
|
+
description=alignment_description,
|
|
852
|
+
status_message=align_result.message,
|
|
853
|
+
)
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
fig_fn = f"{next(figure_counter):02}.png"
|
|
857
|
+
alt.mapview_plot_planetary_to_dem(
|
|
858
|
+
plot_aligned=True,
|
|
859
|
+
save_dir=plots_directory,
|
|
860
|
+
fig_fn=fig_fn,
|
|
861
|
+
)
|
|
862
|
+
sections.append(
|
|
863
|
+
ReportSection(
|
|
864
|
+
title=f"{instrument} Altimetry Map (Aligned DEM)",
|
|
865
|
+
image_path=os.path.join(plots_directory, fig_fn),
|
|
866
|
+
caption=(
|
|
867
|
+
f"Pre- (left) and post-alignment (right) "
|
|
868
|
+
f"map views of {instrument} elevation "
|
|
869
|
+
f"differences. The aligned-DEM hillshade "
|
|
870
|
+
f"is used as the backdrop for both panels."
|
|
871
|
+
),
|
|
872
|
+
)
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
fig_fn = f"{next(figure_counter):02}.png"
|
|
876
|
+
alt.histogram_planetary_to_dem(
|
|
877
|
+
plot_aligned=True,
|
|
878
|
+
save_dir=plots_directory,
|
|
879
|
+
fig_fn=fig_fn,
|
|
880
|
+
)
|
|
881
|
+
sections.append(
|
|
882
|
+
ReportSection(
|
|
883
|
+
title=f"{instrument} Altimetry Histogram (Aligned DEM)",
|
|
884
|
+
image_path=os.path.join(plots_directory, fig_fn),
|
|
885
|
+
caption=(
|
|
886
|
+
f"Pre- (steelblue) and post-alignment "
|
|
887
|
+
f"(orange) distributions of {instrument} "
|
|
888
|
+
f"minus DEM height differences with shared "
|
|
889
|
+
f"bin edges."
|
|
890
|
+
),
|
|
891
|
+
)
|
|
892
|
+
)
|
|
893
|
+
|
|
780
894
|
# Compile report
|
|
781
895
|
processing_parameters = ProcessingParameters(
|
|
782
896
|
processing_directory=directory,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|