Coverage for python/lsst/obs/lsst/rawFormatter.py: 96%
119 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-16 04:20 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-16 04:20 -0700
1# This file is part of obs_lsst.
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 program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
22"""Gen3 Butler Formatters for LSST raw data.
23"""
25__all__ = (
26 "LsstCamRawFormatter",
27 "LatissRawFormatter",
28 "LsstCamImSimRawFormatter",
29 "LsstCamPhoSimRawFormatter",
30 "LsstTS8RawFormatter",
31 "LsstTS3RawFormatter",
32 "LsstComCamRawFormatter",
33 "LsstComCamSimRawFormatter",
34 "LsstUCDCamRawFormatter",
35)
37import numpy as np
38from astro_metadata_translator import fix_header, merge_headers
40import lsst.afw.fits
41from lsst.obs.base import FitsRawFormatterBase
42from lsst.obs.base.formatters.fitsExposure import standardizeAmplifierParameters
43from lsst.afw.cameraGeom import makeUpdatedDetector
45from ._instrument import LsstCam, Latiss, \
46 LsstCamImSim, LsstCamPhoSim, LsstTS8, \
47 LsstTS3, LsstUCDCam, LsstComCam, LsstComCamSim
48from .translators import LatissTranslator, LsstCamTranslator, \
49 LsstUCDCamTranslator, LsstTS3Translator, LsstComCamTranslator, \
50 LsstCamPhoSimTranslator, LsstTS8Translator, LsstCamImSimTranslator, \
51 LsstComCamSimTranslator
52from .assembly import fixAmpsAndAssemble, fixAmpGeometry, readRawAmps, warn_once
55class LsstCamRawFormatter(FitsRawFormatterBase):
56 translatorClass = LsstCamTranslator
57 filterDefinitions = LsstCam.filterDefinitions
58 _instrument = LsstCam
60 # These named HDUs' headers will be checked for and added to metadata.
61 _extraFitsHeaders = ["REB_COND"]
63 def readMetadata(self):
64 """Read all header metadata directly into a PropertyList.
66 Will merge additional headers if required.
68 Returns
69 -------
70 metadata : `~lsst.daf.base.PropertyList`
71 Header metadata.
72 """
73 file = self.fileDescriptor.location.path
75 with lsst.afw.fits.Fits(file, "r") as hdu:
76 hdu.setHdu(0)
77 base_md = hdu.readMetadata()
79 # Any extra HDUs we need to read.
80 ehdrs = []
81 for hduname in self._extraFitsHeaders:
82 try:
83 hdu.setHdu(hduname)
84 ehdr = hdu.readMetadata()
85 except lsst.afw.fits.FitsError:
86 # The header doesn't exist in this file. Skip.
87 continue
88 else:
89 ehdrs.append(ehdr)
91 final_md = merge_headers([base_md] + ehdrs, mode="overwrite")
92 fix_header(final_md, translator_class=self.translatorClass)
93 return final_md
95 def stripMetadata(self):
96 """Remove metadata entries that are parsed into components."""
97 if "CRVAL1" not in self.metadata:
98 # No need to strip WCS since we do not seem to have any WCS.
99 return
100 super().stripMetadata()
102 def getDetector(self, id):
103 in_detector = self._instrument.getCamera()[id]
104 # The detectors attached to the Camera object represent the on-disk
105 # amplifier geometry, not the assembled raw. But Butler users
106 # shouldn't know or care about what's on disk; they want the Detector
107 # that's equivalent to `butler.get("raw", ...).getDetector()`, so we
108 # adjust it accordingly. This parallels the logic in
109 # fixAmpsAndAssemble, but that function and the ISR AssembleCcdTask it
110 # calls aren't set up to handle bare bounding boxes with no pixels. We
111 # also can't remove those without API breakage. So this is fairly
112 # duplicative, and hence fragile.
113 # We start by fixing amp bounding boxes based on the size of the amp
114 # images themselves, because the camera doesn't have the right overscan
115 # regions for all images.
116 filename = self.fileDescriptor.location.path
117 temp_detector = in_detector.rebuild()
118 temp_detector.clear()
119 with warn_once(filename) as logCmd:
120 for n, in_amp in enumerate(in_detector):
121 reader = lsst.afw.image.ImageFitsReader(filename, hdu=(n + 1))
122 out_amp, _ = fixAmpGeometry(in_amp,
123 bbox=reader.readBBox(),
124 metadata=reader.readMetadata(),
125 logCmd=logCmd)
126 temp_detector.append(out_amp)
127 adjusted_detector = temp_detector.finish()
128 # Now we need to apply flips and offsets to reflect assembly. The
129 # function call that does this in fixAmpsAndAssemble is down inside
130 # ip.isr.AssembleCcdTask.
131 return makeUpdatedDetector(adjusted_detector)
133 def readImage(self):
134 # Docstring inherited.
135 return self.readFull().getImage()
137 def readFull(self):
138 # Docstring inherited.
139 rawFile = self.fileDescriptor.location.path
140 amplifier, detector, _ = standardizeAmplifierParameters(
141 self.checked_parameters,
142 self._instrument.getCamera()[self.observationInfo.detector_num],
143 )
144 if amplifier is not None:
145 # LSST raws are already per-amplifier on disk, and in a different
146 # assembly state than all of the other images we see in
147 # DM-maintained formatters. And we also need to deal with the
148 # on-disk image having different overscans from our nominal
149 # detector. So we can't use afw.cameraGeom.AmplifierIsolator for
150 # most of the implementation (as other formatters do), but we can
151 # call most of the same underlying code to do the work.
153 def findAmpHdu(name):
154 """Find the HDU for the amplifier with the given name,
155 according to cameraGeom.
156 """
157 for hdu, amp in enumerate(detector): 157 ↛ 160line 157 didn't jump to line 160, because the loop on line 157 didn't complete
158 if amp.getName() == name:
159 return hdu + 1
160 raise LookupError(f"Could not find HDU for amp with name {name}.")
162 reader = lsst.afw.image.ImageFitsReader(rawFile, hdu=findAmpHdu(amplifier.getName()))
163 image = reader.read(dtype=np.dtype(np.int32), allowUnsafe=True)
164 with warn_once(rawFile) as logCmd:
165 # Extract an amplifier from the on-disk detector and fix its
166 # overscan bboxes as necessary to match the on-disk bbox.
167 adjusted_amplifier_builder, _ = fixAmpGeometry(
168 detector[amplifier.getName()],
169 bbox=image.getBBox(),
170 metadata=reader.readMetadata(),
171 logCmd=logCmd,
172 )
173 on_disk_amplifier = adjusted_amplifier_builder.finish()
174 # We've now got two Amplifier objects in play:
175 # A) 'amplifier' is what the user wants
176 # B) 'on_disk_amplifier' represents the subimage we have.
177 # The one we want has the orientation/shift state of (A) with
178 # the overscan regions of (B).
179 comparison = amplifier.compareGeometry(on_disk_amplifier)
180 # If the flips or origins differ, we need to modify the image
181 # itself.
182 if comparison & comparison.FLIPPED:
183 from lsst.afw.math import flipImage
184 image = flipImage(
185 image,
186 comparison & comparison.FLIPPED_X,
187 comparison & comparison.FLIPPED_Y,
188 )
189 if comparison & comparison.SHIFTED:
190 image.setXY0(amplifier.getRawBBox().getMin())
191 # Make a single-amplifier detector that reflects the image we're
192 # returning.
193 detector_builder = detector.rebuild()
194 detector_builder.clear()
195 detector_builder.unsetCrosstalk()
196 if comparison & comparison.REGIONS_DIFFER: 196 ↛ 201line 196 didn't jump to line 201, because the condition on line 196 was never true
197 # We can't just install the amplifier the user gave us, because
198 # that has the wrong overscan regions; instead we transform the
199 # on-disk amplifier to have the same orientation and offsets as
200 # the given one.
201 adjusted_amplifier_builder.transform(
202 outOffset=on_disk_amplifier.getRawXYOffset(),
203 outFlipX=amplifier.getRawFlipX(),
204 outFlipY=amplifier.getRawFlipY(),
205 )
206 detector_builder.append(adjusted_amplifier_builder)
207 detector_builder.setBBox(adjusted_amplifier_builder.getBBox())
208 else:
209 detector_builder.append(amplifier.rebuild())
210 detector_builder.setBBox(amplifier.getBBox())
211 exposure = lsst.afw.image.makeExposure(lsst.afw.image.makeMaskedImage(image))
212 exposure.setDetector(detector_builder.finish())
213 else:
214 ampExps = readRawAmps(rawFile, detector)
215 exposure = fixAmpsAndAssemble(ampExps, rawFile)
216 self.attachComponentsFromMetadata(exposure)
217 return exposure
220class LatissRawFormatter(LsstCamRawFormatter):
221 translatorClass = LatissTranslator
222 _instrument = Latiss
223 filterDefinitions = Latiss.filterDefinitions
224 wcsFlipX = True
227class LsstCamImSimRawFormatter(LsstCamRawFormatter):
228 translatorClass = LsstCamImSimTranslator
229 _instrument = LsstCamImSim
230 filterDefinitions = LsstCamImSim.filterDefinitions
233class LsstCamPhoSimRawFormatter(LsstCamRawFormatter):
234 translatorClass = LsstCamPhoSimTranslator
235 _instrument = LsstCamPhoSim
236 filterDefinitions = LsstCamPhoSim.filterDefinitions
237 _extraFitsHeaders = [1]
240class LsstTS8RawFormatter(LsstCamRawFormatter):
241 translatorClass = LsstTS8Translator
242 _instrument = LsstTS8
243 filterDefinitions = LsstTS8.filterDefinitions
246class LsstTS3RawFormatter(LsstCamRawFormatter):
247 translatorClass = LsstTS3Translator
248 _instrument = LsstTS3
249 filterDefinitions = LsstTS3.filterDefinitions
252class LsstComCamRawFormatter(LsstCamRawFormatter):
253 translatorClass = LsstComCamTranslator
254 _instrument = LsstComCam
255 filterDefinitions = LsstComCam.filterDefinitions
258class LsstComCamSimRawFormatter(LsstCamRawFormatter):
259 translatorClass = LsstComCamSimTranslator
260 _instrument = LsstComCamSim
261 filterDefinitions = LsstComCamSim.filterDefinitions
264class LsstUCDCamRawFormatter(LsstCamRawFormatter):
265 translatorClass = LsstUCDCamTranslator
266 _instrument = LsstUCDCam
267 filterDefinitions = LsstUCDCam.filterDefinitions