Coverage for python/lsst/cp/verify/interactiveReportLSSTCam.py: 0%

130 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-27 08:49 +0000

1# This file is part of cp_verify. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27import os 

28import re 

29import argparse 

30import logging 

31 

32from collections import defaultdict 

33 

34from lsst.daf.butler import Butler 

35from lsst.obs.lsst import LsstCam 

36 

37from .utils import det_to_index 

38 

39 

40class DetailedReporterLSSTCam(): 

41 """A class to generate calibration verification reports. 

42 

43 Parameters 

44 ---------- 

45 sharedPath : `str` 

46 The location the report will be written to. 

47 instrument : `str` 

48 The instrument associated with the data. 

49 doOverwrite : `bool` 

50 Should the sharedPath be overwritten? 

51 """ 

52 

53 def __init__(self, sharedPath, instrument, doOverwrite): 

54 super().__init__() 

55 # Set source and destination information. 

56 self.sharedPath = sharedPath 

57 self.doOverwrite = doOverwrite 

58 

59 # Check that the output directory 

60 # is organized in the proper format 

61 if not os.path.isdir(self.sharedPath): 

62 raise RuntimeError(f"The shared directory {self.sharedPath} does not exist.") 

63 if not os.path.isdir(self.sharedPath + "/images"): 

64 raise RuntimeError(f"The shared directory {self.sharedPath} does not follow the expected " 

65 "organizational structure, please refer to DMTN-222 for more information.") 

66 

67 # Logging 

68 self.log = logging.getLogger(__name__) 

69 

70 # Instantiate a butler for our repository. 

71 self.butler = Butler("/repo/main") 

72 self.registry = self.butler.registry 

73 

74 # Get the camera associated with these calibrations 

75 if instrument != "LSSTCam": 

76 raise RuntimeError("The only supported camera is currently LSSTCam.") 

77 self.camera = LsstCam.getCamera() 

78 

79 def writeReport(self, renderedHtml, fileName): 

80 output_path = self.sharedPath + "/" + fileName 

81 

82 # Do nothing if the file already exists and doOverwrite==True 

83 if os.path.isfile(output_path) and not self.doOverwrite: 

84 self.log.warning(f"The file {output_path} already exists and doOverwrite=False, skipping.") 

85 return 

86 

87 with open(output_path, "w") as f: 

88 f.write(renderedHtml) 

89 

90 def getAllImages(self): 

91 summaryImages = self.getSummaryImages() 

92 detailedImages = self.getDetailedImages() 

93 

94 return summaryImages, detailedImages 

95 

96 def getSummaryImages(self): 

97 mosaicImages = [] 

98 histImages = [] 

99 

100 # Get focal plane images 

101 imagePath = os.path.join(self.sharedPath, "images") 

102 if os.path.exists(imagePath): 

103 for f in os.listdir(imagePath): 

104 if f.endswith('hist_mosaic.png') and os.path.isfile(os.path.join(imagePath, f)): 

105 histImages.append(f) 

106 elif f.endswith('_mosaic.png') and os.path.isfile(os.path.join(imagePath, f)): 

107 mosaicImages.append(f) 

108 else: 

109 continue 

110 

111 if len(mosaicImages) != len(histImages): 

112 raise RuntimeError("The number of summary mosaic images does not match " 

113 "the number of histogram images.") 

114 

115 mosaicImages = sorted(mosaicImages) 

116 histImages = sorted(histImages) 

117 

118 summaryImages = list(zip(mosaicImages, histImages)) 

119 

120 return summaryImages 

121 

122 def getDetailedImages(self): 

123 tree = defaultdict(list) 

124 # Get tree data 

125 fullNames = [] 

126 for root, dirs, files in os.walk(self.sharedPath): 

127 for file in files: 

128 if file.endswith('.png') and not file.endswith('_fp.png'): 

129 try: 

130 pattern = re.compile(r"(step_\d{2}[a-zA-Z]?)-([a-zA-Z0-9_-]+)_(\d{2,3})\.png") 

131 match = pattern.match(file) 

132 if match is None: 

133 continue 

134 step = match.group(1) 

135 name = match.group(2) 

136 fullName = f"{step}-{name}" 

137 fullNames.append(fullName) 

138 detId = int(match.group(3)) 

139 relPath = os.path.join(".", os.path.relpath(root, self.sharedPath), file) 

140 tree[fullName].append((detId, relPath)) 

141 except (ValueError, IndexError): 

142 continue 

143 

144 # Sort tree data 

145 for fullName in tree: 

146 tree[fullName].sort(key=lambda x: x[0]) 

147 tree = dict(sorted(tree.items())) 

148 

149 # Process tree data into grids 

150 detailedImages = {} 

151 for fullName, images in tree.items(): 

152 # Create 15x15 grid (initialized with None) 

153 grid = [[None for _ in range(15)] for _ in range(15)] 

154 for detId, path in images: 

155 if detId in det_to_index: 

156 r, c = det_to_index[detId] 

157 grid[14 - r][c] = path 

158 detailedImages[fullName] = grid 

159 

160 return detailedImages 

161 

162 def makeSummary(self, summaryImages): 

163 # Get the template for the summary report 

164 summaryReportTemplateFile = os.path.join( 

165 os.environ['CP_VERIFY_DIR'], 

166 "python/lsst/cp/verify/templates/summary.html", 

167 ) 

168 with open(summaryReportTemplateFile, 'r') as file: 

169 summaryReportTemplate = file.read() 

170 

171 addition = "" 

172 for i, (mosaicPath, histPath) in enumerate(summaryImages): 

173 mosaicPath = "./images/" + mosaicPath 

174 histPath = "./images/" + histPath 

175 addition += """ 

176 <a href="{0}" target="_blank"> 

177 <img src="{0}" alt="Mosaic Image {2}"> 

178 </a> 

179 <a href="{1}" target="_blank"> 

180 <img src="{1}" alt="Hist Image {2}"> 

181 </a> 

182 """.format(mosaicPath, histPath, i) 

183 

184 renderedHtml = summaryReportTemplate.replace("summary_images", addition) 

185 

186 self.writeReport(renderedHtml, "summary.html") 

187 

188 def makeDetailed(self, fullName, tree): 

189 """A class to generate the detailed verification reports. 

190 

191 Parameters 

192 ---------- 

193 fullName : `str` 

194 The name of the file to output. 

195 

196 tree : `dict` [`list`, `tuple`] 

197 Dictionary keyed by "{step}-{name}", that contains 

198 a list of tuples (detectorId, relative/path/to/image). 

199 

200 """ 

201 # Get the template for the detailed report 

202 detailedReportTemplateFile = os.path.join( 

203 os.environ['CP_VERIFY_DIR'], 

204 "python/lsst/cp/verify/templates/detailed.html", 

205 ) 

206 with open(detailedReportTemplateFile, 'r') as file: 

207 detailedReportTemplate = file.read() 

208 

209 pattern = re.compile(r"^step_\d{2}[a-zA-Z]?-(.+)$") 

210 match = pattern.match(fullName) 

211 name = match.group(1) 

212 name = name.replace("_", " ").upper() 

213 name = name.replace("AND", "VS").upper() 

214 

215 addition = "" 

216 cell = """ 

217 <div class="grid-cell"> 

218 cell_image 

219 </div> 

220 """ 

221 for i, paths in enumerate(tree): 

222 for path in paths: 

223 if path is not None: 

224 img = """ 

225 <a href="{0}" target="_blank"> 

226 <img src="{0}" 

227 alt="{1} image" 

228 title="Click to view full size"> 

229 </a> 

230 """.format(path, "test") 

231 addition += cell.replace("cell_image", img) 

232 else: 

233 addition += cell.replace("cell_image", "") 

234 continue 

235 

236 renderedHtml = detailedReportTemplate.replace("grid_placeholder", addition).replace("name", name) 

237 

238 self.writeReport(renderedHtml, f"{fullName}.html") 

239 

240 def run(self): 

241 "Get all the images and generate the HTML construction routine." 

242 # What images are there? 

243 summaryImages, detailedImages = self.getAllImages() 

244 

245 # Make summary plots 

246 if len(summaryImages) > 0: 

247 self.makeSummary(summaryImages) 

248 

249 # Make detailed plots 

250 if len(detailedImages) > 0: 

251 for fullName in detailedImages.keys(): 

252 tree = detailedImages[fullName] 

253 self.makeDetailed(fullName, tree) 

254 

255 

256def main(): 

257 r""" 

258 Construct a webpage (html) report of a full focal plane given 

259 a set of images for each detector. 

260 The reporter only requires that the images are organized in a 

261 particular format (the images directory can be anywhere, but 

262 preferably in ones public_html directory on S3D)F: 

263 

264 images/ 

265     |_____R00/ 

266     |          |______S00/ 

267     |          |______S01/ 

268     |               ... 

269     |_____R01/ ... 

270     |_____ ... 

271 

272 Also, the images in the directory need to follow this naming 

273 convention: 

274 

275 ``step_\d{2}[a-zA-Z]?)-([a-zA-Z0-9_-]+)_(\d{2,3})\.png`` 

276 == 

277 step_(## + letter)-(descriptive name)_(detector ###).png 

278 

279 Or, you can adjust line 132 on your local version of 

280 python/lsst/cp/verify/detailedReportsLSSTCam.py to fit your 

281 own format. 

282 

283 The report code will generate a .html file for each unique 

284 step_(## + letter)-(descriptive name) name. 

285 """ 

286 parser = argparse.ArgumentParser( 

287 description="Construct a detailed report. Usage: -p (absolute path " 

288 "to output directory) --instrument (e.g. LSSTCam) " 

289 "--do-overwrite (overwrite the path where the files are " 

290 "written.)") 

291 parser.add_argument("-p", "--shared-path", dest="sharedPath", default="", 

292 help="Absolute path with input 'images' diectory, which will also " 

293 "contain the report pages.") 

294 parser.add_argument("--instrument", dest="instrument", default="", 

295 help="The instrument to use (e.g. LSSTCam)") 

296 parser.add_argument("--do-overwrite", dest="doOverwrite", action="store_true", default=False, 

297 help="Allow existing files to be overwritten?") 

298 args = parser.parse_args() 

299 

300 reporter = DetailedReporterLSSTCam( 

301 sharedPath=args.sharedPath, 

302 instrument=args.instrument, 

303 doOverwrite=args.doOverwrite, 

304 ) 

305 reporter.run()