Coverage for python/lsst/sims/maf/metricBundles/moMetricBundle.py : 8%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import print_function
2from builtins import object
3import os
4import warnings
5import numpy as np
6import numpy.ma as ma
7import matplotlib.pyplot as plt
9from lsst.sims.maf.metrics import BaseMoMetric
10from lsst.sims.maf.metrics import MoCompletenessMetric, ValueAtHMetric
11from lsst.sims.maf.slicers import MoObjSlicer
12from lsst.sims.maf.stackers import BaseMoStacker, MoMagStacker
13from lsst.sims.maf.plots import PlotHandler
14from lsst.sims.maf.plots import MetricVsH
16from .metricBundle import MetricBundle
18__all__ = ['MoMetricBundle', 'MoMetricBundleGroup', 'createEmptyMoMetricBundle', 'makeCompletenessBundle']
21def createEmptyMoMetricBundle():
22 """Create an empty metric bundle.
24 Returns
25 -------
26 ~lsst.sims.maf.metricBundles.MoMetricBundle
27 An empty metric bundle, configured with just the :class:`BaseMetric` and :class:`BaseSlicer`.
28 """
29 return MoMetricBundle(BaseMoMetric(), MoObjSlicer(), None)
32def makeCompletenessBundle(bundle, completenessMetric, Hmark=None, resultsDb=None):
33 """
34 Make a mock metric bundle from a bundle which had MoCompleteness or MoCumulativeCompleteness summary
35 metrics run. This lets us use the plotHandler + plots.MetricVsH to generate plots.
36 Will also work with completeness metric run in order to calculate fraction of the population,
37 or with MoCompletenessAtTime metric.
39 Parameters
40 ----------
41 bundle : ~lsst.sims.maf.metricBundles.MetricBundle
42 The metric bundle with a completeness summary statistic.
43 completenessMetric : ~lsst.sims.maf.metric
44 The summary (completeness) metric to run on the bundle.
45 Hmark : float, opt
46 The Hmark value to add to the plotting dictionary of the new mock bundle. Default None.
47 resultsDb : ~lsst.sims.maf.db.ResultsDb, opt
48 The resultsDb in which to record the summary statistic value at Hmark. Default None.
50 Returns
51 -------
52 ~lsst.sims.maf.metricBundles.MoMetricBundle
53 """
54 bundle.setSummaryMetrics(completenessMetric)
55 bundle.computeSummaryStats(resultsDb)
56 summaryName = completenessMetric.name
57 # Make up the bundle, including the metric values.
58 completeness = ma.MaskedArray(data=bundle.summaryValues[summaryName]['value'],
59 mask=np.zeros(len(bundle.summaryValues[summaryName]['value'])),
60 fill_value=0)
61 mb = MoMetricBundle(completenessMetric, bundle.slicer,
62 constraint=bundle.constraint, runName=bundle.runName,
63 metadata=bundle.metadata)
64 plotDict = {}
65 plotDict.update(bundle.plotDict)
66 plotDict['label'] = bundle.metadata
67 if 'Completeness' not in summaryName:
68 plotDict['label'] += ' ' + summaryName.replace('FractionPop_', '')
69 mb.metricValues = completeness.reshape(1, len(completeness))
70 if Hmark is not None:
71 metric = ValueAtHMetric(Hmark=Hmark)
72 mb.setSummaryMetrics(metric)
73 mb.computeSummaryStats(resultsDb)
74 val = mb.summaryValues['Value At H=%.1f' % Hmark]
75 if summaryName.startswith('Cumulative'):
76 plotDict['label'] += ': @ H(<=%.1f) = %.1f%s' % (Hmark, val * 100, '%')
77 else:
78 plotDict['label'] += ': @ H(=%.1f) = %.1f%s' % (Hmark, val * 100, '%')
79 mb.setPlotDict(plotDict)
80 return mb
83class MoMetricBundle(MetricBundle):
84 def __init__(self, metric, slicer, constraint=None,
85 stackerList=None,
86 runName='opsim', metadata=None,
87 fileRoot=None,
88 plotDict=None, plotFuncs=None,
89 displayDict=None,
90 childMetrics=None,
91 summaryMetrics=None):
92 """
93 Instantiate moving object metric bundle, save metric/slicer/constraint, etc.
94 """
95 self.metric = metric
96 self.slicer = slicer
97 if constraint == '':
98 constraint = None
99 self.constraint = constraint
100 # Set the stackerlist.
101 if stackerList is not None:
102 if isinstance(stackerList, BaseMoStacker):
103 self.stackerList = [stackerList, ]
104 else:
105 self.stackerList = []
106 for s in stackerList:
107 if not isinstance(s, BaseMoStacker):
108 raise ValueError('stackerList must only contain '
109 'lsst.sims.maf.stackers.BaseMoStacker type objs')
110 self.stackerList.append(s)
111 else:
112 self.stackerList = []
113 # Add the basic 'visibility/mag' stacker if not present.
114 magStackerFound = False
115 for s in self.stackerList:
116 if s.__class__.__name__ == 'MoMagStacker':
117 magStackerFound = True
118 break
119 if not magStackerFound:
120 self.stackerList.append(MoMagStacker())
121 # Set a mapsList just for compatibility with generic MetricBundle.
122 self.mapsList = []
123 # Add the summary stats, if applicable.
124 self.setSummaryMetrics(summaryMetrics)
125 # Set the provenance/metadata.
126 self.runName = runName
127 self._buildMetadata(metadata)
128 # Build the output filename root if not provided.
129 if fileRoot is not None:
130 self.fileRoot = fileRoot
131 else:
132 self._buildFileRoot()
133 # Set the plotting classes/functions.
134 self.setPlotFuncs(plotFuncs)
135 # Set the plotDict and displayDicts.
136 self.plotDict = {'units': '@H'}
137 self.setPlotDict(plotDict)
138 # Update/set displayDict.
139 self.displayDict = {}
140 self.setDisplayDict(displayDict)
141 # Set the list of child metrics.
142 self.setChildBundles(childMetrics)
143 # This is where we store the metric values and summary stats.
144 self.metricValues = None
145 self.summaryValues = None
147 def _resetMetricBundle(self):
148 """Reset all properties of MetricBundle.
149 """
150 self.metric = None
151 self.slicer = None
152 self.constraint = None
153 self.stackerList = [MoMagStacker()]
154 self.mapsList = []
155 self.summaryMetrics = []
156 self.plotFuncs = []
157 self.runName = 'opsim'
158 self.metadata = ''
159 self.dbCols = None
160 self.fileRoot = None
161 self.plotDict = {}
162 self.displayDict = {}
163 self.childMetrics = None
164 self.metricValues = None
165 self.summaryValues = None
167 def _buildMetadata(self, metadata):
168 """If no metadata is provided, auto-generate it from the obsFile + constraint.
169 """
170 if metadata is None:
171 try:
172 self.metadata = self.slicer.obsfile.replace('.txt', '').replace('.dat', '')
173 self.metadata = self.metadata.replace('_obs', '').replace('_allObs', '')
174 except AttributeError:
175 self.metadata = 'noObs'
176 # And modify by constraint.
177 if self.constraint is not None:
178 self.metadata += ' ' + self.constraint
179 else:
180 self.metadata = metadata
182 def _findReqCols(self):
183 # Doesn't quite work the same way yet. No stacker list, for example.
184 raise NotImplementedError
186 def setChildBundles(self, childMetrics=None):
187 """
188 Identify any child metrics to be run on this (parent) bundle.
189 and create the new metric bundles that will hold the child values, linking to this bundle.
190 Remove the summaryMetrics from self afterwards.
191 """
192 self.childBundles = {}
193 if childMetrics is None:
194 childMetrics = self.metric.childMetrics
195 for cName, cMetric in childMetrics.items():
196 cBundle = MoMetricBundle(metric=cMetric, slicer=self.slicer,
197 constraint=self.constraint,
198 stackerList=self.stackerList,
199 runName=self.runName, metadata=self.metadata,
200 plotDict=self.plotDict, plotFuncs=self.plotFuncs,
201 displayDict=self.displayDict,
202 summaryMetrics=self.summaryMetrics)
203 self.childBundles[cName] = cBundle
204 if len(childMetrics) > 0:
205 self.summaryMetrics = []
207 def computeSummaryStats(self, resultsDb=None):
208 """
209 Compute summary statistics on metricValues, using summaryMetrics, for self and child bundles.
210 """
211 if self.summaryValues is None:
212 self.summaryValues = {}
213 if self.summaryMetrics is not None:
214 # Build array of metric values, to use for (most) summary statistics.
215 for m in self.summaryMetrics:
216 summaryName = m.name
217 summaryVal = m.run(self.metricValues, self.slicer.slicePoints['H'])
218 self.summaryValues[summaryName] = summaryVal
219 # Add summary metric info to results database, if applicable.
220 if resultsDb:
221 metricId = resultsDb.updateMetric(self.metric.name, self.slicer.slicerName,
222 self.runName, self.constraint, self.metadata, None)
223 resultsDb.updateSummaryStat(metricId, summaryName=summaryName, summaryValue=summaryVal)
225 def reduceMetric(self, reduceFunc, reducePlotDict=None, reduceDisplayDict=None):
226 raise NotImplementedError
229class MoMetricBundleGroup(object):
230 def __init__(self, bundleDict, outDir='.', resultsDb=None, verbose=True):
231 self.verbose = verbose
232 self.bundleDict = bundleDict
233 self.outDir = outDir
234 if not os.path.isdir(self.outDir):
235 os.makedirs(self.outDir)
236 self.resultsDb = resultsDb
238 self.slicer = list(self.bundleDict.values())[0].slicer
239 for b in self.bundleDict.values():
240 if b.slicer != self.slicer:
241 raise ValueError('Currently, the slicers for the MoMetricBundleGroup must be equal,'
242 ' using the same observations and Hvals.')
243 self.constraints = list(set([b.constraint for b in bundleDict.values()]))
245 def _checkCompatible(self, metricBundle1, metricBundle2):
246 """Check if two MetricBundles are "compatible".
247 Compatible indicates that the constraints, the slicers, and the maps are the same, and
248 that the stackers do not interfere with each other
249 (i.e. are not trying to set the same column in different ways).
250 Returns True if the MetricBundles are compatible, False if not.
252 Parameters
253 ----------
254 metricBundle1 : MetricBundle
255 metricBundle2 : MetricBundle
257 Returns
258 -------
259 bool
260 """
261 if metricBundle1.constraint != metricBundle2.constraint:
262 return False
263 if metricBundle1.slicer != metricBundle2.slicer:
264 return False
265 if metricBundle1.mapsList.sort() != metricBundle2.mapsList.sort():
266 return False
267 for stacker in metricBundle1.stackerList:
268 for stacker2 in metricBundle2.stackerList:
269 # If the stackers have different names, that's OK, and if they are identical, that's ok.
270 if (stacker.__class__.__name__ == stacker2.__class__.__name__) & (stacker != stacker2):
271 return False
272 # But if we got this far, everything matches.
273 return True
275 def _findCompatible(self, testKeys):
276 """"Private utility to find which metricBundles with keys in the list 'testKeys' can be calculated
277 at the same time -- having the same slicer, constraint, maps, and compatible stackers.
279 Parameters
280 -----------
281 testKeys : list
282 List of the dictionary keys (of self.bundleDict) to test for compatibilility.
283 Returns
284 --------
285 list of lists
286 Returns testKeys, split into separate lists of compatible metricBundles.
287 """
288 compatibleLists = []
289 for k in testKeys:
290 try:
291 b = self.bundleDict[k]
292 except KeyError:
293 warnings.warn('Received %s in testkeys, but this is not present in self.bundleDict.'
294 'Will continue, but this is not expected.')
295 continue
296 foundCompatible = False
297 checkedAll = False
298 while not(foundCompatible) and not(checkedAll):
299 # Go through the existing lists in compatibleLists, to see if this metricBundle matches.
300 for compatibleList in compatibleLists:
301 # Compare to all the metricBundles in this subset, to check all stackers are compatible.
302 foundCompatible = True
303 for comparisonKey in compatibleList:
304 compatible = self._checkCompatible(self.bundleDict[comparisonKey], b)
305 if not compatible:
306 # Found a metricBundle which is not compatible, so stop and go onto the next subset.
307 foundCompatible = False
308 break
309 checkedAll = True
310 if foundCompatible:
311 compatibleList.append(k)
312 else:
313 compatibleLists.append([k,])
314 return compatibleLists
316 def runConstraint(self, constraint):
317 """Calculate the metric values for all the metricBundles which match this constraint in the
318 metricBundleGroup. Also calculates child metrics and summary statistics, and writes all to disk.
319 (work is actually done in _runCompatible, so that only completely compatible sets of metricBundles
320 run at the same time).
322 Parameters
323 ----------
324 constraint : str
325 SQL-where or pandas constraint for the metricBundles.
326 """
327 # Find the dict keys of the bundles which match this constraint.
328 keysMatchingConstraint = []
329 for k, b in self.bundleDict.items():
330 if b.constraint == constraint:
331 keysMatchingConstraint.append(k)
332 if len(keysMatchingConstraint) == 0:
333 return
334 # Identify the observations which are relevant for this constraint.
335 # This sets slicer.obs (valid for all H values).
336 self.slicer.subsetObs(constraint)
337 # Identify the sets of these metricBundles can be run at the same time (also have the same stackers).
338 compatibleLists = self._findCompatible(keysMatchingConstraint)
340 # And now run each of those subsets of compatible metricBundles.
341 for compatibleList in compatibleLists:
342 self._runCompatible(compatibleList)
344 def _runCompatible(self, compatibleList):
345 """Calculate the metric values for set of (parent and child) bundles, as well as the summary stats,
346 and write to disk.
348 Parameters
349 -----------
350 compatibleList : list
351 List of dictionary keys, of the metricBundles which can be calculated together.
352 This means they are 'compatible' and have the same slicer, constraint, and non-conflicting
353 mappers and stackers.
354 """
355 if self.verbose:
356 print('Running metrics %s' % compatibleList)
358 bDict = self.bundleDict # {key: self.bundleDict.get(key) for key in compatibleList}
360 # Find the unique stackers and maps. These are already "compatible" (as id'd by compatibleList).
361 uniqStackers = []
362 allStackers = []
363 uniqMaps = []
364 allMaps = []
365 for b in bDict.values():
366 allStackers += b.stackerList
367 allMaps += b.mapsList
368 for s in allStackers:
369 if s not in uniqStackers:
370 uniqStackers.append(s)
371 for m in allMaps:
372 if m not in uniqMaps:
373 uniqMaps.append(m)
375 if len(uniqMaps) > 0:
376 print("Got some maps .. that was unexpected at the moment. Can't use them here yet.")
378 # Set up all of the metric values, including for the child bundles.
379 for k in compatibleList:
380 b = self.bundleDict[k]
381 b._setupMetricValues()
382 for cb in b.childBundles.values():
383 cb._setupMetricValues()
384 # Calculate the metric values.
385 for i, slicePoint in enumerate(self.slicer):
386 ssoObs = slicePoint['obs']
387 for j, Hval in enumerate(slicePoint['Hvals']):
388 # Run stackers to add extra columns (that depend on Hval)
389 with warnings.catch_warnings():
390 warnings.simplefilter('ignore')
391 for s in uniqStackers:
392 ssoObs = s.run(ssoObs, slicePoint['orbit']['H'], Hval)
393 # Run all the parent metrics.
394 for k in compatibleList:
395 b = self.bundleDict[k]
396 # Mask the parent metric (and then child metrics) if there was no data.
397 if len(ssoObs) == 0:
398 b.metricValues.mask[i][j] = True
399 for cb in list(b.childBundles.values()):
400 cb.metricValues.mask[i][j] = True
401 # Otherwise, calculate the metric value for the parent, and then child.
402 else:
403 # Calculate for the parent.
404 mVal = b.metric.run(ssoObs, slicePoint['orbit'], Hval)
405 # Mask if the parent metric returned a bad value.
406 if mVal == b.metric.badval:
407 b.metricValues.mask[i][j] = True
408 for cb in b.childBundles.values():
409 cb.metricValues.mask[i][j] = True
410 # Otherwise, set the parent value and calculate the child metric values as well.
411 else:
412 b.metricValues.data[i][j] = mVal
413 for cb in b.childBundles.values():
414 childVal = cb.metric.run(ssoObs, slicePoint['orbit'], Hval, mVal)
415 if childVal == cb.metric.badval:
416 cb.metricValues.mask[i][j] = True
417 else:
418 cb.metricValues.data[i][j] = childVal
419 for k in compatibleList:
420 b = self.bundleDict[k]
421 b.computeSummaryStats(self.resultsDb)
422 for cB in b.childBundles.values():
423 cB.computeSummaryStats(self.resultsDb)
424 # Write to disk.
425 cB.write(outDir=self.outDir, resultsDb=self.resultsDb)
426 # Write to disk.
427 b.write(outDir=self.outDir, resultsDb=self.resultsDb)
429 def runAll(self):
430 """
431 Run all constraints and metrics for these moMetricBundles.
432 """
433 for constraint in self.constraints:
434 self.runConstraint(constraint)
435 if self.verbose:
436 print('Calculated and saved all metrics.')
438 def plotAll(self, savefig=True, outfileSuffix=None, figformat='pdf', dpi=600, thumbnail=True,
439 closefigs=True):
440 """
441 Make a few generically desired plots. This needs more flexibility in the future.
442 """
443 plotHandler = PlotHandler(outDir=self.outDir, resultsDb=self.resultsDb,
444 savefig=savefig, figformat=figformat, dpi=dpi, thumbnail=thumbnail)
445 for b in self.bundleDict.values():
446 try:
447 b.plot(plotHandler=plotHandler, outfileSuffix=outfileSuffix, savefig=savefig)
448 except ValueError as ve:
449 message = 'Plotting failed for metricBundle %s.' % (b.fileRoot)
450 message += ' Error message: %s' % (ve.message)
451 warnings.warn(message)
452 if closefigs:
453 plt.close('all')
454 if self.verbose:
455 print('Plotting all metrics.')