LSSTApplications  18.1.0
LSSTDataManagementBasePackage
kernelCandidateQa.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008-2016 LSST Corporation.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <http://www.lsstcorp.org/LegalNotices/>.
21 #
22 
23 __all__ = ["KernelCandidateQa"]
24 
25 import numpy as np
26 import numpy.ma as ma
27 
28 import lsst.afw.geom as afwGeom
29 import lsst.afw.image as afwImage
30 import lsst.afw.table as afwTable
31 import lsst.afw.math as afwMath
32 from . import diffimLib
33 from .utils import calcCentroid, calcWidth
34 
35 
37  """Quality Assessment class for Kernel Candidates"""
38 
39  def __init__(self, nKernelSpatial):
40  """Class to undertake QA of KernelCandidates after modeling of
41  the Psf-matching kernel. Both directly--fitted diffim (LOCAL)
42  and spatially--interpolated kernel diffim (SPATIAL) metrics
43  are calculated, based on the distribution of residuals in the
44  KernelCandidates stamp.
45 
46  Parameters
47  ----------
48  nKernelSpatial : `int`
49  Number of terms in the spatial model; needed to initialize per-basis QA arrays
50  """
51  self.fields = []
52  self.fields.append(afwTable.Field["PointD"](
53  "RegisterRefPosition",
54  "Position of reference object for registration (radians)."))
55  # TODO check units of the following angles
56  self.fields.append(afwTable.Field["Angle"]("RegisterResidualBearing",
57  "Angle of residual wrt declination parallel in radians"))
58 
59  self.fields.append(afwTable.Field["Angle"]("RegisterResidualDistance",
60  "Offset of residual in radians"))
61  metricMap = self.makeMetricMap()
62 
63  for kType in ("LOCAL", "SPATIAL"):
64  for k in metricMap:
65  commentAndUnit = metricMap[k]['comment']
66  self.fields.append(afwTable.Field[metricMap[k]['type']](k%(kType), *commentAndUnit))
67 
68  self.fields.append(afwTable.Field["I"]("KCKernelStatus_LOCAL",
69  "Status of the KernelCandidate"))
70 
71  self.fields.append(afwTable.Field["ArrayD"]("KernelCoeffValues_LOCAL",
72  "Original basis coefficients",
73  nKernelSpatial))
74 
75  self.fields.append(afwTable.Field["F"]("BackgroundValue_LOCAL",
76  "Evaluation of background model at this point"))
77 
78  self.fields.append(afwTable.Field["F"]("KCDiffimMseKernel_SPATIAL",
79  "Mean squared error of spatial kernel estimate"))
80 
81  def makeMetricMap(self):
82  nameList = ['KCDiffimMean_%s', 'KCDiffimMedian_%s', 'KCDiffimIQR_%s', 'KCDiffimStDev_%s',
83  'KCDiffimKSD_%s', 'KCDiffimKSProb_%s', 'KCDiffimADA2_%s', 'KCDiffimADCrit_%s',
84  'KCDiffimADSig_%s', 'KCDiffimChiSq_%s', 'KCDiffimMseResids_%s', 'KCKernelCentX_%s',
85  'KCKernelCentY_%s', 'KCKernelStdX_%s', 'KCKernelStdY_%s', 'KernelCandidateId_%s']
86  typeList = ['F', 'F', 'F', 'F', 'F', 'F', 'F', 'ArrayD', 'ArrayD', 'F', 'F', 'F',
87  'F', 'F', 'F', 'I']
88  commentList = [
89  ("Mean of KernelCandidate diffim", "sigma"),
90  ("Median of KernelCandidate diffim", "sigma"),
91  ("Inner quartile range of KernelCandidate diffim", "sigma"),
92  ("Standard deviation of KernelCandidate diffim", "sigma"),
93  ("D from K-S test of diffim pixels relative to Normal", ),
94  ("Prob from K-S test of diffim pixels relative to Normal", "likelihood"),
95  ("Anderson-Darling test statistic of diffim pixels relative to Normal", ),
96  ("Critical values for the significance levels in KCDiffimADSig. If A2 is greater " +
97  "than this number, hypothesis that the distributions are similar can be rejected.", 5),
98  ("Anderson-Darling significance levels for the Normal distribution", 5),
99  ("Reduced chi^2 of the residual.", "likelihood"),
100  ("Mean squared error in diffim : Variance + Bias**2",),
101  ("Centroid in X for this Kernel", "pixel"),
102  ("Centroid in Y for this Kernel", "pixel"),
103  ("Standard deviation in X for this Kernel", "pixel"),
104  ("Standard deviation in Y for this Kernel", "pixel"),
105  ("Id for this KernelCandidate",),
106  ]
107  metricMap = {}
108  for name, mtype, comment in zip(nameList, typeList, commentList):
109  metricMap[name] = {'type': mtype, 'comment': comment}
110 
111  return metricMap
112 
113  def addToSchema(self, inSourceCatalog):
114  """Add the to-be-generated QA keys to the Source schema"""
115  schema = inSourceCatalog.getSchema()
116  inKeys = []
117  psfDef = inSourceCatalog.schema.getAliasMap().get("slot_PsfFlux")
118  centroidDef = inSourceCatalog.getCentroidDefinition()
119  shapeDef = inSourceCatalog.getShapeDefinition()
120  for n in schema.getNames():
121  inKeys.append(schema[n].asKey())
122 
123  for field in self.fields:
124  schema.addField(field)
125  outSourceCatalog = afwTable.SourceCatalog(schema)
126  for source in inSourceCatalog:
127  rec = outSourceCatalog.addNew()
128  for k in inKeys:
129  if k.getTypeString() == 'Coord':
130  rec.setCoord(source.getCoord())
131  else:
132  setter = getattr(rec, "set"+k.getTypeString())
133  getter = getattr(source, "get"+k.getTypeString())
134  setter(k, getter(k))
135  outSourceCatalog.definePsfFlux(psfDef)
136  outSourceCatalog.defineCentroid(centroidDef)
137  outSourceCatalog.defineShape(shapeDef)
138  return outSourceCatalog
139 
140  def _calculateStats(self, di, dof=0.):
141  """Calculate the core QA statistics on a difference image"""
142  mask = di.getMask()
143  maskArr = di.getMask().getArray()
144 
145  # Create a mask using BAD, SAT, NO_DATA, EDGE bits. Keep detections
146  maskArr &= mask.getPlaneBitMask(["BAD", "SAT", "NO_DATA", "EDGE"])
147 
148  # Mask out values based on maskArr
149  diArr = ma.array(di.getImage().getArray(), mask=maskArr)
150  varArr = ma.array(di.getVariance().getArray(), mask=maskArr)
151 
152  # Normalize by sqrt variance, units are in sigma
153  diArr /= np.sqrt(varArr)
154  mean = diArr.mean()
155 
156  # This is the maximum-likelihood extimate of the variance stdev**2
157  stdev = diArr.std()
158  median = ma.extras.median(diArr)
159 
160  # Compute IQR of just un-masked data
161  data = ma.getdata(diArr[~diArr.mask])
162  iqr = np.percentile(data, 75.) - np.percentile(data, 25.)
163 
164  # Calculte chisquare of the residual
165  chisq = np.sum(np.power(data, 2.))
166 
167  # Mean squared error: variance + bias**2
168  # Bias = |data - model| = mean of diffim
169  # Variance = |(data - model)**2| = mean of diffim**2
170  bias = mean
171  variance = np.power(data, 2.).mean()
172  mseResids = bias**2 + variance
173 
174  # If scipy is not set up, return zero for the stats
175  try:
176  # In try block because of risk of divide by zero
177  rchisq = chisq/(len(data) - 1 - dof)
178  # K-S test on the diffim to a Normal distribution
179  import scipy.stats
180  D, prob = scipy.stats.kstest(data, 'norm')
181 
182  A2, crit, sig = scipy.stats.anderson(data, 'norm')
183  # Anderson Darling statistic cand be inf for really non-Gaussian distributions.
184  if np.isinf(A2) or np.isnan(A2):
185  A2 = 9999.
186  except ZeroDivisionError:
187  D = 0.
188  prob = 0.
189  A2 = 0.
190  crit = np.zeros(5)
191  sig = np.zeros(5)
192  rchisq = 0
193 
194  return {"mean": mean, "stdev": stdev, "median": median, "iqr": iqr,
195  "D": D, "prob": prob, "A2": A2, "crit": crit, "sig": sig,
196  "rchisq": rchisq, "mseResids": mseResids}
197 
198  def apply(self, candidateList, spatialKernel, spatialBackground, dof=0):
199  """Evaluate the QA metrics for all KernelCandidates in the
200  candidateList; set the values of the metrics in their
201  associated Sources"""
202  for kernelCandidate in candidateList:
203  source = kernelCandidate.getSource()
204  schema = source.schema
205 
206  # Calculate ORIG stats (original basis fit)
207  if kernelCandidate.getStatus() != afwMath.SpatialCellCandidate.UNKNOWN:
208  kType = getattr(diffimLib.KernelCandidateF, "ORIG")
209  di = kernelCandidate.getDifferenceImage(kType)
210  kernelValues = kernelCandidate.getKernel(kType).getKernelParameters()
211  kernelValues = np.asarray(kernelValues)
212 
213  lkim = kernelCandidate.getKernelImage(kType)
214  centx, centy = calcCentroid(lkim.getArray())
215  stdx, stdy = calcWidth(lkim.getArray(), centx, centy)
216  # NOTE
217  # What is the difference between kernelValues and solution?
218 
219  localResults = self._calculateStats(di, dof=dof)
220 
221  metrics = {"KCDiffimMean_LOCAL": localResults["mean"],
222  "KCDiffimMedian_LOCAL": localResults["median"],
223  "KCDiffimIQR_LOCAL": localResults["iqr"],
224  "KCDiffimStDev_LOCAL": localResults["stdev"],
225  "KCDiffimKSD_LOCAL": localResults["D"],
226  "KCDiffimKSProb_LOCAL": localResults["prob"],
227  "KCDiffimADA2_LOCAL": localResults["A2"],
228  "KCDiffimADCrit_LOCAL": localResults["crit"],
229  "KCDiffimADSig_LOCAL": localResults["sig"],
230  "KCDiffimChiSq_LOCAL": localResults["rchisq"],
231  "KCDiffimMseResids_LOCAL": localResults["mseResids"],
232  "KCKernelCentX_LOCAL": centx,
233  "KCKernelCentY_LOCAL": centy,
234  "KCKernelStdX_LOCAL": stdx,
235  "KCKernelStdY_LOCAL": stdy,
236  "KernelCandidateId_LOCAL": kernelCandidate.getId(),
237  "KernelCoeffValues_LOCAL": kernelValues}
238  for k in metrics:
239  key = schema[k].asKey()
240  setter = getattr(source, "set"+key.getTypeString())
241  setter(key, metrics[k])
242  else:
243  try:
244  kType = getattr(diffimLib.KernelCandidateF, "ORIG")
245  lkim = kernelCandidate.getKernelImage(kType)
246  except Exception:
247  lkim = None
248 
249  # Calculate spatial model evaluated at each position, for
250  # all candidates
251  skim = afwImage.ImageD(spatialKernel.getDimensions())
252  spatialKernel.computeImage(skim, False, kernelCandidate.getXCenter(),
253  kernelCandidate.getYCenter())
254  centx, centy = calcCentroid(skim.getArray())
255  stdx, stdy = calcWidth(skim.getArray(), centx, centy)
256 
257  sk = afwMath.FixedKernel(skim)
258  sbg = spatialBackground(kernelCandidate.getXCenter(), kernelCandidate.getYCenter())
259  di = kernelCandidate.getDifferenceImage(sk, sbg)
260  spatialResults = self._calculateStats(di, dof=dof)
261 
262  # Kernel mse
263  if lkim is not None:
264  skim -= lkim
265  bias = np.mean(skim.getArray())
266  variance = np.mean(np.power(skim.getArray(), 2.))
267  mseKernel = bias**2 + variance
268  else:
269  mseKernel = -99.999
270 
271  metrics = {"KCDiffimMean_SPATIAL": spatialResults["mean"],
272  "KCDiffimMedian_SPATIAL": spatialResults["median"],
273  "KCDiffimIQR_SPATIAL": spatialResults["iqr"],
274  "KCDiffimStDev_SPATIAL": spatialResults["stdev"],
275  "KCDiffimKSD_SPATIAL": spatialResults["D"],
276  "KCDiffimKSProb_SPATIAL": spatialResults["prob"],
277  "KCDiffimADA2_SPATIAL": spatialResults["A2"],
278  "KCDiffimADCrit_SPATIAL": spatialResults["crit"],
279  "KCDiffimADSig_SPATIAL": spatialResults["sig"],
280  "KCDiffimChiSq_SPATIAL": spatialResults["rchisq"],
281  "KCDiffimMseResids_SPATIAL": spatialResults["mseResids"],
282  "KCDiffimMseKernel_SPATIAL": mseKernel,
283  "KCKernelCentX_SPATIAL": centx,
284  "KCKernelCentY_SPATIAL": centy,
285  "KCKernelStdX_SPATIAL": stdx,
286  "KCKernelStdY_SPATIAL": stdy,
287  "KernelCandidateId_SPATIAL": kernelCandidate.getId()}
288  for k in metrics:
289  key = schema[k].asKey()
290  setter = getattr(source, "set"+key.getTypeString())
291  setter(key, metrics[k])
292 
293  def aggregate(self, sourceCatalog, metadata, wcsresids, diaSources=None):
294  """Generate aggregate metrics (e.g. total numbers of false
295  positives) from all the Sources in the sourceCatalog"""
296  for source in sourceCatalog:
297  sourceId = source.getId()
298  if sourceId in wcsresids:
299  # Note that the residuals are not delta RA, delta Dec
300  # From the source code "bearing (angle wrt a declination parallel) and distance
301  coord, resids = wcsresids[sourceId]
302  key = source.schema["RegisterResidualBearing"].asKey()
303  setter = getattr(source, "set"+key.getTypeString())
304  setter(key, resids[0])
305  key = source.schema["RegisterResidualDistance"].asKey()
306  setter = getattr(source, "set"+key.getTypeString())
307  setter(key, resids[1])
308  key = source.schema["RegisterRefPosition"].asKey()
309  setter = getattr(source, "set"+key.getTypeString())
310  setter(key, afwGeom.Point2D(coord.getRa().asRadians(),
311  coord.getDec().asRadians()))
312  if diaSources:
313  metadata.add("NFalsePositivesTotal", len(diaSources))
314  nRefMatch = 0
315  nSrcMatch = 0
316  nunmatched = 0
317  for source in diaSources:
318  refId = source.get("refMatchId")
319  srcId = source.get("srcMatchId")
320  if refId > 0:
321  nRefMatch += 1
322  if srcId > 0:
323  nSrcMatch += 1
324  if refId == 0 and srcId == 0:
325  nunmatched += 1
326  metadata.add("NFalsePositivesRefAssociated", nRefMatch)
327  metadata.add("NFalsePositivesSrcAssociated", nSrcMatch)
328  metadata.add("NFalsePositivesUnassociated", nunmatched)
329  for kType in ("LOCAL", "SPATIAL"):
330  for sName in ("KCDiffimMean", "KCDiffimMedian", "KCDiffimIQR", "KCDiffimStDev",
331  "KCDiffimKSProb", "KCDiffimADSig", "KCDiffimChiSq",
332  "KCDiffimMseResids", "KCDiffimMseKernel"):
333  if sName == "KCDiffimMseKernel" and kType == "LOCAL":
334  continue
335  kName = "%s_%s" % (sName, kType)
336  vals = np.array([s.get(kName) for s in sourceCatalog])
337  idx = np.isfinite(vals)
338  metadata.add("%s_MEAN" % (kName), np.mean(vals[idx]))
339  metadata.add("%s_MEDIAN" % (kName), np.median(vals[idx]))
340  metadata.add("%s_STDEV" % (kName), np.std(vals[idx]))
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
A description of a field in a table.
Definition: Field.h:24
def aggregate(self, sourceCatalog, metadata, wcsresids, diaSources=None)
def apply(self, candidateList, spatialKernel, spatialBackground, dof=0)
def calcCentroid(arr)
Definition: utils.py:649
def calcWidth(arr, centx, centy)
Definition: utils.py:664
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
A kernel created from an Image.
Definition: Kernel.h:509