LSSTApplications  18.1.0
LSSTDataManagementBasePackage
makeBrighterFatterKernel.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 #
4 # Copyright 2008-2017 AURA/LSST.
5 #
6 # This product includes software developed by the
7 # LSST Project (http://www.lsst.org/).
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 LSST License Statement and
20 # the GNU General Public License along with this program. If not,
21 # see <https://www.lsstcorp.org/LegalNotices/>.
22 #
23 """Calculation of brighter-fatter effect correlations and kernels."""
24 
25 __all__ = ['MakeBrighterFatterKernelTaskConfig',
26  'MakeBrighterFatterKernelTask',
27  'calcBiasCorr']
28 
29 import os
30 from scipy import stats
31 import numpy as np
32 import matplotlib.pyplot as plt
33 # the following import is required for 3d projection
34 from mpl_toolkits.mplot3d import axes3d # noqa: F401
35 
36 import lsstDebug
37 import lsst.afw.image as afwImage
38 import lsst.afw.math as afwMath
39 import lsst.afw.display as afwDisp
40 from lsst.ip.isr import IsrTask
41 import lsst.pex.config as pexConfig
42 import lsst.pipe.base as pipeBase
43 from .utils import PairedVisitTaskRunner
44 
45 
46 class MakeBrighterFatterKernelTaskConfig(pexConfig.Config):
47  """Config class for bright-fatter effect coefficient calculation."""
48 
49  isr = pexConfig.ConfigurableField(
50  target=IsrTask,
51  doc="""Task to perform instrumental signature removal""",
52  )
53  isrMandatorySteps = pexConfig.ListField(
54  dtype=str,
55  doc="isr operations that must be performed for valid results. Raises if any of these are False",
56  default=['doAssembleCcd']
57  )
58  isrForbiddenSteps = pexConfig.ListField(
59  dtype=str,
60  doc="isr operations that must NOT be performed for valid results. Raises if any of these are True",
61  default=['doFlat', 'doFringe', 'doAddDistortionModel', 'doBrighterFatter', 'doUseOpticsTransmission',
62  'doUseFilterTransmission', 'doUseSensorTransmission', 'doUseAtmosphereTransmission']
63  )
64  isrDesirableSteps = pexConfig.ListField(
65  dtype=str,
66  doc="isr operations that it is advisable to perform, but are not mission-critical." +
67  " WARNs are logged for any of these found to be False.",
68  default=['doBias', 'doDark', 'doCrosstalk', 'doDefect', 'doLinearize']
69  )
70  doCalcGains = pexConfig.Field(
71  dtype=bool,
72  doc="Measure the per-amplifier gains (using the photon transfer curve method)?",
73  default=True,
74  )
75  ccdKey = pexConfig.Field(
76  dtype=str,
77  doc="The key by which to pull a detector from a dataId, e.g. 'ccd' or 'detector'",
78  default='ccd',
79  )
80  maxIterRegression = pexConfig.Field(
81  dtype=int,
82  doc="Maximum number of iterations for the regression fitter",
83  default=10
84  )
85  nSigmaClipGainCalc = pexConfig.Field(
86  dtype=int,
87  doc="Number of sigma to clip the pixel value distribution to during gain calculation",
88  default=5
89  )
90  nSigmaClipRegression = pexConfig.Field(
91  dtype=int,
92  doc="Number of sigma to clip outliers from the line of best fit to during iterative regression",
93  default=3
94  )
95  xcorrCheckRejectLevel = pexConfig.Field(
96  dtype=float,
97  doc="Sanity check level for the sum of the input cross-correlations. Arrays which " +
98  "sum to greater than this are discarded before the clipped mean is calculated.",
99  default=2.0
100  )
101  maxIterSuccessiveOverRelaxation = pexConfig.Field(
102  dtype=int,
103  doc="The maximum number of iterations allowed for the successive over-relaxation method",
104  default=10000
105  )
106  eLevelSuccessiveOverRelaxation = pexConfig.Field(
107  dtype=float,
108  doc="The target residual error for the successive over-relaxation method",
109  default=5.0e-14
110  )
111  nSigmaClipKernelGen = pexConfig.Field(
112  dtype=float,
113  doc="Number of sigma to clip to during pixel-wise clipping when generating the kernel. See " +
114  "the generateKernel docstring for more info.",
115  default=4
116  )
117  nSigmaClipXCorr = pexConfig.Field(
118  dtype=float,
119  doc="Number of sigma to clip when calculating means for the cross-correlation",
120  default=5
121  )
122  maxLag = pexConfig.Field(
123  dtype=int,
124  doc="The maximum lag (in pixels) to use when calculating the cross-correlation/kernel",
125  default=8
126  )
127  nPixBorderGainCalc = pexConfig.Field(
128  dtype=int,
129  doc="The number of border pixels to exclude when calculating the gain",
130  default=10
131  )
132  nPixBorderXCorr = pexConfig.Field(
133  dtype=int,
134  doc="The number of border pixels to exclude when calculating the cross-correlation and kernel",
135  default=10
136  )
137  biasCorr = pexConfig.Field(
138  dtype=float,
139  doc="An empirically determined correction factor, used to correct for the sigma-clipping of" +
140  " a non-Gaussian distribution. Post DM-15277, code will exist here to calulate appropriate values",
141  default=0.9241
142  )
143  backgroundBinSize = pexConfig.Field(
144  dtype=int,
145  doc="Size of the background bins",
146  default=128
147  )
148  fixPtcThroughOrigin = pexConfig.Field(
149  dtype=bool,
150  doc="Constrain the fit of the photon transfer curve to go through the origin when measuring" +
151  "the gain?",
152  default=True
153  )
154  level = pexConfig.ChoiceField(
155  doc="The level at which to calculate the brighter-fatter kernels",
156  dtype=str, default="DETECTOR",
157  allowed={
158  "AMP": "Every amplifier treated separately",
159  "DETECTOR": "One kernel per detector",
160  }
161  )
162  backgroundWarnLevel = pexConfig.Field(
163  dtype=float,
164  doc="Log warnings if the mean of the fitted background is found to be above this level after " +
165  "differencing image pair.",
166  default=0.1
167  )
168 
169 
170 class BrighterFatterKernelTaskDataIdContainer(pipeBase.DataIdContainer):
171  """A DataIdContainer for the MakeBrighterFatterKernelTask."""
172 
173  def makeDataRefList(self, namespace):
174  """Compute refList based on idList.
175 
176  This method must be defined as the dataset does not exist before this
177  task is run.
178 
179  Parameters
180  ----------
181  namespace
182  Results of parsing the command-line.
183 
184  Notes
185  -----
186  Not called if ``add_id_argument`` called
187  with ``doMakeDataRefList=False``.
188  Note that this is almost a copy-and-paste of the vanilla implementation,
189  but without checking if the datasets already exist,
190  as this task exists to make them.
191  """
192  if self.datasetType is None:
193  raise RuntimeError("Must call setDatasetType first")
194  butler = namespace.butler
195  for dataId in self.idList:
196  refList = list(butler.subset(datasetType=self.datasetType, level=self.level, dataId=dataId))
197  # exclude nonexistent data
198  # this is a recursive test, e.g. for the sake of "raw" data
199  if not refList:
200  namespace.log.warn("No data found for dataId=%s", dataId)
201  continue
202  self.refList += refList
203 
204 
206  """A (very) simple class to hold the kernel(s) generated.
207 
208  The kernel.kernel is a dictionary holding the kernels themselves.
209  One kernel if the level is 'DETECTOR' or,
210  nAmps in length, if level is 'AMP'.
211  The dict is keyed by either the detector ID or the amplifier IDs.
212 
213  The level is the level for which the kernel(s) were generated so that one
214  can know how to access the kernels without having to query the shape of
215  the dictionary holding the kernel(s).
216  """
217 
218  def __init__(self, level, kernelDict):
219  assert type(level) == str
220  assert type(kernelDict) == dict
221  if level == 'DETECTOR':
222  assert len(kernelDict.keys()) == 1
223  if level == 'AMP':
224  assert len(kernelDict.keys()) > 1
225 
226  self.level = level
227  self.kernel = kernelDict
228 
229 
230 class MakeBrighterFatterKernelTask(pipeBase.CmdLineTask):
231  """Brighter-fatter effect correction-kernel calculation task.
232 
233  A command line task for calculating the brighter-fatter correction
234  kernel from pairs of flat-field images (with the same exposure length).
235 
236  The following operations are performed:
237 
238  - The configurable isr task is called, which unpersists and assembles the
239  raw images, and performs the selected instrument signature removal tasks.
240  For the purpose of brighter-fatter coefficient calculation is it
241  essential that certain components of isr are *not* performed, and
242  recommended that certain others are. The task checks the selected isr
243  configuration before it is run, and if forbidden components have been
244  selected task will raise, and if recommended ones have not been selected,
245  warnings are logged.
246 
247  - The gain of the each amplifier in the detector is calculated using
248  the photon transfer curve (PTC) method and used to correct the images
249  so that all calculations are done in units of electrons, and so that the
250  level across amplifier boundaries is continuous.
251  Outliers in the PTC are iteratively rejected
252  before fitting, with the nSigma rejection level set by
253  config.nSigmaClipRegression. Individual pixels are ignored in the input
254  images the image based on config.nSigmaClipGainCalc.
255 
256  - Each image is then cross-correlated with the one it's paired with
257  (with the pairing defined by the --visit-pairs command line argument),
258  which is done either the whole-image to whole-image,
259  or amplifier-by-amplifier, depending on config.level.
260 
261  - Once the cross-correlations have been calculated for each visit pair,
262  these are used to generate the correction kernel.
263  The maximum lag used, in pixels, and hence the size of the half-size
264  of the kernel generated, is given by config.maxLag,
265  i.e. a value of 10 will result in a kernel of size 2n-1 = 19x19 pixels.
266  Outlier values in these cross-correlations are rejected by using a
267  pixel-wise sigma-clipped thresholding to each cross-correlation in
268  the visit-pairs-length stack of cross-correlations.
269  The number of sigma clipped to is set by config.nSigmaClipKernelGen.
270 
271  - Once DM-15277 has been completed, a method will exist to calculate the
272  empirical correction factor, config.biasCorr.
273  TODO: DM-15277 update this part of the docstring once the ticket is done.
274  """
275 
276  RunnerClass = PairedVisitTaskRunner
277  ConfigClass = MakeBrighterFatterKernelTaskConfig
278  _DefaultName = "makeBrighterFatterKernel"
279 
280  def __init__(self, *args, **kwargs):
281  pipeBase.CmdLineTask.__init__(self, *args, **kwargs)
282  self.makeSubtask("isr")
283 
284  self.debug = lsstDebug.Info(__name__)
285  if self.debug.enabled:
286  self.log.info("Running with debug enabled...")
287  # If we're displaying, test it works and save displays for later.
288  # It's worth testing here as displays are flaky and sometimes
289  # can't be contacted, and given processing takes a while,
290  # it's a shame to fail late due to display issues.
291  if self.debug.display:
292  try:
293  afwDisp.setDefaultBackend(self.debug.displayBackend)
294  afwDisp.Display.delAllDisplays()
295  self.disp1 = afwDisp.Display(0, open=True)
296  self.disp2 = afwDisp.Display(1, open=True)
297 
298  im = afwImage.ImageF(1, 1)
299  im.array[:] = [[1]]
300  self.disp1.mtv(im)
301  self.disp1.erase()
302  except NameError:
303  self.debug.display = False
304  self.log.warn('Failed to setup/connect to display! Debug display has been disabled')
305 
306  plt.interactive(False) # stop windows popping up when plotting. When headless, use 'agg' backend too
307  self.validateIsrConfig()
308  self.config.validate()
309  self.config.freeze()
310 
311  @classmethod
312  def _makeArgumentParser(cls):
313  """Augment argument parser for the MakeBrighterFatterKernelTask."""
314  parser = pipeBase.ArgumentParser(name=cls._DefaultName)
315  parser.add_argument("--visit-pairs", dest="visitPairs", nargs="*",
316  help="Visit pairs to use. Each pair must be of the form INT,INT e.g. 123,456")
317  parser.add_id_argument("--id", datasetType="brighterFatterKernel",
318  ContainerClass=BrighterFatterKernelTaskDataIdContainer,
319  help="The ccds to use, e.g. --id ccd=0..100")
320  return parser
321 
322  def validateIsrConfig(self):
323  """Check that appropriate ISR settings are being used
324  for brighter-fatter kernel calculation."""
325 
326  # How should we handle saturation/bad regions?
327  # 'doSaturationInterpolation': True
328  # 'doNanInterpAfterFlat': False
329  # 'doSaturation': True
330  # 'doSuspect': True
331  # 'doWidenSaturationTrails': True
332  # 'doSetBadRegions': True
333 
334  configDict = self.config.isr.toDict()
335 
336  for configParam in self.config.isrMandatorySteps:
337  if configDict[configParam] is False:
338  raise RuntimeError('Must set config.isr.%s to True '
339  'for brighter-fatter kernel calulation' % configParam)
340 
341  for configParam in self.config.isrForbiddenSteps:
342  if configDict[configParam] is True:
343  raise RuntimeError('Must set config.isr.%s to False '
344  'for brighter-fatter kernel calulation' % configParam)
345 
346  for configParam in self.config.isrDesirableSteps:
347  if configParam not in configDict:
348  self.log.info('Failed to find key %s in the isr config dict. You probably want ' +
349  'to set the equivalent for your obs_package to True.' % configParam)
350  continue
351  if configDict[configParam] is False:
352  self.log.warn('Found config.isr.%s set to False for brighter-fatter kernel calulation. '
353  'It is probably desirable to have this set to True' % configParam)
354 
355  # subtask settings
356  if not self.config.isr.assembleCcd.doTrim:
357  raise RuntimeError('Must trim when assembling CCDs. Set config.isr.assembleCcd.doTrim to True')
358 
359  @pipeBase.timeMethod
360  def runDataRef(self, dataRef, visitPairs):
361  """Run the brighter-fatter measurement task.
362 
363  For a dataRef (which is each detector here),
364  and given a list of visit pairs, calulate the
365  brighter-fatter kernel for the detector.
366 
367  Parameters
368  ----------
369  dataRef : list of lsst.daf.persistence.ButlerDataRef
370  dataRef for the detector for the visits to be fit.
371  visitPairs : `iterable` of `tuple` of `int`
372  Pairs of visit numbers to be processed together
373  """
374  xcorrs = {} # dict of lists keyed by either amp or detector depending on config.level
375  means = {}
376  kernels = {}
377 
378  # setup necessary objects
379  detNum = dataRef.dataId[self.config.ccdKey]
380  if self.config.level == 'DETECTOR':
381  xcorrs = {detNum: []}
382  means = {detNum: []}
383  elif self.config.level == 'AMP':
384  # NB: don't use dataRef.get('raw_detector')
385  # this currently doesn't work for composites because of the way
386  # composite objects (i.e. LSST images) are handled/constructed
387  # these need to be retrieved from the camera and dereferenced
388  # rather than accessed directly
389  detector = dataRef.get('camera')[dataRef.dataId[self.config.ccdKey]]
390  ampInfoCat = detector.getAmpInfoCatalog()
391  ampNames = [amp.getName() for amp in ampInfoCat]
392  xcorrs = {key: [] for key in ampNames}
393  means = {key: [] for key in ampNames}
394  else:
395  raise RuntimeError("Unsupported level: {}".format(self.config.level))
396 
397  # calculate or retrieve the gains
398  if self.config.doCalcGains:
399  self.log.info('Compute gains for detector %s' % detNum)
400  gains, nomGains = self.estimateGains(dataRef, visitPairs)
401  dataRef.put(gains, datasetType='brighterFatterGain')
402  self.log.debug('Finished gain estimation for detector %s' % detNum)
403  else:
404  gains = dataRef.get('brighterFatterGain')
405  if not gains:
406  raise RuntimeError('Failed to retrieved gains for detector %s' % detNum)
407  self.log.info('Retrieved stored gain for detector %s' % detNum)
408  self.log.debug('Detector %s has gains %s' % (detNum, gains))
409 
410  # Loop over pairs of visits
411  # calculating the cross-correlations at the required level
412  for (v1, v2) in visitPairs:
413 
414  dataRef.dataId['visit'] = v1
415  exp1 = self.isr.runDataRef(dataRef).exposure
416  dataRef.dataId['visit'] = v2
417  exp2 = self.isr.runDataRef(dataRef).exposure
418  del dataRef.dataId['visit']
419  self._checkExpLengthEqual(exp1, exp2, v1, v2)
420 
421  self.log.info('Preparing images for cross-correlation calculation for detector %s' % detNum)
422  # note the shape of these returns depends on level
423  _scaledMaskedIms1, _means1 = self._makeCroppedExposures(exp1, gains, self.config.level)
424  _scaledMaskedIms2, _means2 = self._makeCroppedExposures(exp2, gains, self.config.level)
425 
426  # Compute the cross-correlation and means
427  # at the appropriate config.level:
428  # - "DETECTOR": one key, so compare the two visits to each other
429  # - "AMP": n_amp keys, comparing each amplifier of one visit
430  # to the same amplifier in the visit its paired with
431  for det_object in _scaledMaskedIms1.keys():
432  _xcorr, _ = self._crossCorrelate(_scaledMaskedIms1[det_object],
433  _scaledMaskedIms2[det_object])
434  xcorrs[det_object].append(_xcorr)
435  means[det_object].append([_means1[det_object], _means2[det_object]])
436 
437  # TODO: DM-15305 improve debug functionality here.
438  # This is position 1 for the removed code.
439 
440  # generate the kernel(s)
441  self.log.info('Generating kernel(s) for %s' % detNum)
442  for det_object in xcorrs.keys(): # looping over either detectors or amps
443  if self.config.level == 'DETECTOR':
444  objId = 'detector %s' % det_object
445  elif self.config.level == 'AMP':
446  objId = 'detector %s AMP %s' % (detNum, det_object)
447  kernels[det_object] = self.generateKernel(xcorrs[det_object], means[det_object], objId)
448  dataRef.put(BrighterFatterKernel(self.config.level, kernels))
449 
450  self.log.info('Finished generating kernel(s) for %s' % detNum)
451  return pipeBase.Struct(exitStatus=0)
452 
453  def _makeCroppedExposures(self, exp, gains, level):
454  """Prepare exposure for cross-correlation calculation.
455 
456  For each amp, crop by the border amount, specified by
457  config.nPixBorderXCorr, then rescale by the gain
458  and subtract the sigma-clipped mean.
459  If the level is 'DETECTOR' then this is done
460  to the whole image so that it can be cross-correlated, with a copy
461  being returned.
462  If the level is 'AMP' then this is done per-amplifier,
463  and a copy of each prepared amp-image returned.
464 
465  Parameters:
466  -----------
467  exp : `lsst.afw.image.exposure.ExposureF`
468  The exposure to prepare
469  gains : `dict` of `float`
470  Dictionary of the amplifier gain values, keyed by amplifier name
471  level : `str`
472  Either `AMP` or `DETECTOR`
473 
474  Returns:
475  --------
476  scaledMaskedIms : `dict` of `lsst.afw.image.maskedImage.MaskedImageF`
477  Depending on level, this is either one item, or n_amp items,
478  keyed by detectorId or ampName
479 
480  Notes:
481  ------
482  This function is controlled by the following config parameters:
483  nPixBorderXCorr : `int`
484  The number of border pixels to exclude
485  nSigmaClipXCorr : `float`
486  The number of sigma to be clipped to
487  """
488  assert(isinstance(exp, afwImage.ExposureF))
489 
490  local_exp = exp.clone() # we don't want to modify the image passed in
491  del exp # ensure we don't make mistakes!
492 
493  border = self.config.nPixBorderXCorr
494  sigma = self.config.nSigmaClipXCorr
495 
496  sctrl = afwMath.StatisticsControl()
497  sctrl.setNumSigmaClip(sigma)
498 
499  means = {}
500  returnAreas = {}
501 
502  detector = local_exp.getDetector()
503  ampInfoCat = detector.getAmpInfoCatalog()
504 
505  mi = local_exp.getMaskedImage() # makeStatistics does not seem to take exposures
506  temp = mi.clone()
507 
508  # Rescale each amp by the appropriate gain and subtract the mean.
509  # NB these are views modifying the image in-place
510  for amp in ampInfoCat:
511  ampName = amp.getName()
512  rescaleIm = mi[amp.getBBox()] # the soon-to-be scaled, mean subtractedm, amp image
513  rescaleTemp = temp[amp.getBBox()]
514  mean = afwMath.makeStatistics(rescaleIm, afwMath.MEANCLIP, sctrl).getValue()
515  gain = gains[ampName]
516  rescaleIm *= gain
517  rescaleTemp *= gain
518  self.log.debug("mean*gain = %s, clipped mean = %s" %
519  (mean*gain, afwMath.makeStatistics(rescaleIm, afwMath.MEANCLIP,
520  sctrl).getValue()))
521  rescaleIm -= mean*gain
522 
523  if level == 'AMP': # build the dicts if doing amp-wise
524  means[ampName] = afwMath.makeStatistics(rescaleTemp[border: -border, border: -border,
525  afwImage.LOCAL], afwMath.MEANCLIP, sctrl).getValue()
526  returnAreas[ampName] = rescaleIm
527 
528  if level == 'DETECTOR': # else just average the whole detector
529  detName = local_exp.getDetector().getId()
530  means[detName] = afwMath.makeStatistics(temp[border: -border, border: -border, afwImage.LOCAL],
531  afwMath.MEANCLIP, sctrl).getValue()
532  returnAreas[detName] = rescaleIm
533 
534  return returnAreas, means
535 
536  def _crossCorrelate(self, maskedIm0, maskedIm1, runningBiasCorrSim=False, frameId=None, detId=None):
537  """Calculate the cross-correlation of an area.
538 
539  If the area in question contains multiple amplifiers then they must
540  have been gain corrected.
541 
542  Parameters:
543  -----------
544  maskedIm0 : `lsst.afw.image.MaskedImageF`
545  The first image area
546  maskedIm1 : `lsst.afw.image.MaskedImageF`
547  The first image area
548  frameId : `str`, optional
549  The frame identifier for use in the filename
550  if writing debug outputs.
551  detId : `str`, optional
552  The detector identifier (detector, or detector+amp,
553  depending on config.level) for use in the filename
554  if writing debug outputs.
555  runningBiasCorrSim : `bool`
556  Set to true when using this function to calculate the amount of bias
557  introduced by the sigma clipping. If False, the biasCorr parameter
558  is divided by to remove the bias, but this is, of course, not
559  appropriate when this is the parameter being measured.
560 
561  Returns:
562  --------
563  xcorr : `np.ndarray`
564  The quarter-image cross-correlation
565  mean : `float`
566  The sum of the means of the input images,
567  sigma-clipped, and with borders applied.
568  This is used when using this function with simulations to calculate
569  the biasCorr parameter.
570 
571  Notes:
572  ------
573  This function is controlled by the following config parameters:
574  maxLag : `int`
575  The maximum lag to use in the cross-correlation calculation
576  nPixBorderXCorr : `int`
577  The number of border pixels to exclude
578  nSigmaClipXCorr : `float`
579  The number of sigma to be clipped to
580  biasCorr : `float`
581  Parameter used to correct from the bias introduced
582  by the sigma cuts.
583  """
584  maxLag = self.config.maxLag
585  border = self.config.nPixBorderXCorr
586  sigma = self.config.nSigmaClipXCorr
587  biasCorr = self.config.biasCorr
588 
589  sctrl = afwMath.StatisticsControl()
590  sctrl.setNumSigmaClip(sigma)
591 
592  mean = afwMath.makeStatistics(maskedIm0.getImage()[border: -border, border: -border, afwImage.LOCAL],
593  afwMath.MEANCLIP, sctrl).getValue()
594  mean += afwMath.makeStatistics(maskedIm1.getImage()[border: -border, border: -border, afwImage.LOCAL],
595  afwMath.MEANCLIP, sctrl).getValue()
596 
597  # Diff the images, and apply border
598  diff = maskedIm0.clone()
599  diff -= maskedIm1.getImage()
600  diff = diff[border: -border, border: -border, afwImage.LOCAL]
601 
602  if self.debug.writeDiffImages:
603  filename = '_'.join(['diff', 'detector', detId, frameId, '.fits'])
604  diff.writeFits(os.path.join(self.debug.debugDataPath, filename))
605 
606  # Subtract background. It should be a constant, but it isn't always
607  binsize = self.config.backgroundBinSize
608  nx = diff.getWidth()//binsize
609  ny = diff.getHeight()//binsize
610  bctrl = afwMath.BackgroundControl(nx, ny, sctrl, afwMath.MEANCLIP)
611  bkgd = afwMath.makeBackground(diff, bctrl)
612  bgImg = bkgd.getImageF(afwMath.Interpolate.CUBIC_SPLINE, afwMath.REDUCE_INTERP_ORDER)
613  bgMean = np.mean(bgImg.getArray())
614  if abs(bgMean) >= self.config.backgroundWarnLevel:
615  self.log.warn('Mean of background = %s > config.maxBackground' % bgMean)
616 
617  diff -= bgImg
618 
619  if self.debug.writeDiffImages:
620  filename = '_'.join(['bgSub', 'diff', 'detector', detId, frameId, '.fits'])
621  diff.writeFits(os.path.join(self.debug.debugDataPath, filename))
622  if self.debug.display:
623  self.disp1.mtv(diff, title=frameId)
624 
625  self.log.debug("Median and variance of diff:")
626  self.log.debug("%s" % afwMath.makeStatistics(diff, afwMath.MEDIAN, sctrl).getValue())
627  self.log.debug("%s" % afwMath.makeStatistics(diff, afwMath.VARIANCECLIP,
628  sctrl).getValue(), np.var(diff.getImage().getArray()))
629 
630  # Measure the correlations
631  dim0 = diff[0: -maxLag, : -maxLag, afwImage.LOCAL]
632  dim0 -= afwMath.makeStatistics(dim0, afwMath.MEANCLIP, sctrl).getValue()
633  width, height = dim0.getDimensions()
634  xcorr = np.zeros((maxLag + 1, maxLag + 1), dtype=np.float64)
635 
636  for xlag in range(maxLag + 1):
637  for ylag in range(maxLag + 1):
638  dim_xy = diff[xlag:xlag + width, ylag: ylag + height, afwImage.LOCAL].clone()
639  dim_xy -= afwMath.makeStatistics(dim_xy, afwMath.MEANCLIP, sctrl).getValue()
640  dim_xy *= dim0
641  xcorr[xlag, ylag] = afwMath.makeStatistics(dim_xy, afwMath.MEANCLIP, sctrl).getValue()
642  if not runningBiasCorrSim:
643  xcorr[xlag, ylag] /= biasCorr
644 
645  # TODO: DM-15305 improve debug functionality here.
646  # This is position 2 for the removed code.
647 
648  return xcorr, mean
649 
650  def estimateGains(self, dataRef, visitPairs):
651  """Estimate the amplifier gains using the specified visits.
652 
653  Given a dataRef and list of flats of varying intensity,
654  calculate the gain for each amplifier in the detector
655  using the photon transfer curve (PTC) method.
656 
657  The config.fixPtcThroughOrigin option determines whether the iterative
658  fitting is forced to go through the origin or not.
659  This defaults to True, fitting var=1/gain * mean.
660  If set to False then var=1/g * mean + const is fitted.
661 
662  This is really a photo transfer curve (PTC) gain measurement task.
663  See DM-14063 for results from of a comparison between
664  this task's numbers and the gain values in the HSC camera model,
665  and those measured by the PTC task in eotest.
666 
667  Parameters
668  ----------
669  dataRef : `lsst.daf.persistence.butler.Butler.dataRef`
670  dataRef for the detector for the flats to be used
671  visitPairs : `list` of `tuple`
672  List of visit-pairs to use, as [(v1,v2), (v3,v4)...]
673 
674  Returns
675  -------
676  gains : `dict` of `float`
677  Dict of the as-calculated amplifier gain values,
678  keyed by amplifier name
679  nominalGains : `dict` of `float`
680  Dict of the amplifier gains, as reported by the `detector` object,
681  keyed by amplifier name
682  """
683  # NB: don't use dataRef.get('raw_detector') due to composites
684  detector = dataRef.get('camera')[dataRef.dataId[self.config.ccdKey]]
685  ampInfoCat = detector.getAmpInfoCatalog()
686  ampNames = [amp.getName() for amp in ampInfoCat]
687 
688  ampMeans = {key: [] for key in ampNames} # these get turned into np.arrays later
689  ampCoVariances = {key: [] for key in ampNames}
690  ampVariances = {key: [] for key in ampNames}
691 
692  # Loop over the amps in the detector,
693  # calculating a PTC for each amplifier.
694  # The amplifier iteration is performed in _calcMeansAndVars()
695  # NB: no gain correction is applied
696  for visPairNum, visPair in enumerate(visitPairs):
697  _means, _vars, _covars = self._calcMeansAndVars(dataRef, visPair[0], visPair[1])
698 
699  # Do sanity checks; if these are failed more investigation is needed
700  breaker = 0
701  for amp in detector:
702  ampName = amp.getName()
703  if _means[ampName]*10 < _vars[ampName] or _means[ampName]*10 < _covars[ampName]:
704  msg = 'Sanity check failed; check visit pair %s amp %s' % (visPair, ampName)
705  self.log.warn(msg)
706  breaker += 1
707  if breaker:
708  continue
709 
710  # having made sanity checks
711  # pull the values out into the respective dicts
712  for k in _means.keys(): # keys are necessarily the same
713  if _vars[k]*1.3 < _covars[k] or _vars[k]*0.7 > _covars[k]:
714  self.log.warn('Dropped a value')
715  continue
716  ampMeans[k].append(_means[k])
717  ampVariances[k].append(_vars[k])
718  ampCoVariances[k].append(_covars[k])
719 
720  gains = {}
721  nomGains = {}
722  for amp in detector:
723  ampName = amp.getName()
724  nomGains[ampName] = amp.getGain()
725  slopeRaw, interceptRaw, rVal, pVal, stdErr = \
726  stats.linregress(np.asarray(ampMeans[ampName]), np.asarray(ampCoVariances[ampName]))
727  slopeFix, _ = self._iterativeRegression(np.asarray(ampMeans[ampName]),
728  np.asarray(ampCoVariances[ampName]),
729  fixThroughOrigin=True)
730  slopeUnfix, intercept = self._iterativeRegression(np.asarray(ampMeans[ampName]),
731  np.asarray(ampCoVariances[ampName]),
732  fixThroughOrigin=False)
733  self.log.info("Slope of raw fit: %s, intercept: %s p value: %s" % (slopeRaw,
734  interceptRaw, pVal))
735  self.log.info("slope of fixed fit: %s, difference vs raw:%s" % (slopeFix,
736  slopeFix - slopeRaw))
737  self.log.info("slope of unfixed fit: %s, difference vs fix:%s" % (slopeUnfix,
738  slopeFix - slopeUnfix))
739  if self.config.fixPtcThroughOrigin:
740  slopeToUse = slopeFix
741  else:
742  slopeToUse = slopeUnfix
743 
744  if self.debug.enabled:
745  fig = plt.figure()
746  ax = fig.add_subplot(111)
747  ax.plot(np.asarray(ampMeans[ampName]),
748  np.asarray(ampCoVariances[ampName]), linestyle='None', marker='x', label='data')
749  if self.config.fixPtcThroughOrigin:
750  ax.plot(np.asarray(ampMeans[ampName]),
751  np.asarray(ampMeans[ampName])*slopeToUse, label='Fit through origin')
752  else:
753  ax.plot(np.asarray(ampMeans[ampName]),
754  np.asarray(ampMeans[ampName])*slopeToUse + intercept,
755  label='Fit (intercept unconstrained')
756 
757  dataRef.put(fig, "plotBrighterFatterPtc", amp=ampName)
758  self.log.info('Saved PTC for detector %s amp %s' % (detector.getId(), ampName))
759  gains[ampName] = 1.0/slopeToUse
760  return gains, nomGains
761 
762  @staticmethod
763  def _checkExpLengthEqual(exp1, exp2, v1=None, v2=None):
764  """Check the exposure lengths of two exposures are equal.
765 
766  Parameters:
767  -----------
768  exp1 : `lsst.afw.image.exposure.ExposureF`
769  First exposure to check
770  exp2 : `lsst.afw.image.exposure.ExposureF`
771  Second exposure to check
772  v1 : `int` or `str`, optional
773  First visit of the visit pair
774  v2 : `int` or `str`, optional
775  Second visit of the visit pair
776 
777  Raises:
778  -------
779  RuntimeError
780  Raised if the exposure lengths of the two exposures are not equal
781  """
782  expTime1 = exp1.getInfo().getVisitInfo().getExposureTime()
783  expTime2 = exp2.getInfo().getVisitInfo().getExposureTime()
784  if expTime1 != expTime2:
785  msg = "Exposure lengths for visit pairs must be equal. " + \
786  "Found %s and %s" % (expTime1, expTime2)
787  if v1 and v2:
788  msg += " for visit pair %s, %s" % (v1, v2)
789  raise RuntimeError(msg)
790 
791  def _calcMeansAndVars(self, dataRef, v1, v2):
792  """Calculate the means, vars, covars, and retrieve the nominal gains,
793  for each amp in each detector.
794 
795  This code runs using two visit numbers, and for the detector specified.
796  It calculates the correlations in the individual amps without
797  rescaling any gains. This allows a photon transfer curve
798  to be generated and the gains measured.
799 
800  Images are assembled with use the isrTask, and basic isr is performed.
801 
802  Parameters:
803  -----------
804  dataRef : `lsst.daf.persistence.butler.Butler.dataRef`
805  dataRef for the detector for the repo containg the flats to be used
806  v1 : `int`
807  First visit of the visit pair
808  v2 : `int`
809  Second visit of the visit pair
810 
811  Returns
812  -------
813  means, vars, covars : `tuple` of `dicts`
814  Three dicts, keyed by ampName,
815  containing the sum of the image-means,
816  the variance, and the quarter-image of the xcorr.
817  """
818  sigma = self.config.nSigmaClipGainCalc
819  maxLag = self.config.maxLag
820  border = self.config.nPixBorderGainCalc
821  biasCorr = self.config.biasCorr
822 
823  # NB: don't use dataRef.get('raw_detector') due to composites
824  detector = dataRef.get('camera')[dataRef.dataId[self.config.ccdKey]]
825 
826  ampMeans = {}
827 
828  # manipulate the dataId to get a postISR exposure for each visit
829  # from the detector obj, restoring its original state afterwards
830  originalDataId = dataRef.dataId.copy()
831  dataRef.dataId['visit'] = v1
832  exp1 = self.isr.runDataRef(dataRef).exposure
833  dataRef.dataId['visit'] = v2
834  exp2 = self.isr.runDataRef(dataRef).exposure
835  dataRef.dataId = originalDataId
836  exps = [exp1, exp2]
837  self._checkExpLengthEqual(exp1, exp2, v1, v2)
838 
839  detector = exps[0].getDetector()
840  ims = [self._convertImagelikeToFloatImage(exp) for exp in exps]
841 
842  if self.debug.display:
843  self.disp1.mtv(ims[0], title=str(v1))
844  self.disp2.mtv(ims[1], title=str(v2))
845 
846  sctrl = afwMath.StatisticsControl()
847  sctrl.setNumSigmaClip(sigma)
848  for imNum, im in enumerate(ims):
849 
850  # calculate the sigma-clipped mean, excluding the borders
851  # safest to apply borders to all amps regardless of edges
852  # easier, camera-agnostic, and mitigates potentially dodgy
853  # overscan-biases around edges as well
854  for amp in detector:
855  ampName = amp.getName()
856  ampIm = im[amp.getBBox()]
857  mean = afwMath.makeStatistics(ampIm[border: -border, border: -border, afwImage.LOCAL],
858  afwMath.MEANCLIP, sctrl).getValue()
859  if ampName not in ampMeans.keys():
860  ampMeans[ampName] = []
861  ampMeans[ampName].append(mean)
862  ampIm -= mean
863 
864  diff = ims[0].clone()
865  diff -= ims[1]
866 
867  temp = diff[border: -border, border: -border, afwImage.LOCAL]
868 
869  # Subtract background. It should be a constant,
870  # but it isn't always (e.g. some SuprimeCam flats)
871  # TODO: Check how this looks, and if this is the "right" way to do this
872  binsize = self.config.backgroundBinSize
873  nx = temp.getWidth()//binsize
874  ny = temp.getHeight()//binsize
875  bctrl = afwMath.BackgroundControl(nx, ny, sctrl, afwMath.MEANCLIP)
876  bkgd = afwMath.makeBackground(temp, bctrl)
877 
878  box = diff.getBBox()
879  box.grow(-border)
880  diff[box, afwImage.LOCAL] -= bkgd.getImageF(afwMath.Interpolate.CUBIC_SPLINE,
881  afwMath.REDUCE_INTERP_ORDER)
882 
883  variances = {}
884  coVars = {}
885  for amp in detector:
886  ampName = amp.getName()
887 
888  diffAmpIm = diff[amp.getBBox()].clone()
889  diffAmpImCrop = diffAmpIm[border: -border - maxLag, border: -border - maxLag, afwImage.LOCAL]
890  diffAmpImCrop -= afwMath.makeStatistics(diffAmpImCrop, afwMath.MEANCLIP, sctrl).getValue()
891  w, h = diffAmpImCrop.getDimensions()
892  xcorr = np.zeros((maxLag + 1, maxLag + 1), dtype=np.float64)
893 
894  # calculate the cross-correlation
895  for xlag in range(maxLag + 1):
896  for ylag in range(maxLag + 1):
897  dim_xy = diffAmpIm[border + xlag: border + xlag + w,
898  border + ylag: border + ylag + h,
899  afwImage.LOCAL].clone()
900  dim_xy -= afwMath.makeStatistics(dim_xy, afwMath.MEANCLIP, sctrl).getValue()
901  dim_xy *= diffAmpImCrop
902  xcorr[xlag, ylag] = afwMath.makeStatistics(dim_xy,
903  afwMath.MEANCLIP, sctrl).getValue()/(biasCorr)
904 
905  variances[ampName] = xcorr[0, 0]
906  xcorr_full = self._tileArray(xcorr)
907  coVars[ampName] = np.sum(xcorr_full)
908 
909  msg = "M1: " + str(ampMeans[ampName][0])
910  msg += " M2 " + str(ampMeans[ampName][1])
911  msg += " M_sum: " + str((ampMeans[ampName][0]) + ampMeans[ampName][1])
912  msg += " Var " + str(variances[ampName])
913  msg += " coVar: " + str(coVars[ampName])
914  self.log.debug(msg)
915 
916  means = {}
917  for amp in detector:
918  ampName = amp.getName()
919  means[ampName] = ampMeans[ampName][0] + ampMeans[ampName][1]
920 
921  return means, variances, coVars
922 
923  def _plotXcorr(self, xcorr, mean, zmax=0.05, title=None, fig=None, saveToFileName=None):
924  """Plot the correlation functions."""
925  try:
926  xcorr = xcorr.getArray()
927  except Exception:
928  pass
929 
930  xcorr /= float(mean)
931  # xcorr.getArray()[0,0]=abs(xcorr.getArray()[0,0]-1)
932 
933  if fig is None:
934  fig = plt.figure()
935  else:
936  fig.clf()
937 
938  ax = fig.add_subplot(111, projection='3d')
939  ax.azim = 30
940  ax.elev = 20
941 
942  nx, ny = np.shape(xcorr)
943 
944  xpos, ypos = np.meshgrid(np.arange(nx), np.arange(ny))
945  xpos = xpos.flatten()
946  ypos = ypos.flatten()
947  zpos = np.zeros(nx*ny)
948  dz = xcorr.flatten()
949  dz[dz > zmax] = zmax
950 
951  ax.bar3d(xpos, ypos, zpos, 1, 1, dz, color='b', zsort='max', sort_zpos=100)
952  if xcorr[0, 0] > zmax:
953  ax.bar3d([0], [0], [zmax], 1, 1, 1e-4, color='c')
954 
955  ax.set_xlabel("row")
956  ax.set_ylabel("column")
957  ax.set_zlabel(r"$\langle{(F_i - \bar{F})(F_i - \bar{F})}\rangle/\bar{F}$")
958 
959  if title:
960  fig.suptitle(title)
961  if saveToFileName:
962  fig.savefig(saveToFileName)
963 
964  def _iterativeRegression(self, x, y, fixThroughOrigin=False, nSigmaClip=None, maxIter=None):
965  """Use linear regression to fit a line, iteratively removing outliers.
966 
967  Useful when you have a sufficiently large numbers of points on your PTC.
968  This function iterates until either there are no outliers of
969  config.nSigmaClip magnitude, or until the specified maximum number
970  of iterations has been performed.
971 
972  Parameters:
973  -----------
974  x : `numpy.array`
975  The independent variable. Must be a numpy array, not a list.
976  y : `numpy.array`
977  The dependent variable. Must be a numpy array, not a list.
978  fixThroughOrigin : `bool`, optional
979  Whether to fix the PTC through the origin or allow an y-intercept.
980  nSigmaClip : `float`, optional
981  The number of sigma to clip to.
982  Taken from the task config if not specified.
983  maxIter : `int`, optional
984  The maximum number of iterations allowed.
985  Taken from the task config if not specified.
986 
987  Returns:
988  --------
989  slope : `float`
990  The slope of the line of best fit
991  intercept : `float`
992  The y-intercept of the line of best fit
993  """
994  if not maxIter:
995  maxIter = self.config.maxIterRegression
996  if not nSigmaClip:
997  nSigmaClip = self.config.nSigmaClipRegression
998 
999  nIter = 0
1000  sctrl = afwMath.StatisticsControl()
1001  sctrl.setNumSigmaClip(nSigmaClip)
1002 
1003  if fixThroughOrigin:
1004  while nIter < maxIter:
1005  nIter += 1
1006  self.log.debug("Origin fixed, iteration # %s using %s elements:" % (nIter, np.shape(x)[0]))
1007  TEST = x[:, np.newaxis]
1008  slope, _, _, _ = np.linalg.lstsq(TEST, y)
1009  slope = slope[0]
1010  res = y - slope * x
1011  resMean = afwMath.makeStatistics(res, afwMath.MEANCLIP, sctrl).getValue()
1012  resStd = np.sqrt(afwMath.makeStatistics(res, afwMath.VARIANCECLIP, sctrl).getValue())
1013  index = np.where((res > (resMean + nSigmaClip*resStd)) |
1014  (res < (resMean - nSigmaClip*resStd)))
1015  self.log.debug("%.3f %.3f %.3f %.3f" % (resMean, resStd, np.max(res), nSigmaClip))
1016  if np.shape(np.where(index))[1] == 0 or (nIter >= maxIter): # run out of points or iters
1017  break
1018  x = np.delete(x, index)
1019  y = np.delete(y, index)
1020 
1021  return slope, 0
1022 
1023  while nIter < maxIter:
1024  nIter += 1
1025  self.log.debug("Iteration # %s using %s elements:" % (nIter, np.shape(x)[0]))
1026  xx = np.vstack([x, np.ones(len(x))]).T
1027  ret, _, _, _ = np.linalg.lstsq(xx, y)
1028  slope, intercept = ret
1029  res = y - slope*x - intercept
1030  resMean = afwMath.makeStatistics(res, afwMath.MEANCLIP, sctrl).getValue()
1031  resStd = np.sqrt(afwMath.makeStatistics(res, afwMath.VARIANCECLIP, sctrl).getValue())
1032  index = np.where((res > (resMean + nSigmaClip * resStd)) | (res < resMean - nSigmaClip * resStd))
1033  self.log.debug("%.3f %.3f %.3f %.3f" % (resMean, resStd, np.max(res), nSigmaClip))
1034  if np.shape(np.where(index))[1] == 0 or (nIter >= maxIter): # run out of points, or iterations
1035  break
1036  x = np.delete(x, index)
1037  y = np.delete(y, index)
1038 
1039  return slope, intercept
1040 
1041  def generateKernel(self, corrs, means, objId, rejectLevel=None):
1042  """Generate the full kernel from a list of cross-correlations and means.
1043 
1044  Taking a list of quarter-image, gain-corrected cross-correlations,
1045  do a pixel-wise sigma-clipped mean of each,
1046  and tile into the full-sized kernel image.
1047 
1048  Each corr in corrs is one quarter of the full cross-correlation,
1049  and has been gain-corrected. Each mean in means is a tuple of the means
1050  of the two individual images, corresponding to that corr.
1051 
1052  Parameters:
1053  -----------
1054  corrs : `list` of `numpy.ndarray`, (Ny, Nx)
1055  A list of the quarter-image cross-correlations
1056  means : `dict` of `tuples` of `floats`
1057  The means of the input images for each corr in corrs
1058  rejectLevel : `float`, optional
1059  This is essentially is a sanity check parameter.
1060  If this condition is violated there is something unexpected
1061  going on in the image, and it is discarded from the stack before
1062  the clipped-mean is calculated.
1063  If not provided then config.xcorrCheckRejectLevel is used
1064 
1065  Returns:
1066  --------
1067  kernel : `numpy.ndarray`, (Ny, Nx)
1068  The output kernel
1069  """
1070  if not rejectLevel:
1071  rejectLevel = self.config.xcorrCheckRejectLevel
1072 
1073  # Try to average over a set of possible inputs.
1074  # This generates a simple function of the kernel that
1075  # should be constant across the images, and averages that.
1076  xcorrList = []
1077  sctrl = afwMath.StatisticsControl()
1078  sctrl.setNumSigmaClip(self.config.nSigmaClipKernelGen)
1079 
1080  for corrNum, ((mean1, mean2), corr) in enumerate(zip(means, corrs)):
1081  corr[0, 0] -= (mean1 + mean2)
1082  if corr[0, 0] > 0:
1083  self.log.warn('Skipped item %s due to unexpected value of (variance-mean)' % corrNum)
1084  continue
1085  corr /= -1.0*(mean1**2 + mean2**2)
1086 
1087  fullCorr = self._tileArray(corr)
1088 
1089  xcorrCheck = np.abs(np.sum(fullCorr))/np.sum(np.abs(fullCorr))
1090  if xcorrCheck > rejectLevel:
1091  self.log.warn("Sum of the xcorr is unexpectedly high. Investigate item num %s for %s. \n"
1092  "value = %s" % (corrNum, objId, xcorrCheck))
1093  continue
1094  xcorrList.append(fullCorr)
1095 
1096  if not xcorrList:
1097  raise RuntimeError("Cannot generate kernel because all inputs were discarded. "
1098  "Either the data is bad, or config.xcorrCheckRejectLevel is too low")
1099 
1100  # stack the individual xcorrs and apply a per-pixel clipped-mean
1101  meanXcorr = np.zeros_like(fullCorr)
1102  xcorrList = np.transpose(xcorrList)
1103  for i in range(np.shape(meanXcorr)[0]):
1104  for j in range(np.shape(meanXcorr)[1]):
1105  meanXcorr[i, j] = afwMath.makeStatistics(xcorrList[i, j], afwMath.MEANCLIP, sctrl).getValue()
1106 
1107  return self.successiveOverRelax(meanXcorr)
1108 
1109  def successiveOverRelax(self, source, maxIter=None, eLevel=None):
1110  """An implementation of the successive over relaxation (SOR) method.
1111 
1112  A numerical method for solving a system of linear equations
1113  with faster convergence than the Gauss-Seidel method.
1114 
1115  Parameters:
1116  -----------
1117  source : `numpy.ndarray`
1118  The input array
1119  maxIter : `int`, optional
1120  Maximum number of iterations to attempt before aborting
1121  eLevel : `float`, optional
1122  The target error level at which we deem convergence to have occured
1123 
1124  Returns:
1125  --------
1126  output : `numpy.ndarray`
1127  The solution
1128  """
1129  if not maxIter:
1130  maxIter = self.config.maxIterSuccessiveOverRelaxation
1131  if not eLevel:
1132  eLevel = self.config.eLevelSuccessiveOverRelaxation
1133 
1134  assert source.shape[0] == source.shape[1], "Input array must be square"
1135  # initialise, and set boundary conditions
1136  func = np.zeros([source.shape[0] + 2, source.shape[1] + 2])
1137  resid = np.zeros([source.shape[0] + 2, source.shape[1] + 2])
1138  rhoSpe = np.cos(np.pi/source.shape[0]) # Here a square grid is assummed
1139 
1140  # Calculate the initial error
1141  for i in range(1, func.shape[0] - 1):
1142  for j in range(1, func.shape[1] - 1):
1143  resid[i, j] = (func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1144  func[i + 1, j] - 4*func[i, j] - source[i - 1, j - 1])
1145  inError = np.sum(np.abs(resid))
1146 
1147  # Iterate until convergence
1148  # We perform two sweeps per cycle,
1149  # updating 'odd' and 'even' points separately
1150  nIter = 0
1151  omega = 1.0
1152  dx = 1.0
1153  while nIter < maxIter*2:
1154  outError = 0
1155  if nIter%2 == 0:
1156  for i in range(1, func.shape[0] - 1, 2):
1157  for j in range(1, func.shape[1] - 1, 2):
1158  resid[i, j] = float(func[i, j-1] + func[i, j + 1] + func[i - 1, j] +
1159  func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1160  func[i, j] += omega*resid[i, j]*.25
1161  for i in range(2, func.shape[0] - 1, 2):
1162  for j in range(2, func.shape[1] - 1, 2):
1163  resid[i, j] = float(func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1164  func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1165  func[i, j] += omega*resid[i, j]*.25
1166  else:
1167  for i in range(1, func.shape[0] - 1, 2):
1168  for j in range(2, func.shape[1] - 1, 2):
1169  resid[i, j] = float(func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1170  func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1171  func[i, j] += omega*resid[i, j]*.25
1172  for i in range(2, func.shape[0] - 1, 2):
1173  for j in range(1, func.shape[1] - 1, 2):
1174  resid[i, j] = float(func[i, j - 1] + func[i, j + 1] + func[i - 1, j] +
1175  func[i + 1, j] - 4.0*func[i, j] - dx*dx*source[i - 1, j - 1])
1176  func[i, j] += omega*resid[i, j]*.25
1177  outError = np.sum(np.abs(resid))
1178  if outError < inError*eLevel:
1179  break
1180  if nIter == 0:
1181  omega = 1.0/(1 - rhoSpe*rhoSpe/2.0)
1182  else:
1183  omega = 1.0/(1 - rhoSpe*rhoSpe*omega/4.0)
1184  nIter += 1
1185 
1186  if nIter >= maxIter*2:
1187  self.log.warn("Failure: SuccessiveOverRelaxation did not converge in %s iterations."
1188  "\noutError: %s, inError: %s," % (nIter//2, outError, inError*eLevel))
1189  else:
1190  self.log.info("Success: SuccessiveOverRelaxation converged in %s iterations."
1191  "\noutError: %s, inError: %s", nIter//2, outError, inError*eLevel)
1192  return func[1: -1, 1: -1]
1193 
1194  @staticmethod
1195  def _tileArray(in_array):
1196  """Given an input quarter-image, tile/mirror it and return full image.
1197 
1198  Given a square input of side-length n, of the form
1199 
1200  input = array([[1, 2, 3],
1201  [4, 5, 6],
1202  [7, 8, 9]])
1203 
1204  return an array of size 2n-1 as
1205 
1206  output = array([[ 9, 8, 7, 8, 9],
1207  [ 6, 5, 4, 5, 6],
1208  [ 3, 2, 1, 2, 3],
1209  [ 6, 5, 4, 5, 6],
1210  [ 9, 8, 7, 8, 9]])
1211 
1212  Parameters:
1213  -----------
1214  input : `np.array`
1215  The square input quarter-array
1216 
1217  Returns:
1218  --------
1219  output : `np.array`
1220  The full, tiled array
1221  """
1222  assert(in_array.shape[0] == in_array.shape[1])
1223  length = in_array.shape[0] - 1
1224  output = np.zeros((2*length + 1, 2*length + 1))
1225 
1226  for i in range(length + 1):
1227  for j in range(length + 1):
1228  output[i + length, j + length] = in_array[i, j]
1229  output[-i + length, j + length] = in_array[i, j]
1230  output[i + length, -j + length] = in_array[i, j]
1231  output[-i + length, -j + length] = in_array[i, j]
1232  return output
1233 
1234  @staticmethod
1235  def _convertImagelikeToFloatImage(imagelikeObject):
1236  """Turn an exposure or masked image of any type into an ImageF."""
1237  for attr in ("getMaskedImage", "getImage"):
1238  if hasattr(imagelikeObject, attr):
1239  imagelikeObject = getattr(imagelikeObject, attr)()
1240  try:
1241  floatImage = imagelikeObject.convertF()
1242  except AttributeError:
1243  raise RuntimeError("Failed to convert image to float")
1244  return floatImage
1245 
1246 
1247 def calcBiasCorr(fluxLevels, imageShape, repeats=1, seed=0, addCorrelations=False,
1248  correlationStrength=0.1, maxLag=10, nSigmaClip=5, border=10):
1249  """Calculate the bias induced when sigma-clipping non-Gassian distributions.
1250 
1251  Fill image-pairs of the specified size with Poisson-distributed values,
1252  adding correlations as necessary. Then calculate the cross correlation,
1253  and calculate the bias induced using the cross-correlation image
1254  and the image means.
1255 
1256  Parameters:
1257  -----------
1258  fluxLevels : `list` of `int`
1259  The mean flux levels at which to simiulate.
1260  Nominal values might be something like [70000, 90000, 110000]
1261  imageShape : `tuple` of `int`
1262  The shape of the image array to simulate, nx by ny pixels.
1263  repeats : `int`, optional
1264  Number of repeats to perform so that results
1265  can be averaged to improve SNR.
1266  seed : `int`, optional
1267  The random seed to use for the Poisson points.
1268  addCorrelations : `bool`, optional
1269  Whether to add brighter-fatter-like correlations to the simulated images
1270  If true, a correlation between x_{i,j} and x_{i+1,j+1} is introduced
1271  by adding a*x_{i,j} to x_{i+1,j+1}
1272  correlationStrength : `float`, optional
1273  The strength of the correlations.
1274  This is the value of the coefficient `a` in the above definition.
1275  maxLag : `int`, optional
1276  The maximum lag to work to in pixels
1277  nSigmaClip : `float`, optional
1278  Number of sigma to clip to when calculating the sigma-clipped mean.
1279  border : `int`, optional
1280  Number of border pixels to mask
1281 
1282  Returns:
1283  --------
1284  biases : `dict` of `list` of `float`
1285  A dictionary, keyed by flux level, containing a list of the biases
1286  for each repeat at that flux level
1287  means : `dict` of `list` of `float`
1288  A dictionary, keyed by flux level, containing a list of the average mean
1289  fluxes (average of the mean of the two images)
1290  for the image pairs at that flux level
1291  xcorrs : `dict` of `list` of `np.ndarray`
1292  A dictionary, keyed by flux level, containing a list of the xcorr
1293  images for the image pairs at that flux level
1294  """
1295  means = {f: [] for f in fluxLevels}
1296  xcorrs = {f: [] for f in fluxLevels}
1297  biases = {f: [] for f in fluxLevels}
1298 
1300  config.isrMandatorySteps = [] # no isr but the validation routine is still run
1301  config.isrForbiddenSteps = []
1302  config.nSigmaClipXCorr = nSigmaClip
1303  config.nPixBorderXCorr = border
1304  config.maxLag = maxLag
1305  task = MakeBrighterFatterKernelTask(config=config)
1306 
1307  im0 = afwImage.maskedImage.MaskedImageF(imageShape[1], imageShape[0])
1308  im1 = afwImage.maskedImage.MaskedImageF(imageShape[1], imageShape[0])
1309 
1310  random = np.random.RandomState(seed)
1311 
1312  for rep in range(repeats):
1313  for flux in fluxLevels:
1314  data0 = random.poisson(flux, (imageShape)).astype(float)
1315  data1 = random.poisson(flux, (imageShape)).astype(float)
1316  if addCorrelations:
1317  data0[1:, 1:] += correlationStrength*data0[: -1, : -1]
1318  data1[1:, 1:] += correlationStrength*data1[: -1, : -1]
1319  im0.image.array[:, :] = data0
1320  im1.image.array[:, :] = data1
1321 
1322  _xcorr, _means = task._crossCorrelate(im0, im1, runningBiasCorrSim=True)
1323 
1324  means[flux].append(_means)
1325  xcorrs[flux].append(_xcorr)
1326  if addCorrelations:
1327  bias = xcorrs[flux][-1][1, 1]/means[flux][-1]*(1 + correlationStrength)/correlationStrength
1328  print("Simulated/expected avg. flux: %.1f, %.1f" % (flux, means[flux][-1]/2))
1329  print("Bias: %.6f" % bias)
1330  else:
1331  bias = xcorrs[flux][-1][0, 0]/means[flux][-1]
1332  print("Simulated/expected avg. flux: %.1f, %.1f" % (flux, means[flux][-1]/2))
1333  print("Bias: %.6f" % bias)
1334  biases[flux].append(bias)
1335 
1336  return biases, means, xcorrs
Angle abs(Angle const &a)
Definition: Angle.h:106
def _iterativeRegression(self, x, y, fixThroughOrigin=False, nSigmaClip=None, maxIter=None)
def erase(frame=None)
Definition: ds9.py:97
std::shared_ptr< Background > makeBackground(ImageT const &img, BackgroundControl const &bgCtrl)
A convenience function that uses function overloading to make the correct type of Background...
Definition: Background.h:562
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
Pass parameters to a Background object.
Definition: Background.h:57
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
Definition: Statistics.h:520
Pass parameters to a Statistics object.
Definition: Statistics.h:93
def _crossCorrelate(self, maskedIm0, maskedIm1, runningBiasCorrSim=False, frameId=None, detId=None)
table::Key< int > type
Definition: Detector.cc:167
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:168
def mtv(data, frame=None, title="", wcs=None, args, kwargs)
Definition: ds9.py:93
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
daf::base::PropertyList * list
Definition: fits.cc:885
def calcBiasCorr(fluxLevels, imageShape, repeats=1, seed=0, addCorrelations=False, correlationStrength=0.1, maxLag=10, nSigmaClip=5, border=10)