Coverage for python/lsst/ip/isr/isrMockLSST.py: 17%
220 statements
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-16 03:55 -0700
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-16 03:55 -0700
1# This file is part of ip_isr.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22__all__ = ["IsrMockLSSTConfig", "IsrMockLSST", "RawMockLSST",
23 "CalibratedRawMockLSST", "ReferenceMockLSST",
24 "BiasMockLSST", "DarkMockLSST", "FlatMockLSST", "FringeMockLSST",
25 "BfKernelMockLSST", "DefectMockLSST", "CrosstalkCoeffMockLSST",
26 "TransmissionMockLSST"]
27import numpy as np
29import lsst.geom as geom
30import lsst.pex.config as pexConfig
31from .crosstalk import CrosstalkCalib
32from .isrMock import IsrMockConfig, IsrMock
35class IsrMockLSSTConfig(IsrMockConfig):
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 )
73class IsrMockLSST(IsrMock):
74 """Class to generate consistent mock images for ISR testing.
75 """
76 ConfigClass = IsrMockLSSTConfig
77 _DefaultName = "isrMockLSST"
79 def __init__(self, **kwargs):
80 # cross-talk coeffs, bf kernel are defined in the parent class.
81 super().__init__(**kwargs)
83 def run(self):
84 """Generate a mock ISR product following LSSTCam ISR, and return it.
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.
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.makeImage()
104 elif self.config.doGenerateData:
105 return self.makeData()
106 else:
107 return None
109 def makeImage(self):
110 """Generate a simulated ISR LSST image.
112 Returns
113 -------
114 exposure : `lsst.afw.image.Exposure` or `dict`
115 Simulated ISR image data.
117 Notes
118 -----
119 This method constructs a "raw" data image.
120 """
121 exposure = self.getExposure()
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()):
128 # Get image bbox and data
129 bbox = None
130 if self.config.isTrimmed:
131 bbox = amp.getBBox()
132 else:
133 bbox = amp.getRawDataBBox()
135 ampData = exposure.image[bbox]
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))
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)
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))
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.amplifierMultiplyFlat(amp, ampData, self.config.flatDrop, u0=u0, v0=v0)
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))
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)
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)
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])
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]
216 if self.config.doAddParallelOverscan or self.config.doAddSerialOverscan or self.config.doAddBias:
218 allData = ampData
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]
227 grownImageBBox = bbox.expandedTo(parallelOscanBBox)
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]
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)
243 self.amplifierAddNoise(serialOscanData, 0.0,
244 self.config.readNoise / self.config.gain)
246 grownImageBBoxAll = grownImageBBox.expandedTo(serialOscanBBox)
247 allData = exposure.image[grownImageBBoxAll]
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)
254 # 6. Add Parallel overscan xtalk.
255 # TODO: DM-43286
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)
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)
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
274 def addBiasLevel(self, ampData, biasLevel):
275 """Add bias level to an amplifier's image data.
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
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.
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")
307 sigma = u0 / np.sqrt(2.0 * fracDrop)
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)
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.
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
331 def amplifierAddXGradient(self, ampData, start, end):
332 """Add a x-axis linear gradient to an amplifier's image data.
334 This method operates in the amplifier coordinate frame.
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())
351class RawMockLSST(IsrMockLSST):
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
360 # Add astro effects
361 self.config.doAddSky = True
362 self.config.doAddSource = True
364 # Add optical effects
365 self.config.doAddFringe = True
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
374 self.config.doAddFlat = True
377class TrimmedRawMockLSST(RawMockLSST):
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
387class CalibratedRawMockLSST(RawMockLSST):
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
395 self.config.doAddSky = True
396 self.config.doAddSource = True
398 self.config.doAddFringe = True
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
408 self.config.biasLevel = 0.0
409 # Assume combined calibrations are made with 16 inputs.
410 self.config.readNoise *= 0.25
413class ReferenceMockLSST(IsrMockLSST):
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
421 self.config.calibMode = True
423 self.config.doAddSky = False
424 self.config.doAddSource = False
426 self.config.doAddFringe = False
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
437# Classes to generate calibration products mocks.
438class DarkMockLSST(ReferenceMockLSST):
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
447class BiasMockLSST(ReferenceMockLSST):
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
463class FlatMockLSST(ReferenceMockLSST):
464 """Simulated reference flat calibration.
465 """
466 def __init__(self, **kwargs):
467 super().__init__(**kwargs)
468 self.config.doAddFlat = True
471class FringeMockLSST(ReferenceMockLSST):
472 """Simulated reference fringe calibration.
473 """
474 def __init__(self, **kwargs):
475 super().__init__(**kwargs)
476 self.config.doAddFringe = True
479class BfKernelMockLSST(IsrMockLSST):
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
487 self.config.doBrighterFatter = True
488 self.config.doDefects = False
489 self.config.doCrosstalkCoeffs = False
490 self.config.doTransmissionCurve = False
493class DefectMockLSST(IsrMockLSST):
494 """Simulated defect list.
495 """
496 def __init__(self, **kwargs):
497 super().__init__(**kwargs)
498 self.config.doGenerateImage = False
499 self.config.doGenerateData = True
501 self.config.doBrighterFatter = False
502 self.config.doDefects = True
503 self.config.doCrosstalkCoeffs = False
504 self.config.doTransmissionCurve = False
507class CrosstalkCoeffMockLSST(IsrMockLSST):
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
515 self.config.doBrighterFatter = False
516 self.config.doDefects = False
517 self.config.doCrosstalkCoeffs = True
518 self.config.doTransmissionCurve = False
521class TransmissionMockLSST(IsrMockLSST):
522 """Simulated transmission curve.
523 """
524 def __init__(self, **kwargs):
525 super().__init__(**kwargs)
526 self.config.doGenerateImage = False
527 self.config.doGenerateData = True
529 self.config.doBrighterFatter = False
530 self.config.doDefects = False
531 self.config.doCrosstalkCoeffs = False
532 self.config.doTransmissionCurve = True