Coverage for python/lsst/ip/isr/isrMock.py: 20%
478 statements
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-16 03:55 -0700
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-16 03:55 -0700
1# This file is part of ip_isr.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://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 <https://www.gnu.org/licenses/>.
22__all__ = ["IsrMockConfig", "IsrMock", "RawMock", "TrimmedRawMock", "RawDictMock",
23 "CalibratedRawMock", "MasterMock",
24 "BiasMock", "DarkMock", "FlatMock", "FringeMock", "UntrimmedFringeMock",
25 "BfKernelMock", "DefectMock", "CrosstalkCoeffMock", "TransmissionMock",
26 "MockDataContainer", "MockFringeContainer"]
28import copy
29import numpy as np
30import tempfile
32import lsst.geom
33import lsst.afw.geom as afwGeom
34import lsst.afw.image as afwImage
35from lsstDebug import getDebugFrame
37import lsst.afw.cameraGeom.utils as afwUtils
38import lsst.afw.cameraGeom.testUtils as afwTestUtils
39from lsst.afw.cameraGeom import ReadoutCorner
40import lsst.pex.config as pexConfig
41import lsst.pipe.base as pipeBase
42from .crosstalk import CrosstalkCalib
43from .defects import Defects
46class IsrMockConfig(pexConfig.Config):
47 """Configuration parameters for isrMock.
49 These parameters produce generic fixed position signals from
50 various sources, and combine them in a way that matches how those
51 signals are combined to create real data. The camera used is the
52 test camera defined by the afwUtils code.
53 """
54 # Detector parameters. "Exposure" parameters.
55 isLsstLike = pexConfig.Field(
56 dtype=bool,
57 default=False,
58 doc="If True, products have one raw image per amplifier, otherwise, one raw image per detector.",
59 )
60 plateScale = pexConfig.Field(
61 dtype=float,
62 default=20.0,
63 doc="Plate scale used in constructing mock camera.",
64 )
65 radialDistortion = pexConfig.Field(
66 dtype=float,
67 default=0.925,
68 doc="Radial distortion term used in constructing mock camera.",
69 )
70 isTrimmed = pexConfig.Field(
71 dtype=bool,
72 default=True,
73 doc="If True, amplifiers have been trimmed and mosaicked to remove regions outside the data BBox.",
74 )
75 detectorIndex = pexConfig.Field(
76 dtype=int,
77 default=20,
78 doc="Index for the detector to use. The default value uses a standard 2x4 array of amps.",
79 )
80 rngSeed = pexConfig.Field(
81 dtype=int,
82 default=20000913,
83 doc="Seed for random number generator used to add noise.",
84 )
85 # TODO: DM-18345 Check that mocks scale correctly when gain != 1.0
86 gain = pexConfig.Field(
87 dtype=float,
88 default=1.0,
89 doc="Gain for simulated data in e^-/DN.",
90 )
91 readNoise = pexConfig.Field(
92 dtype=float,
93 default=5.0,
94 doc="Read noise of the detector in e-.",
95 )
96 expTime = pexConfig.Field(
97 dtype=float,
98 default=5.0,
99 doc="Exposure time for simulated data.",
100 )
102 # Signal parameters
103 skyLevel = pexConfig.Field(
104 dtype=float,
105 default=1000.0,
106 doc="Background contribution to be generated from 'the sky' in DN.",
107 )
108 sourceFlux = pexConfig.ListField(
109 dtype=float,
110 default=[45000.0],
111 doc="Peak flux level (in DN) of simulated 'astronomical sources'.",
112 )
113 sourceAmp = pexConfig.ListField(
114 dtype=int,
115 default=[0],
116 doc="Amplifier to place simulated 'astronomical sources'.",
117 )
118 sourceX = pexConfig.ListField(
119 dtype=float,
120 default=[50.0],
121 doc="Peak position (in amplifier coordinates) of simulated 'astronomical sources'.",
122 )
123 sourceY = pexConfig.ListField(
124 dtype=float,
125 default=[25.0],
126 doc="Peak position (in amplifier coordinates) of simulated 'astronomical sources'.",
127 )
128 overscanScale = pexConfig.Field(
129 dtype=float,
130 default=100.0,
131 doc="Amplitude (in DN) of the ramp function to add to overscan data.",
132 )
133 biasLevel = pexConfig.Field(
134 dtype=float,
135 default=8000.0,
136 doc="Background contribution to be generated from the bias offset in DN.",
137 )
138 darkRate = pexConfig.Field(
139 dtype=float,
140 default=5.0,
141 doc="Background level contribution (in e-/s) to be generated from dark current.",
142 )
143 darkTime = pexConfig.Field(
144 dtype=float,
145 default=5.0,
146 doc="Exposure time for the dark current contribution.",
147 )
148 flatDrop = pexConfig.Field(
149 dtype=float,
150 default=0.1,
151 doc="Fractional flux drop due to flat from center to edge of detector along x-axis.",
152 )
153 fringeScale = pexConfig.ListField(
154 dtype=float,
155 default=[200.0],
156 doc="Peak fluxes for the components of the fringe ripple in DN.",
157 )
158 fringeX0 = pexConfig.ListField(
159 dtype=float,
160 default=[-100],
161 doc="Center position for the fringe ripples.",
162 )
163 fringeY0 = pexConfig.ListField(
164 dtype=float,
165 default=[-0],
166 doc="Center position for the fringe ripples.",
167 )
169 # Inclusion parameters
170 doAddSky = pexConfig.Field(
171 dtype=bool,
172 default=True,
173 doc="Apply 'sky' signal to output image.",
174 )
175 doAddSource = pexConfig.Field(
176 dtype=bool,
177 default=True,
178 doc="Add simulated source to output image.",
179 )
180 doAddCrosstalk = pexConfig.Field(
181 dtype=bool,
182 default=False,
183 doc="Apply simulated crosstalk to output image. This cannot be corrected by ISR, "
184 "as detector.hasCrosstalk()==False.",
185 )
186 doAddOverscan = pexConfig.Field(
187 dtype=bool,
188 default=True,
189 doc="If untrimmed, add overscan ramp to overscan and data regions.",
190 )
191 doAddBias = pexConfig.Field(
192 dtype=bool,
193 default=True,
194 doc="Add bias signal to data.",
195 )
196 doAddDark = pexConfig.Field(
197 dtype=bool,
198 default=True,
199 doc="Add dark signal to data.",
200 )
201 doAddFlat = pexConfig.Field(
202 dtype=bool,
203 default=True,
204 doc="Add flat signal to data.",
205 )
206 doAddFringe = pexConfig.Field(
207 dtype=bool,
208 default=True,
209 doc="Add fringe signal to data.",
210 )
212 # Datasets to create and return instead of generating an image.
213 doTransmissionCurve = pexConfig.Field(
214 dtype=bool,
215 default=False,
216 doc="Return a simulated transmission curve.",
217 )
218 doDefects = pexConfig.Field(
219 dtype=bool,
220 default=False,
221 doc="Return a simulated defect list.",
222 )
223 doBrighterFatter = pexConfig.Field(
224 dtype=bool,
225 default=False,
226 doc="Return a simulated brighter-fatter kernel.",
227 )
228 doCrosstalkCoeffs = pexConfig.Field(
229 dtype=bool,
230 default=False,
231 doc="Return the matrix of crosstalk coefficients.",
232 )
233 doDataRef = pexConfig.Field(
234 dtype=bool,
235 default=False,
236 doc="Return a simulated gen2 butler dataRef.",
237 )
238 doGenerateImage = pexConfig.Field(
239 dtype=bool,
240 default=False,
241 doc="Return the generated output image if True.",
242 )
243 doGenerateData = pexConfig.Field(
244 dtype=bool,
245 default=False,
246 doc="Return a non-image data structure if True.",
247 )
248 doGenerateAmpDict = pexConfig.Field(
249 dtype=bool,
250 default=False,
251 doc="Return a dict of exposure amplifiers instead of an afwImage.Exposure.",
252 )
255class IsrMock(pipeBase.Task):
256 """Class to generate consistent mock images for ISR testing.
258 ISR testing currently relies on one-off fake images that do not
259 accurately mimic the full set of detector effects. This class
260 uses the test camera/detector/amplifier structure defined in
261 `lsst.afw.cameraGeom.testUtils` to avoid making the test data
262 dependent on any of the actual obs package formats.
263 """
264 ConfigClass = IsrMockConfig
265 _DefaultName = "isrMock"
267 def __init__(self, **kwargs):
268 super().__init__(**kwargs)
269 self.rng = np.random.RandomState(self.config.rngSeed)
270 self.crosstalkCoeffs = np.array([[0.0, 0.0, 0.0, 0.0, 0.0, -1e-3, 0.0, 0.0],
271 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
272 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
273 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
274 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
275 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
276 [1e-2, 0.0, 0.0, 2.2e-2, 0.0, 0.0, 0.0, 0.0],
277 [1e-2, 5e-3, 5e-4, 3e-3, 4e-2, 5e-3, 5e-3, 0.0]])
278 if getDebugFrame(self._display, "mockCrosstalkCoeffs"):
279 self.crosstalkCoeffs = np.array([[0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
280 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
281 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
282 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
283 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
284 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
285 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
286 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]])
287 self.bfKernel = np.array([[1., 4., 7., 4., 1.],
288 [4., 16., 26., 16., 4.],
289 [7., 26., 41., 26., 7.],
290 [4., 16., 26., 16., 4.],
291 [1., 4., 7., 4., 1.]]) / 273.0
293 def run(self):
294 """Generate a mock ISR product, and return it.
296 Returns
297 -------
298 image : `lsst.afw.image.Exposure`
299 Simulated ISR image with signals added.
300 dataProduct :
301 Simulated ISR data products.
302 None :
303 Returned if no valid configuration was found.
305 Raises
306 ------
307 RuntimeError
308 Raised if both doGenerateImage and doGenerateData are specified.
309 """
310 if self.config.doGenerateImage and self.config.doGenerateData:
311 raise RuntimeError("Only one of doGenerateImage and doGenerateData may be specified.")
312 elif self.config.doGenerateImage:
313 return self.makeImage()
314 elif self.config.doGenerateData:
315 return self.makeData()
316 else:
317 return None
319 def makeData(self):
320 """Generate simulated ISR data.
322 Currently, only the class defined crosstalk coefficient
323 matrix, brighter-fatter kernel, a constant unity transmission
324 curve, or a simple single-entry defect list can be generated.
326 Returns
327 -------
328 dataProduct :
329 Simulated ISR data product.
330 """
331 if sum(map(bool, [self.config.doBrighterFatter,
332 self.config.doDefects,
333 self.config.doTransmissionCurve,
334 self.config.doCrosstalkCoeffs])) != 1:
335 raise RuntimeError("Only one data product can be generated at a time.")
336 elif self.config.doBrighterFatter is True:
337 return self.makeBfKernel()
338 elif self.config.doDefects is True:
339 return self.makeDefectList()
340 elif self.config.doTransmissionCurve is True:
341 return self.makeTransmissionCurve()
342 elif self.config.doCrosstalkCoeffs is True:
343 return self.crosstalkCoeffs
344 else:
345 return None
347 def makeBfKernel(self):
348 """Generate a simple Gaussian brighter-fatter kernel.
350 Returns
351 -------
352 kernel : `numpy.ndarray`
353 Simulated brighter-fatter kernel.
354 """
355 return self.bfKernel
357 def makeDefectList(self):
358 """Generate a simple single-entry defect list.
360 Returns
361 -------
362 defectList : `lsst.meas.algorithms.Defects`
363 Simulated defect list
364 """
365 return Defects([lsst.geom.Box2I(lsst.geom.Point2I(0, 0),
366 lsst.geom.Extent2I(40, 50))])
368 def makeCrosstalkCoeff(self):
369 """Generate the simulated crosstalk coefficients.
371 Returns
372 -------
373 coeffs : `numpy.ndarray`
374 Simulated crosstalk coefficients.
375 """
377 return self.crosstalkCoeffs
379 def makeTransmissionCurve(self):
380 """Generate a simulated flat transmission curve.
382 Returns
383 -------
384 transmission : `lsst.afw.image.TransmissionCurve`
385 Simulated transmission curve.
386 """
388 return afwImage.TransmissionCurve.makeIdentity()
390 def makeImage(self):
391 """Generate a simulated ISR image.
393 Returns
394 -------
395 exposure : `lsst.afw.image.Exposure` or `dict`
396 Simulated ISR image data.
398 Notes
399 -----
400 This method currently constructs a "raw" data image by:
402 * Generating a simulated sky with noise
403 * Adding a single Gaussian "star"
404 * Adding the fringe signal
405 * Multiplying the frame by the simulated flat
406 * Adding dark current (and noise)
407 * Adding a bias offset (and noise)
408 * Adding an overscan gradient parallel to the pixel y-axis
409 * Simulating crosstalk by adding a scaled version of each
410 amplifier to each other amplifier.
412 The exposure with image data constructed this way is in one of
413 three formats.
415 * A single image, with overscan and prescan regions retained
416 * A single image, with overscan and prescan regions trimmed
417 * A `dict`, containing the amplifer data indexed by the
418 amplifier name.
420 The nonlinearity, CTE, and brighter fatter are currently not
421 implemented.
423 Note that this method generates an image in the reverse
424 direction as the ISR processing, as the output image here has
425 had a series of instrument effects added to an idealized
426 exposure.
427 """
428 exposure = self.getExposure()
430 for idx, amp in enumerate(exposure.getDetector()):
431 bbox = None
432 if self.config.isTrimmed is True:
433 bbox = amp.getBBox()
434 else:
435 bbox = amp.getRawDataBBox()
437 ampData = exposure.image[bbox]
439 if self.config.doAddSky is True:
440 self.amplifierAddNoise(ampData, self.config.skyLevel, np.sqrt(self.config.skyLevel))
442 if self.config.doAddSource is True:
443 for sourceAmp, sourceFlux, sourceX, sourceY in zip(self.config.sourceAmp,
444 self.config.sourceFlux,
445 self.config.sourceX,
446 self.config.sourceY):
447 if idx == sourceAmp:
448 self.amplifierAddSource(ampData, sourceFlux, sourceX, sourceY)
450 if self.config.doAddFringe is True:
451 self.amplifierAddFringe(amp, ampData, np.array(self.config.fringeScale),
452 x0=np.array(self.config.fringeX0),
453 y0=np.array(self.config.fringeY0))
455 if self.config.doAddFlat is True:
456 if ampData.getArray().sum() == 0.0:
457 self.amplifierAddNoise(ampData, 1.0, 0.0)
458 u0 = exposure.getDimensions().getX()
459 v0 = exposure.getDimensions().getY()
460 self.amplifierMultiplyFlat(amp, ampData, self.config.flatDrop, u0=u0, v0=v0)
462 if self.config.doAddDark is True:
463 self.amplifierAddNoise(ampData,
464 self.config.darkRate * self.config.darkTime / self.config.gain,
465 np.sqrt(self.config.darkRate
466 * self.config.darkTime / self.config.gain))
468 if self.config.doAddCrosstalk is True:
469 ctCalib = CrosstalkCalib()
470 for idxS, ampS in enumerate(exposure.getDetector()):
471 for idxT, ampT in enumerate(exposure.getDetector()):
472 ampDataT = exposure.image[ampT.getBBox()
473 if self.config.isTrimmed else ampT.getRawDataBBox()]
474 outAmp = ctCalib.extractAmp(exposure.getImage(), ampS, ampT,
475 isTrimmed=self.config.isTrimmed)
476 self.amplifierAddCT(outAmp, ampDataT, self.crosstalkCoeffs[idxS][idxT])
478 for amp in exposure.getDetector():
479 bbox = None
480 if self.config.isTrimmed is True:
481 bbox = amp.getBBox()
482 else:
483 bbox = amp.getRawDataBBox()
485 ampData = exposure.image[bbox]
487 if self.config.doAddBias is True:
488 self.amplifierAddNoise(ampData, self.config.biasLevel,
489 self.config.readNoise / self.config.gain)
491 if self.config.doAddOverscan is True:
492 oscanBBox = amp.getRawHorizontalOverscanBBox()
493 oscanData = exposure.image[oscanBBox]
494 self.amplifierAddNoise(oscanData, self.config.biasLevel,
495 self.config.readNoise / self.config.gain)
497 self.amplifierAddYGradient(ampData, -1.0 * self.config.overscanScale,
498 1.0 * self.config.overscanScale)
499 self.amplifierAddYGradient(oscanData, -1.0 * self.config.overscanScale,
500 1.0 * self.config.overscanScale)
502 if self.config.doGenerateAmpDict is True:
503 expDict = dict()
504 for amp in exposure.getDetector():
505 expDict[amp.getName()] = exposure
506 return expDict
507 else:
508 return exposure
510 # afw primatives to construct the image structure
511 def getCamera(self):
512 """Construct a test camera object.
514 Returns
515 -------
516 camera : `lsst.afw.cameraGeom.camera`
517 Test camera.
518 """
519 cameraWrapper = afwTestUtils.CameraWrapper(
520 plateScale=self.config.plateScale,
521 radialDistortion=self.config.radialDistortion,
522 isLsstLike=self.config.isLsstLike,
523 )
524 camera = cameraWrapper.camera
525 return camera
527 def getExposure(self):
528 """Construct a test exposure.
530 The test exposure has a simple WCS set, as well as a list of
531 unlikely header keywords that can be removed during ISR
532 processing to exercise that code.
534 Returns
535 -------
536 exposure : `lsst.afw.exposure.Exposure`
537 Construct exposure containing masked image of the
538 appropriate size.
539 """
540 camera = self.getCamera()
541 detector = camera[self.config.detectorIndex]
542 image = afwUtils.makeImageFromCcd(detector,
543 isTrimmed=self.config.isTrimmed,
544 showAmpGain=False,
545 rcMarkSize=0,
546 binSize=1,
547 imageFactory=afwImage.ImageF)
549 var = afwImage.ImageF(image.getDimensions())
550 mask = afwImage.Mask(image.getDimensions())
551 image.assign(0.0)
553 maskedImage = afwImage.makeMaskedImage(image, mask, var)
554 exposure = afwImage.makeExposure(maskedImage)
555 exposure.setDetector(detector)
556 exposure.setWcs(self.getWcs())
558 visitInfo = afwImage.VisitInfo(exposureTime=self.config.expTime, darkTime=self.config.darkTime)
559 exposure.getInfo().setVisitInfo(visitInfo)
561 metadata = exposure.getMetadata()
562 metadata.add("SHEEP", 7.3, "number of sheep on farm")
563 metadata.add("MONKEYS", 155, "monkeys per tree")
564 metadata.add("VAMPIRES", 4, "How scary are vampires.")
566 ccd = exposure.getDetector()
567 newCcd = ccd.rebuild()
568 newCcd.clear()
569 readoutMap = {
570 'LL': ReadoutCorner.LL,
571 'LR': ReadoutCorner.LR,
572 'UR': ReadoutCorner.UR,
573 'UL': ReadoutCorner.UL,
574 }
575 for amp in ccd:
576 newAmp = amp.rebuild()
577 newAmp.setLinearityCoeffs((0., 1., 0., 0.))
578 newAmp.setLinearityType("Polynomial")
579 newAmp.setGain(self.config.gain)
580 newAmp.setSuspectLevel(25000.0)
581 newAmp.setSaturation(32000.0)
582 readoutCorner = 'LL'
584 # Apply flips to bbox where needed
585 imageBBox = amp.getRawDataBBox()
586 rawBbox = amp.getRawBBox()
587 parallelOscanBBox = amp.getRawParallelOverscanBBox()
588 serialOscanBBox = amp.getRawSerialOverscanBBox()
589 prescanBBox = amp.getRawPrescanBBox()
591 if self.config.isLsstLike:
592 # This follows cameraGeom.testUtils
593 xoffset, yoffset = amp.getRawXYOffset()
594 offext = lsst.geom.Extent2I(xoffset, yoffset)
595 flipx = bool(amp.getRawFlipX())
596 flipy = bool(amp.getRawFlipY())
597 if flipx:
598 xExt = rawBbox.getDimensions().getX()
599 rawBbox.flipLR(xExt)
600 imageBBox.flipLR(xExt)
601 parallelOscanBBox.flipLR(xExt)
602 serialOscanBBox.flipLR(xExt)
603 prescanBBox.flipLR(xExt)
604 if flipy:
605 yExt = rawBbox.getDimensions().getY()
606 rawBbox.flipTB(yExt)
607 imageBBox.flipTB(yExt)
608 parallelOscanBBox.flipTB(yExt)
609 serialOscanBBox.flipTB(yExt)
610 prescanBBox.flipTB(yExt)
611 if not flipx and not flipy:
612 readoutCorner = 'LL'
613 elif flipx and not flipy:
614 readoutCorner = 'LR'
615 elif flipx and flipy:
616 readoutCorner = 'UR'
617 elif not flipx and flipy:
618 readoutCorner = 'UL'
619 rawBbox.shift(offext)
620 imageBBox.shift(offext)
621 parallelOscanBBox.shift(offext)
622 serialOscanBBox.shift(offext)
623 prescanBBox.shift(offext)
624 newAmp.setReadoutCorner(readoutMap[readoutCorner])
625 newAmp.setRawBBox(rawBbox)
626 newAmp.setRawDataBBox(imageBBox)
627 newAmp.setRawParallelOverscanBBox(parallelOscanBBox)
628 newAmp.setRawSerialOverscanBBox(serialOscanBBox)
629 newAmp.setRawPrescanBBox(prescanBBox)
630 newAmp.setRawFlipX(False)
631 newAmp.setRawFlipY(False)
632 no_offset = lsst.geom.Extent2I(0, 0)
633 newAmp.setRawXYOffset(no_offset)
635 newCcd.append(newAmp)
637 exposure.setDetector(newCcd.finish())
639 exposure.image.array[:] = np.zeros(exposure.getImage().getDimensions()).transpose()
640 exposure.mask.array[:] = np.zeros(exposure.getMask().getDimensions()).transpose()
641 exposure.variance.array[:] = np.zeros(exposure.getVariance().getDimensions()).transpose()
643 return exposure
645 def getWcs(self):
646 """Construct a dummy WCS object.
648 Taken from the deprecated ip_isr/examples/exampleUtils.py.
650 This is not guaranteed, given the distortion and pixel scale
651 listed in the afwTestUtils camera definition.
653 Returns
654 -------
655 wcs : `lsst.afw.geom.SkyWcs`
656 Test WCS transform.
657 """
658 return afwGeom.makeSkyWcs(crpix=lsst.geom.Point2D(0.0, 100.0),
659 crval=lsst.geom.SpherePoint(45.0, 25.0, lsst.geom.degrees),
660 cdMatrix=afwGeom.makeCdMatrix(scale=1.0*lsst.geom.degrees))
662 def localCoordToExpCoord(self, ampData, x, y):
663 """Convert between a local amplifier coordinate and the full
664 exposure coordinate.
666 Parameters
667 ----------
668 ampData : `lsst.afw.image.ImageF`
669 Amplifier image to use for conversions.
670 x : `int`
671 X-coordinate of the point to transform.
672 y : `int`
673 Y-coordinate of the point to transform.
675 Returns
676 -------
677 u : `int`
678 Transformed x-coordinate.
679 v : `int`
680 Transformed y-coordinate.
682 Notes
683 -----
684 The output is transposed intentionally here, to match the
685 internal transpose between numpy and afw.image coordinates.
686 """
687 u = x + ampData.getBBox().getBeginX()
688 v = y + ampData.getBBox().getBeginY()
690 return (v, u)
692 # Simple data values.
693 def amplifierAddNoise(self, ampData, mean, sigma):
694 """Add Gaussian noise to an amplifier's image data.
696 This method operates in the amplifier coordinate frame.
698 Parameters
699 ----------
700 ampData : `lsst.afw.image.ImageF`
701 Amplifier image to operate on.
702 mean : `float`
703 Mean value of the Gaussian noise.
704 sigma : `float`
705 Sigma of the Gaussian noise.
706 """
707 ampArr = ampData.array
708 ampArr[:] = ampArr[:] + self.rng.normal(mean, sigma,
709 size=ampData.getDimensions()).transpose()
711 def amplifierAddYGradient(self, ampData, start, end):
712 """Add a y-axis linear gradient to an amplifier's image data.
714 This method operates in the amplifier coordinate frame.
716 Parameters
717 ----------
718 ampData : `lsst.afw.image.ImageF`
719 Amplifier image to operate on.
720 start : `float`
721 Start value of the gradient (at y=0).
722 end : `float`
723 End value of the gradient (at y=ymax).
724 """
725 nPixY = ampData.getDimensions().getY()
726 ampArr = ampData.array
727 ampArr[:] = ampArr[:] + (np.interp(range(nPixY), (0, nPixY - 1), (start, end)).reshape(nPixY, 1)
728 + np.zeros(ampData.getDimensions()).transpose())
730 def amplifierAddSource(self, ampData, scale, x0, y0):
731 """Add a single Gaussian source to an amplifier.
733 This method operates in the amplifier coordinate frame.
735 Parameters
736 ----------
737 ampData : `lsst.afw.image.ImageF`
738 Amplifier image to operate on.
739 scale : `float`
740 Peak flux of the source to add.
741 x0 : `float`
742 X-coordinate of the source peak.
743 y0 : `float`
744 Y-coordinate of the source peak.
745 """
746 for x in range(0, ampData.getDimensions().getX()):
747 for y in range(0, ampData.getDimensions().getY()):
748 ampData.array[y][x] = (ampData.array[y][x]
749 + scale * np.exp(-0.5 * ((x - x0)**2 + (y - y0)**2) / 3.0**2))
751 def amplifierAddCT(self, ampDataSource, ampDataTarget, scale):
752 """Add a scaled copy of an amplifier to another, simulating crosstalk.
754 This method operates in the amplifier coordinate frame.
756 Parameters
757 ----------
758 ampDataSource : `lsst.afw.image.ImageF`
759 Amplifier image to add scaled copy from.
760 ampDataTarget : `lsst.afw.image.ImageF`
761 Amplifier image to add scaled copy to.
762 scale : `float`
763 Flux scale of the copy to add to the target.
765 Notes
766 -----
767 This simulates simple crosstalk between amplifiers.
768 """
769 ampDataTarget.array[:] = (ampDataTarget.array[:]
770 + scale * ampDataSource.array[:])
772 # Functional form data values.
773 def amplifierAddFringe(self, amp, ampData, scale, x0=100, y0=0):
774 """Add a fringe-like ripple pattern to an amplifier's image data.
776 Parameters
777 ----------
778 amp : `~lsst.afw.ampInfo.AmpInfoRecord`
779 Amplifier to operate on. Needed for amp<->exp coordinate
780 transforms.
781 ampData : `lsst.afw.image.ImageF`
782 Amplifier image to operate on.
783 scale : `numpy.array` or `float`
784 Peak intensity scaling for the ripple.
785 x0 : `numpy.array` or `float`, optional
786 Fringe center
787 y0 : `numpy.array` or `float`, optional
788 Fringe center
790 Notes
791 -----
792 This uses an offset sinc function to generate a ripple
793 pattern. True fringes have much finer structure, but this
794 pattern should be visually identifiable. The (x, y)
795 coordinates are in the frame of the amplifier, and (u, v) in
796 the frame of the full trimmed image.
797 """
798 for x in range(0, ampData.getDimensions().getX()):
799 for y in range(0, ampData.getDimensions().getY()):
800 (u, v) = self.localCoordToExpCoord(amp, x, y)
801 ampData.getArray()[y][x] = np.sum((ampData.getArray()[y][x]
802 + scale * np.sinc(((u - x0) / 50)**2
803 + ((v - y0) / 50)**2)))
805 def amplifierMultiplyFlat(self, amp, ampData, fracDrop, u0=100.0, v0=100.0):
806 """Multiply an amplifier's image data by a flat-like pattern.
808 Parameters
809 ----------
810 amp : `lsst.afw.ampInfo.AmpInfoRecord`
811 Amplifier to operate on. Needed for amp<->exp coordinate
812 transforms.
813 ampData : `lsst.afw.image.ImageF`
814 Amplifier image to operate on.
815 fracDrop : `float`
816 Fractional drop from center to edge of detector along x-axis.
817 u0 : `float`
818 Peak location in detector coordinates.
819 v0 : `float`
820 Peak location in detector coordinates.
822 Notes
823 -----
824 This uses a 2-d Gaussian to simulate an illumination pattern
825 that falls off towards the edge of the detector. The (x, y)
826 coordinates are in the frame of the amplifier, and (u, v) in
827 the frame of the full trimmed image.
828 """
829 if fracDrop >= 1.0:
830 raise RuntimeError("Flat fractional drop cannot be greater than 1.0")
832 sigma = u0 / np.sqrt(-2.0 * np.log(fracDrop))
834 for x in range(0, ampData.getDimensions().getX()):
835 for y in range(0, ampData.getDimensions().getY()):
836 (u, v) = self.localCoordToExpCoord(amp, x, y)
837 f = np.exp(-0.5 * ((u - u0)**2 + (v - v0)**2) / sigma**2)
838 ampData.array[y][x] = (ampData.array[y][x] * f)
841class RawMock(IsrMock):
842 """Generate a raw exposure suitable for ISR.
843 """
844 def __init__(self, **kwargs):
845 super().__init__(**kwargs)
846 self.config.isTrimmed = False
847 self.config.doGenerateImage = True
848 self.config.doGenerateAmpDict = False
849 self.config.doAddOverscan = True
850 self.config.doAddSky = True
851 self.config.doAddSource = True
852 self.config.doAddCrosstalk = False
853 self.config.doAddBias = True
854 self.config.doAddDark = True
857class TrimmedRawMock(RawMock):
858 """Generate a trimmed raw exposure.
859 """
860 def __init__(self, **kwargs):
861 super().__init__(**kwargs)
862 self.config.isTrimmed = True
863 self.config.doAddOverscan = False
866class CalibratedRawMock(RawMock):
867 """Generate a trimmed raw exposure.
868 """
869 def __init__(self, **kwargs):
870 super().__init__(**kwargs)
871 self.config.isTrimmed = True
872 self.config.doGenerateImage = True
873 self.config.doAddOverscan = False
874 self.config.doAddSky = True
875 self.config.doAddSource = True
876 self.config.doAddCrosstalk = False
878 self.config.doAddBias = False
879 self.config.doAddDark = False
880 self.config.doAddFlat = False
881 self.config.doAddFringe = True
883 self.config.biasLevel = 0.0
884 self.config.readNoise = 10.0
887class RawDictMock(RawMock):
888 """Generate a raw exposure dict suitable for ISR.
889 """
890 def __init__(self, **kwargs):
891 super().__init__(**kwargs)
892 self.config.doGenerateAmpDict = True
895class MasterMock(IsrMock):
896 """Parent class for those that make master calibrations.
897 """
898 def __init__(self, **kwargs):
899 super().__init__(**kwargs)
900 self.config.isTrimmed = True
901 self.config.doGenerateImage = True
902 self.config.doAddOverscan = False
903 self.config.doAddSky = False
904 self.config.doAddSource = False
905 self.config.doAddCrosstalk = False
907 self.config.doAddBias = False
908 self.config.doAddDark = False
909 self.config.doAddFlat = False
910 self.config.doAddFringe = False
913class BiasMock(MasterMock):
914 """Simulated master bias calibration.
915 """
916 def __init__(self, **kwargs):
917 super().__init__(**kwargs)
918 self.config.doAddBias = True
919 self.config.readNoise = 10.0
922class DarkMock(MasterMock):
923 """Simulated master dark calibration.
924 """
925 def __init__(self, **kwargs):
926 super().__init__(**kwargs)
927 self.config.doAddDark = True
928 self.config.darkTime = 1.0
931class FlatMock(MasterMock):
932 """Simulated master flat calibration.
933 """
934 def __init__(self, **kwargs):
935 super().__init__(**kwargs)
936 self.config.doAddFlat = True
939class FringeMock(MasterMock):
940 """Simulated master fringe calibration.
941 """
942 def __init__(self, **kwargs):
943 super().__init__(**kwargs)
944 self.config.doAddFringe = True
947class UntrimmedFringeMock(FringeMock):
948 """Simulated untrimmed master fringe calibration.
949 """
950 def __init__(self, **kwargs):
951 super().__init__(**kwargs)
952 self.config.isTrimmed = False
955class BfKernelMock(IsrMock):
956 """Simulated brighter-fatter kernel.
957 """
958 def __init__(self, **kwargs):
959 super().__init__(**kwargs)
960 self.config.doGenerateImage = False
961 self.config.doGenerateData = True
962 self.config.doBrighterFatter = True
963 self.config.doDefects = False
964 self.config.doCrosstalkCoeffs = False
965 self.config.doTransmissionCurve = False
968class DefectMock(IsrMock):
969 """Simulated defect list.
970 """
971 def __init__(self, **kwargs):
972 super().__init__(**kwargs)
973 self.config.doGenerateImage = False
974 self.config.doGenerateData = True
975 self.config.doBrighterFatter = False
976 self.config.doDefects = True
977 self.config.doCrosstalkCoeffs = False
978 self.config.doTransmissionCurve = False
981class CrosstalkCoeffMock(IsrMock):
982 """Simulated crosstalk coefficient matrix.
983 """
984 def __init__(self, **kwargs):
985 super().__init__(**kwargs)
986 self.config.doGenerateImage = False
987 self.config.doGenerateData = True
988 self.config.doBrighterFatter = False
989 self.config.doDefects = False
990 self.config.doCrosstalkCoeffs = True
991 self.config.doTransmissionCurve = False
994class TransmissionMock(IsrMock):
995 """Simulated transmission curve.
996 """
997 def __init__(self, **kwargs):
998 super().__init__(**kwargs)
999 self.config.doGenerateImage = False
1000 self.config.doGenerateData = True
1001 self.config.doBrighterFatter = False
1002 self.config.doDefects = False
1003 self.config.doCrosstalkCoeffs = False
1004 self.config.doTransmissionCurve = True
1007class MockDataContainer(object):
1008 """Container for holding ISR mock objects.
1009 """
1010 dataId = "isrMock Fake Data"
1011 darkval = 2. # e-/sec
1012 oscan = 250. # DN
1013 gradient = .10
1014 exptime = 15.0 # seconds
1015 darkexptime = 15.0 # seconds
1017 def __init__(self, **kwargs):
1018 if 'config' in kwargs.keys():
1019 self.config = kwargs['config']
1020 else:
1021 self.config = None
1023 def expectImage(self):
1024 if self.config is None:
1025 self.config = IsrMockConfig()
1026 self.config.doGenerateImage = True
1027 self.config.doGenerateData = False
1029 def expectData(self):
1030 if self.config is None:
1031 self.config = IsrMockConfig()
1032 self.config.doGenerateImage = False
1033 self.config.doGenerateData = True
1035 def get(self, dataType, **kwargs):
1036 """Return an appropriate data product.
1038 Parameters
1039 ----------
1040 dataType : `str`
1041 Type of data product to return.
1043 Returns
1044 -------
1045 mock : IsrMock.run() result
1046 The output product.
1047 """
1048 if "_filename" in dataType:
1049 self.expectData()
1050 return tempfile.mktemp(), "mock"
1051 elif 'transmission_' in dataType:
1052 self.expectData()
1053 return TransmissionMock(config=self.config).run()
1054 elif dataType == 'ccdExposureId':
1055 self.expectData()
1056 return 20090913
1057 elif dataType == 'camera':
1058 self.expectData()
1059 return IsrMock(config=self.config).getCamera()
1060 elif dataType == 'raw':
1061 self.expectImage()
1062 return RawMock(config=self.config).run()
1063 elif dataType == 'bias':
1064 self.expectImage()
1065 return BiasMock(config=self.config).run()
1066 elif dataType == 'dark':
1067 self.expectImage()
1068 return DarkMock(config=self.config).run()
1069 elif dataType == 'flat':
1070 self.expectImage()
1071 return FlatMock(config=self.config).run()
1072 elif dataType == 'fringe':
1073 self.expectImage()
1074 return FringeMock(config=self.config).run()
1075 elif dataType == 'defects':
1076 self.expectData()
1077 return DefectMock(config=self.config).run()
1078 elif dataType == 'bfKernel':
1079 self.expectData()
1080 return BfKernelMock(config=self.config).run()
1081 elif dataType == 'linearizer':
1082 return None
1083 elif dataType == 'crosstalkSources':
1084 return None
1085 else:
1086 raise RuntimeError("ISR DataRefMock cannot return %s.", dataType)
1089class MockFringeContainer(object):
1090 """Container for mock fringe data.
1091 """
1092 dataId = "isrMock Fake Data"
1093 darkval = 2. # e-/sec
1094 oscan = 250. # DN
1095 gradient = .10
1096 exptime = 15 # seconds
1097 darkexptime = 40. # seconds
1099 def __init__(self, **kwargs):
1100 if 'config' in kwargs.keys():
1101 self.config = kwargs['config']
1102 else:
1103 self.config = IsrMockConfig()
1104 self.config.isTrimmed = True
1105 self.config.doAddFringe = True
1106 self.config.readNoise = 10.0
1108 def get(self, dataType, **kwargs):
1109 """Return an appropriate data product.
1111 Parameters
1112 ----------
1113 dataType : `str`
1114 Type of data product to return.
1116 Returns
1117 -------
1118 mock : IsrMock.run() result
1119 The output product.
1120 """
1121 if "_filename" in dataType:
1122 return tempfile.mktemp(), "mock"
1123 elif 'transmission_' in dataType:
1124 return TransmissionMock(config=self.config).run()
1125 elif dataType == 'ccdExposureId':
1126 return 20090913
1127 elif dataType == 'camera':
1128 return IsrMock(config=self.config).getCamera()
1129 elif dataType == 'raw':
1130 return CalibratedRawMock(config=self.config).run()
1131 elif dataType == 'bias':
1132 return BiasMock(config=self.config).run()
1133 elif dataType == 'dark':
1134 return DarkMock(config=self.config).run()
1135 elif dataType == 'flat':
1136 return FlatMock(config=self.config).run()
1137 elif dataType == 'fringe':
1138 fringes = []
1139 configCopy = copy.deepcopy(self.config)
1140 for scale, x, y in zip(self.config.fringeScale, self.config.fringeX0, self.config.fringeY0):
1141 configCopy.fringeScale = [1.0]
1142 configCopy.fringeX0 = [x]
1143 configCopy.fringeY0 = [y]
1144 fringes.append(FringeMock(config=configCopy).run())
1145 return fringes
1146 elif dataType == 'defects':
1147 return DefectMock(config=self.config).run()
1148 elif dataType == 'bfKernel':
1149 return BfKernelMock(config=self.config).run()
1150 elif dataType == 'linearizer':
1151 return None
1152 elif dataType == 'crosstalkSources':
1153 return None
1154 else:
1155 return None