LSST Applications  21.0.0-131-g8cabc107+528f53ee53,22.0.0+00495a2688,22.0.0+0ef2527977,22.0.0+11a2aa21cd,22.0.0+269b7e55e3,22.0.0+2c6b6677a3,22.0.0+64c1bc5aa5,22.0.0+7b3a3f865e,22.0.0+e1b6d2281c,22.0.0+ff3c34362c,22.0.1-1-g1b65d06+c95cbdf3df,22.0.1-1-g7058be7+1cf78af69b,22.0.1-1-g7dab645+2a65e40b06,22.0.1-1-g8760c09+64c1bc5aa5,22.0.1-1-g949febb+64c1bc5aa5,22.0.1-1-ga324b9c+269b7e55e3,22.0.1-1-gf9d8b05+ff3c34362c,22.0.1-10-g781e53d+9b51d1cd24,22.0.1-10-gba590ab+b9624b875d,22.0.1-13-g76f9b8d+2c6b6677a3,22.0.1-14-g22236948+57af756299,22.0.1-18-g3db9cf4b+9b7092c56c,22.0.1-18-gb17765a+2264247a6b,22.0.1-2-g8ef0a89+2c6b6677a3,22.0.1-2-gcb770ba+c99495d3c6,22.0.1-24-g2e899d296+4206820b0d,22.0.1-3-g7aa11f2+2c6b6677a3,22.0.1-3-g8c1d971+f253ffa91f,22.0.1-3-g997b569+ff3b2f8649,22.0.1-4-g1930a60+6871d0c7f6,22.0.1-4-g5b7b756+6b209d634c,22.0.1-6-ga02864e+6871d0c7f6,22.0.1-7-g3402376+a1a2182ac4,22.0.1-7-g65f59fa+54b92689ce,master-gcc5351303a+e1b6d2281c,w.2021.32
LSST Data Management Base Package
Public Member Functions | Static Public Attributes | List of all members
lsst.cp.pipe.ptc.cpExtractPtcTask.PhotonTransferCurveExtractTask Class Reference
Inheritance diagram for lsst.cp.pipe.ptc.cpExtractPtcTask.PhotonTransferCurveExtractTask:

Public Member Functions

def runQuantum (self, butlerQC, inputRefs, outputRefs)
 
def run (self, inputExp, inputDims)
 
def measureMeanVarCov (self, exposure1, exposure2, region=None, covAstierRealSpace=False)
 

Static Public Attributes

 ConfigClass = PhotonTransferCurveExtractConfig
 

Detailed Description

Task to measure covariances from flat fields.
This task receives as input a list of flat-field images
(flats), and sorts these flats in pairs taken at the
same time (if there's a different number of flats,
those flats are discarded). The mean, variance, and
covariances are measured from the difference of the flat
pairs at a given time. The variance is calculated
via afwMath, and the covariance via the methods in Astier+19
(appendix A). In theory, var = covariance[0,0]. This should
be validated, and in the future, we may decide to just keep
one (covariance).

The measured covariances at a particular time (along with
other quantities such as the mean) are stored in a PTC dataset
object (`PhotonTransferCurveDataset`), which gets partially
filled. The number of partially-filled PTC dataset objects
will be less than the number of input exposures, but gen3
requires/assumes that the number of input dimensions matches
bijectively the number of output dimensions. Therefore, a
number of "dummy" PTC dataset are inserted in the output list
that has the partially-filled PTC datasets with the covariances.
This output list will be used as input of
`PhotonTransferCurveSolveTask`, which will assemble the multiple
`PhotonTransferCurveDataset`s into a single one in order to fit
the measured covariances as a function of flux to a particular
model.

Astier+19: "The Shape of the Photon Transfer Curve of CCD
sensors", arXiv:1905.08677.

Definition at line 156 of file cpExtractPtcTask.py.

Member Function Documentation

◆ measureMeanVarCov()

def lsst.cp.pipe.ptc.cpExtractPtcTask.PhotonTransferCurveExtractTask.measureMeanVarCov (   self,
  exposure1,
  exposure2,
  region = None,
  covAstierRealSpace = False 
)
Calculate the mean of each of two exposures and the variance
and covariance of their difference. The variance is calculated
via afwMath, and the covariance via the methods in Astier+19
(appendix A). In theory, var = covariance[0,0]. This should
be validated, and in the future, we may decide to just keep
one (covariance).

Parameters
----------
exposure1 : `lsst.afw.image.exposure.exposure.ExposureF`
    First exposure of flat field pair.
exposure2 : `lsst.afw.image.exposure.exposure.ExposureF`
    Second exposure of flat field pair.
region : `lsst.geom.Box2I`, optional
    Region of each exposure where to perform the calculations (e.g, an amplifier).
covAstierRealSpace : `bool`, optional
    Should the covariannces in Astier+19 be calculated in real space or via FFT?
    See Appendix A of Astier+19.

Returns
-------
mu : `float` or `NaN`
    0.5*(mu1 + mu2), where mu1, and mu2 are the clipped means of the regions in
    both exposures. If either mu1 or m2 are NaN's, the returned value is NaN.
varDiff : `float` or `NaN`
    Half of the clipped variance of the difference of the regions inthe two input
    exposures. If either mu1 or m2 are NaN's, the returned value is NaN.
covDiffAstier : `list` or `NaN`
    List with tuples of the form (dx, dy, var, cov, npix), where:
        dx : `int`
            Lag in x
        dy : `int`
            Lag in y
        var : `float`
            Variance at (dx, dy).
        cov : `float`
            Covariance at (dx, dy).
        nPix : `int`
            Number of pixel pairs used to evaluate var and cov.
    If either mu1 or m2 are NaN's, the returned value is NaN.

Definition at line 361 of file cpExtractPtcTask.py.

361  def measureMeanVarCov(self, exposure1, exposure2, region=None, covAstierRealSpace=False):
362  """Calculate the mean of each of two exposures and the variance
363  and covariance of their difference. The variance is calculated
364  via afwMath, and the covariance via the methods in Astier+19
365  (appendix A). In theory, var = covariance[0,0]. This should
366  be validated, and in the future, we may decide to just keep
367  one (covariance).
368 
369  Parameters
370  ----------
371  exposure1 : `lsst.afw.image.exposure.exposure.ExposureF`
372  First exposure of flat field pair.
373  exposure2 : `lsst.afw.image.exposure.exposure.ExposureF`
374  Second exposure of flat field pair.
375  region : `lsst.geom.Box2I`, optional
376  Region of each exposure where to perform the calculations (e.g, an amplifier).
377  covAstierRealSpace : `bool`, optional
378  Should the covariannces in Astier+19 be calculated in real space or via FFT?
379  See Appendix A of Astier+19.
380 
381  Returns
382  -------
383  mu : `float` or `NaN`
384  0.5*(mu1 + mu2), where mu1, and mu2 are the clipped means of the regions in
385  both exposures. If either mu1 or m2 are NaN's, the returned value is NaN.
386  varDiff : `float` or `NaN`
387  Half of the clipped variance of the difference of the regions inthe two input
388  exposures. If either mu1 or m2 are NaN's, the returned value is NaN.
389  covDiffAstier : `list` or `NaN`
390  List with tuples of the form (dx, dy, var, cov, npix), where:
391  dx : `int`
392  Lag in x
393  dy : `int`
394  Lag in y
395  var : `float`
396  Variance at (dx, dy).
397  cov : `float`
398  Covariance at (dx, dy).
399  nPix : `int`
400  Number of pixel pairs used to evaluate var and cov.
401  If either mu1 or m2 are NaN's, the returned value is NaN.
402  """
403 
404  if region is not None:
405  im1Area = exposure1.maskedImage[region]
406  im2Area = exposure2.maskedImage[region]
407  else:
408  im1Area = exposure1.maskedImage
409  im2Area = exposure2.maskedImage
410 
411  if self.config.binSize > 1:
412  im1Area = afwMath.binImage(im1Area, self.config.binSize)
413  im2Area = afwMath.binImage(im2Area, self.config.binSize)
414 
415  im1MaskVal = exposure1.getMask().getPlaneBitMask(self.config.maskNameList)
416  im1StatsCtrl = afwMath.StatisticsControl(self.config.nSigmaClipPtc,
417  self.config.nIterSigmaClipPtc,
418  im1MaskVal)
419  im1StatsCtrl.setNanSafe(True)
420  im1StatsCtrl.setAndMask(im1MaskVal)
421 
422  im2MaskVal = exposure2.getMask().getPlaneBitMask(self.config.maskNameList)
423  im2StatsCtrl = afwMath.StatisticsControl(self.config.nSigmaClipPtc,
424  self.config.nIterSigmaClipPtc,
425  im2MaskVal)
426  im2StatsCtrl.setNanSafe(True)
427  im2StatsCtrl.setAndMask(im2MaskVal)
428 
429  # Clipped mean of images; then average of mean.
430  mu1 = afwMath.makeStatistics(im1Area, afwMath.MEANCLIP, im1StatsCtrl).getValue()
431  mu2 = afwMath.makeStatistics(im2Area, afwMath.MEANCLIP, im2StatsCtrl).getValue()
432  if np.isnan(mu1) or np.isnan(mu2):
433  self.log.warn(f"Mean of amp in image 1 or 2 is NaN: {mu1}, {mu2}.")
434  return np.nan, np.nan, None
435  mu = 0.5*(mu1 + mu2)
436 
437  # Take difference of pairs
438  # symmetric formula: diff = (mu2*im1-mu1*im2)/(0.5*(mu1+mu2))
439  temp = im2Area.clone()
440  temp *= mu1
441  diffIm = im1Area.clone()
442  diffIm *= mu2
443  diffIm -= temp
444  diffIm /= mu
445 
446  diffImMaskVal = diffIm.getMask().getPlaneBitMask(self.config.maskNameList)
447  diffImStatsCtrl = afwMath.StatisticsControl(self.config.nSigmaClipPtc,
448  self.config.nIterSigmaClipPtc,
449  diffImMaskVal)
450  diffImStatsCtrl.setNanSafe(True)
451  diffImStatsCtrl.setAndMask(diffImMaskVal)
452 
453  # Variance calculation via afwMath
454  varDiff = 0.5*(afwMath.makeStatistics(diffIm, afwMath.VARIANCECLIP, diffImStatsCtrl).getValue())
455 
456  # Covariances calculations
457  # Get the pixels that were not clipped
458  varClip = afwMath.makeStatistics(diffIm, afwMath.VARIANCECLIP, diffImStatsCtrl).getValue()
459  meanClip = afwMath.makeStatistics(diffIm, afwMath.MEANCLIP, diffImStatsCtrl).getValue()
460  cut = meanClip + self.config.nSigmaClipPtc*np.sqrt(varClip)
461  unmasked = np.where(np.fabs(diffIm.image.array) <= cut, 1, 0)
462 
463  # Get the pixels in the mask planes of teh differenc eimage that were ignored
464  # by the clipping algorithm
465  wDiff = np.where(diffIm.getMask().getArray() == 0, 1, 0)
466  # Combine the two sets of pixels ('1': use; '0': don't use) into a final weight matrix
467  # to be used in the covariance calculations below.
468  w = unmasked*wDiff
469 
470  if np.sum(w) < self.config.minNumberGoodPixelsForCovariance:
471  self.log.warn(f"Number of good points for covariance calculation ({np.sum(w)}) is less "
472  f"(than threshold {self.config.minNumberGoodPixelsForCovariance})")
473  return np.nan, np.nan, None
474 
475  maxRangeCov = self.config.maximumRangeCovariancesAstier
476  if covAstierRealSpace:
477  # Calculate covariances in real space.
478  covDiffAstier = computeCovDirect(diffIm.image.array, w, maxRangeCov)
479  else:
480  # Calculate covariances via FFT (default).
481  shapeDiff = np.array(diffIm.image.array.shape)
482  # Calculate the sizes of FFT dimensions.
483  s = shapeDiff + maxRangeCov
484  tempSize = np.array(np.log(s)/np.log(2.)).astype(int)
485  fftSize = np.array(2**(tempSize+1)).astype(int)
486  fftShape = (fftSize[0], fftSize[1])
487 
488  c = CovFastFourierTransform(diffIm.image.array, w, fftShape, maxRangeCov)
489  covDiffAstier = c.reportCovFastFourierTransform(maxRangeCov)
490 
491  # Compare Cov[0,0] and afwMath.VARIANCECLIP
492  # covDiffAstier[0] is the Cov[0,0] element, [3] is the variance, and there's a factor of 0.5
493  # difference with afwMath.VARIANCECLIP.
494  thresholdPercentage = self.config.thresholdDiffAfwVarVsCov00
495  fractionalDiff = 100*np.fabs(1 - varDiff/(covDiffAstier[0][3]*0.5))
496  if fractionalDiff >= thresholdPercentage:
497  self.log.warn("Absolute fractional difference between afwMatch.VARIANCECLIP and Cov[0,0] "
498  f"is more than {thresholdPercentage}%: {fractionalDiff}")
499 
500  return mu, varDiff, covDiffAstier
Pass parameters to a Statistics object.
Definition: Statistics.h:93
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition: Statistics.h:360
std::shared_ptr< ImageT > binImage(ImageT const &inImage, int const binX, int const binY, lsst::afw::math::Property const flags=lsst::afw::math::MEAN)
Definition: binImage.cc:43
def computeCovDirect(diffImage, weightImage, maxRange)

◆ run()

def lsst.cp.pipe.ptc.cpExtractPtcTask.PhotonTransferCurveExtractTask.run (   self,
  inputExp,
  inputDims 
)
Measure covariances from difference of flat pairs

Parameters
----------
inputExp : `dict` [`float`,
                (`~lsst.afw.image.exposure.exposure.ExposureF`,
                `~lsst.afw.image.exposure.exposure.ExposureF`, ...,
                `~lsst.afw.image.exposure.exposure.ExposureF`)]
    Dictionary that groups flat-field exposures that have the same
    exposure time (seconds).

inputDims : `list`
    List of exposure IDs.

Definition at line 216 of file cpExtractPtcTask.py.

216  def run(self, inputExp, inputDims):
217  """Measure covariances from difference of flat pairs
218 
219  Parameters
220  ----------
221  inputExp : `dict` [`float`,
222  (`~lsst.afw.image.exposure.exposure.ExposureF`,
223  `~lsst.afw.image.exposure.exposure.ExposureF`, ...,
224  `~lsst.afw.image.exposure.exposure.ExposureF`)]
225  Dictionary that groups flat-field exposures that have the same
226  exposure time (seconds).
227 
228  inputDims : `list`
229  List of exposure IDs.
230  """
231  # inputExp.values() returns a view, which we turn into a list. We then
232  # access the first exposure-ID tuple to get the detector.
233  detector = list(inputExp.values())[0][0][0].getDetector()
234  detNum = detector.getId()
235  amps = detector.getAmplifiers()
236  ampNames = [amp.getName() for amp in amps]
237 
238  # Each amp may have a different min and max ADU signal specified in the config.
239  maxMeanSignalDict = {ampName: 1e6 for ampName in ampNames}
240  minMeanSignalDict = {ampName: 0.0 for ampName in ampNames}
241  for ampName in ampNames:
242  if 'ALL_AMPS' in self.config.maxMeanSignal:
243  maxMeanSignalDict[ampName] = self.config.maxMeanSignal['ALL_AMPS']
244  elif ampName in self.config.maxMeanSignal:
245  maxMeanSignalDict[ampName] = self.config.maxMeanSignal[ampName]
246 
247  if 'ALL_AMPS' in self.config.minMeanSignal:
248  minMeanSignalDict[ampName] = self.config.minMeanSignal['ALL_AMPS']
249  elif ampName in self.config.minMeanSignal:
250  minMeanSignalDict[ampName] = self.config.minMeanSignal[ampName]
251  # These are the column names for `tupleRows` below.
252  tags = [('mu', '<f8'), ('afwVar', '<f8'), ('i', '<i8'), ('j', '<i8'), ('var', '<f8'),
253  ('cov', '<f8'), ('npix', '<i8'), ('ext', '<i8'), ('expTime', '<f8'), ('ampName', '<U3')]
254  # Create a dummy ptcDataset
255  dummyPtcDataset = PhotonTransferCurveDataset(ampNames, 'DUMMY',
256  self.config.maximumRangeCovariancesAstier)
257  # Initialize amps of `dummyPtcDatset`.
258  for ampName in ampNames:
259  dummyPtcDataset.setAmpValues(ampName)
260  # Output list with PTC datasets.
261  partialPtcDatasetList = []
262  # The number of output references needs to match that of input references:
263  # initialize outputlist with dummy PTC datasets.
264  for i in range(len(inputDims)):
265  partialPtcDatasetList.append(dummyPtcDataset)
266 
267  if self.config.numEdgeSuspect > 0:
268  isrTask = IsrTask()
269  self.log.info(f"Masking {self.config.numEdgeSuspect} pixels from the edges "
270  "of all exposures as SUSPECT.")
271 
272  for expTime in inputExp:
273  exposures = inputExp[expTime]
274  if len(exposures) == 1:
275  self.log.warn(f"Only one exposure found at expTime {expTime}. Dropping exposure "
276  f"{exposures[0][1]}")
277  continue
278  else:
279  # Only use the first two exposures at expTime. Each elements is a tuple (exposure, expId)
280  exp1, expId1 = exposures[0]
281  exp2, expId2 = exposures[1]
282  if len(exposures) > 2:
283  self.log.warn(f"Already found 2 exposures at expTime {expTime}. "
284  "Ignoring exposures: "
285  f"{i[1] for i in exposures[2:]}")
286  # Mask pixels at the edge of the detector or of each amp
287  if self.config.numEdgeSuspect > 0:
288  isrTask.maskEdges(exp1, numEdgePixels=self.config.numEdgeSuspect,
289  maskPlane="SUSPECT", level=self.config.edgeMaskLevel)
290  isrTask.maskEdges(exp2, numEdgePixels=self.config.numEdgeSuspect,
291  maskPlane="SUSPECT", level=self.config.edgeMaskLevel)
292 
293  nAmpsNan = 0
294  partialPtcDataset = PhotonTransferCurveDataset(ampNames, '',
295  self.config.maximumRangeCovariancesAstier)
296  for ampNumber, amp in enumerate(detector):
297  ampName = amp.getName()
298  # covAstier: [(i, j, var (cov[0,0]), cov, npix) for (i,j) in {maxLag, maxLag}^2]
299  doRealSpace = self.config.covAstierRealSpace
300  if self.config.detectorMeasurementRegion == 'AMP':
301  region = amp.getBBox()
302  elif self.config.detectorMeasurementRegion == 'FULL':
303  region = None
304  # `measureMeanVarCov` is the function that measures the variance and covariances from
305  # the difference image of two flats at the same exposure time.
306  # The variable `covAstier` is of the form: [(i, j, var (cov[0,0]), cov, npix) for (i,j)
307  # in {maxLag, maxLag}^2]
308  muDiff, varDiff, covAstier = self.measureMeanVarCov(exp1, exp2, region=region,
309  covAstierRealSpace=doRealSpace)
310  # Correction factor for sigma clipping. Function returns 1/sqrt(varFactor),
311  # so it needs to be squared. varDiff is calculated via afwMath.VARIANCECLIP.
312  varFactor = sigmaClipCorrection(self.config.nSigmaClipPtc)**2
313  varDiff *= varFactor
314 
315  expIdMask = True
316  if np.isnan(muDiff) or np.isnan(varDiff) or (covAstier is None):
317  msg = (f"NaN mean or var, or None cov in amp {ampName} in exposure pair {expId1},"
318  f" {expId2} of detector {detNum}.")
319  self.log.warn(msg)
320  nAmpsNan += 1
321  expIdMask = False
322  covArray = np.full((1, self.config.maximumRangeCovariancesAstier,
323  self.config.maximumRangeCovariancesAstier), np.nan)
324  covSqrtWeights = np.full_like(covArray, np.nan)
325 
326  if (muDiff <= minMeanSignalDict[ampName]) or (muDiff >= maxMeanSignalDict[ampName]):
327  expIdMask = False
328 
329  if covAstier is not None:
330  tupleRows = [(muDiff, varDiff) + covRow + (ampNumber, expTime,
331  ampName) for covRow in covAstier]
332  tempStructArray = np.array(tupleRows, dtype=tags)
333  covArray, vcov, _ = makeCovArray(tempStructArray,
334  self.config.maximumRangeCovariancesAstier)
335  covSqrtWeights = np.nan_to_num(1./np.sqrt(vcov))
336 
337  # Correct covArray for sigma clipping:
338  # 1) Apply varFactor twice for the whole covariance matrix
339  covArray *= varFactor**2
340  # 2) But, only once for the variance element of the matrix, covArray[0,0]
341  covArray[0, 0] /= varFactor
342 
343  partialPtcDataset.setAmpValues(ampName, rawExpTime=[expTime], rawMean=[muDiff],
344  rawVar=[varDiff], inputExpIdPair=[(expId1, expId2)],
345  expIdMask=[expIdMask], covArray=covArray,
346  covSqrtWeights=covSqrtWeights)
347  # Use location of exp1 to save PTC dataset from (exp1, exp2) pair.
348  # Below, np.where(expId1 == np.array(inputDims)) returns a tuple
349  # with a single-element array, so [0][0]
350  # is necessary to extract the required index.
351  datasetIndex = np.where(expId1 == np.array(inputDims))[0][0]
352  partialPtcDatasetList[datasetIndex] = partialPtcDataset
353 
354  if nAmpsNan == len(ampNames):
355  msg = f"NaN mean in all amps of exposure pair {expId1}, {expId2} of detector {detNum}."
356  self.log.warn(msg)
357  return pipeBase.Struct(
358  outputCovariances=partialPtcDatasetList,
359  )
360 
daf::base::PropertyList * list
Definition: fits.cc:913
def makeCovArray(inputTuple, maxRangeFromTuple=8)
def sigmaClipCorrection(nSigClip)
Definition: utils.py:42
def run(self, skyInfo, tempExpRefList, imageScalerList, weightList, altMaskList=None, mask=None, supplementaryData=None)

◆ runQuantum()

def lsst.cp.pipe.ptc.cpExtractPtcTask.PhotonTransferCurveExtractTask.runQuantum (   self,
  butlerQC,
  inputRefs,
  outputRefs 
)
Ensure that the input and output dimensions are passed along.

Parameters
----------
butlerQC : `~lsst.daf.butler.butlerQuantumContext.ButlerQuantumContext`
    Butler to operate on.
inputRefs : `~lsst.pipe.base.connections.InputQuantizedConnection`
    Input data refs to load.
ouptutRefs : `~lsst.pipe.base.connections.OutputQuantizedConnection`
    Output data refs to persist.

Definition at line 191 of file cpExtractPtcTask.py.

191  def runQuantum(self, butlerQC, inputRefs, outputRefs):
192  """Ensure that the input and output dimensions are passed along.
193 
194  Parameters
195  ----------
196  butlerQC : `~lsst.daf.butler.butlerQuantumContext.ButlerQuantumContext`
197  Butler to operate on.
198  inputRefs : `~lsst.pipe.base.connections.InputQuantizedConnection`
199  Input data refs to load.
200  ouptutRefs : `~lsst.pipe.base.connections.OutputQuantizedConnection`
201  Output data refs to persist.
202  """
203  inputs = butlerQC.get(inputRefs)
204  # Ids of input list of exposures
205  inputs['inputDims'] = [expId.dataId['exposure'] for expId in inputRefs.inputExp]
206 
207  # Dictionary, keyed by expTime, with tuples containing flat exposures and their IDs.
208  if self.config.matchByExposureId:
209  inputs['inputExp'] = arrangeFlatsByExpId(inputs['inputExp'], inputs['inputDims'])
210  else:
211  inputs['inputExp'] = arrangeFlatsByExpTime(inputs['inputExp'], inputs['inputDims'])
212 
213  outputs = self.run(**inputs)
214  butlerQC.put(outputs, outputRefs)
215 
def arrangeFlatsByExpTime(exposureList, exposureIdList)
Definition: utils.py:594
def arrangeFlatsByExpId(exposureList, exposureIdList)
Definition: utils.py:622

Member Data Documentation

◆ ConfigClass

lsst.cp.pipe.ptc.cpExtractPtcTask.PhotonTransferCurveExtractTask.ConfigClass = PhotonTransferCurveExtractConfig
static

Definition at line 188 of file cpExtractPtcTask.py.


The documentation for this class was generated from the following file: