Hide keyboard shortcuts

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 

8 

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 

15 

16from .metricBundle import MetricBundle 

17 

18__all__ = ['MoMetricBundle', 'MoMetricBundleGroup', 'createEmptyMoMetricBundle', 'makeCompletenessBundle'] 

19 

20 

21def createEmptyMoMetricBundle(): 

22 """Create an empty metric bundle. 

23 

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) 

30 

31 

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. 

38 

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. 

49 

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 

81 

82 

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 

146 

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 

166 

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 

181 

182 def _findReqCols(self): 

183 # Doesn't quite work the same way yet. No stacker list, for example. 

184 raise NotImplementedError 

185 

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 = [] 

206 

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) 

224 

225 def reduceMetric(self, reduceFunc, reducePlotDict=None, reduceDisplayDict=None): 

226 raise NotImplementedError 

227 

228 

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 

237 

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()])) 

244 

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. 

251 

252 Parameters 

253 ---------- 

254 metricBundle1 : MetricBundle 

255 metricBundle2 : MetricBundle 

256 

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 

274 

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. 

278 

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 

315 

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). 

321 

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) 

339 

340 # And now run each of those subsets of compatible metricBundles. 

341 for compatibleList in compatibleLists: 

342 self._runCompatible(compatibleList) 

343 

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. 

347 

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) 

357 

358 bDict = self.bundleDict # {key: self.bundleDict.get(key) for key in compatibleList} 

359 

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) 

374 

375 if len(uniqMaps) > 0: 

376 print("Got some maps .. that was unexpected at the moment. Can't use them here yet.") 

377 

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) 

428 

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.') 

437 

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.')