Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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__all__ = ("attachRawWcsFromBoresight", "fixAmpGeometry", "assembleUntrimmedCcd", 

23 "fixAmpsAndAssemble", "readRawAmps") 

24 

25import lsst.log 

26import lsst.afw.image as afwImage 

27from lsst.obs.base import bboxFromIraf, MakeRawVisitInfoViaObsInfo, createInitialSkyWcs 

28from lsst.geom import Box2I, Extent2I 

29from lsst.ip.isr import AssembleCcdTask 

30from astro_metadata_translator import ObservationInfo 

31 

32logger = lsst.log.Log.getLogger("obs.lsst.assembly") 

33 

34 

35def attachRawWcsFromBoresight(exposure, dataIdForErrMsg=None): 

36 """Attach a WCS by extracting boresight, rotation, and camera geometry from 

37 an Exposure. 

38 

39 Parameters 

40 ---------- 

41 exposure : `lsst.afw.image.Exposure` 

42 Image object with attached metadata and detector components. 

43 

44 Return 

45 ------ 

46 attached : `bool` 

47 If True, a WCS component was successfully created and attached to 

48 ``exposure``. 

49 """ 

50 md = exposure.getMetadata() 

51 # Use the generic version since we do not have a mapper available to 

52 # tell us a specific translator to use. 

53 obsInfo = ObservationInfo(md) 

54 visitInfo = MakeRawVisitInfoViaObsInfo.observationInfo2visitInfo(obsInfo, log=logger) 

55 exposure.getInfo().setVisitInfo(visitInfo) 

56 

57 # LATISS (and likely others) need flipping, DC2 etc do not 

58 flipX = False 

59 if obsInfo.instrument in ("LATISS",): 

60 flipX = True 

61 

62 if visitInfo.getBoresightRaDec().isFinite(): 

63 exposure.setWcs(createInitialSkyWcs(visitInfo, exposure.getDetector(), flipX=flipX)) 

64 return True 

65 

66 if obsInfo.observation_type == "science": 

67 logger.warn("Unable to set WCS from header as RA/Dec/Angle are unavailable%s", 

68 ("" if dataIdForErrMsg is None else " for dataId %s" % dataIdForErrMsg)) 

69 return False 

70 

71 

72def fixAmpGeometry(inAmp, bbox, metadata, logCmd=None): 

73 """Make sure a camera geometry amplifier matches an on-disk bounding box. 

74 

75 Bounding box differences that are consistent with differences in overscan 

76 regions are assumed to be overscan regions, which gives us enough 

77 information to correct the camera geometry. 

78 

79 Parameters 

80 ---------- 

81 inAmp : `lsst.afw.cameraGeom.Amplifier` 

82 Amplifier description from camera geometry. 

83 bbox : `lsst.geom.Box2I` 

84 The on-disk bounding box of the amplifer image. 

85 metadata : `lsst.daf.base.PropertyList` 

86 FITS header metadata from the amplifier HDU. 

87 logCmd : `function`, optional 

88 Call back to use to issue log messages. Arguments to this function 

89 should match arguments to be accepted by normal logging functions. 

90 

91 Return 

92 ------ 

93 outAmp : `~lsst.afw.cameraGeom.Amplifier.Builder` 

94 modified : `bool` 

95 `True` if ``amp`` was modified; `False` otherwise. 

96 

97 Raises 

98 ------ 

99 RuntimeError 

100 Raised if the bounding boxes differ in a way that is not consistent 

101 with just a change in overscan. 

102 """ 

103 if logCmd is None: 

104 # Define a null log command 

105 def logCmd(*args): 

106 return 

107 

108 modified = False 

109 

110 outAmp = inAmp.rebuild() 

111 if outAmp.getRawBBox() != bbox: # Oh dear. cameraGeom is wrong -- probably overscan 

112 if outAmp.getRawDataBBox().getDimensions() != outAmp.getBBox().getDimensions(): 

113 raise RuntimeError("Active area is the wrong size: %s v. %s" % 

114 (outAmp.getRawDataBBox().getDimensions(), outAmp.getBBox().getDimensions())) 

115 

116 logCmd("outAmp.getRawBBox() != data.getBBox(); patching. (%s v. %s)", outAmp.getRawBBox(), bbox) 

117 

118 w, h = bbox.getDimensions() 

119 ow, oh = outAmp.getRawBBox().getDimensions() # "old" (cameraGeom) dimensions 

120 # 

121 # We could trust the BIASSEC keyword, or we can just assume that 

122 # they've changed the number of overscan pixels (serial and/or 

123 # parallel). As Jim Chiang points out, the latter is safer 

124 # 

125 fromCamGeom = outAmp.getRawHorizontalOverscanBBox() 

126 hOverscanBBox = Box2I(fromCamGeom.getBegin(), 

127 Extent2I(w - fromCamGeom.getBeginX(), fromCamGeom.getHeight())) 

128 fromCamGeom = outAmp.getRawVerticalOverscanBBox() 

129 vOverscanBBox = Box2I(fromCamGeom.getBegin(), 

130 Extent2I(fromCamGeom.getWidth(), h - fromCamGeom.getBeginY())) 

131 outAmp.setRawBBox(bbox) 

132 outAmp.setRawHorizontalOverscanBBox(hOverscanBBox) 

133 outAmp.setRawVerticalOverscanBBox(vOverscanBBox) 

134 # 

135 # This gets all the geometry right for the amplifier, but the size 

136 # of the untrimmed image will be wrong and we'll put the amp sections 

137 # in the wrong places, i.e. 

138 # outAmp.getRawXYOffset() 

139 # will be wrong. So we need to recalculate the offsets. 

140 # 

141 xRawExtent, yRawExtent = outAmp.getRawBBox().getDimensions() 

142 

143 x0, y0 = outAmp.getRawXYOffset() 

144 ix, iy = x0//ow, y0/oh 

145 x0, y0 = ix*xRawExtent, iy*yRawExtent 

146 outAmp.setRawXYOffset(Extent2I(ix*xRawExtent, iy*yRawExtent)) 

147 

148 modified = True 

149 

150 # 

151 # Check the "IRAF" keywords, but don't abort if they're wrong 

152 # 

153 # Only warn about the first amp, use debug for the others 

154 # 

155 d = metadata.toDict() 

156 detsec = bboxFromIraf(d["DETSEC"]) if "DETSEC" in d else None 

157 datasec = bboxFromIraf(d["DATASEC"]) if "DATASEC" in d else None 

158 biassec = bboxFromIraf(d["BIASSEC"]) if "BIASSEC" in d else None 

159 

160 if detsec and outAmp.getBBox() != detsec: 

161 logCmd("DETSEC doesn't match (%s != %s)", outAmp.getBBox(), detsec) 

162 if datasec and outAmp.getRawDataBBox() != datasec: 

163 logCmd("DATASEC doesn't match for (%s != %s)", outAmp.getRawDataBBox(), detsec) 

164 if biassec and outAmp.getRawHorizontalOverscanBBox() != biassec: 

165 logCmd("BIASSEC doesn't match for (%s != %s)", outAmp.getRawHorizontalOverscanBBox(), detsec) 

166 

167 return outAmp, modified 

168 

169 

170def assembleUntrimmedCcd(ccd, exposures): 

171 """Assemble an untrimmmed CCD from per-amp Exposure objects. 

172 

173 Parameters 

174 ---------- 

175 ccd : `~lsst.afw.cameraGeom.Detector` 

176 The detector geometry for this ccd that will be used as the 

177 framework for the assembly of the input amplifier exposures. 

178 exposures : sequence of `lsst.afw.image.Exposure` 

179 Per-amplifier images, in the same order as ``amps``. 

180 

181 Returns 

182 ------- 

183 ccd : `lsst.afw.image.Exposure` 

184 Assembled CCD image. 

185 """ 

186 ampDict = {} 

187 for amp, exposure in zip(ccd, exposures): 

188 ampDict[amp.getName()] = exposure 

189 config = AssembleCcdTask.ConfigClass() 

190 config.doTrim = False 

191 assembleTask = AssembleCcdTask(config=config) 

192 return assembleTask.assembleCcd(ampDict) 

193 

194 

195def fixAmpsAndAssemble(ampExps, msg): 

196 """Fix amp geometry and assemble into exposure. 

197 

198 Parameters 

199 ---------- 

200 ampExps : sequence of `lsst.afw.image.Exposure` 

201 Per-amplifier images. 

202 msg : `str` 

203 Message to add to log and exception output. 

204 

205 Returns 

206 ------- 

207 exposure : `lsst.afw.image.Exposure` 

208 Exposure with the amps combined into a single image. 

209 

210 Notes 

211 ----- 

212 The returned exposure does not have any metadata or WCS attached. 

213 

214 """ 

215 if not len(ampExps): 

216 raise RuntimeError(f"Unable to read raw_amps for {msg}") 

217 

218 ccd = ampExps[0].getDetector() # the same (full, CCD-level) Detector is attached to all ampExps 

219 # 

220 # Check that the geometry in the metadata matches cameraGeom 

221 # 

222 warned = False 

223 

224 def logCmd(s, *args): 

225 nonlocal warned 

226 if warned: 

227 logger.debug(f"{msg}: {s}", *args) 

228 else: 

229 logger.warn(f"{msg}: {s}", *args) 

230 warned = True 

231 

232 # Rebuild the detector and the amplifiers to use their corrected geometry. 

233 tempCcd = ccd.rebuild() 

234 tempCcd.clear() 

235 for amp, ampExp in zip(ccd, ampExps): 

236 # check that the book-keeping worked and we got the correct EXTNAME 

237 extname = ampExp.getMetadata().get("EXTNAME") 

238 predictedExtname = f"Segment{amp.getName()[1:]}" 

239 if extname is not None and predictedExtname != extname: 

240 logger.warn('%s: expected to see EXTNAME == "%s", but saw "%s"', msg, predictedExtname, extname) 

241 

242 outAmp, modified = fixAmpGeometry(amp, 

243 bbox=ampExp.getBBox(), 

244 metadata=ampExp.getMetadata(), 

245 logCmd=logCmd) 

246 tempCcd.append(outAmp) 

247 

248 ccd = tempCcd.finish() 

249 

250 # Update the data to be combined to point to the newly rebuilt detector. 

251 for ampExp in ampExps: 

252 ampExp.setDetector(ccd) 

253 

254 exposure = assembleUntrimmedCcd(ccd, ampExps) 

255 return exposure 

256 

257 

258def readRawAmps(fileName, detector): 

259 """Given a file name read the amps and attach the detector. 

260 

261 Parameters 

262 ---------- 

263 fileName : `str` 

264 The full path to a file containing data from a single CCD. 

265 detector : `lsst.afw.cameraGeom.Detector` 

266 Detector to associate with the amps. 

267 

268 Returns 

269 ------- 

270 ampExps : `list` of `lsst.afw.image.Exposure` 

271 All the individual amps read from the file. 

272 """ 

273 amps = [] 

274 for hdu in range(1, len(detector)+1): 

275 exp = afwImage.makeExposure(afwImage.makeMaskedImage(afwImage.ImageF(fileName, hdu=hdu))) 

276 exp.setDetector(detector) 

277 amps.append(exp) 

278 return amps