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

257 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-17 09:37 +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 argparse 

28import logging 

29import matplotlib.colors as colors 

30import numpy as np 

31import os 

32import re 

33import yaml 

34 

35from matplotlib import cm 

36from mpl_toolkits.axes_grid1 import make_axes_locatable 

37 

38from lsst.daf.butler import Butler 

39from lsst.daf.butler.datastores.file_datastore.retrieve_artifacts import ( 

40 determine_destination_for_retrieved_artifact, 

41) 

42from lsst.resources import ResourcePath 

43from lsst.utils import getPackageDir 

44from lsst.utils.plotting import make_figure 

45 

46 

47class CpvReporter(): 

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

49 

50 Parameters 

51 ---------- 

52 repo : `str` 

53 The location of the butler repository to retrieve results 

54 from. 

55 instrument : `str` 

56 The instrument associated with this data. 

57 output_path : `str` 

58 The location the report will be written to. 

59 collections : `list` [`str`] 

60 A list of collections to search. 

61 **kwargs : 

62 Other keyword parameters. Currently parsed values: 

63 

64 - ``do_copy``: Should files be copied from butler (`bool`). 

65 - ``do_overwrite``: Should pre-existing files be overwritten (`bool`). 

66 """ 

67 

68 def __init__(self, repo, output_path, collections=[], **kwargs): 

69 super().__init__() 

70 # Set source and destination information. 

71 self.repo = repo 

72 self.output_path = output_path 

73 self.src_dir = os.path.join(self.output_path, "src") 

74 self.collections = collections 

75 

76 # We need a log 

77 self.log = logging.getLogger(__name__) if "log" not in kwargs else kwargs["log"] 

78 # List of the datasets we need to process. The datasets are 

79 # dictionaries of parameters, including the dataset_type_name, 

80 # the source collection, the butler dataIds, file locations, 

81 # etc. 

82 self.datasets = [] 

83 

84 # A dictionary of output html files that will be created. 

85 # These are dictionaries with the key being the filename, and 

86 # the values being arrays of strings that will be written 

87 # one-per-line in the output html page. 

88 self.out_files = { 

89 'index.html': self._init_page(), 

90 } 

91 

92 # Instantiate a butler for our repository. 

93 self.butler = Butler(repo) 

94 

95 # Configure behavior from kwargs: 

96 self.do_copy = kwargs['do_copy'] if 'do_copy' in kwargs else True 

97 self.do_overwrite = kwargs['do_overwrite'] if 'do_overwrite' in kwargs else False 

98 

99 # Read dataset configuration yaml: 

100 self.dataset_map = self._read_dataset_map() 

101 

102 def _read_dataset_map(self): 

103 """Read dataset information from source yaml.""" 

104 filename = os.path.join(getPackageDir("cp_verify"), 

105 "python", "lsst", "cp", "verify", "configs", "report.yaml") 

106 with open(filename) as in_file: 

107 return yaml.safe_load(in_file) 

108 

109 def run(self): 

110 """Generate the report""" 

111 # Generate directories that will hold the output products. 

112 os.makedirs(self.src_dir, exist_ok=True) 

113 

114 # Copy datasets: This populates self.datasets. 

115 self.copy_datasets() 

116 

117 # Parse datasets: This determines what report pages should be made. 

118 self.parse_datasets() 

119 

120 # Write pages: This saves the information to disk. 

121 self.write_pages() 

122 

123 def copy_datasets(self): 

124 """Copy datasets, similar to butler retrieve-artifacts. 

125 

126 See Also 

127 -------- 

128 self.fits_to_png 

129 self.parq_to_html 

130 self.py_to_html 

131 """ 

132 # Based on daf_butler fileDatastore.py#L1978 

133 for stage, dataset_types in self.dataset_map['stages'].items(): 

134 # Iterate over each stage, getting all of that stage's datasets. 

135 # Convert dict dataset_types to a list of strings: 

136 if not dataset_types: 

137 continue 

138 dataset_type_names = list(dataset_types.keys()) 

139 

140 # Find all the references to these datasets in our collections 

141 refs = self.butler.registry.queryDatasets(datasetType=dataset_type_names, 

142 collections=self.collections, 

143 where=None, findFirst=True) 

144 

145 for ref in refs: 

146 # Iterate over each reference 

147 locations = self.butler._datastore._get_dataset_locations_info(ref) 

148 self.log.warn(f"Found {stage} {ref}") 

149 

150 # This is our dataset information: 

151 dataset = {'stage': stage, 

152 'ref': ref, 

153 'type': ref.datasetType.name, 

154 'storage_class': ref.datasetType.storageClass.name, 

155 'dataId': ref.dataId, 

156 'instrument': ref.dataId.get('instrument', None), 

157 'exposure': ref.dataId.get('exposure', None), 

158 'detector': ref.dataId.get('detector', None), 

159 'physical_filter': ref.dataId.get('physical_filter', None), 

160 'collection': ref.run, 

161 } 

162 

163 # Do the actual copy. This may convert the butler 

164 # product to something more web-accessible. 

165 for location, _ in locations: 

166 source_uri = location.uri 

167 target_uri = determine_destination_for_retrieved_artifact( 

168 ResourcePath(self.src_dir), 

169 location.pathInStore, 

170 False 

171 ) 

172 dataset['uri'] = target_uri 

173 willCopy = self.do_copy 

174 

175 if ((target_uri.getExtension().lower() == ".fits" 

176 and dataset['storage_class'] in ('ImageF', 'ExposureF', 'ImageD', 'ExposureD'))): 

177 # Convert this to a PNG image. 

178 png_uri = target_uri.updatedExtension("png") 

179 dataset['uri'] = png_uri 

180 if os.path.exists(png_uri.path): 

181 if willCopy and not self.do_overwrite: 

182 willCopy = False 

183 

184 if willCopy: 

185 data = self.butler.get(ref) 

186 self.fits_to_png(data, png_uri, f"{ref.datasetType.name} {ref.dataId}") 

187 elif (target_uri.getExtension().lower() == ".parq" 

188 and dataset['storage_class'] in ('ArrowAstropy', )): 

189 # Convert to html table. 

190 html_uri = target_uri.updatedExtension("html") 

191 dataset['uri'] = html_uri 

192 if os.path.exists(html_uri.path): 

193 if willCopy and not self.do_overwrite: 

194 willCopy = False 

195 

196 if willCopy: 

197 data = self.butler.get(ref) 

198 self.parq_to_html(data, html_uri) 

199 elif (target_uri.getExtension().lower() == ".py" 

200 and dataset['storage_class'] in ('Config', )): 

201 # Convert configs to html. 

202 html_uri = target_uri.updatedExtension("html") 

203 dataset['uri'] = html_uri 

204 

205 if willCopy: 

206 data = self.butler.get(ref) 

207 self.py_to_html(data, html_uri) 

208 else: 

209 if willCopy: 

210 # Otherwise just copy directly. 

211 target_uri.transfer_from(source_uri, transfer="copy", 

212 overwrite=self.do_overwrite) 

213 

214 # Add this dataset to our list. 

215 self.datasets.append(dataset) 

216 

217 def parse_datasets(self): 

218 """Analyze datasets, and add information to report pages. 

219 

220 See Also 

221 -------- 

222 self._init_page 

223 self.include 

224 self.navigation_init 

225 self.navigation_append 

226 self.title 

227 self.link 

228 """ 

229 populated_stages_list = [] 

230 

231 # Make navigation page. 

232 self.out_files["navigation.html"] = self._init_page("Navigation") 

233 self.navigation_init() 

234 self.include("navigation.html", "index.html") 

235 

236 # Make manifest page: 

237 self.out_files["manifest.html"] = self._init_page("Manifest") 

238 

239 tag = self.link(self.out_files["index.html"], "Parent", "./..") 

240 self.navigation_append("index.html", "Parent", tag, newColumn=True) 

241 self.navigation_append("manifest.html", "Manifest", "", newColumn=False) 

242 

243 for stage in self.dataset_map['stages'].keys(): 

244 # Filter datasets to only those for this stage 

245 dss = [x for x in self.datasets if x['stage'] == stage] 

246 if len(dss) > 0: 

247 populated_stages_list.append(stage) 

248 else: 

249 continue 

250 self.log.warn(f"Parsing data for {stage}") 

251 self.title(self.out_files["index.html"], stage) 

252 

253 self.out_files[f"{stage}_exp.html"] = self._init_page() 

254 self.include("navigation.html", f"{stage}_exp.html") 

255 self.out_files[f"{stage}_det.html"] = self._init_page() 

256 self.include("navigation.html", f"{stage}_det.html") 

257 

258 # Get dimension information we'll want to use to put 

259 # things on appropriate pages. 

260 exps = [x for x in dss if x['exposure'] is not None] 

261 dets = [x for x in dss if x['detector'] is not None] 

262 

263 for ds in sorted(exps, key=lambda x: x['exposure']): 

264 self.block(ds, self.out_files[f"{stage}_exp.html"]) 

265 if ('.png' in ds['uri'].path): 

266 self.image_handler(ds['uri'].path, ds['instrument'], self.out_files[f"{stage}_exp.html"]) 

267 tag = self.link(self.out_files["index.html"], f"{stage} exposures", f"./{stage}_exp.html") 

268 self.navigation_append("index.html", f"{stage} exposures", tag) 

269 

270 for ds in sorted(dets, key=lambda x: x['detector']): 

271 self.block(ds, self.out_files[f"{stage}_det.html"]) 

272 if ('.png' in ds['uri'].path): 

273 self.image_handler(ds['uri'].path, ds['instrument'], self.out_files[f"{stage}_det.html"]) 

274 tag = self.link(self.out_files["index.html"], f"{stage} detectors", f"./{stage}_det.html") 

275 self.navigation_append("index.html", f"{stage} detectors", tag) 

276 

277 for ds in dss: 

278 if ds['storage_class'] == 'ArrowAstropy': 

279 # Is a results catalog 

280 self.block(ds, self.out_files["index.html"], ds['uri']) 

281 self.include(ds['uri'].path, "index.html") 

282 elif ds['exposure'] is None and ds['detector'] is None: 

283 self.block(ds, self.out_files["index.html"]) 

284 if ".png" in ds['uri'].path: 

285 self.image_handler(ds['uri'].path, ds['instrument'], self.out_files["index.html"]) 

286 else: 

287 self.include(ds['uri'].path, "index.html") 

288 

289 # Record all datasets here, and link to them directly: 

290 page = self.out_files["manifest.html"] 

291 page.extend(["<table width='100%'>", 

292 "<tr>", 

293 "<th>stage</th>", "<th>ref</th>", "<th>type</th>", "<th>storage_class</th>", 

294 "<th>dataId</th>", "<th>collection</th>", 

295 "</tr>"]) 

296 for ds in self.datasets: 

297 relative_file = self.relative_file(ds['uri'].path) 

298 page.extend(["<tr>", 

299 f"<td>{ds['stage']}</td>", f"<td>{ds['ref']}</td>", 

300 "<td>", f'<a href="{relative_file}">', f"{ds['type']}", "</a>", 

301 f"<td>{ds['storage_class']}</td>", 

302 f"<td>{ds['dataId']}</td>", f"<td>{ds['collection']}</td>", 

303 "</tr>"]) 

304 

305 # File conversion utilities: 

306 @staticmethod 

307 def fits_to_png(data, target_uri, title): 

308 """Convert FITS to PNG, using the same scaling as on RubinTV. 

309 

310 Parameters 

311 ---------- 

312 data : `lsst.afw.Image`, `lsst.afw.MaskedImage` or `lsst.afw.Exposure` 

313 The fits image data to convert. 

314 target_uri : `str` 

315 Path to the PNG file to write. 

316 title : `str` 

317 Title to add to the figure. 

318 

319 See Also 

320 -------- 

321 rubintv_production / mosaicing.py 

322 """ 

323 # Get array from either an Image or an Exposure: 

324 try: 

325 array = data.image.array 

326 except AttributeError: 

327 array = data.array 

328 

329 fig = make_figure() 

330 ax = fig.gca() 

331 ax.clear() 

332 cmap = cm.gray 

333 # This was using summit_utils.getQuantiles, but that 

334 # was blowing out the scaling more than I wanted. 

335 q25, q50, q75 = np.nanpercentile(array, [25, 50, 75]) 

336 scale = 3.0 * 0.74 * (q75 - q25) 

337 quantiles = np.arange(q50 - scale, q50 + scale, 2.0 * scale / cmap.N) 

338 norm = colors.BoundaryNorm(quantiles, cmap.N) 

339 

340 im = ax.imshow(array, norm=norm, interpolation='None', cmap=cmap, origin='lower') 

341 

342 divider = make_axes_locatable(ax) 

343 cax = divider.append_axes("right", size="5%", pad=0.05) 

344 fig.suptitle(title) 

345 fig.colorbar(im, cax=cax) 

346 fig.tight_layout() 

347 fig.savefig(target_uri.path) 

348 

349 @staticmethod 

350 def parq_to_html(data, target_uri): 

351 """Convert catalogs to html tables. 

352 

353 Parameters 

354 ---------- 

355 data : `astropy.Table` 

356 The table to convert to html. 

357 target_uri : `str` 

358 Path to the HTML file to write. 

359 """ 

360 format_dict = {} 

361 # Remove vectors, as they will be too big. 

362 # Build up format dictionary. 

363 columns_to_remove = [] 

364 for index, name in enumerate(data.dtype.names): 

365 if len(data.dtype[index].shape) != 0: 

366 columns_to_remove.append(name) 

367 continue 

368 if data.dtype[index].kind in ('f', 'c'): 

369 # is float 

370 format_dict[name] = "%.4g" 

371 if name == 'mjd': 

372 # Let's let these be long 

373 format_dict[name] = "12.10f" 

374 elif data.dtype[index].kind in ('i', 'u'): 

375 # is int 

376 format_dict[name] = "%d" 

377 else: 

378 format_dict[name] = "%s" 

379 

380 # Actually remove the vectors: 

381 data.remove_columns(columns_to_remove) 

382 

383 # Write full table: 

384 data.write(target_uri.path, 

385 format='ascii.html', 

386 overwrite=True, 

387 formats=format_dict) 

388 

389 # TODO: Write subset tables, filtered by exposure and by detector. 

390 # files_created[f"{filename_base}_exp{exp}.html"] = {'exposure': exp} 

391 # files_created[f"{filename_base}_det{det}.html"] = {'detector': det} 

392 

393 def py_to_html(self, data, target_uri): 

394 """Convert python configs to html. 

395 

396 Parameters 

397 ---------- 

398 data : `Config` 

399 The python file contents to write. 

400 target_uri : `str` 

401 Path to the HTML file to write. 

402 """ 

403 with open(target_uri.path, 'w') as ff: 

404 for line in self._init_page(): 

405 print(line, file=ff) 

406 for line in data.saveToString().split("\n"): 

407 if len(line) > 0 and line[0] == '#': 

408 print("<pre class='pre-comment'>", line, "</pre>", file=ff) 

409 else: 

410 print("<pre>", line, "</pre>", file=ff) 

411 for line in self._close_page(): 

412 print(line, file=ff) 

413 

414 def write_pages(self): 

415 """Write all page arrays to files.""" 

416 for ff, contents in self.out_files.items(): 

417 reportOut = os.path.join(self.output_path, ff) 

418 contents.extend(self._close_page()) 

419 with open(reportOut, 'w') as ff: 

420 for line in contents: 

421 print(line, file=ff) 

422 

423 # Page construction methods. These all return arrays of strings 

424 # that can be appended to until we write to disk. 

425 @staticmethod 

426 def _init_page(title=""): 

427 """Write boilerplate html for the start of a page.""" 

428 return [ 

429 "<html>", "<head>", "<style> ", 

430 " * { margin: 0; padding: 0;}", 

431 " .imgbox { display: grid; height: 100%; }", 

432 " .pre-comment { color: red; }", 

433 " td { padding: 0 15px; }", 

434 " .ctable { display: table; }", 

435 " .ctable tr { display: table-cell; }", 

436 " .ctable tr td { display: block; }", 

437 "</style>", 

438 "<title>", title, "</title>", 

439 '<base target="_parent">', 

440 "</head>", 

441 "<body>", 

442 ] 

443 

444 @staticmethod 

445 def _close_page(): 

446 """Write boilerplate html for the end of a page.""" 

447 return ["</body>", 

448 "</html>"] 

449 

450 @staticmethod 

451 def relative_file(filename): 

452 """Convert absolute paths to the relative format needed for the html. 

453 """ 

454 return re.sub(r"^.*/src/", "./src/", filename) 

455 

456 def svg_atools_latiss(self, relative_file, page): 

457 """This adds an image to the html, with an SVG overlay for LATISS. 

458 

459 Parameters 

460 ---------- 

461 relative_file : `str` 

462 Relative location of the image to be added. 

463 page : `list` [`str`] 

464 The page to append to. 

465 

466 Notes 

467 ----- 

468 This method assumes the image is made by analysis_tools, and 

469 displays a focal plane plot for LATISS. Under these 

470 assumptions, the boxes defined in SVG map to the amplifier 

471 segments of the single detector. 

472 """ 

473 page.extend(['<svg version="1.1" xmlns="http://www.w3.org/2000/svg" ' 

474 'xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3600 1800">', 

475 f'<image width="3600" height="1800" xlink:href="{relative_file}"></image>', 

476 '<a xlink:href="./c10.html"> <rect x="317" y="216" fill="#fff" opacity="0" ' 

477 'width="176" height="692"></rect> </a>', 

478 '<a xlink:href="./c11.html"> <rect x="490" y="216" fill="#fff" opacity="0" ' 

479 'width="176" height="692"></rect> </a>', 

480 '<a xlink:href="./c12.html"> <rect x="670" y="216" fill="#fff" opacity="0" ' 

481 'width="176" height="692"></rect> </a>', 

482 '<a xlink:href="./c13.html"> <rect x="845" y="216" fill="#fff" opacity="0" ' 

483 'width="176" height="692"></rect> </a>', 

484 '<a xlink:href="./c14.html"> <rect x="1020" y="216" fill="#fff" opacity="0" ' 

485 'width="176" height="692"></rect> </a>', 

486 '<a xlink:href="./c15.html"> <rect x="1197" y="216" fill="#fff" opacity="0" ' 

487 'width="176" height="692"></rect> </a>', 

488 '<a xlink:href="./c16.html"> <rect x="1375" y="216" fill="#fff" opacity="0" ' 

489 'width="176" height="692"></rect> </a>', 

490 '<a xlink:href="./c17.html"> <rect x="1550" y="216" fill="#fff" opacity="0" ' 

491 'width="176" height="692"></rect> </a>', 

492 '<a xlink:href="./c07.html"> <rect x="1550" y="911" fill="#fff" opacity="0" ' 

493 'width="176" height="692"></rect> </a>', 

494 '<a xlink:href="./c06.html"> <rect x="1375" y="911" fill="#fff" opacity="0" ' 

495 'width="176" height="692"></rect> </a>', 

496 '<a xlink:href="./c05.html"> <rect x="1197" y="911" fill="#fff" opacity="0" ' 

497 'width="176" height="692"></rect> </a>', 

498 '<a xlink:href="./c04.html"> <rect x="1020" y="911" fill="#fff" opacity="0" ' 

499 'width="176" height="692"></rect> </a>', 

500 '<a xlink:href="./c03.html"> <rect x="845" y="911" fill="#fff" opacity="0" ' 

501 'width="176" height="692"></rect> </a>', 

502 '<a xlink:href="./c02.html"> <rect x="670" y="911" fill="#fff" opacity="0" ' 

503 'width="176" height="692"></rect> </a>', 

504 '<a xlink:href="./c01.html"> <rect x="490" y="911" fill="#fff" opacity="0" ' 

505 'width="176" height="692"></rect> </a>', 

506 '<a xlink:href="./c00.html"> <rect x="317" y="911" fill="#fff" opacity="0" ' 

507 'width="176" height="692"></rect> </a>', 

508 '</svg>']) 

509 

510 def svg_atools_comcam(self, relative_file, page): 

511 """This adds an image to the html, with an SVG overlay for ComCam. 

512 

513 Parameters 

514 ---------- 

515 relative_file : `str` 

516 Relative location of the image to be added. 

517 page : `list` [`str`] 

518 The page to append to. 

519 

520 Notes 

521 ----- 

522 This method assumes the image is made by analysis_tools, and 

523 displays a focal plane plot for LSSTComCam. Under these 

524 assumptions, the boxes defined in SVG map map to the detectors 

525 in the single raft. 

526 """ 

527 page.extend(['<svg version="1.1" xmlns="http://www.w3.org/2000/svg" ' 

528 'xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3600 1800">', 

529 f'<image width="3600" height="1800" xlink:href="{relative_file}"></image>', 

530 '<a xlink:href="./S00.html"> <rect x="317" y="216" fill="#fff" opacity="0" ' 

531 'width="176" height="692"></rect> </a>', 

532 '<a xlink:href="./S01.html"> <rect x="490" y="216" fill="#fff" opacity="0" ' 

533 'width="176" height="692"></rect> </a>', 

534 '<a xlink:href="./S02.html"> <rect x="670" y="216" fill="#fff" opacity="0" ' 

535 'width="176" height="692"></rect> </a>', 

536 '<a xlink:href="./S10.html"> <rect x="845" y="216" fill="#fff" opacity="0" ' 

537 'width="176" height="692"></rect> </a>', 

538 '<a xlink:href="./S11.html"> <rect x="1020" y="216" fill="#fff" opacity="0" ' 

539 'width="176" height="692"></rect> </a>', 

540 '<a xlink:href="./S12.html"> <rect x="1197" y="216" fill="#fff" opacity="0" ' 

541 'width="176" height="692"></rect> </a>', 

542 '<a xlink:href="./S20.html"> <rect x="1375" y="216" fill="#fff" opacity="0" ' 

543 'width="176" height="692"></rect> </a>', 

544 '<a xlink:href="./S21.html"> <rect x="1550" y="216" fill="#fff" opacity="0" ' 

545 'width="176" height="692"></rect> </a>', 

546 '<a xlink:href="./S22.html"> <rect x="1550" y="911" fill="#fff" opacity="0" ' 

547 'width="176" height="692"></rect> </a>', 

548 '</svg>']) 

549 

550 def svg_mosaic_latiss(self, relative_file, page): 

551 """This adds an image to the html, with an SVG overlay for LATISS. 

552 

553 Parameters 

554 ---------- 

555 relative_file : `str` 

556 Relative location of the image to be added. 

557 page : `list` [`str`] 

558 The page to append to. 

559 

560 Notes 

561 ----- 

562 This method assumes the image is a 8x8 binned focal plane 

563 mosaic of a LATISS exposure made by one of the VisualizeVisit 

564 tasks. Under these assumptions, the boxes defined in SVG map 

565 to the amplifiers of the single detector. 

566 """ 

567 page.extend(['<svg version="1.1" xmlns="http://www.w3.org/2000/svg" ' 

568 'xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3600 1800">', 

569 f'<image width="3600" height="1800" xlink:href="{relative_file}"></image>', 

570 '<a xlink:href="./c10.html"> <rect x="317" y="216" fill="#fff" opacity="0" ' 

571 'width="176" height="692"></rect> </a>', 

572 '<a xlink:href="./c11.html"> <rect x="490" y="216" fill="#fff" opacity="0" ' 

573 'width="176" height="692"></rect> </a>', 

574 '<a xlink:href="./c12.html"> <rect x="670" y="216" fill="#fff" opacity="0" ' 

575 'width="176" height="692"></rect> </a>', 

576 '<a xlink:href="./c13.html"> <rect x="845" y="216" fill="#fff" opacity="0" ' 

577 'width="176" height="692"></rect> </a>', 

578 '<a xlink:href="./c14.html"> <rect x="1020" y="216" fill="#fff" opacity="0" ' 

579 'width="176" height="692"></rect> </a>', 

580 '<a xlink:href="./c15.html"> <rect x="1197" y="216" fill="#fff" opacity="0" ' 

581 'width="176" height="692"></rect> </a>', 

582 '<a xlink:href="./c16.html"> <rect x="1375" y="216" fill="#fff" opacity="0" ' 

583 'width="176" height="692"></rect> </a>', 

584 '<a xlink:href="./c17.html"> <rect x="1550" y="216" fill="#fff" opacity="0" ' 

585 'width="176" height="692"></rect> </a>', 

586 '<a xlink:href="./c07.html"> <rect x="1550" y="911" fill="#fff" opacity="0" ' 

587 'width="176" height="692"></rect> </a>', 

588 '<a xlink:href="./c06.html"> <rect x="1375" y="911" fill="#fff" opacity="0" ' 

589 'width="176" height="692"></rect> </a>', 

590 '<a xlink:href="./c05.html"> <rect x="1197" y="911" fill="#fff" opacity="0" ' 

591 'width="176" height="692"></rect> </a>', 

592 '<a xlink:href="./c04.html"> <rect x="1020" y="911" fill="#fff" opacity="0" ' 

593 'width="176" height="692"></rect> </a>', 

594 '<a xlink:href="./c03.html"> <rect x="845" y="911" fill="#fff" opacity="0" ' 

595 'width="176" height="692"></rect> </a>', 

596 '<a xlink:href="./c02.html"> <rect x="670" y="911" fill="#fff" opacity="0" ' 

597 'width="176" height="692"></rect> </a>', 

598 '<a xlink:href="./c01.html"> <rect x="490" y="911" fill="#fff" opacity="0" ' 

599 'width="176" height="692"></rect> </a>', 

600 '<a xlink:href="./c00.html"> <rect x="317" y="911" fill="#fff" opacity="0" ' 

601 'width="176" height="692"></rect> </a>', 

602 '</svg>']) 

603 

604 def svg_mosaic_comcam(self, relative_file, page): 

605 """This adds an image to the html, with an SVG overlay for ComCam. 

606 

607 Parameters 

608 ---------- 

609 relative_file : `str` 

610 Relative location of the image to be added. 

611 page : `list` [`str`] 

612 The page to append to. 

613 

614 Notes 

615 ----- 

616 This method assumes the image is a 8x8 binned focal plane 

617 mosaic of a ComCam exposure made by one of the VisualizeVisit 

618 tasks. Under these assumptions, the boxes defined in SVG map 

619 to the detectors in the single raft. 

620 """ 

621 page.extend(['<svg version="1.1" xmlns="http://www.w3.org/2000/svg" ' 

622 'xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 640 480">', 

623 f'<image width="640" height="480" xlink:href="{relative_file}"></image>', 

624 '<a xlink:href="./S00.html"> <rect x="111" y="306" fill="#fff" opacity="0"' 

625 'width="128" height="128"></rect> </a>', 

626 '<a xlink:href="./S01.html"> <rect x="111" y="175" fill="#fff" opacity="0"' 

627 'width="128" height="128"></rect> </a>', 

628 '<a xlink:href="./S02.html"> <rect x="111" y="50" fill="#fff" opacity="0" ' 

629 'width="128" height="128"></rect> </a>', 

630 '<a xlink:href="./S10.html"> <rect x="244" y="306" fill="#fff" opacity="0" ' 

631 'width="128" height="128"></rect> </a>', 

632 '<a xlink:href="./S11.html"> <rect x="244" y="175" fill="#fff" opacity="0" ' 

633 'width="128" height="128"></rect> </a>', 

634 '<a xlink:href="./S12.html"> <rect x="244" y="50" fill="#fff" opacity="0" ' 

635 'width="128" height="128"></rect> </a>', 

636 '<a xlink:href="./S20.html"> <rect x="376" y="306" fill="#fff" opacity="0" ' 

637 'width="128" height="128"></rect> </a>', 

638 '<a xlink:href="./S21.html"> <rect x="376" y="175" fill="#fff" opacity="0" ' 

639 'width="128" height="128"></rect> </a>', 

640 '<a xlink:href="./S22.html"> <rect x="376" y="50" fill="#fff" opacity="0" ' 

641 'width="128" height="128"></rect> </a>', 

642 '</svg>']) 

643 

644 def image_handler(self, image_filename, instrument, page): 

645 """Handle images. 

646 

647 This method attempts to find the correct way to add an image 

648 to a page, with the appropriate SVG subregion information. 

649 

650 Parameters 

651 ---------- 

652 image_filename : `str` 

653 Full path to the image file to be added to a page. 

654 instrument : `str` 

655 Instrument name to use to identify special handling. 

656 page : `list` 

657 The page to append to. 

658 

659 """ 

660 relative_file = self.relative_file(image_filename) 

661 if "Mosaic64" not in relative_file: 

662 # Skip Mosaic64 products. 

663 if instrument == 'LATISS': 

664 if "Mosaic" in relative_file: 

665 self.svg_mosaic_latiss(relative_file, page) 

666 else: 

667 self.svg_atools_latiss(relative_file, page) 

668 elif instrument in ('LSSTComCam', 'LSSTComCamSim'): 

669 if "Mosaic" in relative_file: 

670 # This is a fits_to_png thing 

671 self.svg_mosaic_comcam(relative_file, page) 

672 else: 

673 self.svg_atools_comcam(relative_file, page) 

674 else: 

675 # We couldn't find a magic handler, so add it 

676 # manually. 

677 page.append(f'<img class="center-fit" src="{relative_file}">') 

678 

679 def block(self, dataset, page, link=None, doc=None): 

680 """Add a block of information, containing the dataId and collections 

681 for a particular dataset. 

682 

683 Parameters 

684 ---------- 

685 dataset : `dict` 

686 The dataset containing dataId information. 

687 page : `list` 

688 The page to append to. 

689 link : `str`, optional 

690 A link to apply to the dataset type name. 

691 doc : `str`, optional 

692 A documentation string. If not supplied, the documentation 

693 in the configuration is used. 

694 """ 

695 if doc is None: 

696 stage = dataset['stage'] 

697 dsType = dataset['type'] 

698 doc = self.dataset_map['stages'][stage][dsType].get("description", "Undocumented.") 

699 page.extend(["<table width='100%'>", 

700 "<tr>", 

701 "<th colspan='2' align='center'>DataId:</th>", 

702 "<th colspan='2' align='center'>Dataset:</th>", "</tr>"]) 

703 page.extend(["<tr>", 

704 "<td align='right'>Instrument</td>", f"<td>{dataset['instrument']}</td>", 

705 "<td align='right'>Collection</td>", f"<td>{dataset['collection']}</td>", 

706 "</tr>"]) 

707 page.extend(["<tr>", 

708 "<td align='right'>Exposure</td>", f"<td>{dataset['exposure']}</td>", 

709 "<td align='right'>Stage</td>", f"<td>{dataset['stage']}</td>", "</tr>"]) 

710 page.extend(["<tr>", 

711 "<td align='right'>Detector</td>", f"<td>{dataset['detector']}</td>"]), 

712 

713 page.extend(["<a href='" + f"{link}" + "'>" if link else ""]) 

714 page.extend(["<td align='right'>Dataset type</td>", f"<td>{dataset['type']} ", "</td>"]) 

715 page.extend(["</a>" if link else ""]) 

716 page.extend(["</tr>"]) 

717 page.extend(["<tr>", 

718 "<td align='right'>Physical Filter</td>", f"<td>{dataset['physical_filter']}</td>", 

719 "<td align='right'>Docstring</td>", f"<td>{doc}</td>", 

720 "</tr>"]) 

721 page.extend(["</table>"]) 

722 

723 @staticmethod 

724 def link(page, text, link): 

725 text_ref = text.replace(" ", "-") 

726 page.extend([f"<h2 id='{text_ref}'>", f"<a href='{link}'>", 

727 f"{text}" "</a>", "</h2>"]) 

728 return text_ref 

729 

730 def navigation_init(self): 

731 page = self.out_files["navigation.html"] 

732 page.extend(["<table class='ctable' width='100%'>", 

733 "<tr>"]) 

734 

735 def navigation_append(self, destination, text, tag, newColumn=False): 

736 page = self.out_files["navigation.html"] 

737 if newColumn: 

738 page.extend(["</tr>", "</tr>"]) 

739 page.extend(["<td>", 

740 f"<a href='./{destination}#{tag}'>", 

741 f"{text}", 

742 "</a>", "</td>"]) 

743 

744 def title(self, page, text): 

745 text_ref = text.replace(" ", "-") 

746 page.extend(["<center>", 

747 f"<h1 id={text_ref}>", text, "</h1>", 

748 "</center>"]) 

749 self.navigation_append("index.html", text, text_ref, newColumn=True) 

750 

751 def include(self, source, target): 

752 target_page = self.out_files[target] 

753 relative_file = self.relative_file(source) 

754 target_page.extend([f"<iframe width='100%' src='./{relative_file}'>", 

755 "</iframe>"]) 

756 

757 

758def main(): 

759 # Could these use the click/daf_butler CLI tools? 

760 parser = argparse.ArgumentParser(description="Construct a cp_verify metric report.") 

761 parser.add_argument("-r", "--repository", dest="repository", default="", 

762 help="Butler repository to pull results from.") 

763 parser.add_argument("-O", "--output_path", dest="output_path", default="", 

764 help="Output path to write report to.") 

765 parser.add_argument("-c", "--collections", dest="collections", action="append", default=[], 

766 help="Collections to search for results.") 

767 parser.add_argument("--no_copy", dest="do_copy", action="store_false", default=True, 

768 help="Skip copying files for debugging purposes.") 

769 parser.add_argument("--do_overwrite", dest="do_overwrite", action="store_true", default=False, 

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

771 args = parser.parse_args() 

772 

773 reporter = CpvReporter( 

774 repo=args.repository, 

775 output_path=args.output_path, 

776 collections=args.collections, 

777 do_copy=args.do_copy, 

778 do_overwrite=args.do_overwrite, 

779 ) 

780 reporter.run()