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

1"""sfs-report - validation metrics, figures and an HTML report of an SfS work dir.""" 

2 

3import argparse 

4import sys 

5from pathlib import Path 

6 

7COMMAND = "sfs-report" 

8 

9PGDA_REPORT_KINDS = ("ldec", "toterr", "slp", "slperr", "xyzi") 

10 

11 

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 

24 

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 ) 

95 

96 

97def _region_inputs(args: argparse.Namespace) -> dict: 

98 from .. import reference as ref 

99 from ..config import pgda_file 

100 

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 

114 

115 

116def run(args: argparse.Namespace) -> int: 

117 from .. import sfs_report 

118 

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