Coverage for python/lsst/ip/isr/isrMockLSST.py: 17%
205 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-16 03:43 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-16 03:43 -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 # Signal parameters.
46 # Most of them are inherited from isrMockConfig but we update
47 # some to LSSTcam expected values.
48 # TODO: DM-42880 Update values to what is expected in LSSTCam.
49 biasLevel = pexConfig.Field(
50 dtype=float,
51 default=30000.0,
52 doc="Background contribution to be generated from the bias offset in ADU.",
53 )
54 # Inclusion parameters are inherited from isrMock.
55 doAddParallelOverscan = pexConfig.Field(
56 dtype=bool,
57 default=True,
58 doc="Add overscan ramp to parallel overscan and data regions.",
59 )
60 doAddSerialOverscan = pexConfig.Field(
61 dtype=bool,
62 default=True,
63 doc="Add overscan ramp to serial overscan and data regions.",
64 )
65 doApplyGain = pexConfig.Field(
66 dtype=bool,
67 default=True,
68 doc="Add gain to data.",
69 )
72class IsrMockLSST(IsrMock):
73 """Class to generate consistent mock images for ISR testing.
75 ISR testing currently relies on one-off fake images that do not
76 accurately mimic the full set of detector effects. This class
77 uses the test camera/detector/amplifier structure defined in
78 `lsst.afw.cameraGeom.testUtils` to avoid making the test data
79 dependent on any of the actual obs package formats.
80 """
81 ConfigClass = IsrMockLSSTConfig
82 _DefaultName = "isrMockLSST"
84 def __init__(self, **kwargs):
85 # cross-talk coeffs, bf kernel are defined in the parent class.
86 super().__init__(**kwargs)
88 def run(self):
89 """Generate a mock ISR product following LSSTCam ISR, and return it.
91 Returns
92 -------
93 image : `lsst.afw.image.Exposure`
94 Simulated ISR image with signals added.
95 dataProduct :
96 Simulated ISR data products.
97 None :
98 Returned if no valid configuration was found.
100 Raises
101 ------
102 RuntimeError
103 Raised if both doGenerateImage and doGenerateData are specified.
104 """
105 if self.config.doGenerateImage and self.config.doGenerateData:
106 raise RuntimeError("Only one of doGenerateImage and doGenerateData may be specified.")
107 elif self.config.doGenerateImage:
108 return self.makeImage()
109 elif self.config.doGenerateData:
110 return self.makeData()
111 else:
112 return None
114 def makeImage(self):
115 """Generate a simulated ISR LSST image.
117 Returns
118 -------
119 exposure : `lsst.afw.image.Exposure` or `dict`
120 Simulated ISR image data.
122 Notes
123 -----
124 This method constructs a "raw" data image.
125 """
126 exposure = self.getExposure()
128 # We introduce effects as they happen from a source to the signal,
129 # so the effects go from electrons to ADU.
130 # The ISR steps will then correct these effects in the reverse order.
131 for idx, amp in enumerate(exposure.getDetector()):
133 # Get image bbox and data
134 bbox = None
135 if self.config.isTrimmed:
136 bbox = amp.getBBox()
137 else:
138 bbox = amp.getRawDataBBox().shiftedBy(amp.getRawXYOffset())
140 ampData = exposure.image[bbox]
142 # Sky effects in e-
143 if self.config.doAddSky:
144 self.amplifierAddNoise(ampData, self.config.skyLevel, np.sqrt(self.config.skyLevel))
146 if self.config.doAddSource:
147 for sourceAmp, sourceFlux, sourceX, sourceY in zip(self.config.sourceAmp,
148 self.config.sourceFlux,
149 self.config.sourceX,
150 self.config.sourceY):
151 if idx == sourceAmp:
152 self.amplifierAddSource(ampData, sourceFlux, sourceX, sourceY)
154 # Other effects in e-
155 if self.config.doAddFringe:
156 self.amplifierAddFringe(amp, ampData, np.array(self.config.fringeScale),
157 x0=np.array(self.config.fringeX0),
158 y0=np.array(self.config.fringeY0))
160 if self.config.doAddFlat:
161 if ampData.getArray().sum() == 0.0:
162 self.amplifierAddNoise(ampData, 1.0, 0.0)
163 u0 = exposure.getDimensions().getX()
164 v0 = exposure.getDimensions().getY()
165 self.amplifierMultiplyFlat(amp, ampData, self.config.flatDrop, u0=u0, v0=v0)
167 # ISR effects
168 # 1. Add dark in e- (different from isrMock which does it in ADU)
169 if self.config.doAddDark:
170 self.amplifierAddNoise(ampData,
171 self.config.darkRate * self.config.darkTime,
172 np.sqrt(self.config.darkRate * self.config.darkTime))
174 # 2. Gain normalize (from e- to ADU)
175 # TODO: DM-43601 gain from PTC per amplifier
176 # TODO: DM-36639 gain with temperature dependence
177 if self.config.doApplyGain:
178 self.applyGain(ampData, self.config.gain)
180 # 3. Add read noise with or without a bias level
181 # to the image region in ADU.
182 self.amplifierAddNoise(ampData, self.config.biasLevel if self.config.doAddBias else 0.0,
183 self.config.readNoise / self.config.gain)
185 # 4. Apply cross-talk in ADU
186 if self.config.doAddCrosstalk:
187 ctCalib = CrosstalkCalib()
188 for idxS, ampS in enumerate(exposure.getDetector()):
189 for idxT, ampT in enumerate(exposure.getDetector()):
190 ampDataT = exposure.image[ampT.getBBox() if self.config.isTrimmed
191 else ampT.getRawDataBBox().shiftedBy(ampT.getRawXYOffset())]
192 outAmp = ctCalib.extractAmp(exposure.getImage(), ampS, ampT,
193 isTrimmed=self.config.isTrimmed)
194 self.amplifierAddCT(outAmp, ampDataT, self.crosstalkCoeffs[idxS][idxT])
196 # We now apply parallel and serial overscans
197 for amp in exposure.getDetector():
198 # Get image bbox and data
199 bbox = None
200 if self.config.isTrimmed:
201 bbox = amp.getBBox()
202 else:
203 bbox = amp.getRawDataBBox().shiftedBy(amp.getRawXYOffset())
204 ampData = exposure.image[bbox]
206 # Get overscan bbox and data
207 if not self.config.isTrimmed:
208 parallelOscanBBox = amp.getRawParallelOverscanBBox().shiftedBy(amp.getRawXYOffset())
209 parallelOscanData = exposure.image[parallelOscanBBox]
211 serialOscanBBox = amp.getRawSerialOverscanBBox().shiftedBy(amp.getRawXYOffset())
213 # 5. Apply parallel overscan in ADU
214 if self.config.doAddParallelOverscan:
215 if not self.config.isTrimmed:
216 # Add read noise with or without a bias level
217 # to the parallel overscan region.
218 self.amplifierAddNoise(parallelOscanData, self.config.biasLevel
219 if self.config.doAddBias else 0.0,
220 self.config.readNoise / self.config.gain)
221 # Apply gradient along the Y axis
222 # to the parallel overscan region.
223 self.amplifierAddYGradient(parallelOscanData, -1.0 * self.config.overscanScale,
224 1.0 * self.config.overscanScale)
226 # Apply gradient along the Y axis to the image region
227 self.amplifierAddYGradient(ampData, -1.0 * self.config.overscanScale,
228 1.0 * self.config.overscanScale)
230 # 6. Add Parallel overscan xtalk.
231 # TODO: DM-43286
233 if self.config.doAddSerialOverscan:
234 if not self.config.isTrimmed:
235 # We grow the image to the parallel overscan region
236 # (we do this instead of using the whole raw region
237 # in case there are prescan regions)
238 grownImageBBox = bbox.expandedTo(parallelOscanBBox)
239 # Now we grow the serial overscan region
240 # to include the corners
241 serialOscanBBox = geom.Box2I(
242 geom.Point2I(serialOscanBBox.getMinX(),
243 grownImageBBox.getMinY()),
244 geom.Extent2I(serialOscanBBox.getWidth(),
245 grownImageBBox.getHeight()),
246 )
247 serialOscanData = exposure.image[serialOscanBBox]
249 # Add read noise with or without a bias level
250 # to the serial overscan region.
251 self.amplifierAddNoise(serialOscanData, self.config.biasLevel
252 if self.config.doAddBias else 0.0,
253 self.config.readNoise / self.config.gain)
255 # 7. Apply serial overscan in ADU
256 # Apply gradient along the X axis to both overscan regions.
257 self.amplifierAddXGradient(serialOscanData, -1.0 * self.config.overscanScale,
258 1.0 * self.config.overscanScale)
259 self.amplifierAddXGradient(parallelOscanData, -1.0 * self.config.overscanScale,
260 1.0 * self.config.overscanScale)
262 # Apply gradient along the X axis to the image region.
263 self.amplifierAddXGradient(ampData, -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 applyGain(self, ampData, gain):
275 """Apply gain to the amplifier's data.
276 This method divides the data by the gain
277 because the mocks need to convert the data in electron to ADU,
278 so it does the inverse operation to applyGains in isrFunctions.
280 Parameters
281 ----------
282 ampData : `lsst.afw.image.ImageF`
283 Amplifier image to operate on.
284 gain : `float`
285 Gain value in e^-/DN.
286 """
287 ampArr = ampData.array
288 ampArr[:] = ampArr[:] / gain
290 def amplifierAddXGradient(self, ampData, start, end):
291 """Add a x-axis linear gradient to an amplifier's image data.
293 This method operates in the amplifier coordinate frame.
295 Parameters
296 ----------
297 ampData : `lsst.afw.image.ImageF`
298 Amplifier image to operate on.
299 start : `float`
300 Start value of the gradient (at y=0).
301 end : `float`
302 End value of the gradient (at y=ymax).
303 """
304 nPixX = ampData.getDimensions().getX()
305 ampArr = ampData.array
306 ampArr[:] = ampArr[:] + (np.interp(range(nPixX), (0, nPixX - 1), (start, end)).reshape(1, nPixX)
307 + np.zeros(ampData.getDimensions()).transpose())
310class RawMockLSST(IsrMockLSST):
311 """Generate a raw exposure suitable for ISR.
312 """
313 def __init__(self, **kwargs):
314 super().__init__(**kwargs)
315 self.config.isTrimmed = False
316 self.config.doGenerateImage = True
317 self.config.doGenerateAmpDict = False
319 # Add astro effects
320 self.config.doAddSky = True
321 self.config.doAddSource = True
323 # Add optical effects
324 self.config.doAddFringe = True
326 # Add instru effects
327 self.config.doAddParallelOverscan = True
328 self.config.doAddSerialOverscan = True
329 self.config.doAddCrosstalk = False
330 self.config.doAddBias = True
331 self.config.doAddDark = True
333 self.config.doAddFlat = True
336class TrimmedRawMockLSST(RawMockLSST):
337 """Generate a trimmed raw exposure.
338 """
339 def __init__(self, **kwargs):
340 super().__init__(**kwargs)
341 self.config.isTrimmed = True
342 self.config.doAddParallelOverscan = False
343 self.config.doAddSerialOverscan = False
346class CalibratedRawMockLSST(RawMockLSST):
347 """Generate a trimmed raw exposure.
348 """
349 def __init__(self, **kwargs):
350 super().__init__(**kwargs)
351 self.config.isTrimmed = True
352 self.config.doGenerateImage = True
354 self.config.doAddSky = True
355 self.config.doAddSource = True
357 self.config.doAddFringe = True
359 self.config.doAddParallelOverscan = False
360 self.config.doAddSerialOverscan = False
361 self.config.doAddCrosstalk = False
362 self.config.doAddBias = False
363 self.config.doAddDark = False
364 self.config.doApplyGain = False
365 self.config.doAddFlat = False
367 self.config.biasLevel = 0.0
368 # Assume combined calibrations are made with 16 inputs.
369 self.config.readNoise *= 0.25
372class ReferenceMockLSST(IsrMockLSST):
373 """Parent class for those that make reference calibrations.
374 """
375 def __init__(self, **kwargs):
376 super().__init__(**kwargs)
377 self.config.isTrimmed = True
378 self.config.doGenerateImage = True
380 self.config.doAddSky = False
381 self.config.doAddSource = False
383 self.config.doAddFringe = False
385 self.config.doAddParallelOverscan = False
386 self.config.doAddSerialOverscan = False
387 self.config.doAddCrosstalk = False
388 self.config.doAddBias = False
389 self.config.doAddDark = False
390 self.config.doApplyGain = False
391 self.config.doAddFlat = False
394# Classes to generate calibration products mocks.
395class DarkMockLSST(ReferenceMockLSST):
396 """Simulated reference dark calibration.
397 """
398 def __init__(self, **kwargs):
399 super().__init__(**kwargs)
400 self.config.doAddDark = True
401 self.config.darkTime = 1.0
404class BiasMockLSST(ReferenceMockLSST):
405 """Simulated combined bias calibration.
406 """
407 def __init__(self, **kwargs):
408 super().__init__(**kwargs)
409 # A combined bias has mean 0
410 # so we set its bias level to 0.
411 # This is equivalent to doAddBias = False
412 # but we do the following instead to be consistent
413 # with any other bias products we might want to produce.
414 self.config.doAddBias = True
415 self.config.biasLevel - 0.0
416 self.config.doApplyGain = True
417 # Assume combined calibrations are made with 16 inputs.
418 self.config.readNoise = 10.0*0.25
421class FlatMockLSST(ReferenceMockLSST):
422 """Simulated reference flat calibration.
423 """
424 def __init__(self, **kwargs):
425 super().__init__(**kwargs)
426 self.config.doAddFlat = True
429class FringeMockLSST(ReferenceMockLSST):
430 """Simulated reference fringe calibration.
431 """
432 def __init__(self, **kwargs):
433 super().__init__(**kwargs)
434 self.config.doAddFringe = True
437class BfKernelMockLSST(IsrMockLSST):
438 """Simulated brighter-fatter kernel.
439 """
440 def __init__(self, **kwargs):
441 super().__init__(**kwargs)
442 self.config.doGenerateImage = False
443 self.config.doGenerateData = True
445 # calibration products configs
446 self.config.doBrighterFatter = True
447 self.config.doDefects = False
448 self.config.doCrosstalkCoeffs = False
449 self.config.doTransmissionCurve = False
452class DefectMockLSST(IsrMockLSST):
453 """Simulated defect list.
454 """
455 def __init__(self, **kwargs):
456 super().__init__(**kwargs)
457 self.config.doGenerateImage = False
458 self.config.doGenerateData = True
460 self.config.doBrighterFatter = False
461 self.config.doDefects = True
462 self.config.doCrosstalkCoeffs = False
463 self.config.doTransmissionCurve = False
466class CrosstalkCoeffMockLSST(IsrMockLSST):
467 """Simulated crosstalk coefficient matrix.
468 """
469 def __init__(self, **kwargs):
470 super().__init__(**kwargs)
471 self.config.doGenerateImage = False
472 self.config.doGenerateData = True
474 self.config.doBrighterFatter = False
475 self.config.doDefects = False
476 self.config.doCrosstalkCoeffs = True
477 self.config.doTransmissionCurve = False
480class TransmissionMockLSST(IsrMockLSST):
481 """Simulated transmission curve.
482 """
483 def __init__(self, **kwargs):
484 super().__init__(**kwargs)
485 self.config.doGenerateImage = False
486 self.config.doGenerateData = True
488 self.config.doBrighterFatter = False
489 self.config.doDefects = False
490 self.config.doCrosstalkCoeffs = False
491 self.config.doTransmissionCurve = True