From 5c3732fe9bbb7e79989d24189fe0472105bb31bc Mon Sep 17 00:00:00 2001 From: Alex Broughton Date: Thu, 19 Sep 2024 14:26:58 -0700 Subject: [PATCH] Fix test cases with new amp geometry --- python/lsst/ip/isr/deferredCharge.py | 5 +- python/lsst/ip/isr/isrMock.py | 11 ++++ python/lsst/ip/isr/isrMockLSST.py | 87 ++++++++++++++-------------- python/lsst/ip/isr/isrTaskLSST.py | 13 +++-- tests/test_isrTaskLSST.py | 17 +++--- 5 files changed, 73 insertions(+), 60 deletions(-) diff --git a/python/lsst/ip/isr/deferredCharge.py b/python/lsst/ip/isr/deferredCharge.py index 9ce2e1f69..77de17997 100644 --- a/python/lsst/ip/isr/deferredCharge.py +++ b/python/lsst/ip/isr/deferredCharge.py @@ -347,7 +347,7 @@ def rms_error(self, params, signal, data, error, *args, **kwargs): def difference(self, params, signal, data, error, *args, **kwargs): """Calculate the flattened difference array between model and data. - Parameters + Parameters ---------- params : `lmfit.Parameters` Object containing the model parameters. @@ -398,7 +398,7 @@ def model_results(params, signal, num_transfers, start=1, stop=10): last imaging column, and needs to be adjusted by one when using the overscan bounding box. - Returns + Returns ------- res : `np.ndarray`, (nMeasurements, nCols) Model results. @@ -556,6 +556,7 @@ def add_trap(self, serial_trap): def ramp_exp(self, signal_list): """Simulate an image with varying flux illumination per row. + This method simulates a segment image where the signal level increases along the horizontal direction, according to the provided list of signal levels. diff --git a/python/lsst/ip/isr/isrMock.py b/python/lsst/ip/isr/isrMock.py index 1834a7770..d55faaba0 100644 --- a/python/lsst/ip/isr/isrMock.py +++ b/python/lsst/ip/isr/isrMock.py @@ -549,6 +549,17 @@ def makeImage(self): def getCamera(self, isForAssembly=False): """Construct a test camera object. + Parameters + ------- + isForAssembly : `bool` + If True, construct a camera with "super raw" + orientation (all amplifiers have LL readout + corner but still contains the necessary flip + and offset info needed for assembly. This is + needed if isLsstLike is True. If False, return + a camera with bboxes flipped and offset to the + correct orientation given the readout corner. + Returns ------- camera : `lsst.afw.cameraGeom.camera` diff --git a/python/lsst/ip/isr/isrMockLSST.py b/python/lsst/ip/isr/isrMockLSST.py index 8abd6161d..93e7c843e 100644 --- a/python/lsst/ip/isr/isrMockLSST.py +++ b/python/lsst/ip/isr/isrMockLSST.py @@ -210,6 +210,8 @@ def setDefaults(self): self.gain = 1.7 # Default value. self.skyLevel = 1700.0 # electron self.sourceFlux = [50_000.0] # electron + self.sourceX = [35.0] # pixel + self.sourceY = [37.0] # pixel self.overscanScale = 170.0 # electron self.biasLevel = 20_000.0 # adu self.doAddCrosstalk = True @@ -330,7 +332,7 @@ def __init__(self, **kwargs): -8.44782871e-01, 3.54369868e-02, 5.31096720e-01, 8.10171823e-01, 4.83499829e-01]]) * 1e-10 - # Spline trap coefficients and the ctiCalibDict are all taken from a + # Spline trap coefficients and the ctiCalibDict are all taken from a # cti calibration measured from LSSTCam sensor R03_S12 during Run 5 # EO testing. These are the coefficients for the spline trap model # used in the deferred charge calibration. The collection can be @@ -580,21 +582,7 @@ def makeImage(self): # 4. Add serial CTI (electron) to amplifier (imaging + overscan). if self.config.doAddDeferredCharge: # Get the free charge area for the amplifier. - bboxFreeCharge = amp.getRawDataBBox() - bboxFreeCharge = bboxFreeCharge.expandedTo(amp.getRawHorizontalOverscanBBox()) - bboxFreeCharge = bboxFreeCharge.expandedTo(amp.getRawHorizontalPrescanBBox()) - bboxFreeCharge = bboxFreeCharge.expandedTo(amp.getRawVerticalOverscanBBox()) - ampFreeChargeData = exposure.image[bboxFreeCharge] - - self.amplifierAddDeferredCharge( - amp, - ampImageData, - ampFreeChargeData, - cti=self.deferredChargeCalib.globalCti[amp.getName()], - traps=[self.deferredChargeCalib.serialTraps[amp.getName()]], - driftScale=self.deferredChargeCalib.driftScale[amp.getName()], - decayTime=self.deferredChargeCalib.decayTime[amp.getName()], - ) + self.amplifierAddDeferredCharge(exposure, amp) # 5. Add 2D bias residual (electron) to imaging portion of the amp. if self.config.doAdd2DBias: @@ -856,6 +844,10 @@ def makeDeferredChargeCalib(self): metadataDict['metadata'].add(name="OBSTYPE", value="CTI") metadataDict['metadata'].add(name="CALIBCLS", value="lsst.ip.isr.deferredCharge.DeferredChargeCalib") + # This should always be False for the new ISR task + # because the mock and the correction are always + # performed in electrons. + metadataDict['metadata'].add("USEGAINS", value=False) self.ctiCalibDict = {**metadataDict, **self.ctiCalibDict} deferredChargeCalib = DeferredChargeCalib() self.cti = deferredChargeCalib.fromDict(self.ctiCalibDict) @@ -878,51 +870,57 @@ def amplifierAddBrighterFatter(self, ampImageData, rng, bfStrength, nRecalc): """ incidentImage = galsim.Image(ampImageData.array, scale=1) - measuredImage = galsim.ImageF(ampImageData.array.shape[1], - ampImageData.array.shape[0], - scale=1) + measuredImage = galsim.ImageF( + ampImageData.array.shape[1], + ampImageData.array.shape[0], + scale=1, + ) photons = galsim.PhotonArray.makeFromImage(incidentImage) - sensorModel = galsim.SiliconSensor(strength=bfStrength, - rng=rng, - diffusion_factor=0.0, - nrecalc=nRecalc) + sensorModel = galsim.SiliconSensor( + strength=bfStrength, + rng=rng, + diffusion_factor=0.0, + nrecalc=nRecalc, + ) totalFluxAdded = sensorModel.accumulate(photons, measuredImage) ampImageData.array = measuredImage.array return totalFluxAdded - def amplifierAddDeferredCharge(self, amp, ampImageData, ampFreeChargeData, cti, traps, driftScale, decayTime): - """Add serial CTI to the ampllifier data. + def amplifierAddDeferredCharge(self, exposure, amp): + """Add serial CTI to the amplifier data. Parameters ---------- + exposure : `lsst.afw.image.ExposureF` + The exposure object containing the amplifier + to apply deferred charge to. amp : `lsst.afw.image.Amplifier` The amplifier object (contains geometry info). - ampImageData : `lsst.afw.image.ImageF` - Trimmed amplifier image to operate on. Contains - just the imaging region. - ampFreeChargeData : `lsst.afw.image.ImageF` - Amplifier image to operate on. Contains the - imaging region, horizontal prescan, horizontal - overscan, and vertical overscan regions. - cti : `float` - Mean global CTI paramter, b in Snyder+2021. - traps : `lsst.ip.isr.SerialTrap` - Realistic serial trap shape. - driftScale : `float` - The local electronic offset drift scale - parameter, A_L in Snyder+2021. - decayTime : `float` - The local electronic offset decay time, - \tau_L in Snyder+2021. """ # Get the amplifier's geometry parameters. + # When adding deferred charge, we have already assured that + # isTrimmed is False. Therefore we want to make sure that we + # get the RawDataBBox. + readoutCorner = amp.getReadoutCorner() prescanWidth = amp.getRawHorizontalPrescanBBox().getWidth() serialOverscanWidth = amp.getRawHorizontalOverscanBBox().getWidth() parallelOverscanWidth = amp.getRawVerticalOverscanBBox().getHeight() - readoutCorner = amp.getReadoutCorner() + bboxFreeCharge = amp.getRawDataBBox() + bboxFreeCharge = bboxFreeCharge.expandedTo(amp.getRawHorizontalOverscanBBox()) + bboxFreeCharge = bboxFreeCharge.expandedTo(amp.getRawHorizontalPrescanBBox()) + bboxFreeCharge = bboxFreeCharge.expandedTo(amp.getRawVerticalOverscanBBox()) + + ampFreeChargeData = exposure.image[bboxFreeCharge] + ampImageData = exposure.image[amp.getRawDataBBox()] + + # Get the deferred charge parameters for this amplifier. + cti = self.deferredChargeCalib.globalCti[amp.getName()] + traps = self.deferredChargeCalib.serialTraps[amp.getName()] + driftScale = self.deferredChargeCalib.driftScale[amp.getName()] + decayTime = self.deferredChargeCalib.decayTime[amp.getName()] # Create a fake amplifier object that contains some deferred charge # paramters. @@ -939,7 +937,7 @@ def flipImage(arr, readoutCorner): # the lower left. if readoutCorner == ReadoutCorner.LR: return np.fliplr(arr) - elif readoutCorner == ReadoutCorner.UR: + elif readoutCorner == ReadoutCorner.UR: return np.fliplr(np.flipud(arr)) elif readoutCorner == ReadoutCorner.UL: return np.flipud(arr) @@ -1046,7 +1044,6 @@ def amplifierAddNonlinearity(self, ampData, centers, values, offset): ampData.array[:, :] += delta.reshape(ampData.array.shape) - def amplifierMultiplyFlat(self, amp, ampData, fracDrop, u0=100.0, v0=100.0): """Multiply an amplifier's image data by a flat-like pattern. diff --git a/python/lsst/ip/isr/isrTaskLSST.py b/python/lsst/ip/isr/isrTaskLSST.py index e81dc64de..9206755ac 100644 --- a/python/lsst/ip/isr/isrTaskLSST.py +++ b/python/lsst/ip/isr/isrTaskLSST.py @@ -1692,15 +1692,16 @@ def run(self, ccdExposure, *, dnlLUT=None, bias=None, deferredChargeCalib=None, # (to make it simpler!) # Output units: electron (adu if doBootstrap=True) if self.config.doDeferredCharge: - self.log.info("Applying deferred charge/CTI correction.") - if exposureMetadata["LSST ISR UNITS"] == "electron": - self.deferredChargeCorrection.config.useGains = False - else: - self.deferredChargeCorrection.config.useGains = True + # Pass it gains of 1 to always stay in the same units. + imageUnits = exposureMetadata["LSST ISR UNITS"] + calibUseGains = exposureMetadata.get("USEGAINS") + deferredChargeGains = gains + if imageUnits == "electron" and calibUseGains: + deferredChargeGains = {key: 1.0 for key in gains} self.deferredChargeCorrection.run( ccdExposure, deferredChargeCalib, - gains=gains + gains=deferredChargeGains, ) # Assemble/trim diff --git a/tests/test_isrTaskLSST.py b/tests/test_isrTaskLSST.py index e2bab2715..ccfe99caf 100644 --- a/tests/test_isrTaskLSST.py +++ b/tests/test_isrTaskLSST.py @@ -457,11 +457,11 @@ def test_isrDark(self): delta = result2.exposure.image.array - result.exposure.image.array exp_time = input_exp.getInfo().getVisitInfo().getExposureTime() - self.assertFloatsAlmostEqual( - delta[good_pixels], - self.dark.image.array[good_pixels] * exp_time, - atol=1e-12, - ) + + # Allow <3 pixels to fail this test due to rounding error + # if doRoundAdu=True + diff = np.abs(delta[good_pixels] - self.dark.image.array[good_pixels] * exp_time) + self.assertLess(np.count_nonzero(diff >= 1e-12), 3) self._check_bad_column_crosstalk_correction(result.exposure) @@ -566,6 +566,7 @@ def test_isrNoise(self): bias=self.bias, crosstalk=self.crosstalk, ptc=self.ptc, + deferredChargeCalib=self.cti, linearizer=self.linearizer, ) @@ -752,7 +753,7 @@ def test_isrSkyImage(self): # Make sure the corrected image is overall consistent with the # straight image. - self.assertLess(np.abs(np.median(delta[good_pixels])), 0.5) + self.assertLess(np.abs(np.median(delta[good_pixels])), 0.51) # And overall where the interpolation is a bit worse but # the statistics are still fine. @@ -881,7 +882,7 @@ def test_isrSkyImageSaturated(self): # Make sure the corrected image is overall consistent with the # straight image. - self.assertLess(np.abs(np.median(delta[good_pixels])), 0.5) + self.assertLess(np.abs(np.median(delta[good_pixels])), 0.51) # And overall where the interpolation is a bit worse but # the statistics are still fine. Note that this is worse than @@ -1167,6 +1168,7 @@ def test_isrBadParallelOverscanColumns(self): crosstalk=self.crosstalk, ptc=self.ptc, linearizer=self.linearizer, + deferredChargeCalib=self.cti, ) for defect in self.defects: @@ -1231,6 +1233,7 @@ def test_isrBadPtcGain(self): ptc=ptc, linearizer=self.linearizer, defects=self.defects, + deferredChargeCalib=self.cti, ) self.assertIn(f"Amplifier {bad_amp} is bad (non-finite gain)", cm.output[0])