LSST Applications g0603fd7c41+501e3db9f9,g0aad566f14+23d8574c86,g0dd44d6229+a1a4c8b791,g2079a07aa2+86d27d4dc4,g2305ad1205+a62672bbc1,g2bbee38e9b+047b288a59,g337abbeb29+047b288a59,g33d1c0ed96+047b288a59,g3a166c0a6a+047b288a59,g3d1719c13e+23d8574c86,g487adcacf7+cb7fd919b2,g4be5004598+23d8574c86,g50ff169b8f+96c6868917,g52b1c1532d+585e252eca,g591dd9f2cf+4a9e435310,g63cd9335cc+585e252eca,g858d7b2824+23d8574c86,g88963caddf+0cb8e002cc,g99cad8db69+43388bcaec,g9ddcbc5298+9a081db1e4,ga1e77700b3+a912195c07,gae0086650b+585e252eca,gb0e22166c9+60f28cb32d,gb2522980b2+793639e996,gb3a676b8dc+b4feba26a1,gb4b16eec92+63f8520565,gba4ed39666+c2a2e4ac27,gbb8dafda3b+a5d255a82e,gc120e1dc64+d820f8acdb,gc28159a63d+047b288a59,gc3e9b769f7+f4f1cc6b50,gcf0d15dbbd+a1a4c8b791,gdaeeff99f8+f9a426f77a,gdb0af172c8+b6d5496702,ge79ae78c31+047b288a59,w.2024.19
LSST Data Management Base Package
Loading...
Searching...
No Matches
isrMockLSST.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__ = ["IsrMockLSSTConfig", "IsrMockLSST", "RawMockLSST",
23 "CalibratedRawMockLSST", "ReferenceMockLSST",
24 "BiasMockLSST", "DarkMockLSST", "FlatMockLSST", "FringeMockLSST",
25 "BfKernelMockLSST", "DefectMockLSST", "CrosstalkCoeffMockLSST",
26 "TransmissionMockLSST"]
27import numpy as np
28
29import lsst.geom as geom
30import lsst.pex.config as pexConfig
31from .crosstalk import CrosstalkCalib
32from .isrMock import IsrMockConfig, IsrMock
33
34
36 """Configuration parameters for isrMockLSST.
37 """
38 # Detector parameters and "Exposure" parameters,
39 # mostly inherited from IsrMockConfig.
40 isLsstLike = pexConfig.Field(
41 dtype=bool,
42 default=True,
43 doc="If True, products have one raw image per amplifier, otherwise, one raw image per detector.",
44 )
45 # Change bias level to LSSTCam expected values.
46 biasLevel = pexConfig.Field(
47 dtype=float,
48 default=25000.0,
49 doc="Background contribution to be generated from the bias offset in ADU.",
50 )
51 calibMode = pexConfig.Field(
52 dtype=bool,
53 default=False,
54 doc="Set to true to produce mock calibration products, e.g. combined bias, dark, flat, etc.",
55 )
56 doAddParallelOverscan = pexConfig.Field(
57 dtype=bool,
58 default=True,
59 doc="Add overscan ramp to parallel overscan and data regions.",
60 )
61 doAddSerialOverscan = pexConfig.Field(
62 dtype=bool,
63 default=True,
64 doc="Add overscan ramp to serial overscan and data regions.",
65 )
66 doApplyGain = pexConfig.Field(
67 dtype=bool,
68 default=True,
69 doc="Add gain to data.",
70 )
71
72
74 """Class to generate consistent mock images for ISR testing.
75 """
76 ConfigClass = IsrMockLSSTConfig
77 _DefaultName = "isrMockLSST"
78
79 def __init__(self, **kwargs):
80 # cross-talk coeffs, bf kernel are defined in the parent class.
81 super().__init__(**kwargs)
82
83 def run(self):
84 """Generate a mock ISR product following LSSTCam ISR, and return it.
85
86 Returns
87 -------
88 image : `lsst.afw.image.Exposure`
89 Simulated ISR image with signals added.
90 dataProduct :
91 Simulated ISR data products.
92 None :
93 Returned if no valid configuration was found.
94
95 Raises
96 ------
97 RuntimeError
98 Raised if both doGenerateImage and doGenerateData are specified.
99 """
100 if self.config.doGenerateImage and self.config.doGenerateData:
101 raise RuntimeError("Only one of doGenerateImage and doGenerateData may be specified.")
102 elif self.config.doGenerateImage:
103 return self.makeImagemakeImage()
104 elif self.config.doGenerateData:
105 return self.makeData()
106 else:
107 return None
108
109 def makeImage(self):
110 """Generate a simulated ISR LSST image.
111
112 Returns
113 -------
114 exposure : `lsst.afw.image.Exposure` or `dict`
115 Simulated ISR image data.
116
117 Notes
118 -----
119 This method constructs a "raw" data image.
120 """
121 exposure = self.getExposure()
122
123 # We introduce effects as they happen from a source to the signal,
124 # so the effects go from electrons to ADU.
125 # The ISR steps will then correct these effects in the reverse order.
126 for idx, amp in enumerate(exposure.getDetector()):
127
128 # Get image bbox and data
129 bbox = None
130 if self.config.isTrimmed:
131 bbox = amp.getBBox()
132 else:
133 bbox = amp.getRawDataBBox()
134
135 ampData = exposure.image[bbox]
136
137 # Sky effects in e-
138 if self.config.doAddSky:
139 # The sky effects are in electrons,
140 # but the skyLevel is configured in ADU
141 # TODO: DM-42880 to set configs to correct units
142 self.amplifierAddNoise(ampData, self.config.skyLevel * self.config.gain,
143 np.sqrt(self.config.skyLevel * self.config.gain))
144
145 if self.config.doAddSource:
146 for sourceAmp, sourceFlux, sourceX, sourceY in zip(self.config.sourceAmp,
147 self.config.sourceFlux,
148 self.config.sourceX,
149 self.config.sourceY):
150 if idx == sourceAmp:
151 # The source flux is in electrons,
152 # but the sourceFlux is configured in ADU
153 # TODO: DM-42880 to set configs to correct units
154 self.amplifierAddSource(ampData, sourceFlux * self.config.gain, sourceX, sourceY)
155
156 # Other effects in e-
157 if self.config.doAddFringe:
158 # Fringes are added in electrons,
159 # but the fringeScale is configured in ADU
160 self.amplifierAddFringe(amp, ampData, np.array(self.config.fringeScale) * self.config.gain,
161 x0=np.array(self.config.fringeX0),
162 y0=np.array(self.config.fringeY0))
163
164 if self.config.doAddFlat:
165 if self.config.calibMode:
166 # In case we are making a combined flat,
167 # add a non-zero signal so the mock flat can be multiplied
168 self.amplifierAddNoise(ampData, 1.0, 0.0)
169 # Multiply each amplifier by a Gaussian centered on u0 and v0
170 u0 = exposure.getDetector().getBBox().getDimensions().getX()/2.
171 v0 = exposure.getDetector().getBBox().getDimensions().getY()/2.
172 self.amplifierMultiplyFlatamplifierMultiplyFlat(amp, ampData, self.config.flatDrop, u0=u0, v0=v0)
173
174 # ISR effects
175 # 1. Add dark in e- (darkRate is configured in e-/s)
176 # TODO: DM-42880 to set configs to correct units
177 if self.config.doAddDark:
178 self.amplifierAddNoise(ampData,
179 self.config.darkRate * self.config.darkTime,
180 0. if self.config.calibMode
181 else np.sqrt(self.config.darkRate * self.config.darkTime))
182
183 # 2. Gain normalize (from e- to ADU)
184 # TODO: DM-43601 gain from PTC per amplifier
185 # TODO: DM-36639 gain with temperature dependence
186 if self.config.doApplyGain:
187 self.applyGain(ampData, self.config.gain)
188
189 # 3. Add read noise to the image region in ADU.
190 if not self.config.calibMode:
191 self.amplifierAddNoise(ampData, 0.0,
192 self.config.readNoise / self.config.gain)
193
194 # 4. Apply cross-talk in ADU
195 if self.config.doAddCrosstalk:
196 ctCalib = CrosstalkCalib()
197 exposureClean = exposure.clone()
198 for idxS, ampS in enumerate(exposure.getDetector()):
199 for idxT, ampT in enumerate(exposure.getDetector()):
200 ampDataTarget = exposure.image[ampT.getBBox() if self.config.isTrimmed
201 else ampT.getRawDataBBox()]
202 ampDataSource = ctCalib.extractAmp(exposureClean.image, ampS, ampT,
203 isTrimmed=self.config.isTrimmed)
204 self.amplifierAddCT(ampDataSource, ampDataTarget, self.crosstalkCoeffs[idxS][idxT])
205
206 # We now apply parallel and serial overscans
207 for amp in exposure.getDetector():
208 # Get image bbox and data
209 bbox = None
210 if self.config.isTrimmed:
211 bbox = amp.getBBox()
212 else:
213 bbox = amp.getRawDataBBox()
214 ampData = exposure.image[bbox]
215
216 if self.config.doAddParallelOverscan or self.config.doAddSerialOverscan or self.config.doAddBias:
217
218 allData = ampData
219
220 if self.config.doAddParallelOverscan or self.config.doAddSerialOverscan:
221 # 5. Apply parallel overscan in ADU
222 # First get the parallel and serial overscan bbox
223 # and corresponding data
224 parallelOscanBBox = amp.getRawParallelOverscanBBox()
225 parallelOscanData = exposure.image[parallelOscanBBox]
226
227 grownImageBBox = bbox.expandedTo(parallelOscanBBox)
228
229 serialOscanBBox = amp.getRawSerialOverscanBBox()
230 # Extend the serial overscan bbox to include corners
231 serialOscanBBox = geom.Box2I(
232 geom.Point2I(serialOscanBBox.getMinX(),
233 grownImageBBox.getMinY()),
234 geom.Extent2I(serialOscanBBox.getWidth(),
235 grownImageBBox.getHeight()))
236 serialOscanData = exposure.image[serialOscanBBox]
237
238 # Add read noise of mean 0
239 # to the parallel and serial overscan regions
240 self.amplifierAddNoise(parallelOscanData, 0.0,
241 self.config.readNoise / self.config.gain)
242
243 self.amplifierAddNoise(serialOscanData, 0.0,
244 self.config.readNoise / self.config.gain)
245
246 grownImageBBoxAll = grownImageBBox.expandedTo(serialOscanBBox)
247 allData = exposure.image[grownImageBBoxAll]
248
249 if self.config.doAddParallelOverscan:
250 # Apply gradient along the Y axis
251 self.amplifierAddXGradient(allData, -1.0 * self.config.overscanScale,
252 1.0 * self.config.overscanScale)
253
254 # 6. Add Parallel overscan xtalk.
255 # TODO: DM-43286
256
257 # Add bias level to the whole image
258 # (science and overscan regions if any)
259 self.addBiasLevel(allData, self.config.biasLevel if self.config.doAddBias else 0.0)
260
261 if self.config.doAddSerialOverscan:
262 # Apply gradient along the Y axis
263 self.amplifierAddYGradient(allData, -1.0 * self.config.overscanScale,
264 1.0 * self.config.overscanScale)
265
266 if self.config.doGenerateAmpDict:
267 expDict = dict()
268 for amp in exposure.getDetector():
269 expDict[amp.getName()] = exposure
270 return expDict
271 else:
272 return exposure
273
274 def addBiasLevel(self, ampData, biasLevel):
275 """Add bias level to an amplifier's image data.
276
277 Parameters
278 ----------
279 ampData : `lsst.afw.image.ImageF`
280 Amplifier image to operate on.
281 biasLevel : `float`
282 Bias level to be added to the image.
283 """
284 ampArr = ampData.array
285 ampArr[:] = ampArr[:] + biasLevel
286
287 def amplifierMultiplyFlat(self, amp, ampData, fracDrop, u0=100.0, v0=100.0):
288 """Multiply an amplifier's image data by a flat-like pattern.
289
290 Parameters
291 ----------
292 amp : `lsst.afw.ampInfo.AmpInfoRecord`
293 Amplifier to operate on. Needed for amp<->exp coordinate
294 transforms.
295 ampData : `lsst.afw.image.ImageF`
296 Amplifier image to operate on.
297 fracDrop : `float`
298 Fractional drop from center to edge of detector along x-axis.
299 u0 : `float`
300 Peak location in detector coordinates.
301 v0 : `float`
302 Peak location in detector coordinates.
303 """
304 if fracDrop >= 1.0:
305 raise RuntimeError("Flat fractional drop cannot be greater than 1.0")
306
307 sigma = u0 / np.sqrt(2.0 * fracDrop)
308
309 for x in range(0, ampData.getDimensions().getX()):
310 for y in range(0, ampData.getDimensions().getY()):
311 (u, v) = self.localCoordToExpCoord(amp, x, y)
312 f = np.exp(-0.5 * ((u - u0)**2 + (v - v0)**2) / sigma**2)
313 ampData.array[y][x] = (ampData.array[y][x] * f)
314
315 def applyGain(self, ampData, gain):
316 """Apply gain to the amplifier's data.
317 This method divides the data by the gain
318 because the mocks need to convert the data in electron to ADU,
319 so it does the inverse operation to applyGains in isrFunctions.
320
321 Parameters
322 ----------
323 ampData : `lsst.afw.image.ImageF`
324 Amplifier image to operate on.
325 gain : `float`
326 Gain value in e^-/DN.
327 """
328 ampArr = ampData.array
329 ampArr[:] = ampArr[:] / gain
330
331 def amplifierAddXGradient(self, ampData, start, end):
332 """Add a x-axis linear gradient to an amplifier's image data.
333
334 This method operates in the amplifier coordinate frame.
335
336 Parameters
337 ----------
338 ampData : `lsst.afw.image.ImageF`
339 Amplifier image to operate on.
340 start : `float`
341 Start value of the gradient (at x=0).
342 end : `float`
343 End value of the gradient (at x=xmax).
344 """
345 nPixX = ampData.getDimensions().getX()
346 ampArr = ampData.array
347 ampArr[:] = ampArr[:] + (np.interp(range(nPixX), (0, nPixX - 1), (start, end)).reshape(1, nPixX)
348 + np.zeros(ampData.getDimensions()).transpose())
349
350
352 """Generate a raw exposure suitable for ISR.
353 """
354 def __init__(self, **kwargs):
355 super().__init__(**kwargs)
356 self.config.isTrimmed = False
357 self.config.doGenerateImage = True
358 self.config.doGenerateAmpDict = False
359
360 # Add astro effects
361 self.config.doAddSky = True
362 self.config.doAddSource = True
363
364 # Add optical effects
365 self.config.doAddFringe = True
366
367 # Add instru effects
368 self.config.doAddParallelOverscan = True
369 self.config.doAddSerialOverscan = True
370 self.config.doAddCrosstalk = False
371 self.config.doAddBias = True
372 self.config.doAddDark = True
373
374 self.config.doAddFlat = True
375
376
378 """Generate a trimmed raw exposure.
379 """
380 def __init__(self, **kwargs):
381 super().__init__(**kwargs)
382 self.config.isTrimmed = True
383 self.config.doAddParallelOverscan = False
384 self.config.doAddSerialOverscan = False
385
386
388 """Generate a trimmed raw exposure.
389 """
390 def __init__(self, **kwargs):
391 super().__init__(**kwargs)
392 self.config.isTrimmed = True
393 self.config.doGenerateImage = True
394
395 self.config.doAddSky = True
396 self.config.doAddSource = True
397
398 self.config.doAddFringe = True
399
400 self.config.doAddParallelOverscan = False
401 self.config.doAddSerialOverscan = False
402 self.config.doAddCrosstalk = False
403 self.config.doAddBias = False
404 self.config.doAddDark = False
405 self.config.doApplyGain = False
406 self.config.doAddFlat = False
407
408 self.config.biasLevel = 0.0
409 # Assume combined calibrations are made with 16 inputs.
410 self.config.readNoise *= 0.25
411
412
414 """Parent class for those that make reference calibrations.
415 """
416 def __init__(self, **kwargs):
417 super().__init__(**kwargs)
418 self.config.isTrimmed = True
419 self.config.doGenerateImage = True
420
421 self.config.calibMode = True
422
423 self.config.doAddSky = False
424 self.config.doAddSource = False
425
426 self.config.doAddFringe = False
427
428 self.config.doAddParallelOverscan = False
429 self.config.doAddSerialOverscan = False
430 self.config.doAddCrosstalk = False
431 self.config.doAddBias = False
432 self.config.doAddDark = False
433 self.config.doApplyGain = False
434 self.config.doAddFlat = False
435
436
437# Classes to generate calibration products mocks.
439 """Simulated reference dark calibration.
440 """
441 def __init__(self, **kwargs):
442 super().__init__(**kwargs)
443 self.config.doAddDark = True
444 self.config.darkTime = 1.0
445
446
448 """Simulated combined bias calibration.
449 """
450 def __init__(self, **kwargs):
451 super().__init__(**kwargs)
452 # We assume a perfect noiseless bias frame.
453 # A combined bias has mean 0
454 # so we set its bias level to 0.
455 # This is equivalent to doAddBias = False
456 # but we do the following instead to be consistent
457 # with any other bias products we might want to produce.
458 self.config.doAddBias = True
459 self.config.biasLevel = 0.0
460 self.config.doApplyGain = True
461
462
464 """Simulated reference flat calibration.
465 """
466 def __init__(self, **kwargs):
467 super().__init__(**kwargs)
468 self.config.doAddFlat = True
469
470
472 """Simulated reference fringe calibration.
473 """
474 def __init__(self, **kwargs):
475 super().__init__(**kwargs)
476 self.config.doAddFringe = True
477
478
480 """Simulated brighter-fatter kernel.
481 """
482 def __init__(self, **kwargs):
483 super().__init__(**kwargs)
484 self.config.doGenerateImage = False
485 self.config.doGenerateData = True
486
487 self.config.doBrighterFatter = True
488 self.config.doDefects = False
489 self.config.doCrosstalkCoeffs = False
490 self.config.doTransmissionCurve = False
491
492
494 """Simulated defect list.
495 """
496 def __init__(self, **kwargs):
497 super().__init__(**kwargs)
498 self.config.doGenerateImage = False
499 self.config.doGenerateData = True
500
501 self.config.doBrighterFatter = False
502 self.config.doDefects = True
503 self.config.doCrosstalkCoeffs = False
504 self.config.doTransmissionCurve = False
505
506
508 """Simulated crosstalk coefficient matrix.
509 """
510 def __init__(self, **kwargs):
511 super().__init__(**kwargs)
512 self.config.doGenerateImage = False
513 self.config.doGenerateData = True
514
515 self.config.doBrighterFatter = False
516 self.config.doDefects = False
517 self.config.doCrosstalkCoeffs = True
518 self.config.doTransmissionCurve = False
519
520
522 """Simulated transmission curve.
523 """
524 def __init__(self, **kwargs):
525 super().__init__(**kwargs)
526 self.config.doGenerateImage = False
527 self.config.doGenerateData = True
528
529 self.config.doBrighterFatter = False
530 self.config.doDefects = False
531 self.config.doCrosstalkCoeffs = False
532 self.config.doTransmissionCurve = True
An integer coordinate rectangle.
Definition Box.h:55
localCoordToExpCoord(self, ampData, x, y)
Definition isrMock.py:662
amplifierAddSource(self, ampData, scale, x0, y0)
Definition isrMock.py:730
amplifierMultiplyFlat(self, amp, ampData, fracDrop, u0=100.0, v0=100.0)
Definition isrMock.py:805
amplifierAddYGradient(self, ampData, start, end)
Definition isrMock.py:711
amplifierAddNoise(self, ampData, mean, sigma)
Definition isrMock.py:693
amplifierAddCT(self, ampDataSource, ampDataTarget, scale)
Definition isrMock.py:751
amplifierAddFringe(self, amp, ampData, scale, x0=100, y0=0)
Definition isrMock.py:773
addBiasLevel(self, ampData, biasLevel)
amplifierAddXGradient(self, ampData, start, end)
amplifierMultiplyFlat(self, amp, ampData, fracDrop, u0=100.0, v0=100.0)