Skip to content

Commit

Permalink
Merge branch 'tickets/DM-27010'
Browse files Browse the repository at this point in the history
  • Loading branch information
plazas committed Apr 23, 2021
2 parents 07a2928 + 2b0fd27 commit fb99f82
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 12 deletions.
9 changes: 7 additions & 2 deletions python/lsst/ip/isr/isrFunctions.py
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ def attachTransmissionCurve(exposure, opticsTransmission=None, filterTransmissio
return combined


def applyGains(exposure, normalizeGains=False):
def applyGains(exposure, normalizeGains=False, ptcGains=None):
"""Scale an exposure by the amplifier gains.
Parameters
Expand All @@ -753,14 +753,19 @@ def applyGains(exposure, normalizeGains=False):
normalizeGains : `Bool`, optional
If True, then amplifiers are scaled to force the median of
each amplifier to equal the median of those medians.
ptcGains : `dict`[`str`], optional
Dictionary keyed by amp name containing the PTC gains.
"""
ccd = exposure.getDetector()
ccdImage = exposure.getMaskedImage()

medians = []
for amp in ccd:
sim = ccdImage.Factory(ccdImage, amp.getBBox())
sim *= amp.getGain()
if ptcGains:
sim *= ptcGains[amp.getName()]
else:
sim *= amp.getGain()

if normalizeGains:
medians.append(numpy.median(sim.getImage().getArray()))
Expand Down
73 changes: 63 additions & 10 deletions python/lsst/ip/isr/isrTask.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ class IsrTaskConnections(pipeBase.PipelineTaskConnections,
dimensions=["instrument", "physical_filter", "detector"],
isCalibration=True,
)
ptc = cT.PrerequisiteInput(
name="ptc",
doc="Input Photon Transfer Curve dataset",
storageClass="PhotonTransferCurveDataset",
dimensions=["instrument", "detector"],
isCalibration=True,
)
fringes = cT.PrerequisiteInput(
name="fringe",
doc="Input fringe calibration.",
Expand Down Expand Up @@ -274,6 +281,8 @@ def __init__(self, *, config=None):
self.prerequisiteInputs.discard("dark")
if config.doFlat is not True:
self.prerequisiteInputs.discard("flat")
if config.usePtcGains is not True and config.usePtcReadNoise is not True:
self.prerequisiteInputs.discard("ptc")
if config.doAttachTransmissionCurve is not True:
self.prerequisiteInputs.discard("opticsTransmission")
self.prerequisiteInputs.discard("filterTransmission")
Expand Down Expand Up @@ -565,7 +574,11 @@ class IsrTaskConfig(pipeBase.PipelineTaskConfig,
default=False,
doc="Calculate empirical read noise instead of value from AmpInfo data?"
)

usePtcReadNoise = pexConfig.Field(
dtype=bool,
default=False,
doc="Use readnoise values from the Photon Transfer Curve?"
)
# Linearization.
doLinearize = pexConfig.Field(
dtype=bool,
Expand Down Expand Up @@ -712,6 +725,11 @@ class IsrTaskConfig(pipeBase.PipelineTaskConfig,
doc="Correct the amplifiers for their gains instead of applying flat correction",
default=False,
)
usePtcGains = pexConfig.Field(
dtype=bool,
doc="Use the gain values from the Photon Transfer Curve?",
default=False,
)
normalizeGains = pexConfig.Field(
dtype=bool,
doc="Normalize all the amplifiers in each CCD to have the same median value.",
Expand Down Expand Up @@ -1196,9 +1214,9 @@ def readIsrData(self, dataRef, rawExposure):
)

@pipeBase.timeMethod
def run(self, ccdExposure, camera=None, bias=None, linearizer=None,
def run(self, ccdExposure, *, camera=None, bias=None, linearizer=None,
crosstalk=None, crosstalkSources=None,
dark=None, flat=None, bfKernel=None, bfGains=None, defects=None,
dark=None, flat=None, ptc=None, bfKernel=None, bfGains=None, defects=None,
fringes=pipeBase.Struct(fringes=None), opticsTransmission=None, filterTransmission=None,
sensorTransmission=None, atmosphereTransmission=None,
detectorNum=None, strayLightData=None, illumMaskedImage=None,
Expand Down Expand Up @@ -1244,6 +1262,9 @@ def run(self, ccdExposure, camera=None, bias=None, linearizer=None,
Dark calibration frame.
flat : `lsst.afw.image.Exposure`, optional
Flat calibration frame.
ptc : `lsst.ip.isr.PhotonTransferCurveDataset`, optional
Photon transfer curve dataset, with, e.g., gains
and read noise.
bfKernel : `numpy.ndarray`, optional
Brighter-fatter kernel.
bfGains : `dict` of `float`, optional
Expand Down Expand Up @@ -1460,10 +1481,12 @@ def run(self, ccdExposure, camera=None, bias=None, linearizer=None,
ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
if overscanResults is not None:
self.updateVariance(ampExposure, amp,
overscanImage=overscanResults.overscanImage)
overscanImage=overscanResults.overscanImage,
ptcDataset=ptc)
else:
self.updateVariance(ampExposure, amp,
overscanImage=None)
overscanImage=None,
ptcDataset=ptc)
if self.config.qa is not None and self.config.qa.saveStats is True:
qaStats = afwMath.makeStatistics(ampExposure.getVariance(),
afwMath.MEDIAN | afwMath.STDEVCLIP)
Expand Down Expand Up @@ -1588,7 +1611,12 @@ def run(self, ccdExposure, camera=None, bias=None, linearizer=None,

if self.config.doApplyGains:
self.log.info("Applying gain correction instead of flat.")
isrFunctions.applyGains(ccdExposure, self.config.normalizeGains)
if self.config.usePtcGains:
self.log.info("Using gains from the Photon Transfer Curve.")
isrFunctions.applyGains(ccdExposure, self.config.normalizeGains,
ptcGains=ptc.gain)
else:
isrFunctions.applyGains(ccdExposure, self.config.normalizeGains)

if self.config.doFringe and self.config.fringeAfterFlat:
self.log.info("Applying fringe correction after flat.")
Expand Down Expand Up @@ -2065,8 +2093,8 @@ def overscanCorrection(self, ccdExposure, amp):

return overscanResults

def updateVariance(self, ampExposure, amp, overscanImage=None):
"""Set the variance plane using the amplifier gain and read noise
def updateVariance(self, ampExposure, amp, overscanImage=None, ptcDataset=None):
"""Set the variance plane using the gain and read noise
The read noise is calculated from the ``overscanImage`` if the
``doEmpiricalReadNoise`` option is set in the configuration; otherwise
Expand All @@ -2080,13 +2108,32 @@ def updateVariance(self, ampExposure, amp, overscanImage=None):
Amplifier detector data.
overscanImage : `lsst.afw.image.MaskedImage`, optional.
Image of overscan, required only for empirical read noise.
ptcDataset : `lsst.ip.isr.PhotonTransferCurveDataset`, optional
PTC dataset containing the gains and read noise.
Raises
------
RuntimeError
Raised if either ``usePtcGains`` of ``usePtcReadNoise``
are ``True``, but ptcDataset is not provided.
Raised if ```doEmpiricalReadNoise`` is ``True`` but
``overscanImage`` is ``None``.
See also
--------
lsst.ip.isr.isrFunctions.updateVariance
"""
maskPlanes = [self.config.saturatedMaskName, self.config.suspectMaskName]
gain = amp.getGain()
if self.config.usePtcGains:
if ptcDataset is None:
raise RuntimeError("No ptcDataset provided to use PTC gains.")
else:
gain = ptcDataset.gain[amp.getName()]
self.log.info("Using gain from Photon Transfer Curve.")
else:
gain = amp.getGain()

if math.isnan(gain):
gain = 1.0
Expand All @@ -2098,14 +2145,20 @@ def updateVariance(self, ampExposure, amp, overscanImage=None):
gain = patchedGain

if self.config.doEmpiricalReadNoise and overscanImage is None:
self.log.info("Overscan is none for EmpiricalReadNoise.")
raise RuntimeError("Overscan is none for EmpiricalReadNoise.")

if self.config.doEmpiricalReadNoise and overscanImage is not None:
stats = afwMath.StatisticsControl()
stats.setAndMask(overscanImage.mask.getPlaneBitMask(maskPlanes))
readNoise = afwMath.makeStatistics(overscanImage, afwMath.STDEVCLIP, stats).getValue()
self.log.info("Calculated empirical read noise for amp %s: %f.",
amp.getName(), readNoise)
elif self.config.usePtcReadNoise:
if ptcDataset is None:
raise RuntimeError("No ptcDataset provided to use PTC readnoise.")
else:
readNoise = ptcDataset.noise[amp.getName()]
self.log.info("Using read noise from Photon Transfer Curve.")
else:
readNoise = amp.getReadNoise()

Expand Down

0 comments on commit fb99f82

Please sign in to comment.