LSST Applications 29.0.1,g0fba68d861+132dd21e0a,g107a963962+1bb9f809a9,g1fd858c14a+005be21cae,g21d47ad084+8a07b29876,g325378336f+5d73323c8f,g330003fc43+40b4eaffc6,g35bb328faa+fcb1d3bbc8,g36ff55ed5b+9c28a42a87,g4e0f332c67+5fbd1e3e73,g53246c7159+fcb1d3bbc8,g60b5630c4e+9c28a42a87,g67b6fd64d1+a38b34ea13,g78460c75b0+2f9a1b4bcd,g786e29fd12+cf7ec2a62a,g7b71ed6315+fcb1d3bbc8,g86c591e316+6b2b2d0295,g8852436030+bf14db0e33,g89139ef638+a38b34ea13,g8b8da53e10+e3777245af,g9125e01d80+fcb1d3bbc8,g989de1cb63+a38b34ea13,g9f1445be69+9c28a42a87,g9f33ca652e+52c8f07962,ga9baa6287d+9c28a42a87,ga9e4eb89a6+9f84bd6575,gabe3b4be73+1e0a283bba,gb037a4e798+f3cbcd26c0,gb1101e3267+e7be8da0f8,gb58c049af0+f03b321e39,gb89ab40317+a38b34ea13,gcf25f946ba+bf14db0e33,gd6cbbdb0b4+bce7f7457e,gd9a9a58781+fcb1d3bbc8,gde0f65d7ad+53d424b1ae,ge278dab8ac+222406d50a,ge410e46f29+a38b34ea13,ge80e9994a3+664d6357dc,gf67bdafdda+a38b34ea13
LSST Data Management Base Package
Loading...
Searching...
No Matches
isrMock.py
Go to the documentation of this file.
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/>.
21
22__all__ = ["IsrMockConfig", "IsrMock", "RawMock", "TrimmedRawMock", "RawDictMock",
23 "CalibratedRawMock", "MasterMock",
24 "BiasMock", "DarkMock", "FlatMock", "FringeMock", "UntrimmedFringeMock",
25 "BfKernelMock", "DefectMock", "CrosstalkCoeffMock", "TransmissionMock",
26 "MockDataContainer", "MockFringeContainer"]
27
28import copy
29import numpy as np
30import tempfile
31
32import lsst.geom
33import lsst.afw.geom as afwGeom
34import lsst.afw.image as afwImage
35from lsstDebug import getDebugFrame
36
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
44
45
46class IsrMockConfig(pexConfig.Config):
47 """Configuration parameters for isrMock.
48
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 electron/adu.",
90 )
91 readNoise = pexConfig.Field(
92 dtype=float,
93 default=5.0,
94 doc="Read noise of the detector in electron.",
95 )
96 expTime = pexConfig.Field(
97 dtype=float,
98 default=5.0,
99 doc="Exposure time for simulated data.",
100 )
101
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 "
107 "adu (IsrTask) or electron (IsrTaskLSST).",
108 )
109 sourceFlux = pexConfig.ListField(
110 dtype=float,
111 default=[45000.0],
112 doc="Peak flux level of simulated 'astronomical sources' in "
113 "adu (IsrTask) or electron (IsrTaskLSST).",
114 )
115 sourceAmp = pexConfig.ListField(
116 dtype=int,
117 default=[0],
118 doc="Amplifier to place simulated 'astronomical sources'.",
119 )
120 sourceX = pexConfig.ListField(
121 dtype=float,
122 default=[50.0],
123 doc="Peak position (in amplifier coordinates) of simulated 'astronomical sources'.",
124 )
125 sourceY = pexConfig.ListField(
126 dtype=float,
127 default=[25.0],
128 doc="Peak position (in amplifier coordinates) of simulated 'astronomical sources'.",
129 )
130 overscanScale = pexConfig.Field(
131 dtype=float,
132 default=100.0,
133 doc="Amplitude of the ramp function to add to overscan data in "
134 "adu (IsrTask) or electron (IsrTaskLSST)",
135 )
136 biasLevel = pexConfig.Field(
137 dtype=float,
138 default=8000.0,
139 doc="Background contribution to be generated from the bias offset in adu.",
140 )
141 darkRate = pexConfig.Field(
142 dtype=float,
143 default=5.0,
144 doc="Background level contribution (in electron/s) to be generated from dark current.",
145 )
146 darkTime = pexConfig.Field(
147 dtype=float,
148 default=5.0,
149 doc="Exposure time for the dark current contribution.",
150 )
151 flatDrop = pexConfig.Field(
152 dtype=float,
153 default=0.1,
154 doc="Fractional flux drop due to flat from center to edge of detector along x-axis.",
155 )
156 fringeScale = pexConfig.ListField(
157 dtype=float,
158 default=[200.0],
159 doc="Peak fluxes for the components of the fringe ripple in "
160 "adu (IsrTask) or electron (IsrTaskLSST).",
161 )
162 fringeX0 = pexConfig.ListField(
163 dtype=float,
164 default=[-100],
165 doc="Center position for the fringe ripples.",
166 )
167 fringeY0 = pexConfig.ListField(
168 dtype=float,
169 default=[-0],
170 doc="Center position for the fringe ripples.",
171 )
172
173 # Inclusion parameters
174 doAddSky = pexConfig.Field(
175 dtype=bool,
176 default=True,
177 doc="Apply 'sky' signal to output image.",
178 )
179 doAddSource = pexConfig.Field(
180 dtype=bool,
181 default=True,
182 doc="Add simulated source to output image.",
183 )
184 doAddCrosstalk = pexConfig.Field(
185 dtype=bool,
186 default=False,
187 doc="Apply simulated crosstalk to output image. This cannot be corrected by ISR, "
188 "as detector.hasCrosstalk()==False.",
189 )
190 doAddOverscan = pexConfig.Field(
191 dtype=bool,
192 default=True,
193 doc="If untrimmed, add overscan ramp to overscan and data regions.",
194 )
195 doAddBias = pexConfig.Field(
196 dtype=bool,
197 default=True,
198 doc="Add bias signal to data.",
199 )
200 doAddDark = pexConfig.Field(
201 dtype=bool,
202 default=True,
203 doc="Add dark signal to data.",
204 )
205 doAddFlat = pexConfig.Field(
206 dtype=bool,
207 default=True,
208 doc="Add flat signal to data.",
209 )
210 doAddFringe = pexConfig.Field(
211 dtype=bool,
212 default=True,
213 doc="Add fringe signal to data.",
214 )
215
216 # Datasets to create and return instead of generating an image.
217 doTransmissionCurve = pexConfig.Field(
218 dtype=bool,
219 default=False,
220 doc="Return a simulated transmission curve.",
221 )
222 doDefects = pexConfig.Field(
223 dtype=bool,
224 default=False,
225 doc="Return a simulated defect list.",
226 )
227 doBrighterFatter = pexConfig.Field(
228 dtype=bool,
229 default=False,
230 doc="Return a simulated brighter-fatter kernel.",
231 )
232 doDeferredCharge = pexConfig.Field(
233 dtype=bool,
234 default=False,
235 doc="Return a simulated deferred charge calibration.",
236 )
237 doCrosstalkCoeffs = pexConfig.Field(
238 dtype=bool,
239 default=False,
240 doc="Return the matrix of crosstalk coefficients.",
241 )
242 doLinearizer = pexConfig.Field(
243 dtype=bool,
244 default=False,
245 doc="Return linearizer dataset.",
246 )
247 doDataRef = pexConfig.Field(
248 dtype=bool,
249 default=False,
250 doc="Return a simulated gen2 butler dataRef.",
251 )
252 doGenerateImage = pexConfig.Field(
253 dtype=bool,
254 default=False,
255 doc="Return the generated output image if True.",
256 )
257 doGenerateData = pexConfig.Field(
258 dtype=bool,
259 default=False,
260 doc="Return a non-image data structure if True.",
261 )
262 doGenerateAmpDict = pexConfig.Field(
263 dtype=bool,
264 default=False,
265 doc="Return a dict of exposure amplifiers instead of an afwImage.Exposure.",
266 )
267
268
269class IsrMock(pipeBase.Task):
270 """Class to generate consistent mock images for ISR testing.
271
272 ISR testing currently relies on one-off fake images that do not
273 accurately mimic the full set of detector effects. This class
274 uses the test camera/detector/amplifier structure defined in
275 `lsst.afw.cameraGeom.testUtils` to avoid making the test data
276 dependent on any of the actual obs package formats.
277 """
278 ConfigClass = IsrMockConfig
279 _DefaultName = "isrMock"
280
281 def __init__(self, **kwargs):
282 super().__init__(**kwargs)
283 self.rng = np.random.RandomState(self.config.rngSeed)
284 # The coefficients have units adu for IsrTask and have
285 # units electron for IsrTaskLSST.
286 self.crosstalkCoeffs = np.array([[0.0, 0.0, 0.0, 0.0, 0.0, -1e-3, 0.0, 0.0],
287 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
288 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
289 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
290 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
291 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
292 [1e-2, 0.0, 0.0, 2.2e-2, 0.0, 0.0, 0.0, 0.0],
293 [1e-2, 5e-3, 5e-4, 3e-3, 4e-2, 5e-3, 5e-3, 0.0]])
294 if getDebugFrame(self._display, "mockCrosstalkCoeffs"):
295 self.crosstalkCoeffs = np.array([[0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
296 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
297 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
298 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
299 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
300 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
301 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
302 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]])
303 # Generic gaussian BF kernel
304 self.bfKernel = np.array([[1., 4., 7., 4., 1.],
305 [4., 16., 26., 16., 4.],
306 [7., 26., 41., 26., 7.],
307 [4., 16., 26., 16., 4.],
308 [1., 4., 7., 4., 1.]]) / 273.0
309
310 def run(self):
311 """Generate a mock ISR product, and return it.
312
313 Returns
314 -------
315 image : `lsst.afw.image.Exposure`
316 Simulated ISR image with signals added.
317 dataProduct :
318 Simulated ISR data products.
319 None :
320 Returned if no valid configuration was found.
321
322 Raises
323 ------
324 RuntimeError
325 Raised if both doGenerateImage and doGenerateData are specified.
326 """
327 if self.config.doGenerateImage and self.config.doGenerateData:
328 raise RuntimeError("Only one of doGenerateImage and doGenerateData may be specified.")
329 elif self.config.doGenerateImage:
330 return self.makeImage()
331 elif self.config.doGenerateData:
332 return self.makeData()
333 else:
334 return None
335
336 def makeData(self):
337 """Generate simulated ISR data.
338
339 Currently, only the class defined crosstalk coefficient
340 matrix, brighter-fatter kernel, a constant unity transmission
341 curve, or a simple single-entry defect list can be generated.
342
343 Returns
344 -------
345 dataProduct :
346 Simulated ISR data product.
347 """
348 if sum(map(bool, [self.config.doBrighterFatter,
349 self.config.doDeferredCharge,
350 self.config.doDefects,
351 self.config.doTransmissionCurve,
352 self.config.doCrosstalkCoeffs,
353 self.config.doLinearizer])) != 1:
354 raise RuntimeError("Only one data product can be generated at a time.")
355 elif self.config.doBrighterFatter:
356 return self.makeBfKernel()
357 elif self.config.doDeferredCharge:
358 return self.makeDeferredChargeCalib()
359 elif self.config.doDefects:
360 return self.makeDefectList()
361 elif self.config.doTransmissionCurve:
362 return self.makeTransmissionCurve()
363 elif self.config.doCrosstalkCoeffs:
364 return self.crosstalkCoeffs
365 elif self.config.doLinearizer:
366 return self.makeLinearizer()
367 else:
368 return None
369
370 def makeBfKernel(self):
371 """Generate a simple Gaussian brighter-fatter kernel.
372
373 Returns
374 -------
375 kernel : `numpy.ndarray`
376 Simulated brighter-fatter kernel.
377 """
378 return self.bfKernel
379
381 """Generate a CTI calibration.
382 """
383
384 raise NotImplementedError("Mock deferred charge is not implemented for IsrMock.")
385
386 def makeDefectList(self):
387 """Generate a simple single-entry defect list.
388
389 Returns
390 -------
391 defectList : `lsst.meas.algorithms.Defects`
392 Simulated defect list
393 """
395 lsst.geom.Extent2I(40, 50))])
396
398 """Generate the simulated crosstalk coefficients.
399
400 Returns
401 -------
402 coeffs : `numpy.ndarray`
403 Simulated crosstalk coefficients.
404 """
405
406 return self.crosstalkCoeffs
407
409 """Generate a simulated flat transmission curve.
410
411 Returns
412 -------
413 transmission : `lsst.afw.image.TransmissionCurve`
414 Simulated transmission curve.
415 """
416
417 return afwImage.TransmissionCurve.makeIdentity()
418
419 def makeLinearity(self):
420 """Generate a linearity dataset.
421
422 Returns
423 -------
424 linearizer : `lsst.ip.isr.Linearizer`
425 """
426 raise NotImplementedError("Linearizer not implemented for isrMock.")
427
428 def makeImage(self):
429 """Generate a simulated ISR image.
430
431 Returns
432 -------
433 exposure : `lsst.afw.image.Exposure` or `dict`
434 Simulated ISR image data.
435
436 Notes
437 -----
438 This method currently constructs a "raw" data image by:
439
440 * Generating a simulated sky with noise
441 * Adding a single Gaussian "star"
442 * Adding the fringe signal
443 * Multiplying the frame by the simulated flat
444 * Adding dark current (and noise)
445 * Adding a bias offset (and noise)
446 * Adding an overscan gradient parallel to the pixel y-axis
447 * Simulating crosstalk by adding a scaled version of each
448 amplifier to each other amplifier.
449
450 The exposure with image data constructed this way is in one of
451 three formats.
452
453 * A single image, with overscan and prescan regions retained
454 * A single image, with overscan and prescan regions trimmed
455 * A `dict`, containing the amplifer data indexed by the
456 amplifier name.
457
458 The nonlinearity, CTE, and brighter fatter are currently not
459 implemented.
460
461 Note that this method generates an image in the reverse
462 direction as the ISR processing, as the output image here has
463 had a series of instrument effects added to an idealized
464 exposure.
465 """
466 exposure = self.getExposure()
467
468 for idx, amp in enumerate(exposure.getDetector()):
469 bbox = None
470 if self.config.isTrimmed is True:
471 bbox = amp.getBBox()
472 else:
473 bbox = amp.getRawDataBBox()
474
475 ampData = exposure.image[bbox]
476
477 if self.config.doAddSky is True:
478 self.amplifierAddNoise(ampData, self.config.skyLevel, np.sqrt(self.config.skyLevel))
479
480 if self.config.doAddSource is True:
481 for sourceAmp, sourceFlux, sourceX, sourceY in zip(self.config.sourceAmp,
482 self.config.sourceFlux,
483 self.config.sourceX,
484 self.config.sourceY):
485 if idx == sourceAmp:
486 self.amplifierAddSource(ampData, sourceFlux, sourceX, sourceY)
487
488 if self.config.doAddFringe is True:
489 self.amplifierAddFringe(amp, ampData, np.array(self.config.fringeScale),
490 x0=np.array(self.config.fringeX0),
491 y0=np.array(self.config.fringeY0))
492
493 if self.config.doAddFlat is True:
494 if ampData.getArray().sum() == 0.0:
495 self.amplifierAddNoise(ampData, 1.0, 0.0)
496 u0 = exposure.getDimensions().getX()
497 v0 = exposure.getDimensions().getY()
498 self.amplifierMultiplyFlat(amp, ampData, self.config.flatDrop, u0=u0, v0=v0)
499
500 if self.config.doAddDark is True:
501 self.amplifierAddNoise(ampData,
502 self.config.darkRate * self.config.darkTime / self.config.gain,
503 np.sqrt(self.config.darkRate
504 * self.config.darkTime / self.config.gain))
505
506 if self.config.doAddCrosstalk is True:
507 ctCalib = CrosstalkCalib()
508 # We use the regular subtractCrosstalk code but with a negative
509 # sign on the crosstalk coefficients so it adds instead of
510 # subtracts. We only apply the signal plane (ignoreVariance,
511 # subtrahendMasking) with a very large pixel to mask to ensure
512 # no crosstalk mask bits are set.
513 ctCalib.subtractCrosstalk(
514 exposure,
515 crosstalkCoeffs=-1*self.crosstalkCoeffs,
516 doSubtrahendMasking=True,
517 minPixelToMask=1e100,
518 ignoreVariance=True,
519 fullAmplifier=False,
520 )
521
522 for amp in exposure.getDetector():
523 bbox = None
524 if self.config.isTrimmed is True:
525 bbox = amp.getBBox()
526 else:
527 bbox = amp.getRawDataBBox()
528
529 ampData = exposure.image[bbox]
530
531 if self.config.doAddBias is True:
532 self.amplifierAddNoise(ampData, self.config.biasLevel,
533 self.config.readNoise / self.config.gain)
534
535 if self.config.doAddOverscan is True:
536 oscanBBox = amp.getRawHorizontalOverscanBBox()
537 oscanData = exposure.image[oscanBBox]
538 self.amplifierAddNoise(oscanData, self.config.biasLevel,
539 self.config.readNoise / self.config.gain)
540
541 self.amplifierAddYGradient(ampData, -1.0 * self.config.overscanScale,
542 1.0 * self.config.overscanScale)
543 self.amplifierAddYGradient(oscanData, -1.0 * self.config.overscanScale,
544 1.0 * self.config.overscanScale)
545
546 if self.config.doGenerateAmpDict is True:
547 expDict = dict()
548 for amp in exposure.getDetector():
549 expDict[amp.getName()] = exposure
550 return expDict
551 else:
552 return exposure
553
554 # afw primatives to construct the image structure
555 def getCamera(self, isForAssembly=False):
556 """Construct a test camera object.
557
558 Parameters
559 -------
560 isForAssembly : `bool`
561 If True, construct a camera with "super raw"
562 orientation (all amplifiers have LL readout
563 corner but still contains the necessary flip
564 and offset info needed for assembly. This is
565 needed if isLsstLike is True. If False, return
566 a camera with bboxes flipped and offset to the
567 correct orientation given the readout corner.
568
569 Returns
570 -------
571 camera : `lsst.afw.cameraGeom.camera`
572 Test camera.
573 """
574 cameraWrapper = afwTestUtils.CameraWrapper(
575 plateScale=self.config.plateScale,
576 radialDistortion=self.config.radialDistortion,
577 isLsstLike=self.config.isLsstLike and isForAssembly,
578 )
579 camera = cameraWrapper.camera
580 return camera
581
582 def getExposure(self, isTrimmed=None):
583 """Construct a test exposure.
584
585 The test exposure has a simple WCS set, as well as a list of
586 unlikely header keywords that can be removed during ISR
587 processing to exercise that code.
588
589 Parameters
590 ----------
591 isTrimmed : `bool` or `None`, optional
592 Override the configuration isTrimmed?
593
594 Returns
595 -------
596 exposure : `lsst.afw.exposure.Exposure`
597 Construct exposure containing masked image of the
598 appropriate size.
599 """
600 if isTrimmed is None:
601 _isTrimmed = self.config.isTrimmed
602 else:
603 _isTrimmed = isTrimmed
604
605 camera = self.getCamera(isForAssembly=self.config.isLsstLike)
606 detector = camera[self.config.detectorIndex]
607 image = afwUtils.makeImageFromCcd(
608 detector,
609 isTrimmed=_isTrimmed,
610 showAmpGain=False,
611 rcMarkSize=0,
612 binSize=1,
613 imageFactory=afwImage.ImageF,
614 )
615
616 var = afwImage.ImageF(image.getDimensions())
617 mask = afwImage.Mask(image.getDimensions())
618 image.assign(0.0)
619
620 maskedImage = afwImage.makeMaskedImage(image, mask, var)
621 exposure = afwImage.makeExposure(maskedImage)
622 exposure.setDetector(detector)
623 exposure.setWcs(self.getWcs())
624
625 visitInfo = afwImage.VisitInfo(exposureTime=self.config.expTime, darkTime=self.config.darkTime)
626 exposure.getInfo().setVisitInfo(visitInfo)
627 # Set a dummy ID.
628 exposure.getInfo().setId(12345)
629
630 metadata = exposure.getMetadata()
631 metadata.add("SHEEP", 7.3, "number of sheep on farm")
632 metadata.add("MONKEYS", 155, "monkeys per tree")
633 metadata.add("VAMPIRES", 4, "How scary are vampires.")
634
635 ccd = exposure.getDetector()
636 newCcd = ccd.rebuild()
637 newCcd.clear()
638 readoutMap = {
639 'LL': ReadoutCorner.LL,
640 'LR': ReadoutCorner.LR,
641 'UR': ReadoutCorner.UR,
642 'UL': ReadoutCorner.UL,
643 }
644 for amp in ccd:
645 newAmp = amp.rebuild()
646 newAmp.setLinearityCoeffs((0., 1., 0., 0.))
647 newAmp.setLinearityType("Polynomial")
648 newAmp.setGain(self.config.gain)
649 newAmp.setSuspectLevel(25000.0)
650 newAmp.setSaturation(32000.0)
651 readoutCorner = amp.getReadoutCorner().name
652
653 # Apply flips to bbox where needed
654 imageBBox = amp.getRawDataBBox()
655 rawBbox = amp.getRawBBox()
656 parallelOscanBBox = amp.getRawParallelOverscanBBox()
657 serialOscanBBox = amp.getRawSerialOverscanBBox()
658 prescanBBox = amp.getRawPrescanBBox()
659
660 if self.config.isLsstLike:
661 # This follows cameraGeom.testUtils
662 xoffset, yoffset = amp.getRawXYOffset()
663 offext = lsst.geom.Extent2I(xoffset, yoffset)
664 flipx = bool(amp.getRawFlipX())
665 flipy = bool(amp.getRawFlipY())
666 if flipx:
667 xExt = rawBbox.getDimensions().getX()
668 rawBbox.flipLR(xExt)
669 imageBBox.flipLR(xExt)
670 parallelOscanBBox.flipLR(xExt)
671 serialOscanBBox.flipLR(xExt)
672 prescanBBox.flipLR(xExt)
673 if flipy:
674 yExt = rawBbox.getDimensions().getY()
675 rawBbox.flipTB(yExt)
676 imageBBox.flipTB(yExt)
677 parallelOscanBBox.flipTB(yExt)
678 serialOscanBBox.flipTB(yExt)
679 prescanBBox.flipTB(yExt)
680 if not flipx and not flipy:
681 readoutCorner = 'LL'
682 elif flipx and not flipy:
683 readoutCorner = 'LR'
684 elif flipx and flipy:
685 readoutCorner = 'UR'
686 elif not flipx and flipy:
687 readoutCorner = 'UL'
688 rawBbox.shift(offext)
689 imageBBox.shift(offext)
690 parallelOscanBBox.shift(offext)
691 serialOscanBBox.shift(offext)
692 prescanBBox.shift(offext)
693 newAmp.setReadoutCorner(readoutMap[readoutCorner])
694 newAmp.setRawBBox(rawBbox)
695 newAmp.setRawDataBBox(imageBBox)
696 newAmp.setRawParallelOverscanBBox(parallelOscanBBox)
697 newAmp.setRawSerialOverscanBBox(serialOscanBBox)
698 newAmp.setRawPrescanBBox(prescanBBox)
699 newAmp.setRawFlipX(False)
700 newAmp.setRawFlipY(False)
701 no_offset = lsst.geom.Extent2I(0, 0)
702 newAmp.setRawXYOffset(no_offset)
703
704 newCcd.append(newAmp)
705
706 exposure.setDetector(newCcd.finish())
707
708 exposure.image.array[:] = np.zeros(exposure.getImage().getDimensions()).transpose()
709 exposure.mask.array[:] = np.zeros(exposure.getMask().getDimensions()).transpose()
710 exposure.variance.array[:] = np.zeros(exposure.getVariance().getDimensions()).transpose()
711
712 return exposure
713
714 def getWcs(self):
715 """Construct a dummy WCS object.
716
717 Taken from the deprecated ip_isr/examples/exampleUtils.py.
718
719 This is not guaranteed, given the distortion and pixel scale
720 listed in the afwTestUtils camera definition.
721
722 Returns
723 -------
724 wcs : `lsst.afw.geom.SkyWcs`
725 Test WCS transform.
726 """
727 return afwGeom.makeSkyWcs(crpix=lsst.geom.Point2D(0.0, 100.0),
728 crval=lsst.geom.SpherePoint(45.0, 25.0, lsst.geom.degrees),
729 cdMatrix=afwGeom.makeCdMatrix(scale=1.0*lsst.geom.degrees))
730
731 def localCoordToExpCoord(self, ampData, x, y):
732 """Convert between a local amplifier coordinate and the full
733 exposure coordinate.
734
735 Parameters
736 ----------
737 ampData : `lsst.afw.image.ImageF`
738 Amplifier image to use for conversions.
739 x : `int`
740 X-coordinate of the point to transform.
741 y : `int`
742 Y-coordinate of the point to transform.
743
744 Returns
745 -------
746 u : `int`
747 Transformed x-coordinate.
748 v : `int`
749 Transformed y-coordinate.
750
751 Notes
752 -----
753 The output is transposed intentionally here, to match the
754 internal transpose between numpy and afw.image coordinates.
755 """
756 u = x + ampData.getBBox().getBeginX()
757 v = y + ampData.getBBox().getBeginY()
758
759 return (v, u)
760
761 # Simple data values.
762 def amplifierAddNoise(self, ampData, mean, sigma, rng=None):
763 """Add Gaussian noise to an amplifier's image data.
764
765 This method operates in the amplifier coordinate frame.
766
767 Parameters
768 ----------
769 ampData : `lsst.afw.image.ImageF`
770 Amplifier image to operate on.
771 mean : `float`
772 Mean value of the Gaussian noise.
773 sigma : `float`
774 Sigma of the Gaussian noise.
775 rng : `np.random.RandomState`, optional
776 Random state to use instead of self.rng.
777 """
778 if rng is not None:
779 _rng = rng
780 else:
781 _rng = self.rng
782
783 ampArr = ampData.array
784 ampArr[:] = ampArr[:] + _rng.normal(mean, sigma,
785 size=ampData.getDimensions()).transpose()
786
787 def amplifierAddYGradient(self, ampData, start, end):
788 """Add a y-axis linear gradient to an amplifier's image data.
789
790 This method operates in the amplifier coordinate frame.
791
792 Parameters
793 ----------
794 ampData : `lsst.afw.image.ImageF`
795 Amplifier image to operate on.
796 start : `float`
797 Start value of the gradient (at y=0).
798 end : `float`
799 End value of the gradient (at y=ymax).
800 """
801 nPixY = ampData.getDimensions().getY()
802 ampArr = ampData.array
803 ampArr[:] = ampArr[:] + (np.interp(range(nPixY), (0, nPixY - 1), (start, end)).reshape(nPixY, 1)
804 + np.zeros(ampData.getDimensions()).transpose())
805
806 def amplifierAddSource(self, ampData, scale, x0, y0):
807 """Add a single Gaussian source to an amplifier.
808
809 This method operates in the amplifier coordinate frame.
810
811 Parameters
812 ----------
813 ampData : `lsst.afw.image.ImageF`
814 Amplifier image to operate on.
815 scale : `float`
816 Peak flux of the source to add.
817 x0 : `float`
818 X-coordinate of the source peak.
819 y0 : `float`
820 Y-coordinate of the source peak.
821 """
822 for x in range(0, ampData.getDimensions().getX()):
823 for y in range(0, ampData.getDimensions().getY()):
824 ampData.array[y][x] = (ampData.array[y][x]
825 + scale * np.exp(-0.5 * ((x - x0)**2 + (y - y0)**2) / 3.0**2))
826
827 # Functional form data values.
828 def amplifierAddFringe(self, amp, ampData, scale, x0=100, y0=0):
829 """Add a fringe-like ripple pattern to an amplifier's image data.
830
831 Parameters
832 ----------
833 amp : `~lsst.afw.ampInfo.AmpInfoRecord`
834 Amplifier to operate on. Needed for amp<->exp coordinate
835 transforms.
836 ampData : `lsst.afw.image.ImageF`
837 Amplifier image to operate on.
838 scale : `numpy.array` or `float`
839 Peak intensity scaling for the ripple.
840 x0 : `numpy.array` or `float`, optional
841 Fringe center
842 y0 : `numpy.array` or `float`, optional
843 Fringe center
844
845 Notes
846 -----
847 This uses an offset sinc function to generate a ripple
848 pattern. True fringes have much finer structure, but this
849 pattern should be visually identifiable. The (x, y)
850 coordinates are in the frame of the amplifier, and (u, v) in
851 the frame of the full trimmed image.
852 """
853 for x in range(0, ampData.getDimensions().getX()):
854 for y in range(0, ampData.getDimensions().getY()):
855 (u, v) = self.localCoordToExpCoord(amp, x, y)
856 ampData.getArray()[y][x] = np.sum((ampData.getArray()[y][x]
857 + scale * np.sinc(((u - x0) / 50)**2
858 + ((v - y0) / 50)**2)))
859
860 def amplifierMultiplyFlat(self, amp, ampData, fracDrop, u0=100.0, v0=100.0):
861 """Multiply an amplifier's image data by a flat-like pattern.
862
863 Parameters
864 ----------
865 amp : `lsst.afw.ampInfo.AmpInfoRecord`
866 Amplifier to operate on. Needed for amp<->exp coordinate
867 transforms.
868 ampData : `lsst.afw.image.ImageF`
869 Amplifier image to operate on.
870 fracDrop : `float`
871 Fractional drop from center to edge of detector along x-axis.
872 u0 : `float`
873 Peak location in detector coordinates.
874 v0 : `float`
875 Peak location in detector coordinates.
876
877 Notes
878 -----
879 This uses a 2-d Gaussian to simulate an illumination pattern
880 that falls off towards the edge of the detector. The (x, y)
881 coordinates are in the frame of the amplifier, and (u, v) in
882 the frame of the full trimmed image.
883 """
884 if fracDrop >= 1.0:
885 raise RuntimeError("Flat fractional drop cannot be greater than 1.0")
886
887 sigma = u0 / np.sqrt(-2.0 * np.log(fracDrop))
888
889 for x in range(0, ampData.getDimensions().getX()):
890 for y in range(0, ampData.getDimensions().getY()):
891 (u, v) = self.localCoordToExpCoord(amp, x, y)
892 f = np.exp(-0.5 * ((u - u0)**2 + (v - v0)**2) / sigma**2)
893 ampData.array[y][x] = (ampData.array[y][x] * f)
894
895
897 """Generate a raw exposure suitable for ISR.
898 """
899 def __init__(self, **kwargs):
900 super().__init__(**kwargs)
901 self.config.isTrimmed = False
902 self.config.doGenerateImage = True
903 self.config.doGenerateAmpDict = False
904 self.config.doAddOverscan = True
905 self.config.doAddSky = True
906 self.config.doAddSource = True
907 self.config.doAddCrosstalk = False
908 self.config.doAddBias = True
909 self.config.doAddDark = True
910
911
913 """Generate a trimmed raw exposure.
914 """
915 def __init__(self, **kwargs):
916 super().__init__(**kwargs)
917 self.config.isTrimmed = True
918 self.config.doAddOverscan = False
919
920
922 """Generate a trimmed raw exposure.
923 """
924 def __init__(self, **kwargs):
925 super().__init__(**kwargs)
926 self.config.isTrimmed = True
927 self.config.doGenerateImage = True
928 self.config.doAddOverscan = False
929 self.config.doAddSky = True
930 self.config.doAddSource = True
931 self.config.doAddCrosstalk = False
932
933 self.config.doAddBias = False
934 self.config.doAddDark = False
935 self.config.doAddFlat = False
936 self.config.doAddFringe = True
937
938 self.config.biasLevel = 0.0
939 self.config.readNoise = 10.0
940
941
943 """Generate a raw exposure dict suitable for ISR.
944 """
945 def __init__(self, **kwargs):
946 super().__init__(**kwargs)
947 self.config.doGenerateAmpDict = True
948
949
951 """Parent class for those that make master calibrations.
952 """
953 def __init__(self, **kwargs):
954 super().__init__(**kwargs)
955 self.config.isTrimmed = True
956 self.config.doGenerateImage = True
957 self.config.doAddOverscan = False
958 self.config.doAddSky = False
959 self.config.doAddSource = False
960 self.config.doAddCrosstalk = False
961
962 self.config.doAddBias = False
963 self.config.doAddDark = False
964 self.config.doAddFlat = False
965 self.config.doAddFringe = False
966
967
969 """Simulated master bias calibration.
970 """
971 def __init__(self, **kwargs):
972 super().__init__(**kwargs)
973 self.config.doAddBias = True
974 self.config.readNoise = 10.0
975
976
978 """Simulated master dark calibration.
979 """
980 def __init__(self, **kwargs):
981 super().__init__(**kwargs)
982 self.config.doAddDark = True
983 self.config.darkTime = 1.0
984
985
987 """Simulated master flat calibration.
988 """
989 def __init__(self, **kwargs):
990 super().__init__(**kwargs)
991 self.config.doAddFlat = True
992
993
995 """Simulated master fringe calibration.
996 """
997 def __init__(self, **kwargs):
998 super().__init__(**kwargs)
999 self.config.doAddFringe = True
1000
1001
1003 """Simulated untrimmed master fringe calibration.
1004 """
1005 def __init__(self, **kwargs):
1006 super().__init__(**kwargs)
1007 self.config.isTrimmed = False
1008
1009
1011 """Simulated brighter-fatter kernel.
1012 """
1013 def __init__(self, **kwargs):
1014 super().__init__(**kwargs)
1015 self.config.doGenerateImage = False
1016 self.config.doGenerateData = True
1017 self.config.doBrighterFatter = True
1018 self.config.doDefects = False
1019 self.config.doCrosstalkCoeffs = False
1020 self.config.doTransmissionCurve = False
1021
1022
1024 """Simulated deferred charge calibration.
1025 """
1026 def __init__(self, **kwargs):
1027 super().__init__(**kwargs)
1028 self.config.doGenerateImage = False
1029 self.config.doGenerateData = True
1030 self.config.doDeferredCharge = True
1031 self.config.doDefects = False
1032 self.config.doCrosstalkCoeffs = False
1033 self.config.doTransmissionCurve = False
1034
1035
1037 """Simulated defect list.
1038 """
1039 def __init__(self, **kwargs):
1040 super().__init__(**kwargs)
1041 self.config.doGenerateImage = False
1042 self.config.doGenerateData = True
1043 self.config.doBrighterFatter = False
1044 self.config.doDefects = True
1045 self.config.doCrosstalkCoeffs = False
1046 self.config.doTransmissionCurve = False
1047
1048
1050 """Simulated crosstalk coefficient matrix.
1051 """
1052 def __init__(self, **kwargs):
1053 super().__init__(**kwargs)
1054 self.config.doGenerateImage = False
1055 self.config.doGenerateData = True
1056 self.config.doBrighterFatter = False
1057 self.config.doDefects = False
1058 self.config.doCrosstalkCoeffs = True
1059 self.config.doTransmissionCurve = False
1060
1061
1063 """Simulated transmission curve.
1064 """
1065 def __init__(self, **kwargs):
1066 super().__init__(**kwargs)
1067 self.config.doGenerateImage = False
1068 self.config.doGenerateData = True
1069 self.config.doBrighterFatter = False
1070 self.config.doDefects = False
1071 self.config.doCrosstalkCoeffs = False
1072 self.config.doTransmissionCurve = True
1073
1074
1075class MockDataContainer(object):
1076 """Container for holding ISR mock objects.
1077 """
1078 dataId = "isrMock Fake Data"
1079 darkval = 2. # electron/sec
1080 oscan = 250. # adu
1081 gradient = .10
1082 exptime = 15.0 # seconds
1083 darkexptime = 15.0 # seconds
1084
1085 def __init__(self, **kwargs):
1086 if 'config' in kwargs.keys():
1087 self.config = kwargs['config']
1088 else:
1089 self.config = None
1090
1091 def expectImage(self):
1092 if self.config is None:
1093 self.config = IsrMockConfig()
1094 self.config.doGenerateImage = True
1095 self.config.doGenerateData = False
1096
1097 def expectData(self):
1098 if self.config is None:
1099 self.config = IsrMockConfig()
1100 self.config.doGenerateImage = False
1101 self.config.doGenerateData = True
1102
1103 def get(self, dataType, **kwargs):
1104 """Return an appropriate data product.
1105
1106 Parameters
1107 ----------
1108 dataType : `str`
1109 Type of data product to return.
1110
1111 Returns
1112 -------
1113 mock : IsrMock.run() result
1114 The output product.
1115 """
1116 if "_filename" in dataType:
1117 self.expectData()
1118 return tempfile.mktemp(), "mock"
1119 elif 'transmission_' in dataType:
1120 self.expectData()
1121 return TransmissionMock(config=self.config).run()
1122 elif dataType == 'ccdExposureId':
1123 self.expectData()
1124 return 20090913
1125 elif dataType == 'camera':
1126 self.expectData()
1127 return IsrMock(config=self.config).getCamera()
1128 elif dataType == 'raw':
1129 self.expectImage()
1130 return RawMock(config=self.config).run()
1131 elif dataType == 'bias':
1132 self.expectImage()
1133 return BiasMock(config=self.config).run()
1134 elif dataType == 'dark':
1135 self.expectImage()
1136 return DarkMock(config=self.config).run()
1137 elif dataType == 'flat':
1138 self.expectImage()
1139 return FlatMock(config=self.config).run()
1140 elif dataType == 'fringe':
1141 self.expectImage()
1142 return FringeMock(config=self.config).run()
1143 elif dataType == 'defects':
1144 self.expectData()
1145 return DefectMock(config=self.config).run()
1146 elif dataType == 'bfKernel':
1147 self.expectData()
1148 return BfKernelMock(config=self.config).run()
1149 elif dataType == 'linearizer':
1150 return None
1151 elif dataType == 'crosstalkSources':
1152 return None
1153 else:
1154 raise RuntimeError("ISR DataRefMock cannot return %s.", dataType)
1155
1156
1158 """Container for mock fringe data.
1159 """
1160 dataId = "isrMock Fake Data"
1161 darkval = 2. # electron/sec
1162 oscan = 250. # adu
1163 gradient = .10
1164 exptime = 15 # seconds
1165 darkexptime = 40. # seconds
1166
1167 def __init__(self, **kwargs):
1168 if 'config' in kwargs.keys():
1169 self.config = kwargs['config']
1170 else:
1171 self.config = IsrMockConfig()
1172 self.config.isTrimmed = True
1173 self.config.doAddFringe = True
1174 self.config.readNoise = 10.0
1175
1176 def get(self, dataType, **kwargs):
1177 """Return an appropriate data product.
1178
1179 Parameters
1180 ----------
1181 dataType : `str`
1182 Type of data product to return.
1183
1184 Returns
1185 -------
1186 mock : IsrMock.run() result
1187 The output product.
1188 """
1189 if "_filename" in dataType:
1190 return tempfile.mktemp(), "mock"
1191 elif 'transmission_' in dataType:
1192 return TransmissionMock(config=self.config).run()
1193 elif dataType == 'ccdExposureId':
1194 return 20090913
1195 elif dataType == 'camera':
1196 return IsrMock(config=self.config).getCamera()
1197 elif dataType == 'raw':
1198 return CalibratedRawMock(config=self.config).run()
1199 elif dataType == 'bias':
1200 return BiasMock(config=self.config).run()
1201 elif dataType == 'dark':
1202 return DarkMock(config=self.config).run()
1203 elif dataType == 'flat':
1204 return FlatMock(config=self.config).run()
1205 elif dataType == 'fringe':
1206 fringes = []
1207 configCopy = copy.deepcopy(self.config)
1208 for scale, x, y in zip(self.config.fringeScale, self.config.fringeX0, self.config.fringeY0):
1209 configCopy.fringeScale = [1.0]
1210 configCopy.fringeX0 = [x]
1211 configCopy.fringeY0 = [y]
1212 fringes.append(FringeMock(config=configCopy).run())
1213 return fringes
1214 elif dataType == 'defects':
1215 return DefectMock(config=self.config).run()
1216 elif dataType == 'bfKernel':
1217 return BfKernelMock(config=self.config).run()
1218 elif dataType == 'linearizer':
1219 return None
1220 elif dataType == 'crosstalkSources':
1221 return None
1222 else:
1223 return None
Represent a 2-dimensional array of bitmask pixels.
Definition Mask.h:82
Information about a single exposure of an imaging camera.
Definition VisitInfo.h:68
An integer coordinate rectangle.
Definition Box.h:55
Point in an unspecified spherical coordinate system.
Definition SpherePoint.h:57
localCoordToExpCoord(self, ampData, x, y)
Definition isrMock.py:731
amplifierAddNoise(self, ampData, mean, sigma, rng=None)
Definition isrMock.py:762
amplifierAddSource(self, ampData, scale, x0, y0)
Definition isrMock.py:806
amplifierMultiplyFlat(self, amp, ampData, fracDrop, u0=100.0, v0=100.0)
Definition isrMock.py:860
amplifierAddYGradient(self, ampData, start, end)
Definition isrMock.py:787
getCamera(self, isForAssembly=False)
Definition isrMock.py:555
getExposure(self, isTrimmed=None)
Definition isrMock.py:582
amplifierAddFringe(self, amp, ampData, scale, x0=100, y0=0)
Definition isrMock.py:828
__init__(self, **kwargs)
Definition isrMock.py:281
get(self, dataType, **kwargs)
Definition isrMock.py:1103
get(self, dataType, **kwargs)
Definition isrMock.py:1176
__init__(self, **kwargs)
Definition isrMock.py:899
std::shared_ptr< SkyWcs > makeSkyWcs(daf::base::PropertySet &metadata, bool strip=false)
Construct a SkyWcs from FITS keywords.
Definition SkyWcs.cc:521
Eigen::Matrix2d makeCdMatrix(lsst::geom::Angle const &scale, lsst::geom::Angle const &orientation=0 *lsst::geom::degrees, bool flipX=false)
Make a WCS CD matrix.
Definition SkyWcs.cc:133
MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > * makeMaskedImage(typename std::shared_ptr< Image< ImagePixelT > > image, typename std::shared_ptr< Mask< MaskPixelT > > mask=Mask< MaskPixelT >(), typename std::shared_ptr< Image< VariancePixelT > > variance=Image< VariancePixelT >())
A function to return a MaskedImage of the correct type (cf.
std::shared_ptr< Exposure< ImagePixelT, MaskPixelT, VariancePixelT > > makeExposure(MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > &mimage, std::shared_ptr< geom::SkyWcs const > wcs=std::shared_ptr< geom::SkyWcs const >())
A function to return an Exposure of the correct type (cf.
Definition Exposure.h:484