Coverage for python/lsst/cp/verify/interactiveReportLSSTCam.py: 0%
130 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-01 02:29 -0700
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-01 02:29 -0700
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
32from collections import defaultdict
34from lsst.daf.butler import Butler
35from lsst.obs.lsst import LsstCam
37from .utils import det_to_index
40class DetailedReporterLSSTCam():
41 """A class to generate calibration verification reports.
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 """
53 def __init__(self, sharedPath, instrument, doOverwrite):
54 super().__init__()
55 # Set source and destination information.
56 self.sharedPath = sharedPath
57 self.doOverwrite = doOverwrite
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.")
67 # Logging
68 self.log = logging.getLogger(__name__)
70 # Instantiate a butler for our repository.
71 self.butler = Butler("/repo/main")
72 self.registry = self.butler.registry
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()
79 def writeReport(self, renderedHtml, fileName):
80 output_path = self.sharedPath + "/" + fileName
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
87 with open(output_path, "w") as f:
88 f.write(renderedHtml)
90 def getAllImages(self):
91 summaryImages = self.getSummaryImages()
92 detailedImages = self.getDetailedImages()
94 return summaryImages, detailedImages
96 def getSummaryImages(self):
97 mosaicImages = []
98 histImages = []
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
111 if len(mosaicImages) != len(histImages):
112 raise RuntimeError("The number of summary mosaic images does not match "
113 "the number of histogram images.")
115 mosaicImages = sorted(mosaicImages)
116 histImages = sorted(histImages)
118 summaryImages = list(zip(mosaicImages, histImages))
120 return summaryImages
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
144 # Sort tree data
145 for fullName in tree:
146 tree[fullName].sort(key=lambda x: x[0])
147 tree = dict(sorted(tree.items()))
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
160 return detailedImages
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()
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)
184 renderedHtml = summaryReportTemplate.replace("summary_images", addition)
186 self.writeReport(renderedHtml, "summary.html")
188 def makeDetailed(self, fullName, tree):
189 """A class to generate the detailed verification reports.
191 Parameters
192 ----------
193 fullName : `str`
194 The name of the file to output.
196 tree : `dict` [`list`, `tuple`]
197 Dictionary keyed by "{step}-{name}", that contains
198 a list of tuples (detectorId, relative/path/to/image).
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()
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()
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
236 renderedHtml = detailedReportTemplate.replace("grid_placeholder", addition).replace("name", name)
238 self.writeReport(renderedHtml, f"{fullName}.html")
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()
245 # Make summary plots
246 if len(summaryImages) > 0:
247 self.makeSummary(summaryImages)
249 # Make detailed plots
250 if len(detailedImages) > 0:
251 for fullName in detailedImages.keys():
252 tree = detailedImages[fullName]
253 self.makeDetailed(fullName, tree)
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:
264 images/
265 |_____R00/
266 | |______S00/
267 | |______S01/
268 | ...
269 |_____R01/ ...
270 |_____ ...
272 Also, the images in the directory need to follow this naming
273 convention:
275 ``step_\d{2}[a-zA-Z]?)-([a-zA-Z0-9_-]+)_(\d{2,3})\.png``
276 ==
277 step_(## + letter)-(descriptive name)_(detector ###).png
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.
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()
300 reporter = DetailedReporterLSSTCam(
301 sharedPath=args.sharedPath,
302 instrument=args.instrument,
303 doOverwrite=args.doOverwrite,
304 )
305 reporter.run()