Coverage for python/lsst/obs/lsst/assembly.py : 10%

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/>.
22__all__ = ("attachRawWcsFromBoresight", "fixAmpGeometry", "assembleUntrimmedCcd",
23 "fixAmpsAndAssemble", "readRawAmps")
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
32logger = lsst.log.Log.getLogger("obs.lsst.assembly")
35def attachRawWcsFromBoresight(exposure, dataIdForErrMsg=None):
36 """Attach a WCS by extracting boresight, rotation, and camera geometry from
37 an Exposure.
39 Parameters
40 ----------
41 exposure : `lsst.afw.image.Exposure`
42 Image object with attached metadata and detector components.
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)
57 # LATISS (and likely others) need flipping, DC2 etc do not
58 flipX = False
59 if obsInfo.instrument in ("LATISS",):
60 flipX = True
62 if visitInfo.getBoresightRaDec().isFinite():
63 exposure.setWcs(createInitialSkyWcs(visitInfo, exposure.getDetector(), flipX=flipX))
64 return True
66 if obsInfo.observation_type == "science":
67 logger.warn("Unable to set WCS from header as RA/Dec/Angle are unavailable",
68 ("" if dataIdForErrMsg is None else "for dataId %s" % dataIdForErrMsg))
69 return False
72def fixAmpGeometry(inAmp, bbox, metadata, logCmd=None):
73 """Make sure a camera geometry amplifier matches an on-disk bounding box.
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.
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.
91 Return
92 ------
93 outAmp : `~lsst.afw.cameraGeom.Amplifier.Builder`
94 modified : `bool`
95 `True` if ``amp`` was modified; `False` otherwise.
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
108 modified = False
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()))
116 logCmd("outAmp.getRawBBox() != data.getBBox(); patching. (%s v. %s)", outAmp.getRawBBox(), bbox)
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()
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))
148 modified = True
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
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)
167 return outAmp, modified
170def assembleUntrimmedCcd(ccd, exposures):
171 """Assemble an untrimmmed CCD from per-amp Exposure objects.
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``.
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)
195def fixAmpsAndAssemble(ampExps, msg):
196 """Fix amp geometry and assemble into exposure.
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.
205 Returns
206 -------
207 exposure : `lsst.afw.image.Exposure`
208 Exposure with the amps combined into a single image.
210 Notes
211 -----
212 The returned exposure does not have any metadata or WCS attached.
214 """
215 if not len(ampExps):
216 raise RuntimeError(f"Unable to read raw_amps for {msg}")
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
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
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)
242 outAmp, modified = fixAmpGeometry(amp,
243 bbox=ampExp.getBBox(),
244 metadata=ampExp.getMetadata(),
245 logCmd=logCmd)
246 tempCcd.append(outAmp)
248 ccd = tempCcd.finish()
250 # Update the data to be combined to point to the newly rebuilt detector.
251 for ampExp in ampExps:
252 ampExp.setDetector(ccd)
254 exposure = assembleUntrimmedCcd(ccd, ampExps)
255 return exposure
258def readRawAmps(fileName, detector):
259 """Given a file name read the amps and attach the detector.
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.
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, 16+1):
275 exp = afwImage.makeExposure(afwImage.makeMaskedImage(afwImage.ImageF(fileName, hdu=hdu)))
276 exp.setDetector(detector)
277 amps.append(exp)
278 return amps