From 3374d8888ddea5461d696eb5f5dad6a939bf8626 Mon Sep 17 00:00:00 2001 From: Merlin Fisher-Levine Date: Thu, 6 Mar 2025 23:14:16 -0800 Subject: [PATCH 01/13] Update to use new IsrTaskLSST --- python/lsst/summit/utils/bestEffort.py | 6 ++-- python/lsst/summit/utils/quickLook.py | 31 +++++++------------ .../utils/resources/config/quickLookIsr.py | 8 +++-- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/python/lsst/summit/utils/bestEffort.py b/python/lsst/summit/utils/bestEffort.py index cfcf56f5..ad7d3b45 100644 --- a/python/lsst/summit/utils/bestEffort.py +++ b/python/lsst/summit/utils/bestEffort.py @@ -26,7 +26,7 @@ import lsst.afw.image as afwImage import lsst.daf.butler as dafButler from lsst.daf.butler.registry import ConflictingDefinitionError -from lsst.ip.isr import IsrTask +from lsst.ip.isr import IsrTaskLSST from lsst.pex.config import Config from lsst.summit.utils.butlerUtils import getLatissDefaultCollections from lsst.summit.utils.quickLook import QuickLookIsrTask @@ -58,7 +58,7 @@ class BestEffortIsr: Extra collections to add to the butler init. Collections are prepended. defaultExtraIsrOptions : `dict`, optional A dict of extra isr config options to apply. Each key should be an - attribute of an isrTaskConfigClass. + attribute of an isrTaskLSSTConfigClass. doRepairCosmics : `bool`, optional Repair cosmic ray hits? doWrite : `bool`, optional @@ -238,7 +238,7 @@ def getExposure( raise RuntimeError(f"Failed to retrieve raw for exp {dataId}") from None # default options that are probably good for most engineering time - isrConfig = IsrTask.ConfigClass() + isrConfig = IsrTaskLSST.ConfigClass() with importlib.resources.path("lsst.summit.utils", "resources/config/quickLookIsr.py") as cfgPath: isrConfig.load(cfgPath) diff --git a/python/lsst/summit/utils/quickLook.py b/python/lsst/summit/utils/quickLook.py index c309961e..c46c9c6c 100644 --- a/python/lsst/summit/utils/quickLook.py +++ b/python/lsst/summit/utils/quickLook.py @@ -31,15 +31,15 @@ import lsst.pex.config as pexConfig import lsst.pipe.base as pipeBase import lsst.pipe.base.connectionTypes as cT -from lsst.ip.isr import IsrTask -from lsst.ip.isr.isrTask import IsrTaskConnections +from lsst.ip.isr import IsrTaskLSST +from lsst.ip.isr.isrTaskLSST import IsrTaskLSSTConnections from lsst.meas.algorithms.installGaussianPsf import InstallGaussianPsfTask from lsst.pipe.tasks.characterizeImage import CharacterizeImageTask __all__ = ["QuickLookIsrTask", "QuickLookIsrTaskConfig"] -class QuickLookIsrTaskConnections(IsrTaskConnections): +class QuickLookIsrTaskConnections(IsrTaskLSSTConnections): """Copy isrTask's connections, changing prereq min values to zero. Copy all the connections directly for IsrTask, keeping ccdExposure as @@ -50,7 +50,9 @@ class QuickLookIsrTaskConnections(IsrTaskConnections): def __init__(self, *, config: Any = None): # programatically clone all of the connections from isrTask # setting minimum values to zero for everything except the ccdExposure - super().__init__(config=IsrTask.ConfigClass()) # need a dummy config, isn't used other than for ctor + super().__init__( + config=IsrTaskLSST.ConfigClass() + ) # need a dummy config, isn't used other than for ctor for name, connection in self.allConnections.items(): if hasattr(connection, "minimum"): setattr( @@ -95,13 +97,13 @@ class QuickLookIsrTask(pipeBase.PipelineTask): config: QuickLookIsrTaskConfig _DefaultName = "quickLook" - def __init__(self, isrTask: IsrTask = IsrTask, **kwargs: Any): + def __init__(self, isrTask: IsrTaskLSST = IsrTaskLSST, **kwargs: Any): super().__init__(**kwargs) # Pass in IsrTask so that we can modify it slightly for unit tests. # Note that this is not an instance of the IsrTask class, but the class # itself, which is then instantiated later on, in the run() method, # with the dynamically generated config. - self.isrTask = IsrTask + self.isrTask = IsrTaskLSST def run( self, @@ -119,7 +121,7 @@ def run( newBFKernel: ipIsr.BrighterFatterKernel | None = None, ptc: ipIsr.PhotonTransferCurveDataset | None = None, crosstalkSources: list | None = None, - isrBaseConfig: ipIsr.IsrTaskConfig | None = None, + isrBaseConfig: ipIsr.IsrTaskLSSTConfig | None = None, filterTransmission: afwImage.TransmissionCurve | None = None, opticsTransmission: afwImage.TransmissionCurve | None = None, strayLightData: Any | None = None, @@ -169,7 +171,7 @@ def run( and read noise. crosstalkSources : `list`, optional List of possible crosstalk sources. - isrBaseConfig : `lsst.ip.isr.IsrTaskConfig`, optional + isrBaseConfig : `lsst.ip.isr.IsrTaskLSSTConfig`, optional An isrTask config to act as the base configuration. Options which involve applying a calibration product are ignored, but this allows for the configuration of e.g. the number of overscan columns. @@ -204,7 +206,7 @@ def run( The ISRed and cosmic-ray-repaired exposure. """ if not isrBaseConfig: - isrConfig = IsrTask.ConfigClass() + isrConfig = IsrTaskLSST.ConfigClass() with importlib.resources.path("lsst.summit.utils", "resources/config/quickLookIsr.py") as cfgPath: isrConfig.load(cfgPath) else: @@ -213,12 +215,10 @@ def run( isrConfig.doBias = False isrConfig.doDark = False isrConfig.doFlat = False - isrConfig.doFringe = False isrConfig.doDefect = False isrConfig.doLinearize = False isrConfig.doCrosstalk = False isrConfig.doBrighterFatter = False - isrConfig.usePtcGains = False if bias: isrConfig.doBias = True @@ -232,10 +232,6 @@ def run( isrConfig.doFlat = True self.log.info("Running with flat correction") - if fringes: - isrConfig.doFringe = True - self.log.info("Running with fringe correction") - if defects: isrConfig.doDefect = True self.log.info("Running with defect correction") @@ -259,11 +255,6 @@ def run( isrConfig.doBrighterFatter = True self.log.info("Running with brighter-fatter correction") - if ptc: - isrConfig.usePtcGains = True - self.log.info("Running with ptc correction") - - isrConfig.doWrite = False isrTask = self.isrTask(config=isrConfig) if fringes: diff --git a/python/lsst/summit/utils/resources/config/quickLookIsr.py b/python/lsst/summit/utils/resources/config/quickLookIsr.py index f08a75d1..2d8ea268 100644 --- a/python/lsst/summit/utils/resources/config/quickLookIsr.py +++ b/python/lsst/summit/utils/resources/config/quickLookIsr.py @@ -2,7 +2,9 @@ config.doWrite = False # this task writes separately, no need for this config.doSaturation = True # saturation very important for roundness measurement in qfm -config.doSaturationInterpolation = True -config.overscan.fitType = "MEDIAN_PER_ROW" -config.overscan.doParallelOverscan = True config.brighterFatterMaxIter = 2 # Uncomment this to remove test warning +config.doDeferredCharge = False # no calib for this yet +config.doBootstrap = True +config.doApplyGains = False +config.doSuspect = False +config.defaultSaturationSource = "CAMERAMODEL" From 3e58be156233ef98eda82a53af83f982216948a5 Mon Sep 17 00:00:00 2001 From: Merlin Fisher-Levine Date: Thu, 13 Nov 2025 14:15:41 -0800 Subject: [PATCH 02/13] Remove doBootstrap config option --- python/lsst/summit/utils/resources/config/quickLookIsr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lsst/summit/utils/resources/config/quickLookIsr.py b/python/lsst/summit/utils/resources/config/quickLookIsr.py index 2d8ea268..1a464c5c 100644 --- a/python/lsst/summit/utils/resources/config/quickLookIsr.py +++ b/python/lsst/summit/utils/resources/config/quickLookIsr.py @@ -4,7 +4,7 @@ config.doSaturation = True # saturation very important for roundness measurement in qfm config.brighterFatterMaxIter = 2 # Uncomment this to remove test warning config.doDeferredCharge = False # no calib for this yet -config.doBootstrap = True +config.doBootstrap = False config.doApplyGains = False config.doSuspect = False config.defaultSaturationSource = "CAMERAMODEL" From 41ac8ca3cba8547a5ef7f5d98cf621e3d6586216 Mon Sep 17 00:00:00 2001 From: Merlin Fisher-Levine Date: Fri, 14 Nov 2025 15:41:16 -0800 Subject: [PATCH 03/13] Turn gain correction on --- python/lsst/summit/utils/resources/config/quickLookIsr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lsst/summit/utils/resources/config/quickLookIsr.py b/python/lsst/summit/utils/resources/config/quickLookIsr.py index 1a464c5c..60d63c96 100644 --- a/python/lsst/summit/utils/resources/config/quickLookIsr.py +++ b/python/lsst/summit/utils/resources/config/quickLookIsr.py @@ -5,6 +5,6 @@ config.brighterFatterMaxIter = 2 # Uncomment this to remove test warning config.doDeferredCharge = False # no calib for this yet config.doBootstrap = False -config.doApplyGains = False +config.doApplyGains = True config.doSuspect = False config.defaultSaturationSource = "CAMERAMODEL" From 33a4d9030eb6aa0580b0c6db560569eee96f9950 Mon Sep 17 00:00:00 2001 From: Christopher Waters Date: Thu, 20 Nov 2025 15:50:28 -0800 Subject: [PATCH 04/13] CZW/RB: Working through IsrTaskLSST issues. --- pipelines/quickLook.yaml | 2 + python/lsst/summit/utils/quickLook.py | 93 +++++++------------- tests/test_bestEffortIsr.py | 2 +- tests/test_quickLook.py | 119 +++++++++----------------- 4 files changed, 74 insertions(+), 142 deletions(-) diff --git a/pipelines/quickLook.yaml b/pipelines/quickLook.yaml index a48c0cce..ba7be659 100644 --- a/pipelines/quickLook.yaml +++ b/pipelines/quickLook.yaml @@ -4,3 +4,5 @@ tasks: class: lsst.summit.utils.quickLook.QuickLookIsrTask config: doRepairCosmics: true + doCorrectGains: false + doDeferredCharge: false diff --git a/python/lsst/summit/utils/quickLook.py b/python/lsst/summit/utils/quickLook.py index c46c9c6c..9f3ad19f 100644 --- a/python/lsst/summit/utils/quickLook.py +++ b/python/lsst/summit/utils/quickLook.py @@ -23,7 +23,6 @@ import importlib.resources from typing import Any -import numpy as np import lsst.afw.cameraGeom as camGeom import lsst.afw.image as afwImage @@ -103,6 +102,9 @@ def __init__(self, isrTask: IsrTaskLSST = IsrTaskLSST, **kwargs: Any): # Note that this is not an instance of the IsrTask class, but the class # itself, which is then instantiated later on, in the run() method, # with the dynamically generated config. + # import pdb; pdb.set_trace() + if IsrTaskLSST._DefaultName != 'isrLSST': + raise RuntimeError("QuickLookIsrTask should now always use IsrTaskLSST for processing.") self.isrTask = IsrTaskLSST def run( @@ -113,22 +115,14 @@ def run( bias: afwImage.Exposure | None = None, dark: afwImage.Exposure | None = None, flat: afwImage.Exposure | None = None, - fringes: afwImage.Exposure | None = None, defects: ipIsr.Defects | None = None, linearizer: ipIsr.linearize.LinearizeBase | None = None, crosstalk: ipIsr.crosstalk.CrosstalkCalib | None = None, - bfKernel: np.ndarray | None = None, - newBFKernel: ipIsr.BrighterFatterKernel | None = None, + bfKernel: ipIsr.BrighterFatterKernel | None = None, ptc: ipIsr.PhotonTransferCurveDataset | None = None, - crosstalkSources: list | None = None, isrBaseConfig: ipIsr.IsrTaskLSSTConfig | None = None, - filterTransmission: afwImage.TransmissionCurve | None = None, - opticsTransmission: afwImage.TransmissionCurve | None = None, - strayLightData: Any | None = None, - sensorTransmission: afwImage.TransmissionCurve | None = None, - atmosphereTransmission: afwImage.TransmissionCurve | None = None, deferredChargeCalib: Any | None = None, - illumMaskedImage: afwImage.MaskedImage | None = None, + gainCorrection: ipIsr.IsrCalib | None = None, ) -> pipeBase.Struct: """Run isr and cosmic ray repair using, doing as much isr as possible. @@ -162,41 +156,17 @@ def run( Functor for linearization. crosstalk : `lsst.ip.isr.crosstalk.CrosstalkCalib`, optional Calibration for crosstalk. - bfKernel : `numpy.ndarray`, optional - Brighter-fatter kernel. - newBFKernel : `ipIsr.BrighterFatterKernel`, optional + bfKernel : `ipIsr.BrighterFatterKernel`, optional New Brighter-fatter kernel. ptc : `lsst.ip.isr.PhotonTransferCurveDataset`, optional Photon transfer curve dataset, with, e.g., gains and read noise. - crosstalkSources : `list`, optional - List of possible crosstalk sources. + cti : `lsst.ip.isr.DeferredChargeCalib`, optional + Charge transfer inefficiency correction calibration. isrBaseConfig : `lsst.ip.isr.IsrTaskLSSTConfig`, optional An isrTask config to act as the base configuration. Options which involve applying a calibration product are ignored, but this allows for the configuration of e.g. the number of overscan columns. - filterTransmission : `lsst.afw.image.TransmissionCurve` - A ``TransmissionCurve`` that represents the throughput of the - filter itself, to be evaluated in focal-plane coordinates. - opticsTransmission: `lsst.afw.image.TransmissionCurve`, optional - A ``TransmissionCurve`` that represents the throughput of the, - optics, to be evaluated in focal-plane coordinates. - strayLightData : `object`, optional - Opaque object containing calibration information for stray-light - correction. If `None`, no correction will be performed. - sensorTransmission : `lsst.afw.image.TransmissionCurve` - A ``TransmissionCurve`` that represents the throughput of the - sensor itself, to be evaluated in post-assembly trimmed detector - coordinates. - atmosphereTransmission : `lsst.afw.image.TransmissionCurve` - A ``TransmissionCurve`` that represents the throughput of the - atmosphere, assumed to be spatially constant. - illumMaskedImage : `lsst.afw.image.MaskedImage`, optional - Illumination correction image. - bfGains : `dict` of `float`, optional - Gains used to override the detector's nominal gains for the - brighter-fatter correction. A dict keyed by amplifier name for - the detector in question. Returns ------- @@ -219,6 +189,8 @@ def run( isrConfig.doLinearize = False isrConfig.doCrosstalk = False isrConfig.doBrighterFatter = False + isrConfig.doDeferredCharge = False + isrConfig.doCorrectGains = False if bias: isrConfig.doBias = True @@ -244,26 +216,31 @@ def run( isrConfig.doCrosstalk = True self.log.info("Running with crosstalk correction") - if newBFKernel is not None: - bfGains = newBFKernel.gain - isrConfig.doBrighterFatter = True - self.log.info("Running with new brighter-fatter correction") - else: - bfGains = None - - if bfKernel is not None and bfGains is None: + if bfKernel is not None: isrConfig.doBrighterFatter = True self.log.info("Running with brighter-fatter correction") + if deferredChargeCalib is not None: + isrConfig.doDeferredCharge = True + self.log.info("Running with CTI correction") + + if gainCorrection is not None: + isrConfig.doCorrectGains = True + self.log.info("Running with Gain corrections") + + if ptc is None: + raise RuntimeError("IsrTaskLSST requires a PTC.") + isrTask = self.isrTask(config=isrConfig) - if fringes: - # Must be run after isrTask is instantiated. - isrTask.fringe.loadFringes( - fringes, - expId=ccdExposure.info.id, - assembler=isrTask.assembleCcd if isrConfig.doAssembleIsrExposures else None, - ) + # DM-47959: TODO Add fringe correction to IsrTaskLSST. + # if fringes: + # # Must be run after isrTask is instantiated. + # isrTask.fringe.loadFringes( + # fringes, + # expId=ccdExposure.info.id, + # assembler=isrTask.assembleCcd if isrConfig.doAssembleIsrExposures else None, + # ) result = isrTask.run( ccdExposure, @@ -271,21 +248,13 @@ def run( bias=bias, dark=dark, flat=flat, - fringes=fringes, defects=defects, linearizer=linearizer, crosstalk=crosstalk, bfKernel=bfKernel, - bfGains=bfGains, ptc=ptc, - crosstalkSources=crosstalkSources, - filterTransmission=filterTransmission, - opticsTransmission=opticsTransmission, - sensorTransmission=sensorTransmission, - atmosphereTransmission=atmosphereTransmission, - strayLightData=strayLightData, deferredChargeCalib=deferredChargeCalib, - illumMaskedImage=illumMaskedImage, + gainCorrection=gainCorrection, ) postIsr = result.exposure diff --git a/tests/test_bestEffortIsr.py b/tests/test_bestEffortIsr.py index 82f38de8..2bd2cad4 100644 --- a/tests/test_bestEffortIsr.py +++ b/tests/test_bestEffortIsr.py @@ -40,7 +40,7 @@ def setUpClass(cls): # NCSA - LATISS/raw/all # TTS - LATISS-test-data-tts # summit - LATISS_test_data - cls.dataId = {"day_obs": 20210121, "seq_num": 743, "detector": 0} + cls.dataId = {"day_obs": 20251021, "seq_num": 443, "detector": 0} def test_getExposure(self): # in most locations this will load a pre-made image diff --git a/tests/test_quickLook.py b/tests/test_quickLook.py index d2fe2a80..e210ae01 100644 --- a/tests/test_quickLook.py +++ b/tests/test_quickLook.py @@ -23,17 +23,14 @@ import tempfile import unittest -import numpy as np - import lsst.afw.cameraGeom.testUtils as afwTestUtils import lsst.afw.image as afwImage import lsst.daf.butler.tests as butlerTests import lsst.ip.isr as ipIsr -import lsst.ip.isr.isrMock as isrMock +import lsst.ip.isr.isrMockLSST as isrMock import lsst.pex.exceptions import lsst.pipe.base as pipeBase import lsst.pipe.base.testUtils -from lsst.afw.image import TransmissionCurve from lsst.summit.utils.quickLook import QuickLookIsrTask, QuickLookIsrTaskConfig @@ -41,25 +38,23 @@ class QuickLookIsrTaskTestCase(unittest.TestCase): """Tests of the run method with fake data.""" def setUp(self): - self.mockConfig = isrMock.IsrMockConfig() - self.dataContainer = isrMock.MockDataContainer(config=self.mockConfig) - self.camera = isrMock.IsrMock(config=self.mockConfig).getCamera() + self.mockConfig = isrMock.IsrMockLSSTConfig() + self.camera = isrMock.IsrMockLSST(config=self.mockConfig).getCamera() - self.ccdExposure = isrMock.RawMock(config=self.mockConfig).run() + self.ccdExposure = isrMock.RawMockLSST(config=self.mockConfig).run() self.detector = self.ccdExposure.getDetector() amps = self.detector.getAmplifiers() ampNames = [amp.getName() for amp in amps] # # Mock other optional parameters - self.bias = self.dataContainer.get("bias") - self.dark = self.dataContainer.get("dark") - self.flat = self.dataContainer.get("flat") - self.defects = self.dataContainer.get("defects") + self.bias = isrMock.BiasMockLSST(config=self.mockConfig).run() + self.dark = isrMock.DarkMockLSST(config=self.mockConfig).run() + self.flat = isrMock.FlatMockLSST(config=self.mockConfig).run() + self.defects = isrMock.DefectMockLSST(config=self.mockConfig).run() self.ptc = ipIsr.PhotonTransferCurveDataset(ampNames=ampNames) # Mock PTC dataset - self.bfKernel = self.dataContainer.get("bfKernel") - self.newBFKernel = pipeBase.Struct(gain={}) - for amp_i, amp in enumerate(ampNames): - self.newBFKernel.gain[amp] = 0.9 + 0.1 * amp_i + for amp, gain in self.mockConfig.gainDict.items(): + self.ptc.gain[amp] = 1.0 + self.bfKernel = isrMock.BfKernelMockLSST(config=self.mockConfig).run() self.task = QuickLookIsrTask(config=QuickLookIsrTaskConfig()) def test_runQuickLook(self): @@ -74,9 +69,7 @@ def test_runQuickLook(self): linearizer=None, crosstalk=None, bfKernel=self.bfKernel, - newBFKernel=self.newBFKernel, ptc=self.ptc, - crosstalkSources=None, ) self.assertIsNotNone(result, "Result of run method should not be None") self.assertIsInstance(result, pipeBase.Struct, "Result should be of type lsst.pipe.base.Struct") @@ -87,8 +80,8 @@ def test_runQuickLook(self): ) def test_runQuickLookMissingData(self): - # Test without any inputs other than the exposure - result = self.task.run(self.ccdExposure) + # Test without any inputs other than the exposure. And the PTC. + result = self.task.run(self.ccdExposure, ptc=self.ptc) self.assertIsInstance(result.exposure, afwImage.Exposure) def test_runQuickLookBadDark(self): @@ -129,23 +122,16 @@ def setUp(self): dark = "dark" flat = "flat" defects = "defects" - bfKernel = "bfKernel" - newBFKernel = "brighterFatterKernel" + bfKernel = "bfk" ptc = "ptc" - filterTransmission = "transmission_filter" - deferredChargeCalib = "cpCtiCalib" - opticsTransmission = "transmission_optics" - strayLightData = "yBackground" - atmosphereTransmission = "transmission_atmosphere" + deferredChargeCalib = "cti" crosstalk = "crosstalk" - illumMaskedImage = "illum" linearizer = "linearizer" - fringes = "fringe" - sensorTransmission = "transmission_sensor" - crosstalkSources = "isrOverscanCorrected" + gainCorrection = "gain_correction" # outputs outputExposure = "postISRCCD" + outputStatistics = "isrStatistics" # quickLook-only outputs exposure = "quickLookExp" @@ -170,36 +156,17 @@ def setUp(self): butlerTests.addDatasetType(self.repo, defects, {"instrument", "detector"}, "Defects") butlerTests.addDatasetType(self.repo, linearizer, {"instrument", "detector"}, "Linearizer") butlerTests.addDatasetType(self.repo, crosstalk, {"instrument", "detector"}, "CrosstalkCalib") - butlerTests.addDatasetType(self.repo, bfKernel, {"instrument"}, "NumpyArray") - butlerTests.addDatasetType(self.repo, newBFKernel, {"instrument", "detector"}, "BrighterFatterKernel") + butlerTests.addDatasetType(self.repo, bfKernel, {"instrument", "detector"}, "BrighterFatterKernel") butlerTests.addDatasetType(self.repo, ptc, {"instrument", "detector"}, "PhotonTransferCurveDataset") - butlerTests.addDatasetType( - self.repo, filterTransmission, {"instrument", "physical_filter"}, "TransmissionCurve" - ) - butlerTests.addDatasetType(self.repo, opticsTransmission, {"instrument"}, "TransmissionCurve") butlerTests.addDatasetType(self.repo, deferredChargeCalib, {"instrument", "detector"}, "IsrCalib") - butlerTests.addDatasetType( - self.repo, strayLightData, {"instrument", "physical_filter", "detector"}, "Exposure" - ) - butlerTests.addDatasetType(self.repo, atmosphereTransmission, {"instrument"}, "TransmissionCurve") - butlerTests.addDatasetType( - self.repo, illumMaskedImage, {"instrument", "physical_filter", "detector"}, "MaskedImage" - ) - butlerTests.addDatasetType( - self.repo, fringes, {"instrument", "physical_filter", "detector"}, "Exposure" - ) - butlerTests.addDatasetType( - self.repo, sensorTransmission, {"instrument", "detector"}, "TransmissionCurve" - ) - butlerTests.addDatasetType( - self.repo, crosstalkSources, {"instrument", "exposure", "detector"}, "Exposure" - ) + butlerTests.addDatasetType(self.repo, gainCorrection, {"instrument", "detector"}, "IsrCalib") # outputs butlerTests.addDatasetType( self.repo, outputExposure, {"instrument", "exposure", "detector"}, "Exposure" ) butlerTests.addDatasetType(self.repo, exposure, {"instrument", "exposure", "detector"}, "Exposure") + butlerTests.addDatasetType(self.repo, outputStatistics, {"instrument", "exposure", "detector"}, "StructuredDataDict") # dataIds self.exposure_id = self.repo.registry.expandDataId( @@ -220,9 +187,6 @@ def setUp(self): ) # put empty data - transmissionCurve = TransmissionCurve.makeSpatiallyConstant( - np.ones(2), np.linspace(0, 1, 2), 0.0, 0.0 - ) self.butler = butlerTests.makeTestCollection(self.repo) self.butler.put(afwImage.ExposureF(), ccdExposure, self.exposure_id) self.butler.put(afwTestUtils.CameraWrapper().camera, camera, self.instrument_id) @@ -230,22 +194,14 @@ def setUp(self): self.butler.put(afwImage.ExposureF(), dark, self.detector_id) self.butler.put(afwImage.ExposureF(), flat, self.flat_id) self.butler.put(lsst.ip.isr.Defects(), defects, self.detector_id) - self.butler.put(np.zeros(2), bfKernel, self.instrument_id) self.butler.put( - lsst.ip.isr.brighterFatterKernel.BrighterFatterKernel(), newBFKernel, self.detector_id + lsst.ip.isr.brighterFatterKernel.BrighterFatterKernel(), bfKernel, self.detector_id ) self.butler.put(ipIsr.PhotonTransferCurveDataset(), ptc, self.detector_id) - self.butler.put(transmissionCurve, filterTransmission, self.filter_id) self.butler.put(lsst.ip.isr.calibType.IsrCalib(), deferredChargeCalib, self.detector_id) - self.butler.put(transmissionCurve, opticsTransmission, self.instrument_id) - self.butler.put(afwImage.ExposureF(), strayLightData, self.flat_id) - self.butler.put(transmissionCurve, atmosphereTransmission, self.instrument_id) self.butler.put(lsst.ip.isr.crosstalk.CrosstalkCalib(), crosstalk, self.detector_id) - self.butler.put(afwImage.ExposureF().maskedImage, illumMaskedImage, self.flat_id) self.butler.put(lsst.ip.isr.linearize.Linearizer(), linearizer, self.detector_id) - self.butler.put(afwImage.ExposureF(), fringes, self.flat_id) - self.butler.put(transmissionCurve, sensorTransmission, self.detector_id) - self.butler.put(afwImage.ExposureF(), crosstalkSources, self.exposure_id) + self.butler.put(lsst.ip.isr.calibType.IsrCalib(), gainCorrection, self.detector_id) def tearDown(self): del self.repo_path # this removes the temporary directory @@ -260,10 +216,8 @@ def test_runQuantum(self): config.doCalculateStatistics = False # Turn on all optional inputs - config.doAttachTransmissionCurve = True - config.doIlluminationCorrection = True - config.doStrayLight = True - config.doDeferredCharge = True + config.doDeferredCharge = False # There is no CTI calibration + # available for LATISS. config.usePtcReadNoise = True config.doCrosstalk = True config.doBrighterFatter = True @@ -287,22 +241,16 @@ def test_runQuantum(self): "dark": self.detector_id, "flat": self.flat_id, "defects": self.detector_id, - "bfKernel": self.instrument_id, - "newBFKernel": self.detector_id, + "bfKernel": self.detector_id, "ptc": self.detector_id, - "filterTransmission": self.filter_id, "deferredChargeCalib": self.detector_id, - "opticsTransmission": self.instrument_id, - "strayLightData": self.flat_id, - "atmosphereTransmission": self.instrument_id, "crosstalk": self.detector_id, - "illumMaskedImage": self.flat_id, "linearizer": self.detector_id, "fringes": self.flat_id, - "sensorTransmission": self.detector_id, - "crosstalkSources": [self.exposure_id, self.exposure_id], + "gainCorrection": self.detector_id, # outputs "outputExposure": self.exposure_id, + "outputStatistics": self.exposure_id, "exposure": self.exposure_id, }, ) @@ -320,3 +268,16 @@ class ExitMockError(Exception): """A custom exception to catch during a unit test.""" pass + + +class TestMemory(lsst.utils.tests.MemoryTestCase): + pass + + +def setup_module(module): + lsst.utils.tests.init() + + +if __name__ == "__main__": + lsst.utils.tests.init() + unittest.main() From 67916fb1a17f4c8902f0c9c5a210f5fb858c0a63 Mon Sep 17 00:00:00 2001 From: Merlin Fisher-Levine Date: Mon, 26 Jan 2026 12:20:25 -0800 Subject: [PATCH 05/13] Remove non-existent doWrite option --- python/lsst/summit/utils/resources/config/quickLookIsr.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/lsst/summit/utils/resources/config/quickLookIsr.py b/python/lsst/summit/utils/resources/config/quickLookIsr.py index 60d63c96..af7ba969 100644 --- a/python/lsst/summit/utils/resources/config/quickLookIsr.py +++ b/python/lsst/summit/utils/resources/config/quickLookIsr.py @@ -1,6 +1,5 @@ # mypy: disable-error-code="name-defined" -config.doWrite = False # this task writes separately, no need for this config.doSaturation = True # saturation very important for roundness measurement in qfm config.brighterFatterMaxIter = 2 # Uncomment this to remove test warning config.doDeferredCharge = False # no calib for this yet From 92373680cdd34cd4d1673a8edf3bee8ef6b1c739 Mon Sep 17 00:00:00 2001 From: Johnny Esteves Date: Wed, 25 Feb 2026 10:30:29 -0800 Subject: [PATCH 06/13] Fix FootprintSet STDEV detection failure in runSourceDetection Replace detectObjectsInExp with direct detection using Threshold.VALUE and np.std() to avoid STDEVCLIP returning 0 for some guider images. Add starMaskThreshold parameter to exclude bright pixels when computing background standard deviation. --- python/lsst/summit/utils/guiders/detection.py | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/python/lsst/summit/utils/guiders/detection.py b/python/lsst/summit/utils/guiders/detection.py index 4f17b3c2..e109398c 100644 --- a/python/lsst/summit/utils/guiders/detection.py +++ b/python/lsst/summit/utils/guiders/detection.py @@ -37,8 +37,8 @@ from astropy.nddata import Cutout2D from astropy.stats import sigma_clipped_stats +import lsst.afw.detection as afwDetect from lsst.afw.image import ExposureF, ImageF, MaskedImageF -from lsst.summit.utils.utils import detectObjectsInExp from .reading import GuiderData @@ -277,6 +277,7 @@ def runSourceDetection( cutOutSize: int = 25, apertureRadius: int = 5, gain: float = 1.0, + starMaskThreshold: float = 20000.0, ) -> pd.DataFrame: """ Detect sources in an image and measure their properties. @@ -293,6 +294,9 @@ def runSourceDetection( Aperture radius in pixels for photometry. gain : `float` Detector gain (e-/ADU). + starMaskThreshold : `float` + Pixel count threshold for masking bright stars when computing the + background standard deviation. Pixels above this value are excluded. Returns ------- @@ -303,12 +307,30 @@ def runSourceDetection( exposure = ExposureF(MaskedImageF(ImageF(image))) # Step 2: Detect sources - # we assume that we have bright stars - # filter out stamps with no stars + # We use Threshold.VALUE with np.std() instead of Threshold.STDEV because + # STDEV uses STDEVCLIP internally which can return 0 for some guider + # images, causing FootprintSet to fail with "St. dev. must be > 0". + footprints = None if not isBlankImage(image): - footprints = detectObjectsInExp(exposure, nSigma=threshold) - else: - footprints = None + median = np.nanmedian(exposure.image.array) + exposure.image -= median + # Mask bright stars when computing std to get background noise estimate + bgPixels = exposure.image.array[exposure.image.array < starMaskThreshold] + if len(bgPixels) == 0: + raise ValueError( + f"No pixels below starMaskThreshold={starMaskThreshold}. " + "All pixels are saturated or threshold is too low." + ) + imageStd = np.std(bgPixels) + if imageStd <= 0: + raise ValueError( + f"Background standard deviation is {imageStd}. " + "Image may be flat or starMaskThreshold may need adjustment." + ) + absThreshold = threshold * imageStd + thresh = afwDetect.Threshold(absThreshold, afwDetect.Threshold.VALUE) + footprints = afwDetect.FootprintSet(exposure.getMaskedImage(), thresh, "DETECTED", 10) + exposure.image += median if not footprints: return pd.DataFrame(columns=DEFAULT_COLUMNS) From b236831d4da8499cfef2ad7992c64767b2249680 Mon Sep 17 00:00:00 2001 From: Johnny Esteves Date: Mon, 2 Mar 2026 16:17:51 -0800 Subject: [PATCH 07/13] Suppress pre-existing mypy union-attr errors in utils.py --- python/lsst/summit/utils/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/lsst/summit/utils/utils.py b/python/lsst/summit/utils/utils.py index 471ef6ea..bf3a2fe4 100644 --- a/python/lsst/summit/utils/utils.py +++ b/python/lsst/summit/utils/utils.py @@ -488,8 +488,8 @@ def _getAltAzZenithsFromSeqNum( for seqNum in seqNumList: md = butler.get("raw.metadata", day_obs=dayObs, seq_num=seqNum, detector=0) obsInfo = ObservationInfo(md) - alt = obsInfo.altaz_begin.alt.value - az = obsInfo.altaz_begin.az.value + alt = obsInfo.altaz_begin.alt.value # type: ignore[union-attr] + az = obsInfo.altaz_begin.az.value # type: ignore[union-attr] elevations.append(alt) zeniths.append(90 - alt) azimuths.append(az) From afe700652e61315f1b69442bc37c3276be32e4ea Mon Sep 17 00:00:00 2001 From: Johnny Esteves Date: Mon, 2 Mar 2026 16:25:06 -0800 Subject: [PATCH 08/13] Add nPixMin parameter to runSourceDetection --- python/lsst/summit/utils/guiders/detection.py | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/python/lsst/summit/utils/guiders/detection.py b/python/lsst/summit/utils/guiders/detection.py index e109398c..b399e9ce 100644 --- a/python/lsst/summit/utils/guiders/detection.py +++ b/python/lsst/summit/utils/guiders/detection.py @@ -277,7 +277,7 @@ def runSourceDetection( cutOutSize: int = 25, apertureRadius: int = 5, gain: float = 1.0, - starMaskThreshold: float = 20000.0, + nPixMin: int = 10, ) -> pd.DataFrame: """ Detect sources in an image and measure their properties. @@ -294,9 +294,8 @@ def runSourceDetection( Aperture radius in pixels for photometry. gain : `float` Detector gain (e-/ADU). - starMaskThreshold : `float` - Pixel count threshold for masking bright stars when computing the - background standard deviation. Pixels above this value are excluded. + nPixMin : `int` + Minimum number of pixels in a footprint for detection. Returns ------- @@ -307,29 +306,22 @@ def runSourceDetection( exposure = ExposureF(MaskedImageF(ImageF(image))) # Step 2: Detect sources - # We use Threshold.VALUE with np.std() instead of Threshold.STDEV because - # STDEV uses STDEVCLIP internally which can return 0 for some guider + # We use Threshold.VALUE with a sigma68 background estimate instead of + # Threshold.STDEV because STDEVCLIP can return 0 for some guider # images, causing FootprintSet to fail with "St. dev. must be > 0". + # sigma68 = (p84 - p16) / 2 is robust to bright stars (they only + # affect the upper tail) and doesn't fail on flat-background images + # the way MAD does (MAD=0 when >50% of pixels are identical). footprints = None if not isBlankImage(image): - median = np.nanmedian(exposure.image.array) - exposure.image -= median - # Mask bright stars when computing std to get background noise estimate - bgPixels = exposure.image.array[exposure.image.array < starMaskThreshold] - if len(bgPixels) == 0: - raise ValueError( - f"No pixels below starMaskThreshold={starMaskThreshold}. " - "All pixels are saturated or threshold is too low." - ) - imageStd = np.std(bgPixels) + p16, median, p84 = np.nanpercentile(image, [16, 50, 84]) + imageStd = (p84 - p16) / 2.0 if imageStd <= 0: - raise ValueError( - f"Background standard deviation is {imageStd}. " - "Image may be flat or starMaskThreshold may need adjustment." - ) + return pd.DataFrame(columns=DEFAULT_COLUMNS) + exposure.image -= median absThreshold = threshold * imageStd thresh = afwDetect.Threshold(absThreshold, afwDetect.Threshold.VALUE) - footprints = afwDetect.FootprintSet(exposure.getMaskedImage(), thresh, "DETECTED", 10) + footprints = afwDetect.FootprintSet(exposure.getMaskedImage(), thresh, "DETECTED", nPixMin) exposure.image += median if not footprints: From a77189068045305c91d4f1ff3fb356e7da0675ea Mon Sep 17 00:00:00 2001 From: Johnny Esteves Date: Mon, 2 Mar 2026 18:13:53 -0800 Subject: [PATCH 09/13] Add dither to coadd median to prevent STDEVCLIP=0 Add a uniform [0, 1) random dither per stamp before computing the median coadd. This breaks integer quantization that causes the pixel distribution to be so narrow that STDEVCLIP returns 0 on some guider images. Fix suggested by Robert Lupton. (DM-54263) --- python/lsst/summit/utils/guiders/reading.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/python/lsst/summit/utils/guiders/reading.py b/python/lsst/summit/utils/guiders/reading.py index d671fb5f..2ca65206 100644 --- a/python/lsst/summit/utils/guiders/reading.py +++ b/python/lsst/summit/utils/guiders/reading.py @@ -406,10 +406,15 @@ def getStampArrayCoadd(self, detName: str) -> np.ndarray: if len(stamps) == 0: raise ValueError(f"No stamps found for detector {detName!r}") - # Collect arrays, with optional bias subtraction + # Collect arrays, with optional bias subtraction. arrList = [self[detName, idx] for idx in range(len(stamps))] - stack = np.nanmedian(arrList, axis=0) - return stack + # Add a uniform [0, 1) dither per stamp before the median to break + # integer quantization. Without this, the coadd pixel distribution + # can be so narrow that STDEVCLIP returns 0. (See DM-54263.) + stack = np.array(arrList, dtype=np.float32) + rng = np.random.default_rng(seed=0) + stack += rng.uniform(0, 1, size=stack.shape).astype(np.float32) + return np.nanmedian(stack, axis=0) def getGuiderAmpName(self, detName: str) -> str: """ From 57d2450121b095af302eea43c358ab94c1e66d3c Mon Sep 17 00:00:00 2001 From: Johnny Esteves Date: Mon, 2 Mar 2026 19:08:57 -0800 Subject: [PATCH 10/13] Use STDEVCLIP with sigma68 fallback for source detection Replace the manual sigma68-only background estimation with STDEVCLIP from lsst.afw.math, falling back to sigma68 if STDEVCLIP returns 0. The dithered coadds (previous commit) prevent STDEVCLIP from failing, but the fallback provides a safety net. Also adds a configurable nPixMin parameter for footprint detection. (DM-54263) --- python/lsst/summit/utils/guiders/detection.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/python/lsst/summit/utils/guiders/detection.py b/python/lsst/summit/utils/guiders/detection.py index b399e9ce..a2c95bdc 100644 --- a/python/lsst/summit/utils/guiders/detection.py +++ b/python/lsst/summit/utils/guiders/detection.py @@ -39,6 +39,7 @@ import lsst.afw.detection as afwDetect from lsst.afw.image import ExposureF, ImageF, MaskedImageF +from lsst.afw.math import STDEVCLIP, makeStatistics from .reading import GuiderData @@ -305,20 +306,22 @@ def runSourceDetection( # Step 1: Convert numpy image to MaskedImage and Exposure exposure = ExposureF(MaskedImageF(ImageF(image))) - # Step 2: Detect sources - # We use Threshold.VALUE with a sigma68 background estimate instead of - # Threshold.STDEV because STDEVCLIP can return 0 for some guider - # images, causing FootprintSet to fail with "St. dev. must be > 0". - # sigma68 = (p84 - p16) / 2 is robust to bright stars (they only - # affect the upper tail) and doesn't fail on flat-background images - # the way MAD does (MAD=0 when >50% of pixels are identical). + # Step 2: Detect sources using STDEVCLIP for the background noise. + # The input coadd images are dithered (see GuiderData.getStampArrayCoadd) + # to prevent integer quantization from collapsing the pixel distribution + # and causing STDEVCLIP to return 0. (See DM-54263.) footprints = None if not isBlankImage(image): - p16, median, p84 = np.nanpercentile(image, [16, 50, 84]) - imageStd = (p84 - p16) / 2.0 + median = np.nanmedian(image) + exposure.image -= median + imageStd = float(makeStatistics(exposure.getMaskedImage(), STDEVCLIP).getValue(STDEVCLIP)) + if imageStd <= 0: + # Fallback: sigma68 is robust to quantization and bright stars. + p16, p84 = np.nanpercentile(image, [16, 84]) + imageStd = (p84 - p16) / 2.0 if imageStd <= 0: + exposure.image += median return pd.DataFrame(columns=DEFAULT_COLUMNS) - exposure.image -= median absThreshold = threshold * imageStd thresh = afwDetect.Threshold(absThreshold, afwDetect.Threshold.VALUE) footprints = afwDetect.FootprintSet(exposure.getMaskedImage(), thresh, "DETECTED", nPixMin) @@ -627,7 +630,6 @@ def buildReferenceCatalog( apertureRadius = int(config.aperSizeArcsec / pixelScale) array = guiderData.getStampArrayCoadd(guiderName) - # array = np.where(array < 0, 0, array) # Ensure no negative values array = array - np.nanmin(array) # Ensure no negative values sources = runSourceDetection( array, From c4fb3dc46be425dd170108ae0b70b9bd55584c30 Mon Sep 17 00:00:00 2001 From: Johnny Esteves Date: Tue, 3 Mar 2026 06:11:28 -0800 Subject: [PATCH 11/13] Replace type: ignore with assertion for altaz_begin Address review feedback from Merlin: use explicit assertion instead of mypy ignore comment. (DM-54263) --- python/lsst/summit/utils/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python/lsst/summit/utils/utils.py b/python/lsst/summit/utils/utils.py index bf3a2fe4..ec8750e1 100644 --- a/python/lsst/summit/utils/utils.py +++ b/python/lsst/summit/utils/utils.py @@ -488,8 +488,9 @@ def _getAltAzZenithsFromSeqNum( for seqNum in seqNumList: md = butler.get("raw.metadata", day_obs=dayObs, seq_num=seqNum, detector=0) obsInfo = ObservationInfo(md) - alt = obsInfo.altaz_begin.alt.value # type: ignore[union-attr] - az = obsInfo.altaz_begin.az.value # type: ignore[union-attr] + assert obsInfo.altaz_begin is not None, f"altaz_begin is None for seqNum={seqNum}" + alt = obsInfo.altaz_begin.alt.value + az = obsInfo.altaz_begin.az.value elevations.append(alt) zeniths.append(90 - alt) azimuths.append(az) From 2e8de65f0d30d07e863acfc319a10387e14343b1 Mon Sep 17 00:00:00 2001 From: Christopher Waters Date: Fri, 13 Mar 2026 15:55:55 -0700 Subject: [PATCH 12/13] Update quickLook/bestEffortIsr to use IsrTaskLSST. --- python/lsst/summit/utils/quickLook.py | 25 ++++++++++ tests/test_bestEffortIsr.py | 2 +- tests/test_quickLook.py | 71 ++++++++++++++++++++------- 3 files changed, 78 insertions(+), 20 deletions(-) diff --git a/python/lsst/summit/utils/quickLook.py b/python/lsst/summit/utils/quickLook.py index 9f3ad19f..b9afc5b5 100644 --- a/python/lsst/summit/utils/quickLook.py +++ b/python/lsst/summit/utils/quickLook.py @@ -167,6 +167,31 @@ def run( An isrTask config to act as the base configuration. Options which involve applying a calibration product are ignored, but this allows for the configuration of e.g. the number of overscan columns. + filterTransmission : `lsst.afw.image.TransmissionCurve` + A ``TransmissionCurve`` that represents the throughput of the + filter itself, to be evaluated in focal-plane coordinates. + opticsTransmission: `lsst.afw.image.TransmissionCurve`, optional + A ``TransmissionCurve`` that represents the throughput of the, + optics, to be evaluated in focal-plane coordinates. + strayLightData : `object`, optional + Opaque object containing calibration information for stray-light + correction. If `None`, no correction will be performed. + sensorTransmission : `lsst.afw.image.TransmissionCurve` + A ``TransmissionCurve`` that represents the throughput of the + sensor itself, to be evaluated in post-assembly trimmed detector + coordinates. + atmosphereTransmission : `lsst.afw.image.TransmissionCurve` + A ``TransmissionCurve`` that represents the throughput of the + atmosphere, assumed to be spatially constant. + illumMaskedImage : `lsst.afw.image.MaskedImage`, optional + Illumination correction image. + bfGains : `dict` of `float`, optional + Gains used to override the detector's nominal gains for the + brighter-fatter correction. A dict keyed by amplifier name for + the detector in question. + gainCorrection : `lsst.ip.isr.IsrCalib`, optional + Gain correction calibration, to account for small + time-dependent shifts. Returns ------- diff --git a/tests/test_bestEffortIsr.py b/tests/test_bestEffortIsr.py index 2bd2cad4..6923c67b 100644 --- a/tests/test_bestEffortIsr.py +++ b/tests/test_bestEffortIsr.py @@ -40,7 +40,7 @@ def setUpClass(cls): # NCSA - LATISS/raw/all # TTS - LATISS-test-data-tts # summit - LATISS_test_data - cls.dataId = {"day_obs": 20251021, "seq_num": 443, "detector": 0} + cls.dataId = {"day_obs": 20250902, "seq_num": 10, "detector": 0} def test_getExposure(self): # in most locations this will load a pre-made image diff --git a/tests/test_quickLook.py b/tests/test_quickLook.py index e210ae01..afa93736 100644 --- a/tests/test_quickLook.py +++ b/tests/test_quickLook.py @@ -40,8 +40,8 @@ class QuickLookIsrTaskTestCase(unittest.TestCase): def setUp(self): self.mockConfig = isrMock.IsrMockLSSTConfig() self.camera = isrMock.IsrMockLSST(config=self.mockConfig).getCamera() - self.ccdExposure = isrMock.RawMockLSST(config=self.mockConfig).run() + self.detector = self.ccdExposure.getDetector() amps = self.detector.getAmplifiers() ampNames = [amp.getName() for amp in amps] @@ -80,7 +80,9 @@ def test_runQuickLook(self): ) def test_runQuickLookMissingData(self): - # Test without any inputs other than the exposure. And the PTC. + # Test without any inputs other than the exposure + # And the PTC. That's required now. + result = self.task.run(self.ccdExposure, ptc=self.ptc) self.assertIsInstance(result.exposure, afwImage.Exposure) @@ -167,7 +169,6 @@ def setUp(self): ) butlerTests.addDatasetType(self.repo, exposure, {"instrument", "exposure", "detector"}, "Exposure") butlerTests.addDatasetType(self.repo, outputStatistics, {"instrument", "exposure", "detector"}, "StructuredDataDict") - # dataIds self.exposure_id = self.repo.registry.expandDataId( { @@ -188,26 +189,56 @@ def setUp(self): # put empty data self.butler = butlerTests.makeTestCollection(self.repo) - self.butler.put(afwImage.ExposureF(), ccdExposure, self.exposure_id) + + myExposure = isrMock.RawMockLSST().run() + self.butler.put(myExposure, ccdExposure, self.exposure_id) + self.butler.put(afwTestUtils.CameraWrapper().camera, camera, self.instrument_id) - self.butler.put(afwImage.ExposureF(), bias, self.detector_id) - self.butler.put(afwImage.ExposureF(), dark, self.detector_id) - self.butler.put(afwImage.ExposureF(), flat, self.flat_id) + + myBias = isrMock.BiasMockLSST().run() + self.butler.put(myBias, bias, self.detector_id) + + myDark = isrMock.DarkMockLSST().run() + self.butler.put(myDark, dark, self.detector_id) + + myFlat = isrMock.FlatMockLSST().run() + self.butler.put(myFlat, flat, self.flat_id) + self.butler.put(lsst.ip.isr.Defects(), defects, self.detector_id) - self.butler.put( - lsst.ip.isr.brighterFatterKernel.BrighterFatterKernel(), bfKernel, self.detector_id - ) - self.butler.put(ipIsr.PhotonTransferCurveDataset(), ptc, self.detector_id) - self.butler.put(lsst.ip.isr.calibType.IsrCalib(), deferredChargeCalib, self.detector_id) - self.butler.put(lsst.ip.isr.crosstalk.CrosstalkCalib(), crosstalk, self.detector_id) + + myBfk = isrMock.BfKernelMockLSST().run() + self.butler.put(myBfk, "bfk", self.detector_id) + + myPtc = ipIsr.PhotonTransferCurveDataset() + for amp in myExposure.getDetector(): + myPtc.gain[amp.getName()] = myBfk.gain[amp.getName()] + myPtc.noise[amp.getName()] = 5.0 + myPtc.ptcTurnoff[amp.getName()] = 100_000 + self.butler.put(myPtc, ptc, self.detector_id) + + myCti = lsst.ip.isr.deferredCharge.DeferredChargeCalib() + for amp in myExposure.getDetector(): + myCti.driftScale[amp.getName()] = 1.8e-5 + myCti.decayTime[amp.getName()] = 3.0 + myCti.globalCti[amp.getName()] = 1.5e-7 + myCti.serialTraps[amp.getName()] = lsst.ip.isr.deferredCharge.SerialTrap(10, 3, 200, + 'linear', [5, 1e-3]) + self.butler.put(myCti, "cti", self.detector_id) + self.butler.put(lsst.ip.isr.gainCorrection.GainCorrection(), gainCorrection, self.detector_id) + + nAmp = len(myExposure.getDetector()) + myCrosstalk = lsst.ip.isr.crosstalk.CrosstalkCalib(nAmp=nAmp) + myCrosstalk.hasCrosstalk = True + self.butler.put(myCrosstalk, crosstalk, self.detector_id) + self.butler.put(lsst.ip.isr.linearize.Linearizer(), linearizer, self.detector_id) - self.butler.put(lsst.ip.isr.calibType.IsrCalib(), gainCorrection, self.detector_id) + def tearDown(self): del self.repo_path # this removes the temporary directory def test_runQuantum(self): - config = ipIsr.IsrTaskConfig() + config = ipIsr.IsrTaskLSSTConfig() # Remove some outputs config.doBinnedExposures = False config.doSaveInterpPixels = False @@ -218,15 +249,17 @@ def test_runQuantum(self): # Turn on all optional inputs config.doDeferredCharge = False # There is no CTI calibration # available for LATISS. - config.usePtcReadNoise = True config.doCrosstalk = True config.doBrighterFatter = True + # Disable things that do not work with dummy inputs. + config.doCorrectGains = False + # Override a method in IsrTask that is executed early, to instead raise # a custom exception called ExitMock that we can catch and ignore. - isrTask = ipIsr.IsrTask + isrTask = ipIsr.IsrTaskLSST isrTask.ensureExposure = raiseExitMockError - task = QuickLookIsrTask(isrTask=isrTask) + task = QuickLookIsrTask() lsst.pipe.base.testUtils.assertValidInitOutput(task) # Use the names of the connections here, not the Butler dataset name @@ -251,9 +284,9 @@ def test_runQuantum(self): # outputs "outputExposure": self.exposure_id, "outputStatistics": self.exposure_id, - "exposure": self.exposure_id, }, ) + # Check that the proper kwargs are passed to run(). with contextlib.suppress(ExitMockError): lsst.pipe.base.testUtils.runTestQuantum(task, self.butler, quantum, mockRun=False) From e678784b6e8fd147a68f250eaff4174b78c9d52c Mon Sep 17 00:00:00 2001 From: Merlin Fisher-Levine Date: Tue, 17 Mar 2026 07:19:18 -0700 Subject: [PATCH 13/13] Fix linting --- python/lsst/summit/utils/quickLook.py | 6 +++--- tests/test_quickLook.py | 13 +++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/python/lsst/summit/utils/quickLook.py b/python/lsst/summit/utils/quickLook.py index b9afc5b5..6237ceb4 100644 --- a/python/lsst/summit/utils/quickLook.py +++ b/python/lsst/summit/utils/quickLook.py @@ -23,7 +23,6 @@ import importlib.resources from typing import Any - import lsst.afw.cameraGeom as camGeom import lsst.afw.image as afwImage import lsst.ip.isr as ipIsr @@ -103,7 +102,7 @@ def __init__(self, isrTask: IsrTaskLSST = IsrTaskLSST, **kwargs: Any): # itself, which is then instantiated later on, in the run() method, # with the dynamically generated config. # import pdb; pdb.set_trace() - if IsrTaskLSST._DefaultName != 'isrLSST': + if IsrTaskLSST._DefaultName != "isrLSST": raise RuntimeError("QuickLookIsrTask should now always use IsrTaskLSST for processing.") self.isrTask = IsrTaskLSST @@ -264,7 +263,8 @@ def run( # isrTask.fringe.loadFringes( # fringes, # expId=ccdExposure.info.id, - # assembler=isrTask.assembleCcd if isrConfig.doAssembleIsrExposures else None, + # assembler=isrTask.assembleCcd if + # isrConfig.doAssembleIsrExposures else None, # ) result = isrTask.run( diff --git a/tests/test_quickLook.py b/tests/test_quickLook.py index afa93736..fdd82e70 100644 --- a/tests/test_quickLook.py +++ b/tests/test_quickLook.py @@ -168,7 +168,9 @@ def setUp(self): self.repo, outputExposure, {"instrument", "exposure", "detector"}, "Exposure" ) butlerTests.addDatasetType(self.repo, exposure, {"instrument", "exposure", "detector"}, "Exposure") - butlerTests.addDatasetType(self.repo, outputStatistics, {"instrument", "exposure", "detector"}, "StructuredDataDict") + butlerTests.addDatasetType( + self.repo, outputStatistics, {"instrument", "exposure", "detector"}, "StructuredDataDict" + ) # dataIds self.exposure_id = self.repo.registry.expandDataId( { @@ -221,8 +223,9 @@ def setUp(self): myCti.driftScale[amp.getName()] = 1.8e-5 myCti.decayTime[amp.getName()] = 3.0 myCti.globalCti[amp.getName()] = 1.5e-7 - myCti.serialTraps[amp.getName()] = lsst.ip.isr.deferredCharge.SerialTrap(10, 3, 200, - 'linear', [5, 1e-3]) + myCti.serialTraps[amp.getName()] = lsst.ip.isr.deferredCharge.SerialTrap( + 10, 3, 200, "linear", [5, 1e-3] + ) self.butler.put(myCti, "cti", self.detector_id) self.butler.put(lsst.ip.isr.gainCorrection.GainCorrection(), gainCorrection, self.detector_id) @@ -233,7 +236,6 @@ def setUp(self): self.butler.put(lsst.ip.isr.linearize.Linearizer(), linearizer, self.detector_id) - def tearDown(self): del self.repo_path # this removes the temporary directory @@ -247,8 +249,7 @@ def test_runQuantum(self): config.doCalculateStatistics = False # Turn on all optional inputs - config.doDeferredCharge = False # There is no CTI calibration - # available for LATISS. + config.doDeferredCharge = False # There is no CTI calibration available for LATISS. config.doCrosstalk = True config.doBrighterFatter = True