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

123 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-04-26 04:02 -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 "LsstCamSimRawFormatter", 

31 "LsstTS8RawFormatter", 

32 "LsstTS3RawFormatter", 

33 "LsstComCamRawFormatter", 

34 "LsstComCamSimRawFormatter", 

35 "LsstUCDCamRawFormatter", 

36) 

37 

38import numpy as np 

39from astro_metadata_translator import fix_header, merge_headers 

40 

41import lsst.afw.fits 

42from lsst.obs.base import FitsRawFormatterBase 

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

44from lsst.afw.cameraGeom import makeUpdatedDetector 

45 

46from ._instrument import LsstCam, Latiss, \ 

47 LsstCamImSim, LsstCamPhoSim, LsstTS8, \ 

48 LsstTS3, LsstUCDCam, LsstComCam, LsstComCamSim, LsstCamSim 

49from .translators import LatissTranslator, LsstCamTranslator, \ 

50 LsstUCDCamTranslator, LsstTS3Translator, LsstComCamTranslator, \ 

51 LsstCamPhoSimTranslator, LsstTS8Translator, LsstCamImSimTranslator, \ 

52 LsstComCamSimTranslator, LsstCamSimTranslator 

53from .assembly import fixAmpsAndAssemble, fixAmpGeometry, readRawAmps, warn_once 

54 

55 

56class LsstCamRawFormatter(FitsRawFormatterBase): 

57 translatorClass = LsstCamTranslator 

58 filterDefinitions = LsstCam.filterDefinitions 

59 _instrument = LsstCam 

60 

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

62 _extraFitsHeaders = ["REB_COND"] 

63 

64 def readMetadata(self): 

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

66 

67 Will merge additional headers if required. 

68 

69 Returns 

70 ------- 

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

72 Header metadata. 

73 """ 

74 file = self.fileDescriptor.location.path 

75 

76 with lsst.afw.fits.Fits(file, "r") as hdu: 

77 hdu.setHdu(0) 

78 base_md = hdu.readMetadata() 

79 

80 # Any extra HDUs we need to read. 

81 ehdrs = [] 

82 for hduname in self._extraFitsHeaders: 

83 try: 

84 hdu.setHdu(hduname) 

85 ehdr = hdu.readMetadata() 

86 except lsst.afw.fits.FitsError: 

87 # The header doesn't exist in this file. Skip. 

88 continue 

89 else: 

90 ehdrs.append(ehdr) 

91 

92 final_md = merge_headers([base_md] + ehdrs, mode="overwrite") 

93 fix_header(final_md, translator_class=self.translatorClass) 

94 return final_md 

95 

96 def stripMetadata(self): 

97 """Remove metadata entries that are parsed into components.""" 

98 if "CRVAL1" not in self.metadata: 

99 # No need to strip WCS since we do not seem to have any WCS. 

100 return 

101 super().stripMetadata() 

102 

103 def getDetector(self, id): 

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

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

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

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

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

109 # adjust it accordingly. This parallels the logic in 

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

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

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

113 # duplicative, and hence fragile. 

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

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

116 # regions for all images. 

117 filename = self.fileDescriptor.location.path 

118 temp_detector = in_detector.rebuild() 

119 temp_detector.clear() 

120 with warn_once(filename) as logCmd: 

121 for n, in_amp in enumerate(in_detector): 

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

123 out_amp, _ = fixAmpGeometry(in_amp, 

124 bbox=reader.readBBox(), 

125 metadata=reader.readMetadata(), 

126 logCmd=logCmd) 

127 temp_detector.append(out_amp) 

128 adjusted_detector = temp_detector.finish() 

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

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

131 # ip.isr.AssembleCcdTask. 

132 return makeUpdatedDetector(adjusted_detector) 

133 

134 def readImage(self): 

135 # Docstring inherited. 

136 return self.readFull().getImage() 

137 

138 def readFull(self): 

139 # Docstring inherited. 

140 rawFile = self.fileDescriptor.location.path 

141 amplifier, detector, _ = standardizeAmplifierParameters( 

142 self.checked_parameters, 

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

144 ) 

145 if amplifier is not None: 

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

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

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

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

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

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

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

153 

154 def findAmpHdu(name): 

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

156 according to cameraGeom. 

157 """ 

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

159 if amp.getName() == name: 

160 return hdu + 1 

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

162 

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

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

165 with warn_once(rawFile) as logCmd: 

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

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

168 adjusted_amplifier_builder, _ = fixAmpGeometry( 

169 detector[amplifier.getName()], 

170 bbox=image.getBBox(), 

171 metadata=reader.readMetadata(), 

172 logCmd=logCmd, 

173 ) 

174 on_disk_amplifier = adjusted_amplifier_builder.finish() 

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

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

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

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

179 # the overscan regions of (B). 

180 comparison = amplifier.compareGeometry(on_disk_amplifier) 

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

182 # itself. 

183 if comparison & comparison.FLIPPED: 

184 from lsst.afw.math import flipImage 

185 image = flipImage( 

186 image, 

187 comparison & comparison.FLIPPED_X, 

188 comparison & comparison.FLIPPED_Y, 

189 ) 

190 if comparison & comparison.SHIFTED: 

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

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

193 # returning. 

194 detector_builder = detector.rebuild() 

195 detector_builder.clear() 

196 detector_builder.unsetCrosstalk() 

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

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

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

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

201 # the given one. 

202 adjusted_amplifier_builder.transform( 

203 outOffset=on_disk_amplifier.getRawXYOffset(), 

204 outFlipX=amplifier.getRawFlipX(), 

205 outFlipY=amplifier.getRawFlipY(), 

206 ) 

207 detector_builder.append(adjusted_amplifier_builder) 

208 detector_builder.setBBox(adjusted_amplifier_builder.getBBox()) 

209 else: 

210 detector_builder.append(amplifier.rebuild()) 

211 detector_builder.setBBox(amplifier.getBBox()) 

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

213 exposure.setDetector(detector_builder.finish()) 

214 else: 

215 ampExps = readRawAmps(rawFile, detector) 

216 exposure = fixAmpsAndAssemble(ampExps, rawFile) 

217 self.attachComponentsFromMetadata(exposure) 

218 return exposure 

219 

220 

221class LatissRawFormatter(LsstCamRawFormatter): 

222 translatorClass = LatissTranslator 

223 _instrument = Latiss 

224 filterDefinitions = Latiss.filterDefinitions 

225 wcsFlipX = True 

226 

227 

228class LsstCamImSimRawFormatter(LsstCamRawFormatter): 

229 translatorClass = LsstCamImSimTranslator 

230 _instrument = LsstCamImSim 

231 filterDefinitions = LsstCamImSim.filterDefinitions 

232 

233 

234class LsstCamPhoSimRawFormatter(LsstCamRawFormatter): 

235 translatorClass = LsstCamPhoSimTranslator 

236 _instrument = LsstCamPhoSim 

237 filterDefinitions = LsstCamPhoSim.filterDefinitions 

238 _extraFitsHeaders = [1] 

239 

240 

241class LsstCamSimRawFormatter(LsstCamRawFormatter): 

242 translatorClass = LsstCamSimTranslator 

243 _instrument = LsstCamSim 

244 filterDefinitions = LsstCamSim.filterDefinitions 

245 

246 

247class LsstTS8RawFormatter(LsstCamRawFormatter): 

248 translatorClass = LsstTS8Translator 

249 _instrument = LsstTS8 

250 filterDefinitions = LsstTS8.filterDefinitions 

251 

252 

253class LsstTS3RawFormatter(LsstCamRawFormatter): 

254 translatorClass = LsstTS3Translator 

255 _instrument = LsstTS3 

256 filterDefinitions = LsstTS3.filterDefinitions 

257 

258 

259class LsstComCamRawFormatter(LsstCamRawFormatter): 

260 translatorClass = LsstComCamTranslator 

261 _instrument = LsstComCam 

262 filterDefinitions = LsstComCam.filterDefinitions 

263 

264 

265class LsstComCamSimRawFormatter(LsstCamRawFormatter): 

266 translatorClass = LsstComCamSimTranslator 

267 _instrument = LsstComCamSim 

268 filterDefinitions = LsstComCamSim.filterDefinitions 

269 

270 

271class LsstUCDCamRawFormatter(LsstCamRawFormatter): 

272 translatorClass = LsstUCDCamTranslator 

273 _instrument = LsstUCDCam 

274 filterDefinitions = LsstUCDCam.filterDefinitions