Coverage for python/lsst/ip/isr/isrMock.py : 22%

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