Coverage for python / lsst / ip / isr / isrMock.py: 19%
530 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-07 08:28 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-07 08:28 +0000
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", "ElectrostaticBfMock", "DefectMock", "CrosstalkCoeffMock",
26 "TransmissionMock", "MockDataContainer", "MockFringeContainer"]
28import copy
29import numpy as np
30import tempfile
31import astropy.time
33from datetime import datetime, timezone
35import lsst.geom
36import lsst.afw.geom as afwGeom
37import lsst.afw.image as afwImage
38from lsstDebug import getDebugFrame
40import lsst.afw.cameraGeom.utils as afwUtils
41import lsst.afw.cameraGeom.testUtils as afwTestUtils
42from lsst.afw.cameraGeom import ReadoutCorner
43import lsst.pex.config as pexConfig
44import lsst.pipe.base as pipeBase
45from .crosstalk import CrosstalkCalib
46from .defects import Defects
49class IsrMockConfig(pexConfig.Config):
50 """Configuration parameters for isrMock.
52 These parameters produce generic fixed position signals from
53 various sources, and combine them in a way that matches how those
54 signals are combined to create real data. The camera used is the
55 test camera defined by the afwUtils code.
56 """
57 # Detector parameters. "Exposure" parameters.
58 isLsstLike = pexConfig.Field(
59 dtype=bool,
60 default=False,
61 doc="If True, products have one raw image per amplifier, otherwise, one raw image per detector.",
62 )
63 plateScale = pexConfig.Field(
64 dtype=float,
65 default=20.0,
66 doc="Plate scale used in constructing mock camera.",
67 )
68 radialDistortion = pexConfig.Field(
69 dtype=float,
70 default=0.925,
71 doc="Radial distortion term used in constructing mock camera.",
72 )
73 isTrimmed = pexConfig.Field(
74 dtype=bool,
75 default=True,
76 doc="If True, amplifiers have been trimmed and mosaicked to remove regions outside the data BBox.",
77 )
78 detectorIndex = pexConfig.Field(
79 dtype=int,
80 default=20,
81 doc="Index for the detector to use. The default value uses a standard 2x4 array of amps.",
82 )
83 rngSeed = pexConfig.Field(
84 dtype=int,
85 default=20000913,
86 doc="Seed for random number generator used to add noise.",
87 )
88 # TODO: DM-18345 Check that mocks scale correctly when gain != 1.0
89 gain = pexConfig.Field(
90 dtype=float,
91 default=1.0,
92 doc="Gain for simulated data in electron/adu.",
93 )
94 readNoise = pexConfig.Field(
95 dtype=float,
96 default=5.0,
97 doc="Read noise of the detector in electron.",
98 )
99 expTime = pexConfig.Field(
100 dtype=float,
101 default=5.0,
102 doc="Exposure time for simulated data.",
103 )
105 # Signal parameters
106 skyLevel = pexConfig.Field(
107 dtype=float,
108 default=1000.0,
109 doc="Background contribution to be generated from 'the sky' in "
110 "adu (IsrTask) or electron (IsrTaskLSST).",
111 )
112 sourceFlux = pexConfig.ListField(
113 dtype=float,
114 default=[45000.0],
115 doc="Peak flux level of simulated 'astronomical sources' in "
116 "adu (IsrTask) or electron (IsrTaskLSST).",
117 )
118 sourceAmp = pexConfig.ListField(
119 dtype=int,
120 default=[0],
121 doc="Amplifier to place simulated 'astronomical sources'.",
122 )
123 sourceX = pexConfig.ListField(
124 dtype=float,
125 default=[50.0],
126 doc="Peak position (in amplifier coordinates) of simulated 'astronomical sources'.",
127 )
128 sourceY = pexConfig.ListField(
129 dtype=float,
130 default=[25.0],
131 doc="Peak position (in amplifier coordinates) of simulated 'astronomical sources'.",
132 )
133 overscanScale = pexConfig.Field(
134 dtype=float,
135 default=100.0,
136 doc="Amplitude of the ramp function to add to overscan data in "
137 "adu (IsrTask) or electron (IsrTaskLSST)",
138 )
139 biasLevel = pexConfig.Field(
140 dtype=float,
141 default=8000.0,
142 doc="Background contribution to be generated from the bias offset in adu.",
143 )
144 darkRate = pexConfig.Field(
145 dtype=float,
146 default=5.0,
147 doc="Background level contribution (in electron/s) to be generated from dark current.",
148 )
149 darkTime = pexConfig.Field(
150 dtype=float,
151 default=5.0,
152 doc="Exposure time for the dark current contribution.",
153 )
154 flatDrop = pexConfig.Field(
155 dtype=float,
156 default=0.1,
157 doc="Fractional flux drop due to flat from center to edge of detector along x-axis.",
158 )
159 fringeScale = pexConfig.ListField(
160 dtype=float,
161 default=[200.0],
162 doc="Peak fluxes for the components of the fringe ripple in "
163 "adu (IsrTask) or electron (IsrTaskLSST).",
164 )
165 fringeX0 = pexConfig.ListField(
166 dtype=float,
167 default=[-100],
168 doc="Center position for the fringe ripples.",
169 )
170 fringeY0 = pexConfig.ListField(
171 dtype=float,
172 default=[-0],
173 doc="Center position for the fringe ripples.",
174 )
176 # Inclusion parameters
177 doAddSky = pexConfig.Field(
178 dtype=bool,
179 default=True,
180 doc="Apply 'sky' signal to output image.",
181 )
182 doAddSource = pexConfig.Field(
183 dtype=bool,
184 default=True,
185 doc="Add simulated source to output image.",
186 )
187 doAddCrosstalk = pexConfig.Field(
188 dtype=bool,
189 default=False,
190 doc="Apply simulated crosstalk to output image. This cannot be corrected by ISR, "
191 "as detector.hasCrosstalk()==False.",
192 )
193 doAddOverscan = pexConfig.Field(
194 dtype=bool,
195 default=True,
196 doc="If untrimmed, add overscan ramp to overscan and data regions.",
197 )
198 doAddBias = pexConfig.Field(
199 dtype=bool,
200 default=True,
201 doc="Add bias signal to data.",
202 )
203 doAddDark = pexConfig.Field(
204 dtype=bool,
205 default=True,
206 doc="Add dark signal to data.",
207 )
208 doAddFlat = pexConfig.Field(
209 dtype=bool,
210 default=True,
211 doc="Add flat signal to data.",
212 )
213 doAddFringe = pexConfig.Field(
214 dtype=bool,
215 default=True,
216 doc="Add fringe signal to data.",
217 )
219 # Datasets to create and return instead of generating an image.
220 doTransmissionCurve = pexConfig.Field(
221 dtype=bool,
222 default=False,
223 doc="Return a simulated transmission curve.",
224 )
225 doDefects = pexConfig.Field(
226 dtype=bool,
227 default=False,
228 doc="Return a simulated defect list.",
229 )
230 doBrighterFatter = pexConfig.Field(
231 dtype=bool,
232 default=False,
233 doc="Return a simulated brighter-fatter kernel.",
234 )
235 brighterFatterCalibType = pexConfig.ChoiceField(
236 dtype=str,
237 doc="Type of calibration to generate if doGenerateData=True, otherwise ignored.",
238 allowed={
239 "KERNEL": "No default suspect values; only config overrides will be used.",
240 "ELECTROSTATIC": "Use the default from the camera model (old defaults).",
241 },
242 default="KERNEL",
243 )
245 doDeferredCharge = pexConfig.Field(
246 dtype=bool,
247 default=False,
248 doc="Return a simulated deferred charge calibration.",
249 )
250 doCrosstalkCoeffs = pexConfig.Field(
251 dtype=bool,
252 default=False,
253 doc="Return the matrix of crosstalk coefficients.",
254 )
255 doLinearizer = pexConfig.Field(
256 dtype=bool,
257 default=False,
258 doc="Return linearizer dataset.",
259 )
260 doDataRef = pexConfig.Field(
261 dtype=bool,
262 default=False,
263 doc="Return a simulated gen2 butler dataRef.",
264 )
265 doGenerateImage = pexConfig.Field(
266 dtype=bool,
267 default=False,
268 doc="Return the generated output image if True.",
269 )
270 doGenerateData = pexConfig.Field(
271 dtype=bool,
272 default=False,
273 doc="Return a non-image data structure if True.",
274 )
275 doGenerateAmpDict = pexConfig.Field(
276 dtype=bool,
277 default=False,
278 doc="Return a dict of exposure amplifiers instead of an afwImage.Exposure.",
279 )
282class IsrMock(pipeBase.Task):
283 """Class to generate consistent mock images for ISR testing.
285 ISR testing currently relies on one-off fake images that do not
286 accurately mimic the full set of detector effects. This class
287 uses the test camera/detector/amplifier structure defined in
288 `lsst.afw.cameraGeom.testUtils` to avoid making the test data
289 dependent on any of the actual obs package formats.
290 """
291 ConfigClass = IsrMockConfig
292 _DefaultName = "isrMock"
294 def __init__(self, **kwargs):
295 super().__init__(**kwargs)
296 self.rng = np.random.RandomState(self.config.rngSeed)
297 # The coefficients have units adu for IsrTask and have
298 # units electron for IsrTaskLSST.
299 self.crosstalkCoeffs = np.array([[0.0, 0.0, 0.0, 0.0, 0.0, -1e-3, 0.0, 0.0],
300 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
301 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
302 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
303 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
304 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
305 [1e-2, 0.0, 0.0, 2.2e-2, 0.0, 0.0, 0.0, 0.0],
306 [1e-2, 5e-3, 5e-4, 3e-3, 4e-2, 5e-3, 5e-3, 0.0]])
307 if getDebugFrame(self._display, "mockCrosstalkCoeffs"):
308 self.crosstalkCoeffs = np.array([[0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
309 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
310 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
311 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
312 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
313 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
314 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
315 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]])
316 # Generic gaussian BF kernel
317 self.bfKernel = np.array([[1., 4., 7., 4., 1.],
318 [4., 16., 26., 16., 4.],
319 [7., 26., 41., 26., 7.],
320 [4., 16., 26., 16., 4.],
321 [1., 4., 7., 4., 1.]]) / 273.0
323 self.aN = np.array([[1., 0.5, 0.0125, 0., 0.],
324 [0.5, 0.010, 0., 0., 0.],
325 [0.0125, 0., 0., 0., 0.],
326 [0, 0., 0., 0., 0.]]) * -1e-7
327 self.aE = np.array([[1., 0.5, 0.0125, 0., 0.],
328 [0.5, 0.010, 0., 0., 0.],
329 [0.0125, 0., 0., 0., 0.],
330 [0, 0., 0., 0., 0.]]) * -1e-7
331 self.aS = self.aN
332 self.aW = self.aE
333 self.aVector = (self.aN, self.aS, self.aE, self.aW)
335 def run(self):
336 """Generate a mock ISR product, and return it.
338 Returns
339 -------
340 image : `lsst.afw.image.Exposure`
341 Simulated ISR image with signals added.
342 dataProduct :
343 Simulated ISR data products.
344 None :
345 Returned if no valid configuration was found.
347 Raises
348 ------
349 RuntimeError
350 Raised if both doGenerateImage and doGenerateData are specified.
351 """
352 if self.config.doGenerateImage and self.config.doGenerateData:
353 raise RuntimeError("Only one of doGenerateImage and doGenerateData may be specified.")
354 elif self.config.doGenerateImage:
355 return self.makeImage()
356 elif self.config.doGenerateData:
357 return self.makeData()
358 else:
359 return None
361 def makeData(self):
362 """Generate simulated ISR data.
364 Currently, only the class defined crosstalk coefficient
365 matrix, brighter-fatter kernel, a constant unity transmission
366 curve, or a simple single-entry defect list can be generated.
368 Returns
369 -------
370 dataProduct :
371 Simulated ISR data product.
372 """
373 if sum(map(bool, [self.config.doBrighterFatter,
374 self.config.doDeferredCharge,
375 self.config.doDefects,
376 self.config.doTransmissionCurve,
377 self.config.doCrosstalkCoeffs,
378 self.config.doLinearizer])) != 1:
379 raise RuntimeError("Only one data product can be generated at a time.")
380 elif self.config.doBrighterFatter:
381 if self.config.brighterFatterCalibType == "KERNEL":
382 return self.makeBfKernel()
383 if self.config.brighterFatterCalibType == "ELECTROSTATIC":
384 return self.makeElectrostaticBf()
385 else:
386 raise ValueError("%s is an unknown Brighter-Fatter calibration type." %
387 self.config.brighterFatterCalibType)
388 elif self.config.doDeferredCharge:
389 return self.makeDeferredChargeCalib()
390 elif self.config.doDefects:
391 return self.makeDefectList()
392 elif self.config.doTransmissionCurve:
393 return self.makeTransmissionCurve()
394 elif self.config.doCrosstalkCoeffs:
395 return self.crosstalkCoeffs
396 elif self.config.doLinearizer:
397 return self.makeLinearizer()
398 else:
399 return None
401 def makeBfKernel(self):
402 """Generate a simple Gaussian brighter-fatter kernel.
404 Returns
405 -------
406 kernel : `numpy.ndarray`
407 Simulated brighter-fatter kernel.
408 """
409 return self.bfKernel
411 def makeElectrostaticBf(self):
412 """Generate a simple Gaussian brighter-fatter kernel.
414 Returns
415 -------
416 kernel : `numpy.ndarray`
417 Simulated brighter-fatter kernel.
418 """
419 return self.aVector
421 def makeDeferredChargeCalib(self):
422 """Generate a CTI calibration.
423 """
425 raise NotImplementedError("Mock deferred charge is not implemented for IsrMock.")
427 def makeDefectList(self):
428 """Generate a simple single-entry defect list.
430 Returns
431 -------
432 defectList : `lsst.meas.algorithms.Defects`
433 Simulated defect list
434 """
435 return Defects([lsst.geom.Box2I(lsst.geom.Point2I(0, 0),
436 lsst.geom.Extent2I(40, 50))])
438 def makeCrosstalkCoeff(self):
439 """Generate the simulated crosstalk coefficients.
441 Returns
442 -------
443 coeffs : `numpy.ndarray`
444 Simulated crosstalk coefficients.
445 """
447 return self.crosstalkCoeffs
449 def makeTransmissionCurve(self):
450 """Generate a simulated flat transmission curve.
452 Returns
453 -------
454 transmission : `lsst.afw.image.TransmissionCurve`
455 Simulated transmission curve.
456 """
458 return afwImage.TransmissionCurve.makeIdentity()
460 def makeLinearity(self):
461 """Generate a linearity dataset.
463 Returns
464 -------
465 linearizer : `lsst.ip.isr.Linearizer`
466 """
467 raise NotImplementedError("Linearizer not implemented for isrMock.")
469 def makeImage(self):
470 """Generate a simulated ISR image.
472 Returns
473 -------
474 exposure : `lsst.afw.image.Exposure` or `dict`
475 Simulated ISR image data.
477 Notes
478 -----
479 This method currently constructs a "raw" data image by:
481 * Generating a simulated sky with noise
482 * Adding a single Gaussian "star"
483 * Adding the fringe signal
484 * Multiplying the frame by the simulated flat
485 * Adding dark current (and noise)
486 * Adding a bias offset (and noise)
487 * Adding an overscan gradient parallel to the pixel y-axis
488 * Simulating crosstalk by adding a scaled version of each
489 amplifier to each other amplifier.
491 The exposure with image data constructed this way is in one of
492 three formats.
494 * A single image, with overscan and prescan regions retained
495 * A single image, with overscan and prescan regions trimmed
496 * A `dict`, containing the amplifer data indexed by the
497 amplifier name.
499 The nonlinearity, CTE, and brighter fatter are currently not
500 implemented.
502 Note that this method generates an image in the reverse
503 direction as the ISR processing, as the output image here has
504 had a series of instrument effects added to an idealized
505 exposure.
506 """
507 exposure = self.getExposure()
509 for idx, amp in enumerate(exposure.getDetector()):
510 bbox = None
511 if self.config.isTrimmed is True:
512 bbox = amp.getBBox()
513 else:
514 bbox = amp.getRawDataBBox()
516 ampData = exposure.image[bbox]
518 if self.config.doAddSky is True:
519 self.amplifierAddNoise(ampData, self.config.skyLevel, np.sqrt(self.config.skyLevel))
521 if self.config.doAddSource is True:
522 for sourceAmp, sourceFlux, sourceX, sourceY in zip(self.config.sourceAmp,
523 self.config.sourceFlux,
524 self.config.sourceX,
525 self.config.sourceY):
526 if idx == sourceAmp:
527 self.amplifierAddSource(ampData, sourceFlux, sourceX, sourceY)
529 if self.config.doAddFringe is True:
530 self.amplifierAddFringe(amp, ampData, np.array(self.config.fringeScale),
531 x0=np.array(self.config.fringeX0),
532 y0=np.array(self.config.fringeY0))
534 if self.config.doAddFlat is True:
535 if ampData.getArray().sum() == 0.0:
536 self.amplifierAddNoise(ampData, 1.0, 0.0)
537 u0 = exposure.getDimensions().getX()
538 v0 = exposure.getDimensions().getY()
539 self.amplifierMultiplyFlat(amp, ampData, self.config.flatDrop, u0=u0, v0=v0)
541 if self.config.doAddDark is True:
542 self.amplifierAddNoise(ampData,
543 self.config.darkRate * self.config.darkTime / self.config.gain,
544 np.sqrt(self.config.darkRate
545 * self.config.darkTime / self.config.gain))
547 if self.config.doAddCrosstalk is True:
548 ctCalib = CrosstalkCalib()
549 # We use the regular subtractCrosstalk code but with a negative
550 # sign on the crosstalk coefficients so it adds instead of
551 # subtracts. We only apply the signal plane (ignoreVariance,
552 # subtrahendMasking) with a very large pixel to mask to ensure
553 # no crosstalk mask bits are set.
554 ctCalib.subtractCrosstalk(
555 exposure,
556 crosstalkCoeffs=-1*self.crosstalkCoeffs,
557 doSubtrahendMasking=True,
558 minPixelToMask=np.inf,
559 ignoreVariance=True,
560 fullAmplifier=False,
561 )
563 for amp in exposure.getDetector():
564 bbox = None
565 if self.config.isTrimmed is True:
566 bbox = amp.getBBox()
567 else:
568 bbox = amp.getRawDataBBox()
570 ampData = exposure.image[bbox]
572 if self.config.doAddBias is True:
573 self.amplifierAddNoise(ampData, self.config.biasLevel,
574 self.config.readNoise / self.config.gain)
576 if self.config.doAddOverscan is True:
577 oscanBBox = amp.getRawHorizontalOverscanBBox()
578 oscanData = exposure.image[oscanBBox]
579 self.amplifierAddNoise(oscanData, self.config.biasLevel,
580 self.config.readNoise / self.config.gain)
582 self.amplifierAddYGradient(ampData, -1.0 * self.config.overscanScale,
583 1.0 * self.config.overscanScale)
584 self.amplifierAddYGradient(oscanData, -1.0 * self.config.overscanScale,
585 1.0 * self.config.overscanScale)
587 if self.config.doGenerateAmpDict is True:
588 expDict = dict()
589 for amp in exposure.getDetector():
590 expDict[amp.getName()] = exposure
591 return expDict
592 else:
593 return exposure
595 # afw primatives to construct the image structure
596 def getCamera(self, isForAssembly=False):
597 """Construct a test camera object.
599 Parameters
600 -------
601 isForAssembly : `bool`
602 If True, construct a camera with "super raw"
603 orientation (all amplifiers have LL readout
604 corner but still contains the necessary flip
605 and offset info needed for assembly. This is
606 needed if isLsstLike is True. If False, return
607 a camera with bboxes flipped and offset to the
608 correct orientation given the readout corner.
610 Returns
611 -------
612 camera : `lsst.afw.cameraGeom.camera`
613 Test camera.
614 """
615 cameraWrapper = afwTestUtils.CameraWrapper(
616 plateScale=self.config.plateScale,
617 radialDistortion=self.config.radialDistortion,
618 isLsstLike=self.config.isLsstLike and isForAssembly,
619 )
620 camera = cameraWrapper.camera
621 return camera
623 def getExposure(self, isTrimmed=None):
624 """Construct a test exposure.
626 The test exposure has a simple WCS set, as well as a list of
627 unlikely header keywords that can be removed during ISR
628 processing to exercise that code.
630 Parameters
631 ----------
632 isTrimmed : `bool` or `None`, optional
633 Override the configuration isTrimmed?
635 Returns
636 -------
637 exposure : `lsst.afw.exposure.Exposure`
638 Construct exposure containing masked image of the
639 appropriate size.
640 """
641 if isTrimmed is None:
642 _isTrimmed = self.config.isTrimmed
643 else:
644 _isTrimmed = isTrimmed
646 camera = self.getCamera(isForAssembly=self.config.isLsstLike)
647 detector = camera[self.config.detectorIndex]
648 image = afwUtils.makeImageFromCcd(
649 detector,
650 isTrimmed=_isTrimmed,
651 showAmpGain=False,
652 rcMarkSize=0,
653 binSize=1,
654 imageFactory=afwImage.ImageF,
655 )
657 var = afwImage.ImageF(image.getDimensions())
658 mask = afwImage.Mask(image.getDimensions())
659 image.assign(0.0)
661 maskedImage = afwImage.makeMaskedImage(image, mask, var)
662 exposure = afwImage.makeExposure(maskedImage)
663 exposure.setDetector(detector)
664 exposure.setWcs(self.getWcs())
666 visitInfo = afwImage.VisitInfo(exposureTime=self.config.expTime, darkTime=self.config.darkTime)
667 exposure.getInfo().setVisitInfo(visitInfo)
668 # Set a dummy ID.
669 exposure.getInfo().setId(12345)
671 metadata = exposure.getMetadata()
672 metadata.add("SHEEP", 7.3, "number of sheep on farm")
673 metadata.add("MONKEYS", 155, "monkeys per tree")
674 metadata.add("VAMPIRES", 4, "How scary are vampires.")
675 metadata.add("FILTER", "r_57", "My favorite color.")
677 # Add the current time
678 now = datetime.now(timezone.utc)
679 currentMjd = astropy.time.Time(now, format='datetime', scale='utc').mjd
680 metadata.add("MJD", currentMjd, "Modified Julian Date that the file was written")
682 ccd = exposure.getDetector()
683 newCcd = ccd.rebuild()
684 newCcd.clear()
685 readoutMap = {
686 'LL': ReadoutCorner.LL,
687 'LR': ReadoutCorner.LR,
688 'UR': ReadoutCorner.UR,
689 'UL': ReadoutCorner.UL,
690 }
691 for amp in ccd:
692 newAmp = amp.rebuild()
693 newAmp.setLinearityCoeffs((0., 1., 0., 0.))
694 newAmp.setLinearityType("Polynomial")
695 newAmp.setGain(self.config.gain)
696 newAmp.setSuspectLevel(25000.0)
697 newAmp.setSaturation(32000.0)
698 readoutCorner = amp.getReadoutCorner().name
700 # Apply flips to bbox where needed
701 imageBBox = amp.getRawDataBBox()
702 rawBbox = amp.getRawBBox()
703 parallelOscanBBox = amp.getRawParallelOverscanBBox()
704 serialOscanBBox = amp.getRawSerialOverscanBBox()
705 prescanBBox = amp.getRawPrescanBBox()
707 if self.config.isLsstLike:
708 # This follows cameraGeom.testUtils
709 xoffset, yoffset = amp.getRawXYOffset()
710 offext = lsst.geom.Extent2I(xoffset, yoffset)
711 flipx = bool(amp.getRawFlipX())
712 flipy = bool(amp.getRawFlipY())
713 if flipx:
714 xExt = rawBbox.getDimensions().getX()
715 rawBbox.flipLR(xExt)
716 imageBBox.flipLR(xExt)
717 parallelOscanBBox.flipLR(xExt)
718 serialOscanBBox.flipLR(xExt)
719 prescanBBox.flipLR(xExt)
720 if flipy:
721 yExt = rawBbox.getDimensions().getY()
722 rawBbox.flipTB(yExt)
723 imageBBox.flipTB(yExt)
724 parallelOscanBBox.flipTB(yExt)
725 serialOscanBBox.flipTB(yExt)
726 prescanBBox.flipTB(yExt)
727 if not flipx and not flipy:
728 readoutCorner = 'LL'
729 elif flipx and not flipy:
730 readoutCorner = 'LR'
731 elif flipx and flipy:
732 readoutCorner = 'UR'
733 elif not flipx and flipy:
734 readoutCorner = 'UL'
735 rawBbox.shift(offext)
736 imageBBox.shift(offext)
737 parallelOscanBBox.shift(offext)
738 serialOscanBBox.shift(offext)
739 prescanBBox.shift(offext)
740 newAmp.setReadoutCorner(readoutMap[readoutCorner])
741 newAmp.setRawBBox(rawBbox)
742 newAmp.setRawDataBBox(imageBBox)
743 newAmp.setRawParallelOverscanBBox(parallelOscanBBox)
744 newAmp.setRawSerialOverscanBBox(serialOscanBBox)
745 newAmp.setRawPrescanBBox(prescanBBox)
746 newAmp.setRawFlipX(False)
747 newAmp.setRawFlipY(False)
748 no_offset = lsst.geom.Extent2I(0, 0)
749 newAmp.setRawXYOffset(no_offset)
751 newCcd.append(newAmp)
753 exposure.setDetector(newCcd.finish())
755 exposure.image.array[:] = np.zeros(exposure.getImage().getDimensions()).transpose()
756 exposure.mask.array[:] = np.zeros(exposure.getMask().getDimensions()).transpose()
757 exposure.variance.array[:] = np.zeros(exposure.getVariance().getDimensions()).transpose()
759 return exposure
761 def getWcs(self):
762 """Construct a dummy WCS object.
764 Taken from the deprecated ip_isr/examples/exampleUtils.py.
766 This is not guaranteed, given the distortion and pixel scale
767 listed in the afwTestUtils camera definition.
769 Returns
770 -------
771 wcs : `lsst.afw.geom.SkyWcs`
772 Test WCS transform.
773 """
774 return afwGeom.makeSkyWcs(crpix=lsst.geom.Point2D(0.0, 100.0),
775 crval=lsst.geom.SpherePoint(45.0, 25.0, lsst.geom.degrees),
776 cdMatrix=afwGeom.makeCdMatrix(scale=1.0*lsst.geom.degrees))
778 def localCoordToExpCoord(self, ampData, x, y):
779 """Convert between a local amplifier coordinate and the full
780 exposure coordinate.
782 Parameters
783 ----------
784 ampData : `lsst.afw.image.ImageF`
785 Amplifier image to use for conversions.
786 x : `int`
787 X-coordinate of the point to transform.
788 y : `int`
789 Y-coordinate of the point to transform.
791 Returns
792 -------
793 u : `int`
794 Transformed x-coordinate.
795 v : `int`
796 Transformed y-coordinate.
798 Notes
799 -----
800 The output is transposed intentionally here, to match the
801 internal transpose between numpy and afw.image coordinates.
802 """
803 u = x + ampData.getBBox().getBeginX()
804 v = y + ampData.getBBox().getBeginY()
806 return (v, u)
808 # Simple data values.
809 def amplifierAddNoise(self, ampData, mean, sigma, rng=None):
810 """Add Gaussian noise to an amplifier's image data.
812 This method operates in the amplifier coordinate frame.
814 Parameters
815 ----------
816 ampData : `lsst.afw.image.ImageF`
817 Amplifier image to operate on.
818 mean : `float`
819 Mean value of the Gaussian noise.
820 sigma : `float`
821 Sigma of the Gaussian noise.
822 rng : `np.random.RandomState`, optional
823 Random state to use instead of self.rng.
824 """
825 if rng is not None:
826 _rng = rng
827 else:
828 _rng = self.rng
830 ampArr = ampData.array
831 ampArr[:] = ampArr[:] + _rng.normal(mean, sigma,
832 size=ampData.getDimensions()).transpose()
834 def amplifierAddYGradient(self, ampData, start, end):
835 """Add a y-axis linear gradient to an amplifier's image data.
837 This method operates in the amplifier coordinate frame.
839 Parameters
840 ----------
841 ampData : `lsst.afw.image.ImageF`
842 Amplifier image to operate on.
843 start : `float`
844 Start value of the gradient (at y=0).
845 end : `float`
846 End value of the gradient (at y=ymax).
847 """
848 nPixY = ampData.getDimensions().getY()
849 ampArr = ampData.array
850 ampArr[:] = ampArr[:] + (np.interp(range(nPixY), (0, nPixY - 1), (start, end)).reshape(nPixY, 1)
851 + np.zeros(ampData.getDimensions()).transpose())
853 def amplifierAddSource(self, ampData, scale, x0, y0):
854 """Add a single Gaussian source to an amplifier.
856 This method operates in the amplifier coordinate frame.
858 Parameters
859 ----------
860 ampData : `lsst.afw.image.ImageF`
861 Amplifier image to operate on.
862 scale : `float`
863 Peak flux of the source to add.
864 x0 : `float`
865 X-coordinate of the source peak.
866 y0 : `float`
867 Y-coordinate of the source peak.
868 """
869 for x in range(0, ampData.getDimensions().getX()):
870 for y in range(0, ampData.getDimensions().getY()):
871 ampData.array[y][x] = (ampData.array[y][x]
872 + scale * np.exp(-0.5 * ((x - x0)**2 + (y - y0)**2) / 3.0**2))
874 # Functional form data values.
875 def amplifierAddFringe(self, amp, ampData, scale, x0=100, y0=0):
876 """Add a fringe-like ripple pattern to an amplifier's image data.
878 Parameters
879 ----------
880 amp : `~lsst.afw.ampInfo.AmpInfoRecord`
881 Amplifier to operate on. Needed for amp<->exp coordinate
882 transforms.
883 ampData : `lsst.afw.image.ImageF`
884 Amplifier image to operate on.
885 scale : `numpy.array` or `float`
886 Peak intensity scaling for the ripple.
887 x0 : `numpy.array` or `float`, optional
888 Fringe center
889 y0 : `numpy.array` or `float`, optional
890 Fringe center
892 Notes
893 -----
894 This uses an offset sinc function to generate a ripple
895 pattern. True fringes have much finer structure, but this
896 pattern should be visually identifiable. The (x, y)
897 coordinates are in the frame of the amplifier, and (u, v) in
898 the frame of the full trimmed image.
899 """
900 for x in range(0, ampData.getDimensions().getX()):
901 for y in range(0, ampData.getDimensions().getY()):
902 (u, v) = self.localCoordToExpCoord(amp, x, y)
903 ampData.getArray()[y][x] = np.sum((ampData.getArray()[y][x]
904 + scale * np.sinc(((u - x0) / 50)**2
905 + ((v - y0) / 50)**2)))
907 def amplifierMultiplyFlat(self, amp, ampData, fracDrop, u0=100.0, v0=100.0):
908 """Multiply an amplifier's image data by a flat-like pattern.
910 Parameters
911 ----------
912 amp : `lsst.afw.ampInfo.AmpInfoRecord`
913 Amplifier to operate on. Needed for amp<->exp coordinate
914 transforms.
915 ampData : `lsst.afw.image.ImageF`
916 Amplifier image to operate on.
917 fracDrop : `float`
918 Fractional drop from center to edge of detector along x-axis.
919 u0 : `float`
920 Peak location in detector coordinates.
921 v0 : `float`
922 Peak location in detector coordinates.
924 Notes
925 -----
926 This uses a 2-d Gaussian to simulate an illumination pattern
927 that falls off towards the edge of the detector. The (x, y)
928 coordinates are in the frame of the amplifier, and (u, v) in
929 the frame of the full trimmed image.
930 """
931 if fracDrop >= 1.0:
932 raise RuntimeError("Flat fractional drop cannot be greater than 1.0")
934 sigma = u0 / np.sqrt(-2.0 * np.log(fracDrop))
936 for x in range(0, ampData.getDimensions().getX()):
937 for y in range(0, ampData.getDimensions().getY()):
938 (u, v) = self.localCoordToExpCoord(amp, x, y)
939 f = np.exp(-0.5 * ((u - u0)**2 + (v - v0)**2) / sigma**2)
940 ampData.array[y][x] = (ampData.array[y][x] * f)
943class RawMock(IsrMock):
944 """Generate a raw exposure suitable for ISR.
945 """
946 def __init__(self, **kwargs):
947 super().__init__(**kwargs)
948 self.config.isTrimmed = False
949 self.config.doGenerateImage = True
950 self.config.doGenerateAmpDict = False
951 self.config.doAddOverscan = True
952 self.config.doAddSky = True
953 self.config.doAddSource = True
954 self.config.doAddCrosstalk = False
955 self.config.doAddBias = True
956 self.config.doAddDark = True
959class TrimmedRawMock(RawMock):
960 """Generate a trimmed raw exposure.
961 """
962 def __init__(self, **kwargs):
963 super().__init__(**kwargs)
964 self.config.isTrimmed = True
965 self.config.doAddOverscan = False
968class CalibratedRawMock(RawMock):
969 """Generate a trimmed raw exposure.
970 """
971 def __init__(self, **kwargs):
972 super().__init__(**kwargs)
973 self.config.isTrimmed = True
974 self.config.doGenerateImage = True
975 self.config.doAddOverscan = False
976 self.config.doAddSky = True
977 self.config.doAddSource = True
978 self.config.doAddCrosstalk = False
980 self.config.doAddBias = False
981 self.config.doAddDark = False
982 self.config.doAddFlat = False
983 self.config.doAddFringe = True
985 self.config.biasLevel = 0.0
986 self.config.readNoise = 10.0
989class RawDictMock(RawMock):
990 """Generate a raw exposure dict suitable for ISR.
991 """
992 def __init__(self, **kwargs):
993 super().__init__(**kwargs)
994 self.config.doGenerateAmpDict = True
997class MasterMock(IsrMock):
998 """Parent class for those that make master calibrations.
999 """
1000 def __init__(self, **kwargs):
1001 super().__init__(**kwargs)
1002 self.config.isTrimmed = True
1003 self.config.doGenerateImage = True
1004 self.config.doAddOverscan = False
1005 self.config.doAddSky = False
1006 self.config.doAddSource = False
1007 self.config.doAddCrosstalk = False
1009 self.config.doAddBias = False
1010 self.config.doAddDark = False
1011 self.config.doAddFlat = False
1012 self.config.doAddFringe = False
1015class BiasMock(MasterMock):
1016 """Simulated master bias calibration.
1017 """
1018 def __init__(self, **kwargs):
1019 super().__init__(**kwargs)
1020 self.config.doAddBias = True
1021 self.config.readNoise = 10.0
1024class DarkMock(MasterMock):
1025 """Simulated master dark calibration.
1026 """
1027 def __init__(self, **kwargs):
1028 super().__init__(**kwargs)
1029 self.config.doAddDark = True
1030 self.config.darkTime = 1.0
1033class FlatMock(MasterMock):
1034 """Simulated master flat calibration.
1035 """
1036 def __init__(self, **kwargs):
1037 super().__init__(**kwargs)
1038 self.config.doAddFlat = True
1041class FringeMock(MasterMock):
1042 """Simulated master fringe calibration.
1043 """
1044 def __init__(self, **kwargs):
1045 super().__init__(**kwargs)
1046 self.config.doAddFringe = True
1049class UntrimmedFringeMock(FringeMock):
1050 """Simulated untrimmed master fringe calibration.
1051 """
1052 def __init__(self, **kwargs):
1053 super().__init__(**kwargs)
1054 self.config.isTrimmed = False
1057class BfKernelMock(IsrMock):
1058 """Simulated brighter-fatter kernel.
1059 """
1060 def __init__(self, **kwargs):
1061 super().__init__(**kwargs)
1062 self.config.doGenerateImage = False
1063 self.config.doGenerateData = True
1064 self.config.doBrighterFatter = True
1065 self.config.doDefects = False
1066 self.config.doCrosstalkCoeffs = False
1067 self.config.doTransmissionCurve = False
1070class ElectrostaticBfMock(IsrMock):
1071 """Simulated brighter-fatter kernel.
1072 """
1073 def __init__(self, **kwargs):
1074 super().__init__(**kwargs)
1075 self.config.doGenerateImage = False
1076 self.config.doGenerateData = True
1077 self.config.doBrighterFatter = True
1078 self.config.doDefects = False
1079 self.config.doCrosstalkCoeffs = False
1080 self.config.doTransmissionCurve = False
1083class DeferredChargeMock(IsrMock):
1084 """Simulated deferred charge calibration.
1085 """
1086 def __init__(self, **kwargs):
1087 super().__init__(**kwargs)
1088 self.config.doGenerateImage = False
1089 self.config.doGenerateData = True
1090 self.config.doDeferredCharge = True
1091 self.config.doDefects = False
1092 self.config.doCrosstalkCoeffs = False
1093 self.config.doTransmissionCurve = False
1096class DefectMock(IsrMock):
1097 """Simulated defect list.
1098 """
1099 def __init__(self, **kwargs):
1100 super().__init__(**kwargs)
1101 self.config.doGenerateImage = False
1102 self.config.doGenerateData = True
1103 self.config.doBrighterFatter = False
1104 self.config.doDefects = True
1105 self.config.doCrosstalkCoeffs = False
1106 self.config.doTransmissionCurve = False
1109class CrosstalkCoeffMock(IsrMock):
1110 """Simulated crosstalk coefficient matrix.
1111 """
1112 def __init__(self, **kwargs):
1113 super().__init__(**kwargs)
1114 self.config.doGenerateImage = False
1115 self.config.doGenerateData = True
1116 self.config.doBrighterFatter = False
1117 self.config.doDefects = False
1118 self.config.doCrosstalkCoeffs = True
1119 self.config.doTransmissionCurve = False
1122class TransmissionMock(IsrMock):
1123 """Simulated transmission curve.
1124 """
1125 def __init__(self, **kwargs):
1126 super().__init__(**kwargs)
1127 self.config.doGenerateImage = False
1128 self.config.doGenerateData = True
1129 self.config.doBrighterFatter = False
1130 self.config.doDefects = False
1131 self.config.doCrosstalkCoeffs = False
1132 self.config.doTransmissionCurve = True
1135class MockDataContainer(object):
1136 """Container for holding ISR mock objects.
1137 """
1138 dataId = "isrMock Fake Data"
1139 darkval = 2. # electron/sec
1140 oscan = 250. # adu
1141 gradient = .10
1142 exptime = 15.0 # seconds
1143 darkexptime = 15.0 # seconds
1145 def __init__(self, **kwargs):
1146 if 'config' in kwargs.keys():
1147 self.config = kwargs['config']
1148 else:
1149 self.config = None
1151 def expectImage(self):
1152 if self.config is None:
1153 self.config = IsrMockConfig()
1154 self.config.doGenerateImage = True
1155 self.config.doGenerateData = False
1157 def expectData(self):
1158 if self.config is None:
1159 self.config = IsrMockConfig()
1160 self.config.doGenerateImage = False
1161 self.config.doGenerateData = True
1163 def get(self, dataType, **kwargs):
1164 """Return an appropriate data product.
1166 Parameters
1167 ----------
1168 dataType : `str`
1169 Type of data product to return.
1171 Returns
1172 -------
1173 mock : IsrMock.run() result
1174 The output product.
1175 """
1176 if "_filename" in dataType:
1177 self.expectData()
1178 return tempfile.mktemp(), "mock"
1179 elif 'transmission_' in dataType:
1180 self.expectData()
1181 return TransmissionMock(config=self.config).run()
1182 elif dataType == 'ccdExposureId':
1183 self.expectData()
1184 return 20090913
1185 elif dataType == 'camera':
1186 self.expectData()
1187 return IsrMock(config=self.config).getCamera()
1188 elif dataType == 'raw':
1189 self.expectImage()
1190 return RawMock(config=self.config).run()
1191 elif dataType == 'bias':
1192 self.expectImage()
1193 return BiasMock(config=self.config).run()
1194 elif dataType == 'dark':
1195 self.expectImage()
1196 return DarkMock(config=self.config).run()
1197 elif dataType == 'flat':
1198 self.expectImage()
1199 return FlatMock(config=self.config).run()
1200 elif dataType == 'fringe':
1201 self.expectImage()
1202 return FringeMock(config=self.config).run()
1203 elif dataType == 'defects':
1204 self.expectData()
1205 return DefectMock(config=self.config).run()
1206 elif dataType == 'bfKernel':
1207 self.expectData()
1208 return BfKernelMock(config=self.config).run()
1209 elif dataType == 'ebf':
1210 self.expectData()
1211 return ElectrostaticBfMock(config=self.config).run()
1212 elif dataType == 'linearizer':
1213 return None
1214 elif dataType == 'crosstalkSources':
1215 return None
1216 else:
1217 raise RuntimeError("ISR DataRefMock cannot return %s.", dataType)
1220class MockFringeContainer(object):
1221 """Container for mock fringe data.
1222 """
1223 dataId = "isrMock Fake Data"
1224 darkval = 2. # electron/sec
1225 oscan = 250. # adu
1226 gradient = .10
1227 exptime = 15 # seconds
1228 darkexptime = 40. # seconds
1230 def __init__(self, **kwargs):
1231 if 'config' in kwargs.keys():
1232 self.config = kwargs['config']
1233 else:
1234 self.config = IsrMockConfig()
1235 self.config.isTrimmed = True
1236 self.config.doAddFringe = True
1237 self.config.readNoise = 10.0
1239 def get(self, dataType, **kwargs):
1240 """Return an appropriate data product.
1242 Parameters
1243 ----------
1244 dataType : `str`
1245 Type of data product to return.
1247 Returns
1248 -------
1249 mock : IsrMock.run() result
1250 The output product.
1251 """
1252 if "_filename" in dataType:
1253 return tempfile.mktemp(), "mock"
1254 elif 'transmission_' in dataType:
1255 return TransmissionMock(config=self.config).run()
1256 elif dataType == 'ccdExposureId':
1257 return 20090913
1258 elif dataType == 'camera':
1259 return IsrMock(config=self.config).getCamera()
1260 elif dataType == 'raw':
1261 return CalibratedRawMock(config=self.config).run()
1262 elif dataType == 'bias':
1263 return BiasMock(config=self.config).run()
1264 elif dataType == 'dark':
1265 return DarkMock(config=self.config).run()
1266 elif dataType == 'flat':
1267 return FlatMock(config=self.config).run()
1268 elif dataType == 'fringe':
1269 fringes = []
1270 configCopy = copy.deepcopy(self.config)
1271 for scale, x, y in zip(self.config.fringeScale, self.config.fringeX0, self.config.fringeY0):
1272 configCopy.fringeScale = [1.0]
1273 configCopy.fringeX0 = [x]
1274 configCopy.fringeY0 = [y]
1275 fringes.append(FringeMock(config=configCopy).run())
1276 return fringes
1277 elif dataType == 'defects':
1278 return DefectMock(config=self.config).run()
1279 elif dataType == 'bfKernel':
1280 return BfKernelMock(config=self.config).run()
1281 elif dataType == 'ebf':
1282 return ElectrostaticBfMock(config=self.config).run()
1283 elif dataType == 'linearizer':
1284 return None
1285 elif dataType == 'crosstalkSources':
1286 return None
1287 else:
1288 return None