Commit 8c13a3e7 authored by Luke Campagnola's avatar Luke Campagnola
Browse files

copy from acq4

parent 008ca76d
......@@ -80,6 +80,12 @@ class Point(QtCore.QPointF):
def __div__(self, a):
return self._math_('__div__', a)
def __truediv__(self, a):
return self._math_('__truediv__', a)
def __rtruediv__(self, a):
return self._math_('__rtruediv__', a)
def __rpow__(self, a):
return self._math_('__rpow__', a)
......
......@@ -130,11 +130,14 @@ class SRTTransform(QtGui.QTransform):
self._state['angle'] = angle
self.update()
def __div__(self, t):
def __truediv__(self, t):
"""A / B == B^-1 * A"""
dt = t.inverted()[0] * self
return SRTTransform(dt)
def __div__(self, t):
return self.__truediv__(t)
def __mul__(self, t):
return SRTTransform(QtGui.QTransform.__mul__(self, t))
......
......@@ -123,7 +123,6 @@ class SRTTransform3D(pg.Transform3D):
m = self.matrix().reshape(4,4)
## translation is 4th column
self._state['pos'] = m[:3,3]
## scale is vector-length of first three columns
scale = (m[:3,:3]**2).sum(axis=0)**0.5
## see whether there is an inversion
......@@ -141,18 +140,30 @@ class SRTTransform3D(pg.Transform3D):
print("Scale: %s" % str(scale))
print("Original matrix: %s" % str(m))
raise
eigIndex = np.argwhere(np.abs(evals-1) < 1e-7)
eigIndex = np.argwhere(np.abs(evals-1) < 1e-6)
if len(eigIndex) < 1:
print("eigenvalues: %s" % str(evals))
print("eigenvectors: %s" % str(evecs))
print("index: %s, %s" % (str(eigIndex), str(evals-1)))
raise Exception("Could not determine rotation axis.")
axis = evecs[eigIndex[0,0]].real
axis = evecs[:,eigIndex[0,0]].real
axis /= ((axis**2).sum())**0.5
self._state['axis'] = axis
## trace(r) == 2 cos(angle) + 1, so:
self._state['angle'] = np.arccos((r.trace()-1)*0.5) * 180 / np.pi
cos = (r.trace()-1)*0.5 ## this only gets us abs(angle)
## The off-diagonal values can be used to correct the angle ambiguity,
## but we need to figure out which element to use:
axisInd = np.argmax(np.abs(axis))
rInd,sign = [((1,2), -1), ((0,2), 1), ((0,1), -1)][axisInd]
## Then we have r-r.T = sin(angle) * 2 * sign * axis[axisInd];
## solve for sin(angle)
sin = (r-r.T)[rInd] / (2. * sign * axis[axisInd])
## finally, we get the complete angle from arctan(sin/cos)
self._state['angle'] = np.arctan2(sin, cos) * 180 / np.pi
if self._state['angle'] == 0:
self._state['axis'] = (0,0,1)
......
......@@ -28,6 +28,15 @@ def ftrace(func):
return rv
return w
def warnOnException(func):
"""Decorator which catches/ignores exceptions and prints a stack trace."""
def w(*args, **kwds):
try:
func(*args, **kwds)
except:
printExc('Ignored exception:')
return w
def getExc(indent=4, prefix='| '):
tb = traceback.format_exc()
lines = []
......
......@@ -24,7 +24,15 @@ class BinOpNode(Node):
})
def process(self, **args):
fn = getattr(args['A'], self.fn)
if isinstance(self.fn, tuple):
for name in self.fn:
try:
fn = getattr(args['A'], name)
break
except AttributeError:
pass
else:
fn = getattr(args['A'], self.fn)
out = fn(args['B'])
if out is NotImplemented:
raise Exception("Operation %s not implemented between %s and %s" % (fn, str(type(args['A'])), str(type(args['B']))))
......@@ -60,5 +68,7 @@ class DivideNode(BinOpNode):
"""Returns A / B. Does not check input types."""
nodeName = 'Divide'
def __init__(self, name):
BinOpNode.__init__(self, name, '__div__')
# try truediv first, followed by div
BinOpNode.__init__(self, name, ('__truediv__', '__div__'))
......@@ -264,6 +264,7 @@ def mkPen(*args, **kargs):
color = kargs.get('color', None)
width = kargs.get('width', 1)
style = kargs.get('style', None)
dash = kargs.get('dash', None)
cosmetic = kargs.get('cosmetic', True)
hsv = kargs.get('hsv', None)
......@@ -291,6 +292,8 @@ def mkPen(*args, **kargs):
pen.setCosmetic(cosmetic)
if style is not None:
pen.setStyle(style)
if dash is not None:
pen.setDashPattern(dash)
return pen
def hsvColor(hue, sat=1.0, val=1.0, alpha=1.0):
......@@ -1948,6 +1951,8 @@ def pseudoScatter(data, spacing=None, shuffle=True, bidir=False):
s2 = spacing**2
yvals = np.empty(len(data))
if len(data) == 0:
return yvals
yvals[0] = 0
for i in range(1,len(data)):
x = data[i] # current x value to be placed
......
......@@ -42,12 +42,18 @@ class AxisItem(GraphicsWidget):
self.label.rotate(-90)
self.style = {
'tickTextOffset': 3, ## spacing between text and axis
'tickTextOffset': (5, 2), ## (horizontal, vertical) spacing between text and axis
'tickTextWidth': 30, ## space reserved for tick text
'tickTextHeight': 18,
'autoExpandTextSpace': True, ## automatically expand text space if needed
'tickFont': None,
'stopAxisAtTick': (False, False), ## whether axis is drawn to edge of box or to last tick
'textFillLimits': [ ## how much of the axis to fill up with tick text, maximally.
(0, 0.8), ## never fill more than 80% of the axis
(2, 0.6), ## If we already have 2 ticks with text, fill no more than 60% of the axis
(4, 0.4), ## If we already have 4 ticks with text, fill no more than 40% of the axis
(6, 0.2), ## If we already have 6 ticks with text, fill no more than 20% of the axis
]
}
self.textWidth = 30 ## Keeps track of maximum width / height of tick text
......@@ -209,14 +215,14 @@ class AxisItem(GraphicsWidget):
## to accomodate.
if self.orientation in ['left', 'right']:
mx = max(self.textWidth, x)
if mx > self.textWidth:
if mx > self.textWidth or mx < self.textWidth-10:
self.textWidth = mx
if self.style['autoExpandTextSpace'] is True:
self.setWidth()
#return True ## size has changed
else:
mx = max(self.textHeight, x)
if mx > self.textHeight:
if mx > self.textHeight or mx < self.textHeight-10:
self.textHeight = mx
if self.style['autoExpandTextSpace'] is True:
self.setHeight()
......@@ -236,7 +242,7 @@ class AxisItem(GraphicsWidget):
h = self.textHeight
else:
h = self.style['tickTextHeight']
h += max(0, self.tickLength) + self.style['tickTextOffset']
h += max(0, self.tickLength) + self.style['tickTextOffset'][1]
if self.label.isVisible():
h += self.label.boundingRect().height() * 0.8
self.setMaximumHeight(h)
......@@ -252,7 +258,7 @@ class AxisItem(GraphicsWidget):
w = self.textWidth
else:
w = self.style['tickTextWidth']
w += max(0, self.tickLength) + self.style['tickTextOffset']
w += max(0, self.tickLength) + self.style['tickTextOffset'][0]
if self.label.isVisible():
w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate
self.setMaximumWidth(w)
......@@ -430,7 +436,7 @@ class AxisItem(GraphicsWidget):
return []
## decide optimal minor tick spacing in pixels (this is just aesthetics)
pixelSpacing = np.log(size+10) * 5
pixelSpacing = size / np.log(size)
optimalTickCount = max(2., size / pixelSpacing)
## optimal minor tick spacing
......@@ -720,7 +726,7 @@ class AxisItem(GraphicsWidget):
textOffset = self.style['tickTextOffset'] ## spacing between axis and text
textOffset = self.style['tickTextOffset'][axis] ## spacing between axis and text
#if self.style['autoExpandTextSpace'] is True:
#textWidth = self.textWidth
#textHeight = self.textHeight
......@@ -728,7 +734,7 @@ class AxisItem(GraphicsWidget):
#textWidth = self.style['tickTextWidth'] ## space allocated for horizontal text
#textHeight = self.style['tickTextHeight'] ## space allocated for horizontal text
textSize2 = 0
textRects = []
textSpecs = [] ## list of draw
for i in range(len(tickLevels)):
......@@ -770,9 +776,16 @@ class AxisItem(GraphicsWidget):
textSize = np.sum([r.width() for r in textRects])
textSize2 = np.max([r.height() for r in textRects])
## If the strings are too crowded, stop drawing text now
## If the strings are too crowded, stop drawing text now.
## We use three different crowding limits based on the number
## of texts drawn so far.
textFillRatio = float(textSize) / lengthInPixels
if textFillRatio > 0.7:
finished = False
for nTexts, limit in self.style['textFillLimits']:
if len(textSpecs) >= nTexts and textFillRatio >= limit:
finished = True
break
if finished:
break
#spacing, values = tickLevels[best]
......
......@@ -533,6 +533,7 @@ class GraphicsItem(object):
def viewTransformChanged(self):
"""
Called whenever the transformation matrix of the view has changed.
(eg, the view range has changed or the view was resized)
"""
pass
......
......@@ -375,6 +375,7 @@ class PlotCurveItem(GraphicsObject):
return QtGui.QPainterPath()
return self.path
@pg.debug.warnOnException ## raising an exception here causes crash
def paint(self, p, opt, widget):
prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True)
if self.xData is None:
......
......@@ -84,24 +84,28 @@ class PlotDataItem(GraphicsObject):
**Optimization keyword arguments:**
============ =====================================================================
antialias (bool) By default, antialiasing is disabled to improve performance.
Note that in some cases (in particluar, when pxMode=True), points
will be rendered antialiased even if this is set to False.
decimate (int) Sub-sample data by selecting every nth sample before plotting
onlyVisible (bool) If True, only plot data that is visible within the X range of
the containing ViewBox. This can improve performance when plotting
very large data sets where only a fraction of the data is visible
at any time.
autoResample (bool) If True, resample the data before plotting to avoid plotting
multiple line segments per pixel. This can improve performance when
viewing very high-density data, but increases the initial overhead
and memory usage.
sampleRate (float) The sample rate of the data along the X axis (for data with
a fixed sample rate). Providing this value improves performance of
the *onlyVisible* and *autoResample* options.
identical *deprecated*
============ =====================================================================
================ =====================================================================
antialias (bool) By default, antialiasing is disabled to improve performance.
Note that in some cases (in particluar, when pxMode=True), points
will be rendered antialiased even if this is set to False.
decimate deprecated.
downsample (int) Reduce the number of samples displayed by this value
downsampleMethod 'subsample': Downsample by taking the first of N samples.
This method is fastest and least accurate.
'mean': Downsample by taking the mean of N samples.
'peak': Downsample by drawing a saw wave that follows the min
and max of the original data. This method produces the best
visual representation of the data but is slower.
autoDownsample (bool) If True, resample the data before plotting to avoid plotting
multiple line segments per pixel. This can improve performance when
viewing very high-density data, but increases the initial overhead
and memory usage.
clipToView (bool) If True, only plot data that is visible within the X range of
the containing ViewBox. This can improve performance when plotting
very large data sets where only a fraction of the data is visible
at any time.
identical *deprecated*
================ =====================================================================
**Meta-info keyword arguments:**
......@@ -131,7 +135,6 @@ class PlotDataItem(GraphicsObject):
self.opts = {
'fftMode': False,
'logMode': [False, False],
'downsample': False,
'alphaHint': 1.0,
'alphaMode': False,
......@@ -149,6 +152,11 @@ class PlotDataItem(GraphicsObject):
'antialias': pg.getConfigOption('antialias'),
'pointMode': None,
'downsample': 1,
'autoDownsample': False,
'downsampleMethod': 'peak',
'clipToView': False,
'data': None,
}
self.setData(*args, **kargs)
......@@ -175,6 +183,7 @@ class PlotDataItem(GraphicsObject):
return
self.opts['fftMode'] = mode
self.xDisp = self.yDisp = None
self.xClean = self.yClean = None
self.updateItems()
self.informViewBoundsChanged()
......@@ -183,6 +192,7 @@ class PlotDataItem(GraphicsObject):
return
self.opts['logMode'] = [xMode, yMode]
self.xDisp = self.yDisp = None
self.xClean = self.yClean = None
self.updateItems()
self.informViewBoundsChanged()
......@@ -269,13 +279,51 @@ class PlotDataItem(GraphicsObject):
#self.scatter.setSymbolSize(symbolSize)
self.updateItems()
def setDownsampling(self, ds):
if self.opts['downsample'] == ds:
def setDownsampling(self, ds=None, auto=None, method=None):
"""
Set the downsampling mode of this item. Downsampling reduces the number
of samples drawn to increase performance.
=========== =================================================================
Arguments
ds (int) Reduce visible plot samples by this factor. To disable,
set ds=1.
auto (bool) If True, automatically pick *ds* based on visible range
mode 'subsample': Downsample by taking the first of N samples.
This method is fastest and least accurate.
'mean': Downsample by taking the mean of N samples.
'peak': Downsample by drawing a saw wave that follows the min
and max of the original data. This method produces the best
visual representation of the data but is slower.
=========== =================================================================
"""
changed = False
if ds is not None:
if self.opts['downsample'] != ds:
changed = True
self.opts['downsample'] = ds
if auto is not None and self.opts['autoDownsample'] != auto:
self.opts['autoDownsample'] = auto
changed = True
if method is not None:
if self.opts['downsampleMethod'] != method:
changed = True
self.opts['downsampleMethod'] = method
if changed:
self.xDisp = self.yDisp = None
self.updateItems()
def setClipToView(self, clip):
if self.opts['clipToView'] == clip:
return
self.opts['downsample'] = ds
self.opts['clipToView'] = clip
self.xDisp = self.yDisp = None
self.updateItems()
def setData(self, *args, **kargs):
"""
Clear any data displayed by this item and display new data.
......@@ -315,7 +363,7 @@ class PlotDataItem(GraphicsObject):
raise Exception('Invalid data type %s' % type(data))
elif len(args) == 2:
seq = ('listOfValues', 'MetaArray')
seq = ('listOfValues', 'MetaArray', 'empty')
if dataType(args[0]) not in seq or dataType(args[1]) not in seq:
raise Exception('When passing two unnamed arguments, both must be a list or array of values. (got %s, %s)' % (str(type(args[0])), str(type(args[1]))))
if not isinstance(args[0], np.ndarray):
......@@ -376,6 +424,7 @@ class PlotDataItem(GraphicsObject):
self.xData = x.view(np.ndarray) ## one last check to make sure there are no MetaArrays getting by
self.yData = y.view(np.ndarray)
self.xClean = self.yClean = None
self.xDisp = None
self.yDisp = None
prof.mark('set data')
......@@ -423,23 +472,28 @@ class PlotDataItem(GraphicsObject):
def getData(self):
if self.xData is None:
return (None, None)
if self.xDisp is None:
if self.xClean is None:
nanMask = np.isnan(self.xData) | np.isnan(self.yData) | np.isinf(self.xData) | np.isinf(self.yData)
if any(nanMask):
self.dataMask = ~nanMask
x = self.xData[self.dataMask]
y = self.yData[self.dataMask]
self.xClean = self.xData[self.dataMask]
self.yClean = self.yData[self.dataMask]
else:
self.dataMask = None
x = self.xData
y = self.yData
self.xClean = self.xData
self.yClean = self.yData
ds = self.opts['downsample']
if ds > 1:
x = x[::ds]
#y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing
y = y[::ds]
if self.xDisp is None:
x = self.xClean
y = self.yClean
#ds = self.opts['downsample']
#if isinstance(ds, int) and ds > 1:
#x = x[::ds]
##y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing
#y = y[::ds]
if self.opts['fftMode']:
f = np.fft.fft(y) / len(y)
y = abs(f[1:len(f)/2])
......@@ -457,6 +511,53 @@ class PlotDataItem(GraphicsObject):
y = y[self.dataMask]
else:
self.dataMask = None
ds = self.opts['downsample']
if not isinstance(ds, int):
ds = 1
if self.opts['autoDownsample']:
# this option presumes that x-values have uniform spacing
range = self.viewRect()
if range is not None:
dx = float(x[-1]-x[0]) / (len(x)-1)
x0 = (range.left()-x[0]) / dx
x1 = (range.right()-x[0]) / dx
width = self.getViewBox().width()
ds = int(max(1, int(0.2 * (x1-x0) / width)))
## downsampling is expensive; delay until after clipping.
if self.opts['clipToView']:
# this option presumes that x-values have uniform spacing
range = self.viewRect()
if range is not None:
dx = float(x[-1]-x[0]) / (len(x)-1)
# clip to visible region extended by downsampling value
x0 = np.clip(int((range.left()-x[0])/dx)-1*ds , 0, len(x)-1)
x1 = np.clip(int((range.right()-x[0])/dx)+2*ds , 0, len(x)-1)
x = x[x0:x1]
y = y[x0:x1]
if ds > 1:
if self.opts['downsampleMethod'] == 'subsample':
x = x[::ds]
y = y[::ds]
elif self.opts['downsampleMethod'] == 'mean':
n = len(x) / ds
x = x[:n*ds:ds]
y = y[:n*ds].reshape(n,ds).mean(axis=1)
elif self.opts['downsampleMethod'] == 'peak':
n = len(x) / ds
x1 = np.empty((n,2))
x1[:] = x[:n*ds:ds,np.newaxis]
x = x1.reshape(n*2)
y1 = np.empty((n,2))
y2 = y[:n*ds].reshape((n, ds))
y1[:,0] = y2.max(axis=1)
y1[:,1] = y2.min(axis=1)
y = y1.reshape(n*2)
self.xDisp = x
self.yDisp = y
#print self.yDisp.shape, self.yDisp.min(), self.yDisp.max()
......@@ -542,6 +643,8 @@ class PlotDataItem(GraphicsObject):
#self.scatters = []
self.xData = None
self.yData = None
self.xClean = None
self.yClean = None
self.xDisp = None
self.yDisp = None
self.curve.setData([])
......@@ -557,6 +660,14 @@ class PlotDataItem(GraphicsObject):
self.sigClicked.emit(self)
self.sigPointsClicked.emit(self, points)
def viewRangeChanged(self):
# view range has changed; re-plot if needed
if self.opts['clipToView'] or self.opts['autoDownsample']:
self.xDisp = self.yDisp = None
self.updateItems()
def dataType(obj):
if hasattr(obj, '__len__') and len(obj) == 0:
......
......@@ -256,6 +256,11 @@ class PlotItem(GraphicsWidget):
c.logYCheck.toggled.connect(self.updateLogMode)
c.downsampleSpin.valueChanged.connect(self.updateDownsampling)
c.downsampleCheck.toggled.connect(self.updateDownsampling)
c.autoDownsampleCheck.toggled.connect(self.updateDownsampling)
c.subsampleRadio.toggled.connect(self.updateDownsampling)
c.meanRadio.toggled.connect(self.updateDownsampling)
c.clipToViewCheck.toggled.connect(self.updateDownsampling)
self.ctrl.avgParamList.itemClicked.connect(self.avgParamListClicked)
self.ctrl.averageGroup.toggled.connect(self.avgToggled)
......@@ -526,7 +531,8 @@ class PlotItem(GraphicsWidget):
(alpha, auto) = self.alphaState()
item.setAlpha(alpha, auto)
item.setFftMode(self.ctrl.fftCheck.isChecked())
item.setDownsampling(self.downsampleMode())
item.setDownsampling(*self.downsampleMode())
item.setClipToView(self.clipToViewMode())
item.setPointMode(self.pointMode())
## Hide older plots if needed
......@@ -568,8 +574,8 @@ class PlotItem(GraphicsWidget):
:func:`InfiniteLine.__init__() <pyqtgraph.InfiniteLine.__init__>`.
Returns the item created.
"""
angle = 0 if x is None else 90
pos = x if x is not None else y
pos = kwds.get('pos', x if x is not None else y)
angle = kwds.get('angle', 0 if x is None else 90)
line = InfiniteLine(pos, angle, **kwds)
self.addItem(line)
if z is not None:
......@@ -941,23 +947,81 @@ class PlotItem(GraphicsWidget):
self.enableAutoRange()
self.recomputeAverages()
def setDownsampling(self, ds=None, auto=None, mode=None):
"""Change the default downsampling mode for all PlotDataItems managed by this plot.
=========== =================================================================
Arguments
ds (int) Reduce visible plot samples by this factor, or
(bool) To enable/disable downsampling without changing the value.
auto (bool) If True, automatically pick *ds* based on visible range
mode 'subsample': Downsample by taking the first of N samples.
This method is fastest and least accurate.
'mean': Downsample by taking the mean of N samples.
'peak': Downsample by drawing a saw wave that follows the min
and max of the original data. This method produces the best
visual representation of the data but is slower.
=========== =================================================================
"""
if ds is not None:
if ds is False:
self.ctrl.downsampleCheck.setChecked(False)
elif ds is True:
self.ctrl.downsampleCheck.setChecked(True)
else:
self.ctrl.downsampleCheck.setChecked(True)
self.ctrl.downsampleSpin.setValue(ds)
if auto is not None:
if auto and ds is not False:
self.ctrl.</