Coverage for python/lsst/obs/lsst/rawFormatter.py: 96%
115 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-24 11:06 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-24 11:06 +0000
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 "LsstUCDCamRawFormatter",
34)
36import numpy as np
37from astro_metadata_translator import fix_header, merge_headers
39import lsst.afw.fits
40from lsst.obs.base import FitsRawFormatterBase
41from lsst.obs.base.formatters.fitsExposure import standardizeAmplifierParameters
42from lsst.afw.cameraGeom import makeUpdatedDetector
44from ._instrument import LsstCam, Latiss, \
45 LsstCamImSim, LsstCamPhoSim, LsstTS8, \
46 LsstTS3, LsstUCDCam, LsstComCam
47from .translators import LatissTranslator, LsstCamTranslator, \
48 LsstUCDCamTranslator, LsstTS3Translator, LsstComCamTranslator, \
49 LsstCamPhoSimTranslator, LsstTS8Translator, LsstCamImSimTranslator
50from .assembly import fixAmpsAndAssemble, fixAmpGeometry, readRawAmps, warn_once
53class LsstCamRawFormatter(FitsRawFormatterBase):
54 translatorClass = LsstCamTranslator
55 filterDefinitions = LsstCam.filterDefinitions
56 _instrument = LsstCam
58 # These named HDUs' headers will be checked for and added to metadata.
59 _extraFitsHeaders = ["REB_COND"]
61 def readMetadata(self):
62 """Read all header metadata directly into a PropertyList.
64 Will merge additional headers if required.
66 Returns
67 -------
68 metadata : `~lsst.daf.base.PropertyList`
69 Header metadata.
70 """
71 file = self.fileDescriptor.location.path
73 with lsst.afw.fits.Fits(file, "r") as hdu:
74 hdu.setHdu(0)
75 base_md = hdu.readMetadata()
77 # Any extra HDUs we need to read.
78 ehdrs = []
79 for hduname in self._extraFitsHeaders:
80 try:
81 hdu.setHdu(hduname)
82 ehdr = hdu.readMetadata()
83 except lsst.afw.fits.FitsError:
84 # The header doesn't exist in this file. Skip.
85 continue
86 else:
87 ehdrs.append(ehdr)
89 final_md = merge_headers([base_md] + ehdrs, mode="overwrite")
90 fix_header(final_md, translator_class=self.translatorClass)
91 return final_md
93 def stripMetadata(self):
94 """Remove metadata entries that are parsed into components."""
95 if "CRVAL1" not in self.metadata:
96 # No need to strip WCS since we do not seem to have any WCS.
97 return
98 super().stripMetadata()
100 def getDetector(self, id):
101 in_detector = self._instrument.getCamera()[id]
102 # The detectors attached to the Camera object represent the on-disk
103 # amplifier geometry, not the assembled raw. But Butler users
104 # shouldn't know or care about what's on disk; they want the Detector
105 # that's equivalent to `butler.get("raw", ...).getDetector()`, so we
106 # adjust it accordingly. This parallels the logic in
107 # fixAmpsAndAssemble, but that function and the ISR AssembleCcdTask it
108 # calls aren't set up to handle bare bounding boxes with no pixels. We
109 # also can't remove those without API breakage. So this is fairly
110 # duplicative, and hence fragile.
111 # We start by fixing amp bounding boxes based on the size of the amp
112 # images themselves, because the camera doesn't have the right overscan
113 # regions for all images.
114 filename = self.fileDescriptor.location.path
115 temp_detector = in_detector.rebuild()
116 temp_detector.clear()
117 with warn_once(filename) as logCmd:
118 for n, in_amp in enumerate(in_detector):
119 reader = lsst.afw.image.ImageFitsReader(filename, hdu=(n + 1))
120 out_amp, _ = fixAmpGeometry(in_amp,
121 bbox=reader.readBBox(),
122 metadata=reader.readMetadata(),
123 logCmd=logCmd)
124 temp_detector.append(out_amp)
125 adjusted_detector = temp_detector.finish()
126 # Now we need to apply flips and offsets to reflect assembly. The
127 # function call that does this in fixAmpsAndAssemble is down inside
128 # ip.isr.AssembleCcdTask.
129 return makeUpdatedDetector(adjusted_detector)
131 def readImage(self):
132 # Docstring inherited.
133 return self.readFull().getImage()
135 def readFull(self):
136 # Docstring inherited.
137 rawFile = self.fileDescriptor.location.path
138 amplifier, detector, _ = standardizeAmplifierParameters(
139 self.checked_parameters,
140 self._instrument.getCamera()[self.observationInfo.detector_num],
141 )
142 if amplifier is not None:
143 # LSST raws are already per-amplifier on disk, and in a different
144 # assembly state than all of the other images we see in
145 # DM-maintained formatters. And we also need to deal with the
146 # on-disk image having different overscans from our nominal
147 # detector. So we can't use afw.cameraGeom.AmplifierIsolator for
148 # most of the implementation (as other formatters do), but we can
149 # call most of the same underlying code to do the work.
151 def findAmpHdu(name):
152 """Find the HDU for the amplifier with the given name,
153 according to cameraGeom.
154 """
155 for hdu, amp in enumerate(detector): 155 ↛ 158line 155 didn't jump to line 158, because the loop on line 155 didn't complete
156 if amp.getName() == name:
157 return hdu + 1
158 raise LookupError(f"Could not find HDU for amp with name {name}.")
160 reader = lsst.afw.image.ImageFitsReader(rawFile, hdu=findAmpHdu(amplifier.getName()))
161 image = reader.read(dtype=np.dtype(np.int32), allowUnsafe=True)
162 with warn_once(rawFile) as logCmd:
163 # Extract an amplifier from the on-disk detector and fix its
164 # overscan bboxes as necessary to match the on-disk bbox.
165 adjusted_amplifier_builder, _ = fixAmpGeometry(
166 detector[amplifier.getName()],
167 bbox=image.getBBox(),
168 metadata=reader.readMetadata(),
169 logCmd=logCmd,
170 )
171 on_disk_amplifier = adjusted_amplifier_builder.finish()
172 # We've now got two Amplifier objects in play:
173 # A) 'amplifier' is what the user wants
174 # B) 'on_disk_amplifier' represents the subimage we have.
175 # The one we want has the orientation/shift state of (A) with
176 # the overscan regions of (B).
177 comparison = amplifier.compareGeometry(on_disk_amplifier)
178 # If the flips or origins differ, we need to modify the image
179 # itself.
180 if comparison & comparison.FLIPPED:
181 from lsst.afw.math import flipImage
182 image = flipImage(
183 image,
184 comparison & comparison.FLIPPED_X,
185 comparison & comparison.FLIPPED_Y,
186 )
187 if comparison & comparison.SHIFTED:
188 image.setXY0(amplifier.getRawBBox().getMin())
189 # Make a single-amplifier detector that reflects the image we're
190 # returning.
191 detector_builder = detector.rebuild()
192 detector_builder.clear()
193 detector_builder.unsetCrosstalk()
194 if comparison & comparison.REGIONS_DIFFER: 194 ↛ 199line 194 didn't jump to line 199, because the condition on line 194 was never true
195 # We can't just install the amplifier the user gave us, because
196 # that has the wrong overscan regions; instead we transform the
197 # on-disk amplifier to have the same orientation and offsets as
198 # the given one.
199 adjusted_amplifier_builder.transform(
200 outOffset=on_disk_amplifier.getRawXYOffset(),
201 outFlipX=amplifier.getRawFlipX(),
202 outFlipY=amplifier.getRawFlipY(),
203 )
204 detector_builder.append(adjusted_amplifier_builder)
205 detector_builder.setBBox(adjusted_amplifier_builder.getBBox())
206 else:
207 detector_builder.append(amplifier.rebuild())
208 detector_builder.setBBox(amplifier.getBBox())
209 exposure = lsst.afw.image.makeExposure(lsst.afw.image.makeMaskedImage(image))
210 exposure.setDetector(detector_builder.finish())
211 else:
212 ampExps = readRawAmps(rawFile, detector)
213 exposure = fixAmpsAndAssemble(ampExps, rawFile)
214 self.attachComponentsFromMetadata(exposure)
215 return exposure
218class LatissRawFormatter(LsstCamRawFormatter):
219 translatorClass = LatissTranslator
220 _instrument = Latiss
221 filterDefinitions = Latiss.filterDefinitions
222 wcsFlipX = True
225class LsstCamImSimRawFormatter(LsstCamRawFormatter):
226 translatorClass = LsstCamImSimTranslator
227 _instrument = LsstCamImSim
228 filterDefinitions = LsstCamImSim.filterDefinitions
231class LsstCamPhoSimRawFormatter(LsstCamRawFormatter):
232 translatorClass = LsstCamPhoSimTranslator
233 _instrument = LsstCamPhoSim
234 filterDefinitions = LsstCamPhoSim.filterDefinitions
235 _extraFitsHeaders = [1]
238class LsstTS8RawFormatter(LsstCamRawFormatter):
239 translatorClass = LsstTS8Translator
240 _instrument = LsstTS8
241 filterDefinitions = LsstTS8.filterDefinitions
244class LsstTS3RawFormatter(LsstCamRawFormatter):
245 translatorClass = LsstTS3Translator
246 _instrument = LsstTS3
247 filterDefinitions = LsstTS3.filterDefinitions
250class LsstComCamRawFormatter(LsstCamRawFormatter):
251 translatorClass = LsstComCamTranslator
252 _instrument = LsstComCam
253 filterDefinitions = LsstComCam.filterDefinitions
256class LsstUCDCamRawFormatter(LsstCamRawFormatter):
257 translatorClass = LsstUCDCamTranslator
258 _instrument = LsstUCDCam
259 filterDefinitions = LsstUCDCam.filterDefinitions