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