Coverage for python/lsst/obs/lsst/rawFormatter.py: 94%

110 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-09 03:15 -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/>. 

21 

22"""Gen3 Butler Formatters for LSST raw data. 

23""" 

24 

25__all__ = ( 

26 "LsstCamRawFormatter", 

27 "LatissRawFormatter", 

28 "LsstCamImSimRawFormatter", 

29 "LsstCamPhoSimRawFormatter", 

30 "LsstTS8RawFormatter", 

31 "LsstTS3RawFormatter", 

32 "LsstComCamRawFormatter", 

33 "LsstUCDCamRawFormatter", 

34) 

35 

36import numpy as np 

37from astro_metadata_translator import fix_header, merge_headers 

38 

39import lsst.afw.fits 

40from lsst.obs.base import FitsRawFormatterBase 

41from lsst.obs.base.formatters.fitsExposure import standardizeAmplifierParameters 

42from lsst.afw.cameraGeom import makeUpdatedDetector 

43 

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 

51 

52 

53class LsstCamRawFormatter(FitsRawFormatterBase): 

54 translatorClass = LsstCamTranslator 

55 filterDefinitions = LsstCam.filterDefinitions 

56 _instrument = LsstCam 

57 

58 # These named HDUs' headers will be checked for and added to metadata. 

59 _extraFitsHeaders = ["REB_COND", "CONFIG_COND"] 

60 

61 def readMetadata(self): 

62 """Read all header metadata directly into a PropertyList. 

63 

64 Specialist version since some of our data does not 

65 set INHERIT=T so we have to merge the headers manually. 

66 

67 Returns 

68 ------- 

69 metadata : `~lsst.daf.base.PropertyList` 

70 Header metadata. 

71 """ 

72 file = self.fileDescriptor.location.path 

73 phdu = lsst.afw.fits.readMetadata(file, 0) 

74 

75 if "INHERIT" in phdu: 75 ↛ 77line 75 didn't jump to line 77, because the condition on line 75 was never true

76 # Trust the inheritance flag 

77 base_md = super().readMetadata() 

78 

79 # Merge ourselves 

80 else: 

81 base_md = merge_headers([phdu, lsst.afw.fits.readMetadata(file)], 

82 mode="overwrite") 

83 

84 ehdrs = [] 

85 for hduname in self._extraFitsHeaders: 

86 try: 

87 ehdr = lsst.afw.fits.readMetadata(file, hduname) 

88 except lsst.afw.fits.FitsError: 

89 # We can ignore this, the header doesn't exist in this file. 

90 continue 

91 else: 

92 ehdrs.append(ehdr) 

93 

94 final_md = merge_headers([base_md] + ehdrs, mode="first") 

95 fix_header(final_md) 

96 return final_md 

97 

98 def getDetector(self, id): 

99 in_detector = self._instrument.getCamera()[id] 

100 # The detectors attached to the Camera object represent the on-disk 

101 # amplifier geometry, not the assembled raw. But Butler users 

102 # shouldn't know or care about what's on disk; they want the Detector 

103 # that's equivalent to `butler.get("raw", ...).getDetector()`, so we 

104 # adjust it accordingly. This parallels the logic in 

105 # fixAmpsAndAssemble, but that function and the ISR AssembleCcdTask it 

106 # calls aren't set up to handle bare bounding boxes with no pixels. We 

107 # also can't remove those without API breakage. So this is fairly 

108 # duplicative, and hence fragile. 

109 # We start by fixing amp bounding boxes based on the size of the amp 

110 # images themselves, because the camera doesn't have the right overscan 

111 # regions for all images. 

112 filename = self.fileDescriptor.location.path 

113 temp_detector = in_detector.rebuild() 

114 temp_detector.clear() 

115 with warn_once(filename) as logCmd: 

116 for n, in_amp in enumerate(in_detector): 

117 reader = lsst.afw.image.ImageFitsReader(filename, hdu=(n + 1)) 

118 out_amp, _ = fixAmpGeometry(in_amp, 

119 bbox=reader.readBBox(), 

120 metadata=reader.readMetadata(), 

121 logCmd=logCmd) 

122 temp_detector.append(out_amp) 

123 adjusted_detector = temp_detector.finish() 

124 # Now we need to apply flips and offsets to reflect assembly. The 

125 # function call that does this in fixAmpsAndAssemble is down inside 

126 # ip.isr.AssembleCcdTask. 

127 return makeUpdatedDetector(adjusted_detector) 

128 

129 def readImage(self): 

130 # Docstring inherited. 

131 return self.readFull().getImage() 

132 

133 def readFull(self): 

134 # Docstring inherited. 

135 rawFile = self.fileDescriptor.location.path 

136 amplifier, detector, _ = standardizeAmplifierParameters( 

137 self.checked_parameters, 

138 self._instrument.getCamera()[self.observationInfo.detector_num], 

139 ) 

140 if amplifier is not None: 

141 # LSST raws are already per-amplifier on disk, and in a different 

142 # assembly state than all of the other images we see in 

143 # DM-maintained formatters. And we also need to deal with the 

144 # on-disk image having different overscans from our nominal 

145 # detector. So we can't use afw.cameraGeom.AmplifierIsolator for 

146 # most of the implementation (as other formatters do), but we can 

147 # call most of the same underlying code to do the work. 

148 

149 def findAmpHdu(name): 

150 """Find the HDU for the amplifier with the given name, 

151 according to cameraGeom. 

152 """ 

153 for hdu, amp in enumerate(detector): 153 ↛ 156line 153 didn't jump to line 156, because the loop on line 153 didn't complete

154 if amp.getName() == name: 

155 return hdu + 1 

156 raise LookupError(f"Could not find HDU for amp with name {name}.") 

157 

158 reader = lsst.afw.image.ImageFitsReader(rawFile, hdu=findAmpHdu(amplifier.getName())) 

159 image = reader.read(dtype=np.dtype(np.int32), allowUnsafe=True) 

160 with warn_once(rawFile) as logCmd: 

161 # Extract an amplifier from the on-disk detector and fix its 

162 # overscan bboxes as necessary to match the on-disk bbox. 

163 adjusted_amplifier_builder, _ = fixAmpGeometry( 

164 detector[amplifier.getName()], 

165 bbox=image.getBBox(), 

166 metadata=reader.readMetadata(), 

167 logCmd=logCmd, 

168 ) 

169 on_disk_amplifier = adjusted_amplifier_builder.finish() 

170 # We've now got two Amplifier objects in play: 

171 # A) 'amplifier' is what the user wants 

172 # B) 'on_disk_amplifier' represents the subimage we have. 

173 # The one we want has the orientation/shift state of (A) with 

174 # the overscan regions of (B). 

175 comparison = amplifier.compareGeometry(on_disk_amplifier) 

176 # If the flips or origins differ, we need to modify the image 

177 # itself. 

178 if comparison & comparison.FLIPPED: 

179 from lsst.afw.math import flipImage 

180 image = flipImage( 

181 image, 

182 comparison & comparison.FLIPPED_X, 

183 comparison & comparison.FLIPPED_Y, 

184 ) 

185 if comparison & comparison.SHIFTED: 

186 image.setXY0(amplifier.getRawBBox().getMin()) 

187 # Make a single-amplifier detector that reflects the image we're 

188 # returning. 

189 detector_builder = detector.rebuild() 

190 detector_builder.clear() 

191 detector_builder.unsetCrosstalk() 

192 if comparison & comparison.REGIONS_DIFFER: 192 ↛ 197line 192 didn't jump to line 197, because the condition on line 192 was never true

193 # We can't just install the amplifier the user gave us, because 

194 # that has the wrong overscan regions; instead we transform the 

195 # on-disk amplifier to have the same orientation and offsets as 

196 # the given one. 

197 adjusted_amplifier_builder.transform( 

198 outOffset=on_disk_amplifier.getRawXYOffset(), 

199 outFlipX=amplifier.getRawFlipX(), 

200 outFlipY=amplifier.getRawFlipY(), 

201 ) 

202 detector_builder.append(adjusted_amplifier_builder) 

203 detector_builder.setBBox(adjusted_amplifier_builder.getBBox()) 

204 else: 

205 detector_builder.append(amplifier.rebuild()) 

206 detector_builder.setBBox(amplifier.getBBox()) 

207 exposure = lsst.afw.image.makeExposure(lsst.afw.image.makeMaskedImage(image)) 

208 exposure.setDetector(detector_builder.finish()) 

209 else: 

210 ampExps = readRawAmps(rawFile, detector) 

211 exposure = fixAmpsAndAssemble(ampExps, rawFile) 

212 self.attachComponentsFromMetadata(exposure) 

213 return exposure 

214 

215 

216class LatissRawFormatter(LsstCamRawFormatter): 

217 translatorClass = LatissTranslator 

218 _instrument = Latiss 

219 filterDefinitions = Latiss.filterDefinitions 

220 wcsFlipX = True 

221 

222 

223class LsstCamImSimRawFormatter(LsstCamRawFormatter): 

224 translatorClass = LsstCamImSimTranslator 

225 _instrument = LsstCamImSim 

226 filterDefinitions = LsstCamImSim.filterDefinitions 

227 

228 

229class LsstCamPhoSimRawFormatter(LsstCamRawFormatter): 

230 translatorClass = LsstCamPhoSimTranslator 

231 _instrument = LsstCamPhoSim 

232 filterDefinitions = LsstCamPhoSim.filterDefinitions 

233 

234 

235class LsstTS8RawFormatter(LsstCamRawFormatter): 

236 translatorClass = LsstTS8Translator 

237 _instrument = LsstTS8 

238 filterDefinitions = LsstTS8.filterDefinitions 

239 

240 

241class LsstTS3RawFormatter(LsstCamRawFormatter): 

242 translatorClass = LsstTS3Translator 

243 _instrument = LsstTS3 

244 filterDefinitions = LsstTS3.filterDefinitions 

245 

246 

247class LsstComCamRawFormatter(LsstCamRawFormatter): 

248 translatorClass = LsstComCamTranslator 

249 _instrument = LsstComCam 

250 filterDefinitions = LsstComCam.filterDefinitions 

251 

252 

253class LsstUCDCamRawFormatter(LsstCamRawFormatter): 

254 translatorClass = LsstUCDCamTranslator 

255 _instrument = LsstUCDCam 

256 filterDefinitions = LsstUCDCam.filterDefinitions