Coverage for python/lsst/sims/maf/metricBundles/metricBundle.py : 7%

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 builtins import zip
2from builtins import object
3import os
4from copy import deepcopy
5import numpy as np
6import numpy.ma as ma
7import warnings
9import lsst.sims.maf.metrics as metrics
10import lsst.sims.maf.slicers as slicers
11import lsst.sims.maf.stackers as stackers
12import lsst.sims.maf.maps as maps
13import lsst.sims.maf.plots as plots
14from lsst.sims.maf.stackers import ColInfo
15import lsst.sims.maf.utils as utils
17__all__ = ['MetricBundle', 'createEmptyMetricBundle']
20def createEmptyMetricBundle():
21 """Create an empty metric bundle.
23 Returns
24 -------
25 MetricBundle
26 An empty metric bundle, configured with just the :class:`BaseMetric` and :class:`BaseSlicer`.
27 """
28 return MetricBundle(metrics.BaseMetric(), slicers.BaseSlicer(), '')
31class MetricBundle(object):
32 """The MetricBundle is defined by a combination of a (single) metric, slicer and
33 constraint - together these define a unique combination of an opsim benchmark.
34 An example would be: a CountMetric, a HealpixSlicer, and a sqlconstraint 'filter="r"'.
36 After the metric is evaluated over the slicePoints of the slicer, the resulting
37 metric values are saved in the MetricBundle.
39 The MetricBundle also saves the summary metrics to be used to generate summary
40 statistics over those metric values, as well as the resulting summary statistic values.
42 Plotting parameters and display parameters (for showMaf) are saved in the MetricBundle,
43 as well as additional metadata such as the opsim run name, and relevant stackers and maps
44 to apply when calculating the metric values.
45 """
46 colInfo = ColInfo()
48 def __init__(self, metric, slicer, constraint=None, sqlconstraint=None,
49 stackerList=None, runName='opsim', metadata=None,
50 plotDict=None, displayDict=None,
51 summaryMetrics=None, mapsList=None,
52 fileRoot=None, plotFuncs=None):
53 # Set the metric.
54 if not isinstance(metric, metrics.BaseMetric):
55 raise ValueError('metric must be an lsst.sims.maf.metrics object')
56 self.metric = metric
57 # Set the slicer.
58 if not isinstance(slicer, slicers.BaseSlicer):
59 raise ValueError('slicer must be an lsst.sims.maf.slicers object')
60 self.slicer = slicer
61 # Set the constraint.
62 self.constraint = constraint
63 if self.constraint is None:
64 # Provide backwards compatibility for now - phase out sqlconstraint eventually.
65 if sqlconstraint is not None:
66 warnings.warn('Future warning - "sqlconstraint" will be deprecated in favor of '
67 '"constraint" in a future release.')
68 self.constraint = sqlconstraint
69 if self.constraint is None:
70 self.constraint = ''
71 # Set the stackerlist if applicable.
72 if stackerList is not None:
73 if isinstance(stackerList, stackers.BaseStacker):
74 self.stackerList = [stackerList, ]
75 else:
76 self.stackerList = []
77 for s in stackerList:
78 if s is None:
79 pass
80 else:
81 if not isinstance(s, stackers.BaseStacker):
82 raise ValueError('stackerList must only contain lsst.sims.maf.stackers objs')
83 self.stackerList.append(s)
84 else:
85 self.stackerList = []
86 # Set the 'maps' to apply to the slicer, if applicable.
87 if mapsList is not None:
88 if isinstance(mapsList, maps.BaseMap):
89 self.mapsList = [mapsList, ]
90 else:
91 self.mapsList = []
92 for m in mapsList:
93 if not isinstance(m, maps.BaseMap):
94 raise ValueError('mapsList must only contain lsst.sims.maf.maps objects')
95 self.mapsList.append(m)
96 else:
97 self.mapsList = []
98 # If the metric knows it needs a particular map, add it to the list.
99 mapNames = [mapName.__class__.__name__ for mapName in self.mapsList]
100 if hasattr(self.metric, 'maps'):
101 for mapName in self.metric.maps:
102 if mapName not in mapNames:
103 tempMap = getattr(maps, mapName)()
104 self.mapsList.append(tempMap)
105 mapNames.append(mapName)
107 # Add the summary stats, if applicable.
108 self.setSummaryMetrics(summaryMetrics)
109 # Set the provenance/metadata.
110 self._buildMetadata(metadata)
111 # Set the run name and build the output filename base (fileRoot).
112 self.setRunName(runName)
113 # Reset fileRoot, if provided.
114 if fileRoot is not None:
115 self.fileRoot = fileRoot
116 # Determine the columns needed from the database.
117 self._findReqCols()
118 # Set the plotting classes/functions.
119 self.setPlotFuncs(plotFuncs)
120 # Set the plotDict and displayDicts.
121 self.plotDict = {}
122 self.setPlotDict(plotDict)
123 # Update/set displayDict.
124 self.displayDict = {}
125 self.setDisplayDict(displayDict)
126 # This is where we store the metric values and summary stats.
127 self.metricValues = None
128 self.summaryValues = None
130 def _resetMetricBundle(self):
131 """Reset all properties of MetricBundle.
132 """
133 self.metric = None
134 self.slicer = None
135 self.constraint = None
136 self.stackerList = []
137 self.summaryMetrics = []
138 self.plotFuncs = []
139 self.mapsList = None
140 self.runName = 'opsim'
141 self.metadata = ''
142 self.dbCols = None
143 self.fileRoot = None
144 self.plotDict = {}
145 self.displayDict = {}
146 self.metricValues = None
147 self.summaryValues = None
149 def _setupMetricValues(self):
150 """Set up the numpy masked array to store the metric value data.
151 """
152 dtype = self.metric.metricDtype
153 # Can't store healpix slicer mask values in an int array.
154 if dtype == 'int':
155 dtype = 'float'
156 if self.metric.shape == 1:
157 shape = self.slicer.shape
158 else:
159 shape = (self.slicer.shape, self.metric.shape)
160 self.metricValues = ma.MaskedArray(data=np.empty(shape, dtype),
161 mask=np.zeros(shape, 'bool'),
162 fill_value=self.slicer.badval)
164 def _buildMetadata(self, metadata):
165 """If no metadata is provided, process the constraint
166 (by removing extra spaces, quotes, the word 'filter' and equal signs) to make a metadata version.
167 e.g. 'filter = "r"' becomes 'r'
168 """
169 if metadata is None:
170 self.metadata = self.constraint.replace('=', '').replace('filter', '').replace("'", '')
171 self.metadata = self.metadata.replace('"', '').replace(' ', ' ')
172 self.metadata.strip(' ')
173 else:
174 self.metadata = metadata
176 def _buildFileRoot(self):
177 """
178 Build an auto-generated output filename root (i.e. minus the plot type or .npz ending).
179 """
180 # Build basic version.
181 self.fileRoot = '_'.join([self.runName, self.metric.name, self.metadata,
182 self.slicer.slicerName[:4].upper()])
183 # Sanitize output name if needed.
184 self.fileRoot = utils.nameSanitize(self.fileRoot)
186 def _findReqCols(self):
187 """Find the columns needed by the metrics, slicers, and stackers.
188 If there are any additional stackers required, instatiate them and add them to
189 the self.stackers list.
190 (default stackers have to be instantiated to determine what additional columns
191 are needed from database).
192 """
193 # Find all the columns needed by metric and slicer.
194 knownCols = self.slicer.columnsNeeded + list(self.metric.colNameArr)
195 # For the stackers already set up, find their required columns.
196 for s in self.stackerList:
197 knownCols += s.colsReq
198 knownCols = set(knownCols)
199 # Track sources of all of these columns.
200 self.dbCols = set()
201 newstackers = set()
202 for col in knownCols:
203 if self.colInfo.getDataSource(col) == self.colInfo.defaultDataSource:
204 self.dbCols.add(col)
205 else:
206 # New default stackers could come from metric/slicer or stackers.
207 newstackers.add(self.colInfo.getDataSource(col))
208 # Remove already-specified stackers from default list.
209 for s in self.stackerList:
210 if s.__class__ in newstackers:
211 newstackers.remove(s.__class__)
212 # Loop and check if stackers are introducing new columns or stackers.
213 while len(newstackers) > 0:
214 # Check for the sources of the columns in any of the new stackers.
215 newCols = []
216 for s in newstackers:
217 newstacker = s()
218 newCols += newstacker.colsReq
219 self.stackerList.append(newstacker)
220 newCols = set(newCols)
221 newstackers = set()
222 for col in newCols:
223 if self.colInfo.getDataSource(col) == self.colInfo.defaultDataSource:
224 self.dbCols.add(col)
225 else:
226 newstackers.add(self.colInfo.getDataSource(col))
227 for s in self.stackerList:
228 if s.__class__ in newstackers:
229 newstackers.remove(s.__class__)
230 # A Bit of cleanup.
231 # Remove 'metricdata' from dbcols if it ended here by default.
232 if 'metricdata' in self.dbCols:
233 self.dbCols.remove('metricdata')
234 if 'None' in self.dbCols:
235 self.dbCols.remove('None')
237 def setSummaryMetrics(self, summaryMetrics):
238 """Set (or reset) the summary metrics for the metricbundle.
240 Parameters
241 ----------
242 summaryMetrics : List[BaseMetric]
243 Instantiated summary metrics to use to calculate summary statistics for this metric.
244 """
245 if summaryMetrics is not None:
246 if isinstance(summaryMetrics, metrics.BaseMetric):
247 self.summaryMetrics = [summaryMetrics]
248 else:
249 self.summaryMetrics = []
250 for s in summaryMetrics:
251 if not isinstance(s, metrics.BaseMetric):
252 raise ValueError('SummaryStats must only contain lsst.sims.maf.metrics objects')
253 self.summaryMetrics.append(s)
254 else:
255 # Add identity metric to unislicer metric values (to get them into resultsDB).
256 if self.slicer.slicerName == 'UniSlicer':
257 self.summaryMetrics = [metrics.IdentityMetric()]
258 else:
259 self.summaryMetrics = []
261 def setPlotFuncs(self, plotFuncs):
262 """Set or reset the plotting functions.
264 The default is to use all the plotFuncs associated with the slicer, which
265 is what happens in self.plot if setPlotFuncs is not used to override self.plotFuncs.
267 Parameters
268 ----------
269 plotFuncs : List[BasePlotter]
270 The plotter or plotters to use to generate visuals for this metric.
271 """
272 if plotFuncs is not None:
273 if plotFuncs is isinstance(plotFuncs, plots.BasePlotter):
274 self.plotFuncs = [plotFuncs]
275 else:
276 self.plotFuncs = []
277 for pFunc in plotFuncs:
278 if not isinstance(pFunc, plots.BasePlotter):
279 raise ValueError('plotFuncs should contain instantiated ' +
280 'lsst.sims.maf.plotter objects.')
281 self.plotFuncs.append(pFunc)
282 else:
283 self.plotFuncs = []
284 for pFunc in self.slicer.plotFuncs:
285 if isinstance(pFunc, plots.BasePlotter):
286 self.plotFuncs.append(pFunc)
287 else:
288 self.plotFuncs.append(pFunc())
290 def setPlotDict(self, plotDict):
291 """Set or update any property of plotDict.
293 Parameters
294 ----------
295 plotDict : dict
296 A dictionary of plotting parameters.
297 The usable keywords vary with each lsst.sims.maf.plots Plotter.
298 """
299 # Don't auto-generate anything here - the plotHandler does it.
300 if plotDict is not None:
301 self.plotDict.update(plotDict)
302 # Check for bad zp or normVal values.
303 if 'zp' in self.plotDict:
304 if self.plotDict['zp'] is not None:
305 if not np.isfinite(self.plotDict['zp']):
306 warnings.warn('Warning! Plot zp for %s was infinite: removing zp from plotDict'
307 % (self.fileRoot))
308 del self.plotDict['zp']
309 if 'normVal' in self.plotDict:
310 if self.plotDict['normVal'] == 0:
311 warnings.warn('Warning! Plot normalization value for %s was 0: removing normVal from plotDict'
312 % (self.fileRoot))
313 del self.plotDict['normVal']
315 def setDisplayDict(self, displayDict=None, resultsDb=None):
316 """Set or update any property of displayDict.
318 Parameters
319 ----------
320 displayDict : Optional[dict]
321 Dictionary of display parameters for showMaf.
322 Expected keywords: 'group', 'subgroup', 'order', 'caption'.
323 'group', 'subgroup', and 'order' control where the metric results are shown on the showMaf page.
324 'caption' provides a caption to use with the metric results.
325 These values are saved in the results database.
326 resultsDb : Optional[ResultsDb]
327 A MAF results database, used to save the display parameters.
328 """
329 # Set up a temporary dictionary with the default values.
330 tmpDisplayDict = {'group': None, 'subgroup': None, 'order': 0, 'caption': None}
331 # Update from self.displayDict (to use existing values, if present).
332 tmpDisplayDict.update(self.displayDict)
333 # And then update from any values being passed now.
334 if displayDict is not None:
335 tmpDisplayDict.update(displayDict)
336 # Reset self.displayDict to this updated dictionary.
337 self.displayDict = tmpDisplayDict
338 # If we still need to auto-generate a caption, do it.
339 if self.displayDict['caption'] is None:
340 if self.metric.comment is None:
341 caption = self.metric.name + ' calculated on a %s basis' % (self.slicer.slicerName)
342 if self.constraint!='' and self.constraint is not None:
343 caption += ' using a subset of data selected via %s.' % (self.constraint)
344 else:
345 caption += '.'
346 else:
347 caption = self.metric.comment
348 if 'zp' in self.plotDict:
349 caption += ' Values plotted with a zeropoint of %.2f.' % (self.plotDict['zp'])
350 if 'normVal' in self.plotDict:
351 caption += ' Values plotted with a normalization value of %.2f.' % (self.plotDict['normVal'])
352 self.displayDict['caption'] = caption
353 if resultsDb:
354 # Update the display values in the resultsDb.
355 metricId = resultsDb.updateMetric(self.metric.name, self.slicer.slicerName,
356 self.runName, self.constraint,
357 self.metadata, None)
358 resultsDb.updateDisplay(metricId, self.displayDict)
360 def setRunName(self, runName, updateFileRoot=True):
361 """Set (or reset) the runName. FileRoot will be updated accordingly if desired.
363 Parameters
364 ----------
365 runName: str
366 Run Name, which will become part of the fileRoot.
367 fileRoot: bool, opt
368 Flag to update the fileRoot with the runName. Default True.
369 """
370 self.runName = runName
371 if updateFileRoot:
372 self._buildFileRoot()
374 def writeDb(self, resultsDb=None, outfileSuffix=None):
375 """Write the metricValues to the database
376 """
377 if outfileSuffix is not None:
378 outfile = self.fileRoot + '_' + outfileSuffix + '.npz'
379 else:
380 outfile = self.fileRoot + '.npz'
381 if resultsDb is not None:
382 metricId = resultsDb.updateMetric(self.metric.name, self.slicer.slicerName,
383 self.runName, self.constraint,
384 self.metadata, outfile)
385 resultsDb.updateDisplay(metricId, self.displayDict)
387 def write(self, comment='', outDir='.', outfileSuffix=None, resultsDb=None):
388 """Write metricValues (and associated metadata) to disk.
390 Parameters
391 ----------
392 comment : Optional[str]
393 Any additional comments to add to the output file
394 outDir : Optional[str]
395 The output directory
396 outfileSuffix : Optional[str]
397 Additional suffix to add to the output files (typically a numerical suffix for movies)
398 resultsD : Optional[ResultsDb]
399 Results database to store information on the file output
400 """
401 if outfileSuffix is not None:
402 outfile = self.fileRoot + '_' + outfileSuffix + '.npz'
403 else:
404 outfile = self.fileRoot + '.npz'
405 self.slicer.writeData(os.path.join(outDir, outfile),
406 self.metricValues,
407 metricName=self.metric.name,
408 simDataName=self.runName,
409 constraint=self.constraint,
410 metadata=self.metadata + comment,
411 displayDict=self.displayDict,
412 plotDict=self.plotDict)
413 if resultsDb is not None:
414 self.writeDb(resultsDb=resultsDb)
416 def outputJSON(self):
417 """Set up and call the baseSlicer outputJSON method, to output to IO string.
419 Returns
420 -------
421 io
422 IO object containing JSON data representing the metric bundle data.
423 """
424 io = self.slicer.outputJSON(self.metricValues,
425 metricName=self.metric.name,
426 simDataName=self.runName,
427 metadata=self.metadata,
428 plotDict=self.plotDict)
429 return io
431 def read(self, filename):
432 """Read metricValues and associated metadata from disk.
433 Overwrites any data currently in metricbundle.
435 Parameters
436 ----------
437 filename : str
438 The file from which to read the metric bundle data.
439 """
440 if not os.path.isfile(filename):
441 raise IOError('%s not found' % filename)
443 self._resetMetricBundle()
444 # Set up a base slicer to read data (we don't know type yet).
445 baseslicer = slicers.BaseSlicer()
446 # Use baseslicer to read file.
447 metricValues, slicer, header = baseslicer.readData(filename)
448 self.slicer = slicer
449 self.metricValues = metricValues
450 self.metricValues.fill_value = slicer.badval
451 # It's difficult to reinstantiate the metric object, as we don't
452 # know what it is necessarily -- the metricName can be changed.
453 self.metric = metrics.BaseMetric()
454 # But, for plot label building, we do need to try to recreate the
455 # metric name and units.
456 self.metric.units = ''
457 if header is not None:
458 self.metric.name = header['metricName']
459 if 'plotDict' in header:
460 if 'units' in header['plotDict']:
461 self.metric.units = header['plotDict']['units']
462 self.runName = header['simDataName']
463 try:
464 self.constraint = header['constraint']
465 except KeyError:
466 self.constraint = header['sqlconstraint']
467 self.metadata = header['metadata']
468 if 'plotDict' in header:
469 self.setPlotDict(header['plotDict'])
470 if 'displayDict' in header:
471 self.setDisplayDict(header['displayDict'])
472 if self.metadata is None:
473 self._buildMetadata()
474 path, head = os.path.split(filename)
475 self.fileRoot = head.replace('.npz', '')
476 self.setPlotFuncs(None)
478 def computeSummaryStats(self, resultsDb=None):
479 """Compute summary statistics on metricValues, using summaryMetrics (metricbundle list).
481 Parameters
482 ----------
483 resultsDb : Optional[ResultsDb]
484 ResultsDb object to use to store the summary statistic values on disk.
485 """
486 if self.summaryValues is None:
487 self.summaryValues = {}
488 if self.summaryMetrics is not None:
489 # Build array of metric values, to use for (most) summary statistics.
490 rarr_std = np.array(list(zip(self.metricValues.compressed())),
491 dtype=[('metricdata', self.metricValues.dtype)])
492 for m in self.summaryMetrics:
493 # The summary metric colname should already be set to 'metricdata', but in case it's not:
494 m.colname = 'metricdata'
495 summaryName = m.name.replace(' metricdata', '').replace(' None', '')
496 if hasattr(m, 'maskVal'):
497 # summary metric requests to use the mask value, as specified by itself,
498 # rather than skipping masked vals.
499 rarr = np.array(list(zip(self.metricValues.filled(m.maskVal))),
500 dtype=[('metricdata', self.metricValues.dtype)])
501 else:
502 rarr = rarr_std
503 if np.size(rarr) == 0:
504 summaryVal = self.slicer.badval
505 else:
506 summaryVal = m.run(rarr)
507 self.summaryValues[summaryName] = summaryVal
508 # Add summary metric info to results database, if applicable.
509 if resultsDb:
510 metricId = resultsDb.updateMetric(self.metric.name, self.slicer.slicerName,
511 self.runName, self.constraint, self.metadata, None)
512 resultsDb.updateSummaryStat(metricId, summaryName=summaryName, summaryValue=summaryVal)
514 def reduceMetric(self, reduceFunc, reducePlotDict=None, reduceDisplayDict=None):
515 """Run 'reduceFunc' (any function that operates on self.metricValues).
516 Typically reduceFunc will be the metric reduce functions, as they are tailored to expect the
517 metricValues format.
518 reduceDisplayDict and reducePlotDicts are displayDicts and plotDicts to be
519 applied to the new metricBundle.
521 Parameters
522 ----------
523 reduceFunc : Func
524 Any function that will operate on self.metricValues (typically metric.reduce* function).
525 reducePlotDict : Optional[dict]
526 Plot dictionary for the results of the reduce function.
527 reduceDisplayDict : Optional[dict]
528 Display dictionary for the results of the reduce function.
530 Returns
531 -------
532 MetricBundle
533 New metric bundle, inheriting metadata from this metric bundle, but containing the new
534 metric values calculated with the 'reduceFunc'.
535 """
536 # Generate a name for the metric values processed by the reduceFunc.
537 rName = reduceFunc.__name__.replace('reduce', '')
538 reduceName = self.metric.name + '_' + rName
539 # Set up metricBundle to store new metric values, and add plotDict/displayDict.
540 newmetric = deepcopy(self.metric)
541 newmetric.name = reduceName
542 newmetric.metricDtype = 'float'
543 if reducePlotDict is not None:
544 if 'units' in reducePlotDict:
545 newmetric.units = reducePlotDict['units']
546 newmetricBundle = MetricBundle(metric=newmetric, slicer=self.slicer,
547 stackerList=self.stackerList,
548 constraint=self.constraint,
549 metadata=self.metadata,
550 runName=self.runName,
551 plotDict=None, plotFuncs=self.plotFuncs,
552 displayDict=None,
553 summaryMetrics=self.summaryMetrics,
554 mapsList=self.mapsList, fileRoot='')
555 # Build a new output file root name.
556 newmetricBundle._buildFileRoot()
557 # Add existing plotDict (except for title/xlabels etc) into new plotDict.
558 for k, v in self.plotDict.items():
559 if k not in newmetricBundle.plotDict:
560 newmetricBundle.plotDict[k] = v
561 # Update newmetricBundle's plot dictionary with any set explicitly by reducePlotDict.
562 newmetricBundle.setPlotDict(reducePlotDict)
563 # Copy the parent metric's display dict into the reduce display dict.
564 newmetricBundle.setDisplayDict(self.displayDict)
565 # Set the reduce function display 'order' (this is set in the BaseMetric
566 # by default, but can be overriden in a metric).
567 order = newmetric.reduceOrder[rName]
568 newmetricBundle.displayDict['order'] = order
569 # And then update the newmetricBundle's display dictionary with any set
570 # explicitly by reduceDisplayDict.
571 newmetricBundle.setDisplayDict(reduceDisplayDict)
572 # Set up new metricBundle's metricValues masked arrays, copying metricValue's mask.
573 newmetricBundle.metricValues = ma.MaskedArray(data=np.empty(len(self.slicer), 'float'),
574 mask=self.metricValues.mask,
575 fill_value=self.slicer.badval)
576 # Fill the reduced metric data using the reduce function.
577 for i, (mVal, mMask) in enumerate(zip(self.metricValues.data, self.metricValues.mask)):
578 if not mMask:
579 newmetricBundle.metricValues.data[i] = reduceFunc(mVal)
580 return newmetricBundle
582 def plot(self, plotHandler=None, plotFunc=None, outfileSuffix=None, savefig=False):
583 """
584 Create all plots available from the slicer. plotHandler holds the output directory info, etc.
586 Parameters
587 ----------
588 plotHandler : Optional[PlotHandler]
589 The plotHandler saves the output location and resultsDb connection for a set of plots.
590 plotFunc : Optional[BasePlotter]
591 Any plotter function. If not specified, the plotters in self.plotFuncs will be used.
592 outfileSuffix : Optional[str]
593 Optional string to append to the end of the plot output files.
594 Useful when creating sequences of images for movies.
595 savefig : Optional[bool]
596 Flag indicating whether or not to save the figure to disk. Default is False.
598 Returns
599 -------
600 dict
601 Dictionary of plotType:figure number key/value pairs, indicating what plots were created
602 and what matplotlib figure numbers were used.
603 """
604 # Generate a plotHandler if none was set.
605 if plotHandler is None:
606 plotHandler = plots.PlotHandler(savefig=savefig)
607 # Make plots.
608 if plotFunc is not None:
609 if isinstance(plotFunc, plots.BasePlotter):
610 plotFunc = plotFunc
611 else:
612 plotFunc = plotFunc()
614 plotHandler.setMetricBundles([self])
615 plotHandler.setPlotDicts(plotDicts=[self.plotDict], reset=True)
616 madePlots = {}
617 if plotFunc is not None:
618 fignum = plotHandler.plot(plotFunc, outfileSuffix=outfileSuffix)
619 madePlots[plotFunc.plotType] = fignum
620 else:
621 for plotFunc in self.plotFuncs:
622 fignum = plotHandler.plot(plotFunc, outfileSuffix=outfileSuffix)
623 madePlots[plotFunc.plotType] = fignum
624 return madePlots