Coverage for lunar_toolkit/cli/sfs_report.py: 86%
56 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-19 02:58 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-19 02:58 +0000
1"""sfs-report - validation metrics, figures and an HTML report of an SfS work dir."""
3import argparse
4import sys
5from pathlib import Path
7COMMAND = "sfs-report"
9PGDA_REPORT_KINDS = ("ldec", "toterr", "slp", "slperr", "xyzi")
12def add_parser(sub: argparse._SubParsersAction) -> None:
13 p = sub.add_parser(
14 COMMAND,
15 help="Metrics, figures and an HTML report of an SfS work dir (Bertone et al. 2026 validation).",
16 formatter_class=argparse.RawDescriptionHelpFormatter,
17 epilog="""
18Examples
19--------
20 lunar sfs-report malapert_crop # work dir files only
21 lunar sfs-report malapert_crop --region malapert # + PGDA LDEC, uncertainty maps, LOLA points
22 lunar sfs-report malapert_crop --region malapert --fetch-published # + published SDEM
23 lunar sfs-report w --toterr Site23_final_adj_5mpp_toterr.tif --lola-points lola.csv -o w/report2
25Sections (skipped when their inputs are missing): bundle-adjustment residuals before /
26after and match offsets (Fig. 5); removed images; illumination coverage with 0/1/2/>=3
27non-redundant Sun directions (Figs. 3, 6); SfS minus prior DEM, split by LOLA count
28(Table 2); SfS minus LOLA points vs slope (Fig. 7); ensemble uncertainty (Fig. 8);
29SfS - LDEM residuals over the LDEM uncertainty maps (Fig. 9); SfS minus the published SDEM.
30Writes OUTPUT_DIR/report.html (figures embedded), metrics.json, tables/*.csv, figures/*.png.
31""",
32 )
33 p.add_argument("work_dir", type=Path, metavar="WORK_DIR", help="lunar sfs work directory.")
34 p.add_argument(
35 "-o",
36 "--output-dir",
37 type=Path,
38 default=None,
39 metavar="DIR",
40 help="Destination (default: WORK_DIR/report).",
41 )
42 p.add_argument("--sfs-dem", type=Path, metavar="TIF", help="SfS DEM (default: products GLDELEV or sfs/).")
43 p.add_argument("--prior-dem", type=Path, metavar="TIF", help="Prior DEM (default: dem/prior-<RES>m.tif).")
44 p.add_argument("--region-id", metavar="ID", help="Product name prefix (default: from config.yaml).")
45 p.add_argument(
46 "--res", type=float, metavar="M", help="Product resolution label (default: from config.yaml)."
47 )
48 p.add_argument("--ldec", type=Path, metavar="TIF", help="PGDA LOLA count raster (*_ldec.tif).")
49 p.add_argument("--toterr", type=Path, metavar="TIF", help="PGDA elevation uncertainty (*_toterr.tif).")
50 p.add_argument("--slperr", type=Path, metavar="TIF", help="PGDA slope uncertainty (*_slperr.tif).")
51 p.add_argument("--ldem-slope", type=Path, metavar="TIF", help="PGDA LDEM slope (*_slp.tif).")
52 p.add_argument(
53 "--lola-points",
54 type=Path,
55 metavar="CSV",
56 help="LOLA points as an ASP CSV (easting, northing, height).",
57 )
58 p.add_argument("--lola-xyzi", type=Path, metavar="XYZI", help="PGDA LOLA point cloud (*.xyzi).")
59 p.add_argument(
60 "--published-dem", type=Path, metavar="TIF", help="Published Bertone et al. SDEM (GLDELEV)."
61 )
62 p.add_argument(
63 "--region",
64 metavar="REGION",
65 help="Artemis III region: download its PGDA files (see --pgda) for the inputs not given.",
66 )
67 p.add_argument(
68 "--pgda",
69 nargs="+",
70 default=list(PGDA_REPORT_KINDS),
71 choices=PGDA_REPORT_KINDS,
72 metavar="KIND",
73 help=f"PGDA files fetched with --region (default: {' '.join(PGDA_REPORT_KINDS)}).",
74 )
75 p.add_argument(
76 "--fetch-published",
77 action="store_true",
78 help="With --region: extract the published GLDELEV from the Zenodo zip (HTTP range reads).",
79 )
80 p.add_argument(
81 "--reference-dir",
82 type=Path,
83 default=None,
84 metavar="DIR",
85 help="Download directory for --region files (default: the reference cache).",
86 )
87 p.add_argument("--sections", nargs="+", metavar="NAME", help="Only these sections (default: all).")
88 p.add_argument(
89 "--preview-size",
90 type=int,
91 default=800,
92 metavar="PX",
93 help="Longer side of the map figures (default: 800).",
94 )
97def _region_inputs(args: argparse.Namespace) -> dict:
98 from .. import reference as ref
99 from ..config import pgda_file
101 reg = ref.get_region(args.region)
102 names = {"ldec": "ldec", "toterr": "toterr", "slp": "ldem_slope", "slperr": "slperr", "xyzi": "lola_xyzi"}
103 out = {}
104 for kind in args.pgda:
105 if getattr(args, names[kind]) is None: 105 ↛ 104line 105 didn't jump to line 104 because the condition on line 105 was always true
106 out[names[kind]] = pgda_file(
107 reg, kind, args.reference_dir / "pgda" if args.reference_dir else None
108 )
109 if args.fetch_published and args.published_dem is None: 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true
110 base = args.reference_dir or ref.REFERENCE_CACHE_DIR
111 dest = base / "sdem" / reg.zenodo_file.removesuffix(".zip")
112 out["published_dem"] = ref.extract_sdem(reg, ["GLDELEV"], dest)[0]
113 return out
116def run(args: argparse.Namespace) -> int:
117 from .. import sfs_report
119 if not args.work_dir.is_dir():
120 print(f"Error: {args.work_dir} is not a directory", file=sys.stderr)
121 return 1
122 given = {
123 k: getattr(args, k)
124 for k in (
125 "sfs_dem",
126 "prior_dem",
127 "region_id",
128 "ldec",
129 "toterr",
130 "slperr",
131 "ldem_slope",
132 "lola_points",
133 "lola_xyzi",
134 "published_dem",
135 )
136 }
137 try:
138 if args.region: 138 ↛ 140line 138 didn't jump to line 140 because the condition on line 138 was always true
139 given.update(_region_inputs(args))
140 inputs = sfs_report.ReportInputs.from_work_dir(
141 args.work_dir, resolution=args.res, preview_size=args.preview_size, **given
142 )
143 out = sfs_report.make_report(inputs, args.output_dir, args.sections)
144 except (KeyError, ValueError, OSError) as exc:
145 print(f"Error: {exc.args[0] if isinstance(exc, KeyError) and exc.args else exc}", file=sys.stderr)
146 return 1
147 print(f"Report -> {out}")
148 return 0