def __init__(self, evaluate, defaultArg, swpInds, domain, nTrials=1): ''' Args: evaluate (function): called at each point with array args/returns of equal length defaultArg (ndarray): default value that will be sent to the evaluate function swpIndeces (tuple, int): which channels to sweep domain (tuple, iterable): the values over which the sweep channels will be swept ''' super().__init__() self.evaluate = evaluate self.isScalar = np.isscalar(defaultArg) if self.isScalar: defaultArg = [defaultArg] self.defaultArg = np.array(defaultArg, dtype=float) self.allDims = len(self.defaultArg) self.swpInds = argFlatten(swpInds, typs=tuple) self.swpDims = len(self.swpInds) self.domain = argFlatten(domain, typs=tuple) self.swpShape = tuple(len(dom) for dom in self.domain) if len(self.domain) != self.swpDims: raise ValueError('domain and swpInds must have the same dimension.' + 'Got {} and {}'.format(len(self.domain), len(self.swpInds))) self.plotOptions = {'plType': 'curves'} self.monitorOptions = {'livePlot': False, 'plotEvery': 1, 'stdoutPrint': True, 'runServer': False, 'cmdCtrlPrint': True} # Generate actuation sweep grid self.cmdGrid = np.array(np.meshgrid(*self.domain)).T assert(self.cmdGrid.shape == self.swpShape + (self.swpDims,)) self.nTrials = nTrials
def _reparse(self, parseKeys=None): ''' Reprocess measured data into parsed data. If there is not enough data present, it does nothing. If the parser depends on Args: parseKeys (tuple, str, None): which parsers to recalculate. If None, does all. Execution order depends on addParser calls, not parseKeys Returns: None ''' if self.data is None: return if parseKeys is None: parseKeys = tuple(self.parse.keys()) else: parseKeys = argFlatten(parseKeys, typs=tuple) for pk, pFun in self.parse.items( ): # We're indexing this way to make sure parsing is done in the order of parse attribute, not the order of parseKeys if pk not in parseKeys: continue tempDataMat = np.zeros(self.swpShape) for index in np.ndindex(self.swpShape): dataOfPt = OrderedDict() for datKey, datVal in self.data.items(): if np.any(datVal.shape != self.swpShape): logger.warning( 'Data member %s is wrong size for reparsing %s. Skipping.', datKey, pk) else: dataOfPt[datKey] = datVal[index] try: tempDataMat[index] = pFun(dataOfPt) except KeyError: logger.warning( 'Parser %s depends on unpresent data. Skipping.', pk) break else: self.data[pk] = tempDataMat
def plot(self, slicer=None, tempData=None, index=None, axArr=None, pltKwargs=None): ''' Plots Much of the behavior to figure out labels and numbers for axes comes from the plotOptions attribute. The xKeys and yKeys are keys within this objects **data** dictionary (actuation, measurement, and parsers) The total number of plots will be the product of len('xKey') and len('yKey'). xKeys can be anything, including parsed data members. By default it is the minor actuation variable yKeys can also be anything that has scalar elements. By default it is everything that is currently present, except xKeys and non-scalars When doing line plots in 2D sweeps, the legend does automatic labelling. Each line must correspond to an actuation dimension, otherwise it doesn't make sense. This is despite the fact that the xKeys can still be anything. Usually, each line corresponds to a particular domain value of the major sweep axis; however, if that is specified as an xKey, the lines will correspond to the minor axis. Surface plotting: Ignores whatever is in xKeys. The plotting domain is locked to the actuation domain in order to keep a rectangular grid. The values indicated in yKeys will become color data. Args: slicer (tuple, slice): domain slices axArr (ndarray), plt.axis): axes to plot on. Equivalent to what is returned by this method pltKwargs: passed through to plotting function Todo: * Graphics caching for 2D line plots ''' global hCurves # pylint: disable=global-statement if index is None or np.all(np.array(index) == 0): hCurves = None if pltKwargs is None: pltKwargs = {} # Which data dict to use and its dimensionality if tempData is None: fullData = self.data else: fullData = tempData if fullData is not None: plotDims = list(fullData.values())[0].ndim # Instead of self.actuDims else: plotDims = self.actuDims assertValidPlotType(self.plotOptions['plType'], plotDims, type(self)) # Cuts down the domain to the region of interest if slicer is None: slicer = (slice(None),) * plotDims else: slicer = argFlatten(slicer, typs=tuple) # Figure out what the keys of data are actuationKeys = list(self.actuate.keys()) xKeys = argFlatten(self.plotOptions['xKey'], typs=tuple) yKeys = argFlatten(self.plotOptions['yKey'], typs=tuple) if len(xKeys) == 0: # default is the most minor sweep domain xKeys = (actuationKeys[-1], ) if len(yKeys) == 0: # default is all scalar ranges for datKey, datVal in fullData.items(): if (datKey not in xKeys and datKey not in actuationKeys and np.isscalar(datVal.item(0))): yKeys += (datKey, ) # Check it if (len(xKeys) == 0 or len(yKeys) == 0): raise ValueError('No axis key specified explicitly or found in self.actuate') for k in xKeys + yKeys: if k not in fullData.keys(): raise KeyError(k + ' not found in data keys. ' + 'Available data are ' + ', '.join(fullData.keys())) # Make grid of axes based on number of pairs of variables plotArrShape = np.array([len(yKeys), len(xKeys)]) if axArr is not None: pass elif self.plotOptions['axArr'] is not None: axArr = self.plotOptions['axArr'] else: _, axArr = plt.subplots(nrows=plotArrShape[0], ncols=plotArrShape[1], sharex='col', figsize=(10, plotArrShape[0] * 2.5)) # pylint: disable=unused-variable axArr = np.array(axArr) # Force into a two dimensional array if axArr.ndim == 2: pass elif axArr.ndim == 1: if np.all(plotArrShape == 1): axArr = np.expand_dims(axArr, 0) elif plotArrShape[0] == 1: axArr = np.expand_dims(axArr, 0) elif plotArrShape[1] == 1: axArr = np.expand_dims(axArr, 1) elif axArr.ndim == 0: if np.all(plotArrShape == 1): axArr = np.expand_dims(np.expand_dims(axArr, 0), 0) # Check it if np.any(axArr.shape != plotArrShape): raise ValueError('Shape of axArray does not match plotArrShape') # Prepare options for plotting that do not depend on index or line no. sample_xK = xKeys[0] sample_xData = fullData[sample_xK][slicer] if self.plotOptions['plType'] == 'curves': pltArgs = ('.-', ) if plotDims == 1: if hCurves is None: hCurves = np.empty(axArr.shape, dtype=object) elif plotDims == 2: invertDomainPriority = False autoLabeling = (plotDims == self.actuDims) if autoLabeling: if actuationKeys[0] != sample_xK: curveKey = actuationKeys[0] else: curveKey = actuationKeys[1] if index is not None: index = index[::-1] invertDomainPriority = True nLines = sample_xData.shape[0 if not invertDomainPriority else 1] colors = self.plotOptions['cmap-curves'](np.linspace(0, 1, nLines)) # Loop over axes (i.e. axis key variables) and plot for iAx, ax in np.ndenumerate(axArr): xK = xKeys[iAx[1]] yK = yKeys[iAx[0]] # dereference and slice xData = fullData[xK][slicer] yData = fullData[yK][slicer] if self.plotOptions['plType'] == 'curves': if plotDims == 1: # slice it if index is not None: xData = xData[:index[0] + 1] yData = yData[:index[0] + 1] ax.cla() curv = ax.plot(xData, yData, *pltArgs, **pltKwargs) # caching the part of the line that has already been drawn if hCurves[iAx] is not None: # pylint:disable=unsubscriptable-object try: hCurves[iAx][0].remove() except ValueError: # it was probably an old one pass hCurves[iAx] = curv elif plotDims == 2: ax.cla() # no caching, just clear if invertDomainPriority: xData = xData.T yData = yData.T for iLine in range(nLines): # slicing data based on what the line and index are xLine = xData[iLine, :] yLine = yData[iLine, :] if index is None: pass elif iLine < index[-2]: # these lines are complete pass elif iLine == index[-2]: # these lines are in-progress xLine = xLine[slice(index[-1] + 1)] yLine = yLine[slice(index[-1] + 1)] elif iLine > index[-2]: # these have not been started break # line options pltKwargs['color'] = colors[iLine][:3] if autoLabeling: curveValue = self.actuate[curveKey].domain[iLine] pltKwargs['label'] = '{} = {:.2f}'.format(curveKey, curveValue) ax.plot(xLine, yLine, *pltArgs, **pltKwargs) # legend if autoLabeling and iAx[0] == 0 and iAx[1] == plotArrShape[1] - 1: # AND it is the top right ax.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.) else: raise ValueError('Too many dimensions in sweep to plot. ' 'This should have been caught by assertValidPlotType.') if iAx[0] == plotArrShape[0] - 1: ax.set_xlabel(xK) else: ax.tick_params(labelbottom=False) if iAx[1] == 0: ax.set_ylabel(yK) else: ax.tick_params(labelleft=False) elif self.plotOptions['plType'] == 'surf': # xKeys we treat as meaningless. just use the actuation domains # We treat yData as color data doms = [None] * 2 for iDim, actuObj in enumerate(self.actuate.values()): doms[iDim] = actuObj.domain[slicer[iDim]] domainGrids = np.meshgrid(*doms[::-1], indexing='xy') pltKwargs['cmap'] = pltKwargs.pop('cmap', self.plotOptions['cmap-surf']) pltKwargs['shading'] = pltKwargs.pop('shading', 'gouraud') cax = ax.pcolormesh(*domainGrids, yData, **pltKwargs) plt.gcf().colorbar(cax, ax=ax) ax.autoscale(tight=True) ax.set_title(yK) if iAx[0] == plotArrShape[0] - 1: ax.set_xlabel(actuationKeys[1]) else: ax.tick_params(labelbottom=False) ax.set_ylabel(actuationKeys[0]) return axArr