From b8840e22245bb505b54777c386238a6014fb5ce4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 15 Apr 2014 15:49:17 -0400 Subject: [PATCH 01/50] Removed absolute imports --- pyqtgraph/__init__.py | 2 +- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 +- pyqtgraph/opengl/MeshData.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 30160565..01e84c49 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -257,7 +257,7 @@ from .graphicsWindows import * from .SignalProxy import * from .colormap import * from .ptime import time -from pyqtgraph.Qt import isQObjectAlive +from .Qt import isQObjectAlive ############################################################## diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 4bd2d980..5fdbdf08 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -10,7 +10,7 @@ from copy import deepcopy from ... import debug as debug from ... import getConfigOption import sys -from pyqtgraph.Qt import isQObjectAlive +from ...Qt import isQObjectAlive __all__ = ['ViewBox'] diff --git a/pyqtgraph/opengl/MeshData.py b/pyqtgraph/opengl/MeshData.py index 34a6e3fc..5adf4b64 100644 --- a/pyqtgraph/opengl/MeshData.py +++ b/pyqtgraph/opengl/MeshData.py @@ -1,5 +1,5 @@ -from pyqtgraph.Qt import QtGui -import pyqtgraph.functions as fn +from ..Qt import QtGui +from .. import functions as fn import numpy as np class MeshData(object): @@ -501,4 +501,4 @@ class MeshData(object): faces[start+cols:start+(cols*2)] = rowtemplate2 + row * cols return MeshData(vertexes=verts, faces=faces) - \ No newline at end of file + From 5cea58545f4140d5958c633f267d194d729da273 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 20 Apr 2014 14:04:33 -0400 Subject: [PATCH 02/50] Fixed mouse wheel in RemoteGraphicsView + PySide --- pyqtgraph/widgets/RemoteGraphicsView.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index cb9a7052..75ce90b0 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -108,7 +108,7 @@ class RemoteGraphicsView(QtGui.QWidget): return QtGui.QWidget.mouseMoveEvent(self, ev) def wheelEvent(self, ev): - self._view.wheelEvent(ev.pos(), ev.globalPos(), ev.delta(), int(ev.buttons()), int(ev.modifiers()), ev.orientation(), _callSync='off') + self._view.wheelEvent(ev.pos(), ev.globalPos(), ev.delta(), int(ev.buttons()), int(ev.modifiers()), int(ev.orientation()), _callSync='off') ev.accept() return QtGui.QWidget.wheelEvent(self, ev) @@ -243,6 +243,7 @@ class Renderer(GraphicsView): def wheelEvent(self, pos, gpos, d, btns, mods, ori): btns = QtCore.Qt.MouseButtons(btns) mods = QtCore.Qt.KeyboardModifiers(mods) + ori = (None, QtCore.Qt.Horizontal, QtCore.Qt.Vertical)[ori] return GraphicsView.wheelEvent(self, QtGui.QWheelEvent(pos, gpos, d, btns, mods, ori)) def keyEvent(self, typ, mods, text, autorep, count): From 4e555e0bf3ae92daa47b24f5706d139f326e8e30 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 26 Apr 2014 11:13:32 -0400 Subject: [PATCH 03/50] Fix OSX division-by-zero in ViewBox --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 5fdbdf08..46ed2984 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1454,9 +1454,10 @@ class ViewBox(GraphicsWidget): if aspect is not False and aspect != 0 and tr.height() != 0 and bounds.height() != 0: ## This is the view range aspect ratio we have requested - targetRatio = tr.width() / tr.height() + targetRatio = tr.width() / tr.height() if tr.height() != 0 else 1 ## This is the view range aspect ratio we need to obey aspect constraint - viewRatio = (bounds.width() / bounds.height()) / aspect + viewRatio = (bounds.width() / bounds.height() if bounds.height() != 0 else 1) / aspect + viewRatio = 1 if viewRatio == 0 else viewRatio # Decide which range to keep unchanged #print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange'] From 5a8d77d6f263eaf90f282f23bd4dc9806eed7b45 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 26 Apr 2014 13:45:34 -0400 Subject: [PATCH 04/50] Fixed unicode issues with PySide loadUiType Added unit test for loadUiType --- pyqtgraph/Qt.py | 17 +++++++++++++++-- pyqtgraph/tests/test_qt.py | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 2fcff32f..4fe8c3ab 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -11,6 +11,8 @@ This module exists to smooth out some of the differences between PySide and PyQt import sys, re +from .python2_3 import asUnicode + ## Automatically determine whether to use PyQt or PySide. ## This is done by first checking to see whether one of the libraries ## is already imported. If not, then attempt to import PyQt4, then PySide. @@ -56,18 +58,29 @@ if USE_PYSIDE: # Credit: # http://stackoverflow.com/questions/4442286/python-code-genration-with-pyside-uic/14195313#14195313 + class StringIO(object): + """Alternative to built-in StringIO needed to circumvent unicode/ascii issues""" + def __init__(self): + self.data = [] + + def write(self, data): + self.data.append(data) + + def getvalue(self): + return ''.join(map(asUnicode, self.data)).encode('utf8') + def loadUiType(uiFile): """ Pyside "loadUiType" command like PyQt4 has one, so we have to convert the ui file to py code in-memory first and then execute it in a special frame to retrieve the form_class. """ import pysideuic import xml.etree.ElementTree as xml - from io import StringIO + #from io import StringIO parsed = xml.parse(uiFile) widget_class = parsed.find('widget').get('class') form_class = parsed.find('class').text - + with open(uiFile, 'r') as f: o = StringIO() frame = {} diff --git a/pyqtgraph/tests/test_qt.py b/pyqtgraph/tests/test_qt.py index cef54777..729bf695 100644 --- a/pyqtgraph/tests/test_qt.py +++ b/pyqtgraph/tests/test_qt.py @@ -1,5 +1,7 @@ import pyqtgraph as pg -import gc +import gc, os + +app = pg.mkQApp() def test_isQObjectAlive(): o1 = pg.QtCore.QObject() @@ -8,3 +10,14 @@ def test_isQObjectAlive(): del o1 gc.collect() assert not pg.Qt.isQObjectAlive(o2) + + +def test_loadUiType(): + path = os.path.dirname(__file__) + formClass, baseClass = pg.Qt.loadUiType(os.path.join(path, 'uictest.ui')) + w = baseClass() + ui = formClass() + ui.setupUi(w) + w.show() + app.processEvents() + From 8dd7f07158a2bb9135c3f056fb614659b293b3a5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 3 Apr 2014 13:37:07 -0400 Subject: [PATCH 05/50] Start stability tests: - randomly add / remove widgets and graphicsitems to provoke a crash - create and delete various objects and ensure that nothing is left in memory --- pyqtgraph/tests/test_ref_cycles.py | 45 ++++++++++++++++++++++++++++++ pyqtgraph/tests/test_stability.py | 39 ++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 pyqtgraph/tests/test_ref_cycles.py create mode 100644 pyqtgraph/tests/test_stability.py diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py new file mode 100644 index 00000000..ac65eaf9 --- /dev/null +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -0,0 +1,45 @@ +""" +Test for unwanted reference cycles + +""" +import pyqtgraph as pg +import numpy as np +import gc +app = pg.mkQApp() + +def processEvents(): + for i in range(3): + gc.collect() + app.processEvents() + # processEvents ignored DeferredDelete events; we must process these + # manually. + app.sendPostedEvents(None, pg.QtCore.QEvent.DeferredDelete) + +def test_PlotItem(): + for i in range(10): + plt = pg.PlotItem() + plt.plot(np.random.normal(size=10000)) + processEvents() + + ot = pg.debug.ObjTracker() + + plots = [] + for i in range(10): + plt = pg.PlotItem() + plt.plot(np.random.normal(size=10000)) + plots.append(plt) + processEvents() + + ot.diff() + + del plots + processEvents() + + ot.diff() + + + return ot + + +if __name__ == '__main__': + ot = test_PlotItem() diff --git a/pyqtgraph/tests/test_stability.py b/pyqtgraph/tests/test_stability.py new file mode 100644 index 00000000..3838af7d --- /dev/null +++ b/pyqtgraph/tests/test_stability.py @@ -0,0 +1,39 @@ +""" +PyQt/PySide stress test: + +Create lots of random widgets and graphics items, connect them together randomly, +the tear them down repeatedly. + +The purpose of this is to attempt to generate segmentation faults. +""" +import pyqtgraph as pg +import random + +random.seed(12345) + +widgetTypes = [pg.PlotWidget, pg.ImageView, pg.GraphicsView, pg.QtGui.QWidget, + pg.QtGui.QTreeWidget, pg.QtGui.QPushButton] + +itemTypes = [pg.PlotCurveItem, pg.ImageItem, pg.PlotDataItem, pg.ViewBox, + pg.QtGui.QGraphicsRectItem] + +while True: + action = random.randint(0,5) + if action == 0: + # create a widget + pass + elif action == 1: + # set parent (widget or None), possibly create a reference in either direction + pass + elif action == 2: + # + pass + elif action == 3: + pass + + + + + + + From 82bd5ef584e782ca08888a0c29a93a6901aa6a55 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 13 Apr 2014 22:51:54 -0400 Subject: [PATCH 06/50] stability test is successfully crashing, ref_cycle test exposes cycles! --- pyqtgraph/tests/test_ref_cycles.py | 51 +++++---- pyqtgraph/tests/test_stability.py | 162 ++++++++++++++++++++++++----- 2 files changed, 170 insertions(+), 43 deletions(-) diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index ac65eaf9..87e0d71d 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -4,7 +4,7 @@ Test for unwanted reference cycles """ import pyqtgraph as pg import numpy as np -import gc +import gc, weakref app = pg.mkQApp() def processEvents(): @@ -15,31 +15,46 @@ def processEvents(): # manually. app.sendPostedEvents(None, pg.QtCore.QEvent.DeferredDelete) -def test_PlotItem(): - for i in range(10): - plt = pg.PlotItem() - plt.plot(np.random.normal(size=10000)) - processEvents() +#def test_PlotItem(): + #for i in range(10): + #plt = pg.PlotItem() + #plt.plot(np.random.normal(size=10000)) + #processEvents() - ot = pg.debug.ObjTracker() + #ot = pg.debug.ObjTracker() - plots = [] - for i in range(10): - plt = pg.PlotItem() - plt.plot(np.random.normal(size=10000)) - plots.append(plt) - processEvents() + #plots = [] + #for i in range(10): + #plt = pg.PlotItem() + #plt.plot(np.random.normal(size=10000)) + #plots.append(plt) + #processEvents() - ot.diff() + #ot.diff() - del plots - processEvents() + #del plots + #processEvents() - ot.diff() + #ot.diff() - return ot + #return ot +def test_PlotWidget(): + def mkref(*args, **kwds): + iv = pg.PlotWidget(*args, **kwds) + return weakref.ref(iv) + + for i in range(100): + assert mkref()() is None + +def test_ImageView(): + def mkref(*args, **kwds): + iv = pg.ImageView(*args, **kwds) + return weakref.ref(iv) + + for i in range(100): + assert mkref()() is None if __name__ == '__main__': ot = test_PlotItem() diff --git a/pyqtgraph/tests/test_stability.py b/pyqtgraph/tests/test_stability.py index 3838af7d..709080ac 100644 --- a/pyqtgraph/tests/test_stability.py +++ b/pyqtgraph/tests/test_stability.py @@ -6,34 +6,146 @@ the tear them down repeatedly. The purpose of this is to attempt to generate segmentation faults. """ +from PyQt4.QtTest import QTest import pyqtgraph as pg -import random +from random import seed, randint +import sys, gc, weakref -random.seed(12345) +app = pg.mkQApp() -widgetTypes = [pg.PlotWidget, pg.ImageView, pg.GraphicsView, pg.QtGui.QWidget, - pg.QtGui.QTreeWidget, pg.QtGui.QPushButton] +seed(12345) -itemTypes = [pg.PlotCurveItem, pg.ImageItem, pg.PlotDataItem, pg.ViewBox, - pg.QtGui.QGraphicsRectItem] +widgetTypes = [ + pg.PlotWidget, + pg.ImageView, + pg.GraphicsView, + pg.QtGui.QWidget, + pg.QtGui.QTreeWidget, + pg.QtGui.QPushButton, + ] + +itemTypes = [ + pg.PlotCurveItem, + pg.ImageItem, + pg.PlotDataItem, + pg.ViewBox, + pg.QtGui.QGraphicsRectItem + ] + +widgets = [] +items = [] +allWidgets = weakref.WeakSet() + +def test_stability(): + global allWidgets + try: + gc.disable() + actions = [ + createWidget, + #setParent, + forgetWidget, + showWidget, + processEvents, + #raiseException, + #addReference, + ] + + thread = WorkThread() + #thread.start() + + while True: + try: + action = randItem(actions) + action() + print('[%d widgets alive]' % len(allWidgets)) + except KeyboardInterrupt: + thread.kill() + break + except: + sys.excepthook(*sys.exc_info()) + finally: + gc.enable() + + + +class WorkThread(pg.QtCore.QThread): + '''Intended to give the gc an opportunity to run from a non-gui thread.''' + def run(self): + i = 0 + while True: + i += 1 + if (i % 1000000) == 0: + print('--worker--') + + +def randItem(items): + return items[randint(0, len(items)-1)] + +def p(msg): + print(msg) + sys.stdout.flush() + +def createWidget(): + p('create widget') + global widgets, allWidgets + widget = randItem(widgetTypes)() + widgets.append(widget) + allWidgets.add(widget) + p(" %s" % widget) + return widget + +def setParent(): + p('set parent') + global widgets + if len(widgets) < 2: + return + child = parent = None + while child is parent: + child = randItem(widgets) + parent = randItem(widgets) + p(" %s parent of %s" % (parent, child)) + child.setParent(parent) + +def forgetWidget(): + p('forget widget') + global widgets + if len(widgets) < 1: + return + widget = randItem(widgets) + p(' %s' % widget) + widgets.remove(widget) + +def showWidget(): + p('show widget') + global widgets + if len(widgets) < 1: + return + widget = randItem(widgets) + p(' %s' % widget) + widget.show() + +def processEvents(): + p('process events') + QTest.qWait(25) + +class TestException(Exception): + pass + +def raiseException(): + p('raise exception') + raise TestException("A test exception") + +def addReference(): + p('add reference') + global widgets + if len(widgets) < 1: + return + obj1 = randItem(widgets) + obj2 = randItem(widgets) + p(' %s -> %s' % (obj1, obj2)) + obj1._testref = obj2 + -while True: - action = random.randint(0,5) - if action == 0: - # create a widget - pass - elif action == 1: - # set parent (widget or None), possibly create a reference in either direction - pass - elif action == 2: - # - pass - elif action == 3: - pass - - - - - - +if __name__ == '__main__': + test_stability() \ No newline at end of file From ef7949a35eca727f3f4d4bc05d684871db640b85 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 13 Apr 2014 22:59:57 -0400 Subject: [PATCH 07/50] Fixed ref cycle in SignalProxy --- pyqtgraph/SignalProxy.py | 5 +++-- pyqtgraph/tests/test_ref_cycles.py | 4 ++-- pyqtgraph/tests/test_stability.py | 10 +++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/SignalProxy.py b/pyqtgraph/SignalProxy.py index 6f9b9112..d36282fa 100644 --- a/pyqtgraph/SignalProxy.py +++ b/pyqtgraph/SignalProxy.py @@ -2,6 +2,7 @@ from .Qt import QtCore from .ptime import time from . import ThreadsafeTimer +import weakref __all__ = ['SignalProxy'] @@ -34,7 +35,7 @@ class SignalProxy(QtCore.QObject): self.timer = ThreadsafeTimer.ThreadsafeTimer() self.timer.timeout.connect(self.flush) self.block = False - self.slot = slot + self.slot = weakref.ref(slot) self.lastFlushTime = None if slot is not None: self.sigDelayed.connect(slot) @@ -80,7 +81,7 @@ class SignalProxy(QtCore.QObject): except: pass try: - self.sigDelayed.disconnect(self.slot) + self.sigDelayed.disconnect(self.slot()) except: pass diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index 87e0d71d..2154632e 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -45,7 +45,7 @@ def test_PlotWidget(): iv = pg.PlotWidget(*args, **kwds) return weakref.ref(iv) - for i in range(100): + for i in range(5): assert mkref()() is None def test_ImageView(): @@ -53,7 +53,7 @@ def test_ImageView(): iv = pg.ImageView(*args, **kwds) return weakref.ref(iv) - for i in range(100): + for i in range(5): assert mkref()() is None if __name__ == '__main__': diff --git a/pyqtgraph/tests/test_stability.py b/pyqtgraph/tests/test_stability.py index 709080ac..dfcfb15e 100644 --- a/pyqtgraph/tests/test_stability.py +++ b/pyqtgraph/tests/test_stability.py @@ -41,11 +41,11 @@ def test_stability(): try: gc.disable() actions = [ - createWidget, - #setParent, - forgetWidget, - showWidget, - processEvents, + createWidget, + #setParent, + forgetWidget, + showWidget, + #processEvents, #raiseException, #addReference, ] From d45eadc9a5faaea03fd2d7f911fbb1f9302fae70 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 15 Apr 2014 15:10:04 -0400 Subject: [PATCH 08/50] update stability test --- pyqtgraph/tests/test_stability.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/tests/test_stability.py b/pyqtgraph/tests/test_stability.py index dfcfb15e..6d9acd0d 100644 --- a/pyqtgraph/tests/test_stability.py +++ b/pyqtgraph/tests/test_stability.py @@ -45,22 +45,27 @@ def test_stability(): #setParent, forgetWidget, showWidget, - #processEvents, + processEvents, #raiseException, #addReference, ] thread = WorkThread() - #thread.start() + thread.start() while True: try: action = randItem(actions) action() - print('[%d widgets alive]' % len(allWidgets)) + print('[%d widgets alive, %d zombie]' % (len(allWidgets), len(allWidgets) - len(widgets))) except KeyboardInterrupt: - thread.kill() - break + print("Caught interrupt; send another to exit.") + try: + for i in range(100): + QTest.qWait(100) + except KeyboardInterrupt: + thread.terminate() + break except: sys.excepthook(*sys.exc_info()) finally: @@ -88,7 +93,10 @@ def p(msg): def createWidget(): p('create widget') global widgets, allWidgets + if len(widgets) > 50: + return widget = randItem(widgetTypes)() + widget.setWindowTitle(widget.__class__.__name__) widgets.append(widget) allWidgets.add(widget) p(" %s" % widget) From 0479507dbbba308e9b416e917cf5aca9310a3164 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 27 Apr 2014 13:07:31 -0400 Subject: [PATCH 09/50] Expanded ref checks Fixed ref cycle in ImageItem -> HistogramLutItem --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 17 +++--- pyqtgraph/tests/test_ref_cycles.py | 66 +++++++++------------ 2 files changed, 36 insertions(+), 47 deletions(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 71577422..6a915902 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -17,6 +17,7 @@ from .. import functions as fn import numpy as np from .. import debug as debug +import weakref __all__ = ['HistogramLUTItem'] @@ -42,7 +43,7 @@ class HistogramLUTItem(GraphicsWidget): """ GraphicsWidget.__init__(self) self.lut = None - self.imageItem = None + self.imageItem = lambda: None # fake a dead weakref self.layout = QtGui.QGraphicsGridLayout() self.setLayout(self.layout) @@ -138,7 +139,7 @@ class HistogramLUTItem(GraphicsWidget): #self.region.setBounds([vr.top(), vr.bottom()]) def setImageItem(self, img): - self.imageItem = img + self.imageItem = weakref.ref(img) img.sigImageChanged.connect(self.imageChanged) img.setLookupTable(self.getLookupTable) ## send function pointer, not the result #self.gradientChanged() @@ -150,11 +151,11 @@ class HistogramLUTItem(GraphicsWidget): self.update() def gradientChanged(self): - if self.imageItem is not None: + if self.imageItem() is not None: if self.gradient.isLookupTrivial(): - self.imageItem.setLookupTable(None) #lambda x: x.astype(np.uint8)) + self.imageItem().setLookupTable(None) #lambda x: x.astype(np.uint8)) else: - self.imageItem.setLookupTable(self.getLookupTable) ## send function pointer, not the result + self.imageItem().setLookupTable(self.getLookupTable) ## send function pointer, not the result self.lut = None #if self.imageItem is not None: @@ -178,14 +179,14 @@ class HistogramLUTItem(GraphicsWidget): #self.update() def regionChanging(self): - if self.imageItem is not None: - self.imageItem.setLevels(self.region.getRegion()) + if self.imageItem() is not None: + self.imageItem().setLevels(self.region.getRegion()) self.sigLevelsChanged.emit(self) self.update() def imageChanged(self, autoLevel=False, autoRange=False): profiler = debug.Profiler() - h = self.imageItem.getHistogram() + h = self.imageItem().getHistogram() profiler('get histogram') if h[0] is None: return diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index 2154632e..3e78b382 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -7,54 +7,42 @@ import numpy as np import gc, weakref app = pg.mkQApp() -def processEvents(): - for i in range(3): - gc.collect() - app.processEvents() - # processEvents ignored DeferredDelete events; we must process these - # manually. - app.sendPostedEvents(None, pg.QtCore.QEvent.DeferredDelete) +def assert_alldead(refs): + for ref in refs: + assert ref() is None -#def test_PlotItem(): - #for i in range(10): - #plt = pg.PlotItem() - #plt.plot(np.random.normal(size=10000)) - #processEvents() - - #ot = pg.debug.ObjTracker() - - #plots = [] - #for i in range(10): - #plt = pg.PlotItem() - #plt.plot(np.random.normal(size=10000)) - #plots.append(plt) - #processEvents() - - #ot.diff() - - #del plots - #processEvents() - - #ot.diff() - - - #return ot +def mkrefs(*objs): + return map(weakref.ref, objs) def test_PlotWidget(): - def mkref(*args, **kwds): - iv = pg.PlotWidget(*args, **kwds) - return weakref.ref(iv) + def mkobjs(*args, **kwds): + w = pg.PlotWidget(*args, **kwds) + data = pg.np.array([1,5,2,4,3]) + c = w.plot(data, name='stuff') + w.addLegend() + + # test that connections do not keep objects alive + w.plotItem.vb.sigRangeChanged.connect(mkrefs) + app.focusChanged.connect(w.plotItem.vb.invertY) + + # return weakrefs to a bunch of objects that should die when the scope exits. + return mkrefs(w, c, data, w.plotItem, w.plotItem.vb, w.plotItem.getMenu(), w.plotItem.getAxis('left')) for i in range(5): - assert mkref()() is None + assert_alldead(mkobjs()) def test_ImageView(): - def mkref(*args, **kwds): - iv = pg.ImageView(*args, **kwds) - return weakref.ref(iv) + def mkobjs(): + iv = pg.ImageView() + data = np.zeros((10,10,5)) + iv.setImage(data) + + return mkrefs(iv, iv.imageItem, iv.view, iv.ui.histogram, data) for i in range(5): - assert mkref()() is None + assert_alldead(mkobjs()) + + if __name__ == '__main__': ot = test_PlotItem() From 27f24d1a6a6aa512a87e7b74517c22962ca958fd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 27 Apr 2014 13:27:25 -0400 Subject: [PATCH 10/50] Expand ref cycle check to include all child QObjects --- pyqtgraph/tests/test_ref_cycles.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index 3e78b382..9e3fee19 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -11,8 +11,27 @@ def assert_alldead(refs): for ref in refs: assert ref() is None +def qObjectTree(root): + """Return root and its entire tree of qobject children""" + childs = [root] + for ch in pg.QtCore.QObject.children(root): + childs += qObjectTree(ch) + return childs + def mkrefs(*objs): - return map(weakref.ref, objs) + """Return a list of weakrefs to each object in *objs. + QObject instances are expanded to include all child objects. + """ + allObjs = {} + for obj in objs: + if isinstance(obj, pg.QtCore.QObject): + obj = qObjectTree(obj) + else: + obj = [obj] + for o in obj: + allObjs[id(o)] = o + + return map(weakref.ref, allObjs.values()) def test_PlotWidget(): def mkobjs(*args, **kwds): From 13cca1d9cab3156845efe99b5f81ab8858cdf076 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 27 Apr 2014 13:44:58 -0400 Subject: [PATCH 11/50] Add GraphicsWindow ref check --- pyqtgraph/tests/test_ref_cycles.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index 9e3fee19..0284852c 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -61,7 +61,17 @@ def test_ImageView(): for i in range(5): assert_alldead(mkobjs()) +def test_GraphicsWindow(): + def mkobjs(): + w = pg.GraphicsWindow() + p1 = w.addPlot() + v1 = w.addViewBox() + return mkrefs(w, p1, v1) + + for i in range(5): + assert_alldead(mkobjs()) + if __name__ == '__main__': ot = test_PlotItem() From 24c25b604ac29453c9c999b1d347376f9bbdd088 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 27 Apr 2014 14:02:55 -0400 Subject: [PATCH 12/50] disabled stability test (because it is expected to crash) --- pyqtgraph/tests/test_stability.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/tests/test_stability.py b/pyqtgraph/tests/test_stability.py index 6d9acd0d..a64e30e4 100644 --- a/pyqtgraph/tests/test_stability.py +++ b/pyqtgraph/tests/test_stability.py @@ -36,7 +36,8 @@ widgets = [] items = [] allWidgets = weakref.WeakSet() -def test_stability(): + +def crashtest(): global allWidgets try: gc.disable() @@ -136,12 +137,12 @@ def processEvents(): p('process events') QTest.qWait(25) -class TestException(Exception): +class TstException(Exception): pass def raiseException(): p('raise exception') - raise TestException("A test exception") + raise TstException("A test exception") def addReference(): p('add reference') From c6f2e9c2a92cbdbf1c58738de9771b80cbbe54b9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 28 Apr 2014 07:36:59 -0400 Subject: [PATCH 13/50] Added code for inverting X axis --- CHANGELOG | 1 + pyqtgraph/graphicsItems/AxisItem.py | 5 +++- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 3 +- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 29 +++++++++++++++++-- .../graphicsItems/ViewBox/ViewBoxMenu.py | 11 +++---- 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b9f51c84..6ae13037 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -48,6 +48,7 @@ pyqtgraph-0.9.9 [unreleased] - Added AxisItem.setStyle() - Added configurable formatting for TableWidget - Added 'stepMode' argument to PlotDataItem() + - Added ViewBox.invertX() Bugfixes: - PlotCurveItem now has correct clicking behavior--clicks within a few px diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index c25f7a7f..95093251 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -454,7 +454,10 @@ class AxisItem(GraphicsWidget): else: if newRange is None: newRange = view.viewRange()[0] - self.setRange(*newRange) + if view.xInverted(): + self.setRange(*newRange[::-1]) + else: + self.setRange(*newRange) def boundingRect(self): linkedView = self.linkedView() diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 5c102d95..8292875c 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -78,6 +78,7 @@ class PlotItem(GraphicsWidget): :func:`disableAutoRange `, :func:`setAspectLocked `, :func:`invertY `, + :func:`invertX `, :func:`register `, :func:`unregister ` @@ -299,7 +300,7 @@ class PlotItem(GraphicsWidget): for m in ['setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', # NOTE: 'setAutoVisible', 'setRange', 'autoRange', 'viewRect', 'viewRange', # If you update this list, please 'setMouseEnabled', 'setLimits', 'enableAutoRange', 'disableAutoRange', # update the class docstring - 'setAspectLocked', 'invertY', 'register', 'unregister']: # as well. + 'setAspectLocked', 'invertY', 'invertX', 'register', 'unregister']: # as well. def _create_method(name): def method(self, *args, **kwargs): diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 46ed2984..0c5792e4 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -104,7 +104,7 @@ class ViewBox(GraphicsWidget): NamedViews = weakref.WeakValueDictionary() # name: ViewBox AllViews = weakref.WeakKeyDictionary() # ViewBox: None - def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None): + def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None, invertX=False): """ ============== ============================================================= **Arguments:** @@ -115,6 +115,7 @@ class ViewBox(GraphicsWidget): coorinates to. (or False to allow the ratio to change) *enableMouse* (bool) Whether mouse can be used to scale/pan the view *invertY* (bool) See :func:`invertY ` + *invertX* (bool) See :func:`invertX ` ============== ============================================================= """ @@ -139,6 +140,7 @@ class ViewBox(GraphicsWidget): 'viewRange': [[0,1], [0,1]], ## actual range viewed 'yInverted': invertY, + 'xInverted': invertX, 'aspectLocked': False, ## False if aspect is unlocked, otherwise float specifies the locked ratio. 'autoRange': [True, True], ## False if auto range is disabled, ## otherwise float gives the fraction of data that is visible @@ -996,7 +998,10 @@ class ViewBox(GraphicsWidget): x2 = vr.right() else: ## views overlap; line them up upp = float(vr.width()) / vg.width() - x1 = vr.left() + (sg.x()-vg.x()) * upp + if self.xInverted(): + x1 = vr.left() + (sg.right()-vg.right()) * upp + else: + x1 = vr.left() + (sg.x()-vg.x()) * upp x2 = x1 + sg.width() * upp self.enableAutoRange(ViewBox.XAxis, False) self.setXRange(x1, x2, padding=0) @@ -1054,10 +1059,27 @@ class ViewBox(GraphicsWidget): #self.updateMatrix(changed=(False, True)) self.updateViewRange() self.sigStateChanged.emit(self) + self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) def yInverted(self): return self.state['yInverted'] + def invertX(self, b=True): + """ + By default, the positive x-axis points rightward on the screen. Use invertX(True) to reverse the x-axis. + """ + if self.state['xInverted'] == b: + return + + self.state['xInverted'] = b + #self.updateMatrix(changed=(False, True)) + self.updateViewRange() + self.sigStateChanged.emit(self) + self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) + + def xInverted(self): + return self.state['xInverted'] + def setAspectLocked(self, lock=True, ratio=1): """ If the aspect ratio is locked, view scaling must always preserve the aspect ratio. @@ -1555,6 +1577,7 @@ class ViewBox(GraphicsWidget): if link is not None: link.linkedViewChanged(self, ax) + self.update() self._matrixNeedsUpdate = True def updateMatrix(self, changed=None): @@ -1567,6 +1590,8 @@ class ViewBox(GraphicsWidget): scale = Point(bounds.width()/vr.width(), bounds.height()/vr.height()) if not self.state['yInverted']: scale = scale * Point(1, -1) + if self.state['xInverted']: + scale = scale * Point(-1, 1) m = QtGui.QTransform() ## First center the viewport at 0 diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index af142771..0e7d7912 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -56,7 +56,7 @@ class ViewBoxMenu(QtGui.QMenu): for sig, fn in connects: sig.connect(getattr(self, axis.lower()+fn)) - self.ctrl[0].invertCheck.hide() ## no invert for x-axis + self.ctrl[0].invertCheck.toggled.connect(self.xInvertToggled) self.ctrl[1].invertCheck.toggled.connect(self.yInvertToggled) ## exporting is handled by GraphicsScene now #self.export = QtGui.QMenu("Export") @@ -139,8 +139,9 @@ class ViewBoxMenu(QtGui.QMenu): self.ctrl[i].autoPanCheck.setChecked(state['autoPan'][i]) self.ctrl[i].visibleOnlyCheck.setChecked(state['autoVisibleOnly'][i]) - - self.ctrl[1].invertCheck.setChecked(state['yInverted']) + xy = ['x', 'y'][i] + self.ctrl[i].invertCheck.setChecked(state.get(xy+'Inverted', False)) + self.valid = True def popup(self, *args): @@ -217,19 +218,19 @@ class ViewBoxMenu(QtGui.QMenu): def yInvertToggled(self, b): self.view().invertY(b) + def xInvertToggled(self, b): + self.view().invertX(b) def exportMethod(self): act = self.sender() self.exportMethods[str(act.text())]() - def set3ButtonMode(self): self.view().setLeftButtonAction('pan') def set1ButtonMode(self): self.view().setLeftButtonAction('rect') - def setViewList(self, views): names = [''] self.viewMap.clear() From f202d5ab8b635698c972c0730ee86f7311b36463 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 28 Apr 2014 08:19:53 -0400 Subject: [PATCH 14/50] Update AxisItem.setGrid docstring --- pyqtgraph/graphicsItems/AxisItem.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 95093251..af393fdc 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -161,7 +161,11 @@ class AxisItem(GraphicsWidget): self.scene().removeItem(self) def setGrid(self, grid): - """Set the alpha value for the grid, or False to disable.""" + """Set the alpha value (0-255) for the grid, or False to disable. + + When grid lines are enabled, the axis tick lines are extended to cover + the extent of the linked ViewBox, if any. + """ self.grid = grid self.picture = None self.prepareGeometryChange() From 7b862f4f87af830d02a0e544217521d62aaad789 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 28 Apr 2014 08:22:07 -0400 Subject: [PATCH 15/50] docstring indentation fix --- pyqtgraph/graphicsItems/AxisItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index af393fdc..5eef4ae0 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -233,7 +233,7 @@ class AxisItem(GraphicsWidget): without any scaling prefix (eg, 'V' instead of 'mV'). The scaling prefix will be automatically prepended based on the range of data displayed. - **args All extra keyword arguments become CSS style options for + **args All extra keyword arguments become CSS style options for the tag which will surround the axis label and units. ============== ============================================================= From 1729416914707e88607cddcaaf6e88dbdaf333a3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 29 Apr 2014 17:17:05 -0400 Subject: [PATCH 16/50] Updated ImageView and ViewBox documentation --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 23 +++++---- pyqtgraph/imageview/ImageView.py | 55 ++++++++++++++++------ 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 0c5792e4..cf9e7f4a 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -74,12 +74,11 @@ class ViewBox(GraphicsWidget): Features: - - Scaling contents by mouse or auto-scale when contents change - - View linking--multiple views display the same data ranges - - Configurable by context menu - - Item coordinate mapping methods + * Scaling contents by mouse or auto-scale when contents change + * View linking--multiple views display the same data ranges + * Configurable by context menu + * Item coordinate mapping methods - Not really compatible with GraphicsView having the same functionality. """ sigYRangeChanged = QtCore.Signal(object, object) @@ -116,11 +115,15 @@ class ViewBox(GraphicsWidget): *enableMouse* (bool) Whether mouse can be used to scale/pan the view *invertY* (bool) See :func:`invertY ` *invertX* (bool) See :func:`invertX ` + *enableMenu* (bool) Whether to display a context menu when + right-clicking on the ViewBox background. + *name* (str) Used to register this ViewBox so that it appears + in the "Link axis" dropdown inside other ViewBox + context menus. This allows the user to manually link + the axes of any other view to this one. ============== ============================================================= """ - - GraphicsWidget.__init__(self, parent) self.name = None self.linksBlocked = False @@ -220,7 +223,11 @@ class ViewBox(GraphicsWidget): def register(self, name): """ Add this ViewBox to the registered list of views. - *name* will appear in the drop-down lists for axis linking in all other views. + + This allows users to manually link the axes of any other ViewBox to + this one. The specified *name* will appear in the drop-down lists for + axis linking in the context menus of all other views. + The same can be accomplished by initializing the ViewBox with the *name* attribute. """ ViewBox.AllViews[self] = None diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index c7c3206e..c9f421b4 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -12,8 +12,10 @@ Widget used for displaying 2D or 3D data. Features: - ROI plotting - Image normalization through a variety of methods """ -from ..Qt import QtCore, QtGui, USE_PYSIDE +import sys +import numpy as np +from ..Qt import QtCore, QtGui, USE_PYSIDE if USE_PYSIDE: from .ImageViewTemplate_pyside import * else: @@ -24,25 +26,14 @@ from ..graphicsItems.ROI import * from ..graphicsItems.LinearRegionItem import * from ..graphicsItems.InfiniteLine import * from ..graphicsItems.ViewBox import * -#from widgets import ROI -import sys -#from numpy import ndarray from .. import ptime as ptime -import numpy as np from .. import debug as debug - from ..SignalProxy import SignalProxy try: from bottleneck import nanmin, nanmax except ImportError: from numpy import nanmin, nanmax - -#try: - #from .. import metaarray as metaarray - #HAVE_METAARRAY = True -#except: - #HAVE_METAARRAY = False class PlotROI(ROI): @@ -72,6 +63,16 @@ class ImageView(QtGui.QWidget): imv = pg.ImageView() imv.show() imv.setImage(data) + + **Keyboard interaction** + + * left/right arrows step forward/backward 1 frame when pressed, + seek at 20fps when held. + * up/down arrows seek at 100fps + * pgup/pgdn seek at 1000fps + * home/end seek immediately to the first/last frame + * space begins playing frames. If time values (in seconds) are given + for each frame, then playback is in realtime. """ sigTimeChanged = QtCore.Signal(object, object) sigProcessingChanged = QtCore.Signal(object) @@ -79,8 +80,31 @@ class ImageView(QtGui.QWidget): def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *args): """ By default, this class creates an :class:`ImageItem ` to display image data - and a :class:`ViewBox ` to contain the ImageItem. Custom items may be given instead - by specifying the *view* and/or *imageItem* arguments. + and a :class:`ViewBox ` to contain the ImageItem. + + ============= ========================================================= + **Arguments** + parent (QWidget) Specifies the parent widget to which + this ImageView will belong. If None, then the ImageView + is created with no parent. + name (str) The name used to register both the internal ViewBox + and the PlotItem used to display ROI data. See the *name* + argument to :func:`ViewBox.__init__() + `. + view (ViewBox or PlotItem) If specified, this will be used + as the display area that contains the displayed image. + Any :class:`ViewBox `, + :class:`PlotItem `, or other + compatible object is acceptable. + imageItem (ImageItem) If specified, this object will be used to + display the image. Must be an instance of ImageItem + or other compatible object. + ============= ========================================================= + + Note: to display axis ticks inside the ImageView, instantiate it + with a PlotItem instance as its view:: + + pg.ImageView(view=pg.PlotItem()) """ QtGui.QWidget.__init__(self, parent, *args) self.levelMax = 4096 @@ -165,6 +189,7 @@ class ImageView(QtGui.QWidget): self.normRoi.sigRegionChangeFinished.connect(self.updateNorm) self.ui.roiPlot.registerPlot(self.name + '_ROI') + self.view.register(self.name) self.noRepeatKeys = [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown] @@ -318,7 +343,7 @@ class ImageView(QtGui.QWidget): self.ui.histogram.setLevels(min, max) def autoRange(self): - """Auto scale and pan the view around the image.""" + """Auto scale and pan the view around the image such that the image fills the view.""" image = self.getProcessedImage() self.view.autoRange() From f18f2b11c8cd376d9947712c928cd0024f45825f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 30 Apr 2014 13:00:56 -0400 Subject: [PATCH 17/50] Fix: TextParameterItem now obeys 'readonly' option --- pyqtgraph/parametertree/parameterTypes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 1f3eb692..53abe429 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -636,6 +636,7 @@ class TextParameterItem(WidgetParameterItem): def makeWidget(self): self.textBox = QtGui.QTextEdit() self.textBox.setMaximumHeight(100) + self.textBox.setReadOnly(self.param.opts.get('readonly', False)) self.textBox.value = lambda: str(self.textBox.toPlainText()) self.textBox.setValue = self.textBox.setPlainText self.textBox.sigChanged = self.textBox.textChanged From de022be634677cc20f1e94f6b662de1ccb5fbd57 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 4 May 2014 12:24:46 -0400 Subject: [PATCH 18/50] Fixed Parameter 'readonly' option for bool, color, and text parameter types --- CHANGELOG | 1 + pyqtgraph/parametertree/parameterTypes.py | 7 +++++++ .../parametertree/tests/test_parametertypes.py | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 pyqtgraph/parametertree/tests/test_parametertypes.py diff --git a/CHANGELOG b/CHANGELOG index 6ae13037..e0723ca5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -86,6 +86,7 @@ pyqtgraph-0.9.9 [unreleased] - Fixed TableWidget append / sort issues - Fixed AxisItem not resizing text area when setTicks() is used - Removed a few cyclic references + - Fixed Parameter 'readonly' option for bool, color, and text parameter types pyqtgraph-0.9.8 2013-11-24 diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 53abe429..62e935fd 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -125,6 +125,7 @@ class WidgetParameterItem(ParameterItem): w.sigChanged = w.toggled w.value = w.isChecked w.setValue = w.setChecked + w.setEnabled(not opts.get('readonly', False)) self.hideWidget = False elif t == 'str': w = QtGui.QLineEdit() @@ -140,6 +141,7 @@ class WidgetParameterItem(ParameterItem): w.setValue = w.setColor self.hideWidget = False w.setFlat(True) + w.setEnabled(not opts.get('readonly', False)) elif t == 'colormap': from ..widgets.GradientWidget import GradientWidget ## need this here to avoid import loop w = GradientWidget(orientation='bottom') @@ -274,6 +276,8 @@ class WidgetParameterItem(ParameterItem): if 'readonly' in opts: self.updateDefaultBtn() + if isinstance(self.widget, (QtGui.QCheckBox,ColorButton)): + w.setEnabled(not opts['readonly']) ## If widget is a SpinBox, pass options straight through if isinstance(self.widget, SpinBox): @@ -281,6 +285,9 @@ class WidgetParameterItem(ParameterItem): opts['suffix'] = opts['units'] self.widget.setOpts(**opts) self.updateDisplayLabel() + + + class EventProxy(QtCore.QObject): def __init__(self, qobj, callback): diff --git a/pyqtgraph/parametertree/tests/test_parametertypes.py b/pyqtgraph/parametertree/tests/test_parametertypes.py new file mode 100644 index 00000000..c7cd2cb3 --- /dev/null +++ b/pyqtgraph/parametertree/tests/test_parametertypes.py @@ -0,0 +1,18 @@ +import pyqtgraph.parametertree as pt +import pyqtgraph as pg +app = pg.mkQApp() + +def test_opts(): + paramSpec = [ + dict(name='bool', type='bool', readonly=True), + dict(name='color', type='color', readonly=True), + ] + + param = pt.Parameter.create(name='params', type='group', children=paramSpec) + tree = pt.ParameterTree() + tree.setParameters(param) + + assert param.param('bool').items.keys()[0].widget.isEnabled() is False + assert param.param('color').items.keys()[0].widget.isEnabled() is False + + From 1e0034904ef4d48c860eed083f8c01bc04f4178b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 5 May 2014 11:16:00 -0400 Subject: [PATCH 19/50] Initialize drag variable in Dock.py --- pyqtgraph/dockarea/Dock.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index d3cfcbb6..99808eee 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -244,6 +244,7 @@ class DockLabel(VerticalLabel): sigClicked = QtCore.Signal(object, object) def __init__(self, text, dock): + self.startedDrag = False self.dim = False self.fixedWidth = False VerticalLabel.__init__(self, text, orientation='horizontal', forceWidth=False) From 4c37b75afeee0dc5b75e51786191e5f6bd25e9fb Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 7 May 2014 12:49:30 -0400 Subject: [PATCH 20/50] Added Dock close button (from Stefan H) --- pyqtgraph/dockarea/Dock.py | 97 ++++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 41 deletions(-) diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index 99808eee..b124b125 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -8,11 +8,13 @@ class Dock(QtGui.QWidget, DockDrop): sigStretchChanged = QtCore.Signal() - def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True): + def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True, closeable = False): QtGui.QWidget.__init__(self) DockDrop.__init__(self) self.area = area - self.label = DockLabel(name, self) + self.label = DockLabel(name, self, closeable) + if closeable: + self.label.sigCloseClicked.connect(self.close) self.labelHidden = False self.moveLabel = True ## If false, the dock is no longer allowed to move the label. self.autoOrient = autoOrientation @@ -35,30 +37,30 @@ class Dock(QtGui.QWidget, DockDrop): #self.titlePos = 'top' self.raiseOverlay() self.hStyle = """ - Dock > QWidget { - border: 1px solid #000; - border-radius: 5px; - border-top-left-radius: 0px; - border-top-right-radius: 0px; + Dock > QWidget { + border: 1px solid #000; + border-radius: 5px; + border-top-left-radius: 0px; + border-top-right-radius: 0px; border-top-width: 0px; }""" self.vStyle = """ - Dock > QWidget { - border: 1px solid #000; - border-radius: 5px; - border-top-left-radius: 0px; - border-bottom-left-radius: 0px; + Dock > QWidget { + border: 1px solid #000; + border-radius: 5px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; border-left-width: 0px; }""" self.nStyle = """ - Dock > QWidget { - border: 1px solid #000; - border-radius: 5px; + Dock > QWidget { + border: 1px solid #000; + border-radius: 5px; }""" self.dragStyle = """ - Dock > QWidget { - border: 4px solid #00F; - border-radius: 5px; + Dock > QWidget { + border: 4px solid #00F; + border-radius: 5px; }""" self.setAutoFillBackground(False) self.widgetArea.setStyleSheet(self.hStyle) @@ -79,7 +81,7 @@ class Dock(QtGui.QWidget, DockDrop): def setStretch(self, x=None, y=None): """ - Set the 'target' size for this Dock. + Set the 'target' size for this Dock. The actual size will be determined by comparing this Dock's stretch value to the rest of the docks it shares space with. """ @@ -130,7 +132,7 @@ class Dock(QtGui.QWidget, DockDrop): Sets the orientation of the title bar for this Dock. Must be one of 'auto', 'horizontal', or 'vertical'. By default ('auto'), the orientation is determined - based on the aspect ratio of the Dock. + based on the aspect ratio of the Dock. """ #print self.name(), "setOrientation", o, force if o == 'auto' and self.autoOrient: @@ -175,7 +177,7 @@ class Dock(QtGui.QWidget, DockDrop): def addWidget(self, widget, row=None, col=0, rowspan=1, colspan=1): """ - Add a new widget to the interior of this Dock. + Add a new widget to the interior of this Dock. Each Dock uses a QGridLayout to arrange widgets within. """ if row is None: @@ -242,9 +244,9 @@ class Dock(QtGui.QWidget, DockDrop): class DockLabel(VerticalLabel): sigClicked = QtCore.Signal(object, object) + sigCloseClicked = QtCore.Signal() - def __init__(self, text, dock): - self.startedDrag = False + def __init__(self, text, dock, showCloseButton): self.dim = False self.fixedWidth = False VerticalLabel.__init__(self, text, orientation='horizontal', forceWidth=False) @@ -252,6 +254,13 @@ class DockLabel(VerticalLabel): self.dock = dock self.updateStyle() self.setAutoFillBackground(False) + self.startedDrag = False + + self.closeButton = None + if showCloseButton: + self.closeButton = QtGui.QToolButton(self) + self.closeButton.pressed.connect(self.sigCloseClicked) + self.closeButton.setIcon(QtGui.QApplication.style().standardIcon(QtGui.QStyle.SP_TitleBarCloseButton)) #def minimumSizeHint(self): ##sh = QtGui.QWidget.minimumSizeHint(self) @@ -269,28 +278,28 @@ class DockLabel(VerticalLabel): border = '#55B' if self.orientation == 'vertical': - self.vStyle = """DockLabel { - background-color : %s; - color : %s; - border-top-right-radius: 0px; - border-top-left-radius: %s; - border-bottom-right-radius: 0px; - border-bottom-left-radius: %s; - border-width: 0px; + self.vStyle = """DockLabel { + background-color : %s; + color : %s; + border-top-right-radius: 0px; + border-top-left-radius: %s; + border-bottom-right-radius: 0px; + border-bottom-left-radius: %s; + border-width: 0px; border-right: 2px solid %s; padding-top: 3px; padding-bottom: 3px; }""" % (bg, fg, r, r, border) self.setStyleSheet(self.vStyle) else: - self.hStyle = """DockLabel { - background-color : %s; - color : %s; - border-top-right-radius: %s; - border-top-left-radius: %s; - border-bottom-right-radius: 0px; - border-bottom-left-radius: 0px; - border-width: 0px; + self.hStyle = """DockLabel { + background-color : %s; + color : %s; + border-top-right-radius: %s; + border-top-left-radius: %s; + border-bottom-right-radius: 0px; + border-bottom-left-radius: 0px; + border-width: 0px; border-bottom: 2px solid %s; padding-left: 3px; padding-right: 3px; @@ -336,5 +345,11 @@ class DockLabel(VerticalLabel): #VerticalLabel.paintEvent(self, ev) - - + def resizeEvent (self, ev): + if self.closeButton: + if self.orientation == 'vertical': + closeButtonSize = ev.size().width() + else: + closeButtonSize = ev.size().height() + self.closeButton.setFixedSize(QtCore.QSize(closeButtonSize,closeButtonSize)) + super(DockLabel,self).resizeEvent(ev) From 51f0a063ee6ea5e0180bf71f75581f803de7ee65 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 8 May 2014 09:50:26 -0400 Subject: [PATCH 21/50] minor cleanups --- examples/dockarea.py | 2 +- pyqtgraph/dockarea/Dock.py | 32 +++++++++++--------------------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/examples/dockarea.py b/examples/dockarea.py index 2b33048d..9cc79f1b 100644 --- a/examples/dockarea.py +++ b/examples/dockarea.py @@ -35,7 +35,7 @@ win.setWindowTitle('pyqtgraph example: dockarea') ## Note that size arguments are only a suggestion; docks will still have to ## fill the entire dock area and obey the limits of their internal widgets. d1 = Dock("Dock1", size=(1, 1)) ## give this dock the minimum possible size -d2 = Dock("Dock2 - Console", size=(500,300)) +d2 = Dock("Dock2 - Console", size=(500,300), closable=True) d3 = Dock("Dock3", size=(500,400)) d4 = Dock("Dock4 (tabbed) - Plot", size=(500,200)) d5 = Dock("Dock5 - Image", size=(500,200)) diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index b124b125..28d4244b 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -8,12 +8,12 @@ class Dock(QtGui.QWidget, DockDrop): sigStretchChanged = QtCore.Signal() - def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True, closeable = False): + def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True, closable=False): QtGui.QWidget.__init__(self) DockDrop.__init__(self) self.area = area - self.label = DockLabel(name, self, closeable) - if closeable: + self.label = DockLabel(name, self, closable) + if closable: self.label.sigCloseClicked.connect(self.close) self.labelHidden = False self.moveLabel = True ## If false, the dock is no longer allowed to move the label. @@ -241,6 +241,7 @@ class Dock(QtGui.QWidget, DockDrop): def dropEvent(self, *args): DockDrop.dropEvent(self, *args) + class DockLabel(VerticalLabel): sigClicked = QtCore.Signal(object, object) @@ -259,13 +260,9 @@ class DockLabel(VerticalLabel): self.closeButton = None if showCloseButton: self.closeButton = QtGui.QToolButton(self) - self.closeButton.pressed.connect(self.sigCloseClicked) + self.closeButton.clicked.connect(self.sigCloseClicked) self.closeButton.setIcon(QtGui.QApplication.style().standardIcon(QtGui.QStyle.SP_TitleBarCloseButton)) - #def minimumSizeHint(self): - ##sh = QtGui.QWidget.minimumSizeHint(self) - #return QtCore.QSize(20, 20) - def updateStyle(self): r = '3px' if self.dim: @@ -325,11 +322,9 @@ class DockLabel(VerticalLabel): if not self.startedDrag and (ev.pos() - self.pressPos).manhattanLength() > QtGui.QApplication.startDragDistance(): self.dock.startDrag() ev.accept() - #print ev.pos() def mouseReleaseEvent(self, ev): if not self.startedDrag: - #self.emit(QtCore.SIGNAL('clicked'), self, ev) self.sigClicked.emit(self, ev) ev.accept() @@ -337,19 +332,14 @@ class DockLabel(VerticalLabel): if ev.button() == QtCore.Qt.LeftButton: self.dock.float() - #def paintEvent(self, ev): - #p = QtGui.QPainter(self) - ##p.setBrush(QtGui.QBrush(QtGui.QColor(100, 100, 200))) - #p.setPen(QtGui.QPen(QtGui.QColor(50, 50, 100))) - #p.drawRect(self.rect().adjusted(0, 0, -1, -1)) - - #VerticalLabel.paintEvent(self, ev) - def resizeEvent (self, ev): if self.closeButton: if self.orientation == 'vertical': - closeButtonSize = ev.size().width() + size = ev.size().width() + pos = QtCore.QPoint(0, 0) else: - closeButtonSize = ev.size().height() - self.closeButton.setFixedSize(QtCore.QSize(closeButtonSize,closeButtonSize)) + size = ev.size().height() + pos = QtCore.QPoint(ev.size().width() - size, 0) + self.closeButton.setFixedSize(QtCore.QSize(size, size)) + self.closeButton.move(pos) super(DockLabel,self).resizeEvent(ev) From 23cfdf7239a9afd4501aebf84a404ca2021d3b80 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 8 May 2014 10:37:23 -0400 Subject: [PATCH 22/50] fixed dock insert order bug --- pyqtgraph/dockarea/Container.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyqtgraph/dockarea/Container.py b/pyqtgraph/dockarea/Container.py index 277375f3..c3225edf 100644 --- a/pyqtgraph/dockarea/Container.py +++ b/pyqtgraph/dockarea/Container.py @@ -22,6 +22,9 @@ class Container(object): return None def insert(self, new, pos=None, neighbor=None): + # remove from existing parent first + new.setParent(None) + if not isinstance(new, list): new = [new] if neighbor is None: From 89b0a91c86419d2d321cd90966ce23e90453a22f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 8 May 2014 10:40:58 -0400 Subject: [PATCH 23/50] Update contrib list --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b585a6bd..2dff1031 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Contributors * Fabio Zadrozny * Mikhail Terekhov * Pietro Zambelli + * Stefan Holzmann Requirements ------------ From f30c1a59d18298c7efb392e0372e68c03fa9f9c1 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 8 May 2014 22:37:08 -0400 Subject: [PATCH 24/50] Fixed GLScatterPlotItem to allow opaque spots --- CHANGELOG | 3 +++ pyqtgraph/opengl/items/GLScatterPlotItem.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index fd6589d4..a31b356f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,8 @@ pyqtgraph-0.9.9 [unreleased] - GLViewWidget.itemsAt() now measures y from top of widget to match mouse event position. - Made setPen() methods consistent throughout the package + - Fix in GLScatterPlotItem requires that points will appear slightly more opaque + (so you may need to adjust to lower alpha to achieve the same results) New Features: - Added ViewBox.setLimits() method @@ -88,6 +90,7 @@ pyqtgraph-0.9.9 [unreleased] - Fixed AxisItem not resizing text area when setTicks() is used - Removed a few cyclic references - Fixed Parameter 'readonly' option for bool, color, and text parameter types + - Fixed alpha on GLScatterPlotItem spots (formerly maxed out at alpha=200) pyqtgraph-0.9.8 2013-11-24 diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py index bb2c89a3..6cfcc6aa 100644 --- a/pyqtgraph/opengl/items/GLScatterPlotItem.py +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -59,7 +59,7 @@ class GLScatterPlotItem(GLGraphicsItem): w = 64 def fn(x,y): r = ((x-w/2.)**2 + (y-w/2.)**2) ** 0.5 - return 200 * (w/2. - np.clip(r, w/2.-1.0, w/2.)) + return 255 * (w/2. - np.clip(r, w/2.-1.0, w/2.)) pData = np.empty((w, w, 4)) pData[:] = 255 pData[:,:,3] = np.fromfunction(fn, pData.shape[:2]) From 8ef2cb7e48276d23fda5c809525439c1a66141aa Mon Sep 17 00:00:00 2001 From: Mikhail Terekhov Date: Thu, 8 May 2014 23:01:53 -0400 Subject: [PATCH 25/50] CSVExporter: fix the case when stepMode=True --- pyqtgraph/exporters/CSVExporter.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/exporters/CSVExporter.py b/pyqtgraph/exporters/CSVExporter.py index b0cf5af5..8cd089d1 100644 --- a/pyqtgraph/exporters/CSVExporter.py +++ b/pyqtgraph/exporters/CSVExporter.py @@ -53,9 +53,13 @@ class CSVExporter(Exporter): for i in range(numRows): for d in data: if i < len(d[0]): - fd.write(numFormat % d[0][i] + sep + numFormat % d[1][i] + sep) + fd.write(numFormat % d[0][i] + sep) else: - fd.write(' %s %s' % (sep, sep)) + fd.write(' %s' % sep) + if i < len(d[1]): + fd.write(numFormat % d[1][i] + sep) + else: + fd.write(' %s' % sep) fd.write('\n') fd.close() From 6a4a653989a379d3e02728f52797e56fbd99d4a6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 10 May 2014 14:19:27 -0400 Subject: [PATCH 26/50] Added InfiniteLine.setHoverPen --- CHANGELOG | 1 + pyqtgraph/graphicsItems/InfiniteLine.py | 48 ++++++++++++------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a31b356f..1a1ba126 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -52,6 +52,7 @@ pyqtgraph-0.9.9 [unreleased] - Added 'stepMode' argument to PlotDataItem() - Added ViewBox.invertX() - Docks now have optional close button + - Added InfiniteLine.setHoverPen Bugfixes: - PlotCurveItem now has correct clicking behavior--clicks within a few px diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index dfe2a4c1..8108c3cf 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -59,7 +59,9 @@ class InfiniteLine(GraphicsObject): if pen is None: pen = (200, 200, 100) + self.setPen(pen) + self.setHoverPen(color=(255,0,0), width=self.pen.width()) self.currentPen = self.pen #self.setFlag(self.ItemSendsScenePositionChanges) @@ -77,8 +79,22 @@ class InfiniteLine(GraphicsObject): """Set the pen for drawing the line. Allowable arguments are any that are valid for :func:`mkPen `.""" self.pen = fn.mkPen(*args, **kwargs) - self.currentPen = self.pen - self.update() + if not self.mouseHovering: + self.currentPen = self.pen + self.update() + + def setHoverPen(self, *args, **kwargs): + """Set the pen for drawing the line while the mouse hovers over it. + Allowable arguments are any that are valid + for :func:`mkPen `. + + If the line is not movable, then hovering is also disabled. + + Added in version 0.9.9.""" + self.hoverPen = fn.mkPen(*args, **kwargs) + if self.mouseHovering: + self.currentPen = self.hoverPen + self.update() def setAngle(self, angle): """ @@ -168,8 +184,9 @@ class InfiniteLine(GraphicsObject): px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line if px is None: px = 0 - br.setBottom(-px*4) - br.setTop(px*4) + w = (max(4, self.pen.width()/2, self.hoverPen.width()/2)+1) * px + br.setBottom(-w) + br.setTop(w) return br.normalized() def paint(self, p, *args): @@ -183,25 +200,6 @@ class InfiniteLine(GraphicsObject): return None ## x axis should never be auto-scaled else: return (0,0) - - #def mousePressEvent(self, ev): - #if self.movable and ev.button() == QtCore.Qt.LeftButton: - #ev.accept() - #self.pressDelta = self.mapToParent(ev.pos()) - QtCore.QPointF(*self.p) - #else: - #ev.ignore() - - #def mouseMoveEvent(self, ev): - #self.setPos(self.mapToParent(ev.pos()) - self.pressDelta) - ##self.emit(QtCore.SIGNAL('dragged'), self) - #self.sigDragged.emit(self) - #self.hasMoved = True - - #def mouseReleaseEvent(self, ev): - #if self.hasMoved and ev.button() == QtCore.Qt.LeftButton: - #self.hasMoved = False - ##self.emit(QtCore.SIGNAL('positionChangeFinished'), self) - #self.sigPositionChangeFinished.emit(self) def mouseDragEvent(self, ev): if self.movable and ev.button() == QtCore.Qt.LeftButton: @@ -239,12 +237,12 @@ class InfiniteLine(GraphicsObject): self.setMouseHover(False) def setMouseHover(self, hover): - ## Inform the item that the mouse is(not) hovering over it + ## Inform the item that the mouse is (not) hovering over it if self.mouseHovering == hover: return self.mouseHovering = hover if hover: - self.currentPen = fn.mkPen(255, 0,0) + self.currentPen = self.hoverPen else: self.currentPen = self.pen self.update() From 88c55c9f986a11c9787112fe118d76da57a03d2b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 10 May 2014 15:30:51 -0400 Subject: [PATCH 27/50] Docstring updates for ParameterTree --- pyqtgraph/parametertree/ParameterTree.py | 43 +++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/parametertree/ParameterTree.py b/pyqtgraph/parametertree/ParameterTree.py index 953f3bb7..ef7c1030 100644 --- a/pyqtgraph/parametertree/ParameterTree.py +++ b/pyqtgraph/parametertree/ParameterTree.py @@ -7,9 +7,16 @@ from .ParameterItem import ParameterItem class ParameterTree(TreeWidget): - """Widget used to display or control data from a ParameterSet""" + """Widget used to display or control data from a hierarchy of Parameters""" def __init__(self, parent=None, showHeader=True): + """ + ============== ======================================================== + **Arguments:** + parent (QWidget) An optional parent widget + showHeader (bool) If True, then the QTreeView header is displayed. + ============== ======================================================== + """ TreeWidget.__init__(self, parent) self.setVerticalScrollMode(self.ScrollPerPixel) self.setHorizontalScrollMode(self.ScrollPerPixel) @@ -25,10 +32,35 @@ class ParameterTree(TreeWidget): self.setRootIsDecorated(False) def setParameters(self, param, showTop=True): + """ + Set the top-level :class:`Parameter ` + to be displayed in this ParameterTree. + + If *showTop* is False, then the top-level parameter is hidden and only + its children will be visible. This is a convenience method equivalent + to:: + + tree.clear() + tree.addParameters(param, showTop) + """ self.clear() self.addParameters(param, showTop=showTop) def addParameters(self, param, root=None, depth=0, showTop=True): + """ + Adds one top-level :class:`Parameter ` + to the view. + + ============== ========================================================== + **Arguments:** + param The :class:`Parameter ` + to add. + root The item within the tree to which *param* should be added. + By default, *param* is added as a top-level item. + showTop If False, then *param* will be hidden, and only its + children will be visible in the tree. + ============== ========================================================== + """ item = param.makeTreeItem(depth=depth) if root is None: root = self.invisibleRootItem() @@ -45,11 +77,14 @@ class ParameterTree(TreeWidget): self.addParameters(ch, root=item, depth=depth+1) def clear(self): - self.invisibleRootItem().takeChildren() - + """ + Remove all parameters from the tree. + """ + self.invisibleRootItem().takeChildren() def focusNext(self, item, forward=True): - ## Give input focus to the next (or previous) item after 'item' + """Give input focus to the next (or previous) item after *item* + """ while True: parent = item.parent() if parent is None: From ab411012f8d0c8b0e72cfc7c3dc181d11d6fbf47 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 18 May 2014 17:57:16 -0400 Subject: [PATCH 28/50] Added some ViewBox unit tests, fixed minor API bug --- pyqtgraph/Qt.py | 8 ++ pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 10 ++- .../ViewBox/tests/test_ViewBox.py | 85 +++++++++++++++++++ 3 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 4fe8c3ab..efbe66c4 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -33,6 +33,10 @@ else: if USE_PYSIDE: from PySide import QtGui, QtCore, QtOpenGL, QtSvg + try: + from PySide import QtTest + except ImportError: + pass import PySide try: from PySide import shiboken @@ -106,6 +110,10 @@ else: from PyQt4 import QtOpenGL except ImportError: pass + try: + from PyQt4 import QtTest + except ImportError: + pass import sip diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index cf9e7f4a..3fa079f2 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -671,7 +671,10 @@ class ViewBox(GraphicsWidget): Added in version 0.9.9 """ update = False - + allowed = ['xMin', 'xMax', 'yMin', 'yMax', 'minXRange', 'maxXRange', 'minYRange', 'maxYRange'] + for kwd in kwds: + if kwd not in allowed: + raise ValueError("Invalid keyword argument '%s'." % kwd) #for kwd in ['xLimits', 'yLimits', 'minRange', 'maxRange']: #if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]: #self.state['limits'][kwd] = kwds[kwd] @@ -1511,7 +1514,8 @@ class ViewBox(GraphicsWidget): if dx != 0: changed[0] = True viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] - + + # ----------- Make corrections for view limits ----------- limits = (self.state['limits']['xLimits'], self.state['limits']['yLimits']) @@ -1562,7 +1566,7 @@ class ViewBox(GraphicsWidget): changed[axis] = True #print "after applying edge limits:", viewRange[axis] - + changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) or (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)] self.state['viewRange'] = viewRange diff --git a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py new file mode 100644 index 00000000..7cb366c2 --- /dev/null +++ b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py @@ -0,0 +1,85 @@ +#import PySide +import pyqtgraph as pg + +app = pg.mkQApp() +qtest = pg.Qt.QtTest.QTest + +def assertMapping(vb, r1, r2): + assert vb.mapFromView(r1.topLeft()) == r2.topLeft() + assert vb.mapFromView(r1.bottomLeft()) == r2.bottomLeft() + assert vb.mapFromView(r1.topRight()) == r2.topRight() + assert vb.mapFromView(r1.bottomRight()) == r2.bottomRight() + +def test_ViewBox(): + global app, win, vb + QRectF = pg.QtCore.QRectF + + win = pg.GraphicsWindow() + win.ci.layout.setContentsMargins(0,0,0,0) + win.resize(200, 200) + win.show() + vb = win.addViewBox() + + vb.setRange(xRange=[0, 10], yRange=[0, 10], padding=0) + + # required to make mapFromView work properly. + qtest.qWaitForWindowShown(win) + vb.update() + + g = pg.GridItem() + vb.addItem(g) + + app.processEvents() + + w = vb.geometry().width() + h = vb.geometry().height() + view1 = QRectF(0, 0, 10, 10) + size1 = QRectF(0, h, w, -h) + assertMapping(vb, view1, size1) + + # test resize + win.resize(400, 400) + app.processEvents() + w = vb.geometry().width() + h = vb.geometry().height() + size1 = QRectF(0, h, w, -h) + assertMapping(vb, view1, size1) + + # now lock aspect + vb.setAspectLocked() + + # test wide resize + win.resize(800, 400) + app.processEvents() + w = vb.geometry().width() + h = vb.geometry().height() + view1 = QRectF(-5, 0, 20, 10) + size1 = QRectF(0, h, w, -h) + assertMapping(vb, view1, size1) + + # test tall resize + win.resize(400, 800) + app.processEvents() + w = vb.geometry().width() + h = vb.geometry().height() + view1 = QRectF(0, -5, 10, 20) + size1 = QRectF(0, h, w, -h) + assertMapping(vb, view1, size1) + + # test limits + resize (aspect ratio constraint has priority over limits + win.resize(400, 400) + app.processEvents() + vb.setLimits(xMin=0, xMax=10, yMin=0, yMax=10) + win.resize(800, 400) + app.processEvents() + w = vb.geometry().width() + h = vb.geometry().height() + view1 = QRectF(-5, 0, 20, 10) + size1 = QRectF(0, h, w, -h) + assertMapping(vb, view1, size1) + + +if __name__ == '__main__': + import user,sys + test_ViewBox() + \ No newline at end of file From 279ad1bee0cf7b1ac8c408a7932062f588c32b5e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 18 May 2014 19:14:57 -0400 Subject: [PATCH 29/50] Fixed ViewBox error when accessing zoom history before having zoomed. --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 3fa079f2..d66f32ad 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1312,6 +1312,8 @@ class ViewBox(GraphicsWidget): ev.ignore() def scaleHistory(self, d): + if len(self.axHistory) == 0: + return ptr = max(0, min(len(self.axHistory)-1, self.axHistoryPointer+d)) if ptr != self.axHistoryPointer: self.axHistoryPointer = ptr From 9dbdeaa1e0ccae9d9ce170aed6f1bb36bfa1cb1f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 19 May 2014 18:25:05 -0400 Subject: [PATCH 30/50] Added missing .ui file needed for uic unit test --- pyqtgraph/tests/uictest.ui | 53 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 pyqtgraph/tests/uictest.ui diff --git a/pyqtgraph/tests/uictest.ui b/pyqtgraph/tests/uictest.ui new file mode 100644 index 00000000..25d14f2b --- /dev/null +++ b/pyqtgraph/tests/uictest.ui @@ -0,0 +1,53 @@ + + + Form + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + 10 + 10 + 120 + 80 + + + + + + + 10 + 110 + 120 + 80 + + + + + + + PlotWidget + QWidget +
pyqtgraph
+ 1 +
+ + ImageView + QWidget +
pyqtgraph
+ 1 +
+
+ + +
From c3fdcc9ae55148993ae285543318fdfc17104526 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 21 May 2014 23:25:07 -0400 Subject: [PATCH 31/50] Minor fix in list parameter --- pyqtgraph/parametertree/parameterTypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 62e935fd..8aba4bca 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -539,8 +539,8 @@ class ListParameter(Parameter): self.forward, self.reverse = self.mapping(limits) Parameter.setLimits(self, limits) - #print self.name(), self.value(), limits - if len(self.reverse) > 0 and self.value() not in self.reverse[0]: + #print self.name(), self.value(), limits, self.reverse + if len(self.reverse[0]) > 0 and self.value() not in self.reverse[0]: self.setValue(self.reverse[0][0]) #def addItem(self, name, value=None): From 0524bfa6e8e125a58db45ad4f99b81189baf1a62 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 22 May 2014 01:22:12 -0400 Subject: [PATCH 32/50] Added new demos: - Relativity simulator - Optics simulator - Mechanical chain simulator --- examples/__main__.py | 5 + examples/optics/__init__.py | 1 + examples/optics/pyoptic.py | 582 ++++++++++++++++++++++++++ examples/optics/schott_glasses.csv.gz | Bin 0 -> 37232 bytes examples/optics_demos.py | 170 ++++++++ examples/relativity | 1 + examples/relativity_demo.py | 23 + examples/verlet_chain/__init__.py | 1 + examples/verlet_chain/chain.py | 110 +++++ examples/verlet_chain/make | 3 + examples/verlet_chain/maths.so | Bin 0 -> 8017 bytes examples/verlet_chain/relax.c | 48 +++ examples/verlet_chain/relax.py | 23 + examples/verlet_chain_demo.py | 111 +++++ 14 files changed, 1078 insertions(+) create mode 100644 examples/optics/__init__.py create mode 100644 examples/optics/pyoptic.py create mode 100644 examples/optics/schott_glasses.csv.gz create mode 100644 examples/optics_demos.py create mode 160000 examples/relativity create mode 100644 examples/relativity_demo.py create mode 100644 examples/verlet_chain/__init__.py create mode 100644 examples/verlet_chain/chain.py create mode 100755 examples/verlet_chain/make create mode 100755 examples/verlet_chain/maths.so create mode 100644 examples/verlet_chain/relax.c create mode 100644 examples/verlet_chain/relax.py create mode 100644 examples/verlet_chain_demo.py diff --git a/examples/__main__.py b/examples/__main__.py index e972c60a..6c2e8b97 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -32,6 +32,11 @@ examples = OrderedDict([ ('Auto-range', 'PlotAutoRange.py'), ('Remote Plotting', 'RemoteSpeedTest.py'), ('HDF5 big data', 'hdf5.py'), + ('Demos', OrderedDict([ + ('Optics', 'optics_demos.py'), + ('Special relativity', 'relativity_demo.py'), + ('Verlet chain', 'verlet_chain_demo.py'), + ])), ('GraphicsItems', OrderedDict([ ('Scatter Plot', 'ScatterPlot.py'), #('PlotItem', 'PlotItem.py'), diff --git a/examples/optics/__init__.py b/examples/optics/__init__.py new file mode 100644 index 00000000..577c24da --- /dev/null +++ b/examples/optics/__init__.py @@ -0,0 +1 @@ +from pyoptic import * \ No newline at end of file diff --git a/examples/optics/pyoptic.py b/examples/optics/pyoptic.py new file mode 100644 index 00000000..486f653d --- /dev/null +++ b/examples/optics/pyoptic.py @@ -0,0 +1,582 @@ +# -*- coding: utf-8 -*- +from PyQt4 import QtGui, QtCore +import pyqtgraph as pg +#from pyqtgraph.canvas import Canvas, CanvasItem +import numpy as np +import csv, gzip, os +from pyqtgraph import Point + +class GlassDB: + """ + Database of dispersion coefficients for Schott glasses + + Corning 7980 + """ + def __init__(self, fileName='schott_glasses.csv'): + path = os.path.dirname(__file__) + fh = gzip.open(os.path.join(path, 'schott_glasses.csv.gz'), 'rb') + r = csv.reader(fh.readlines()) + lines = [x for x in r] + self.data = {} + header = lines[0] + for l in lines[1:]: + info = {} + for i in range(1, len(l)): + info[header[i]] = l[i] + self.data[l[0]] = info + self.data['Corning7980'] = { ## Thorlabs UV fused silica--not in schott catalog. + 'B1': 0.68374049400, + 'B2': 0.42032361300, + 'B3': 0.58502748000, + 'C1': 0.00460352869, + 'C2': 0.01339688560, + 'C3': 64.49327320000, + 'TAUI25/250': 0.95, ## transmission data is fabricated, but close. + 'TAUI25/1400': 0.98, + } + + for k in self.data: + self.data[k]['ior_cache'] = {} + + + def ior(self, glass, wl): + """ + Return the index of refraction for *glass* at wavelength *wl*. + + The *glass* argument must be a key in self.data. + """ + info = self.data[glass] + cache = info['ior_cache'] + if wl not in cache: + B = map(float, [info['B1'], info['B2'], info['B3']]) + C = map(float, [info['C1'], info['C2'], info['C3']]) + w2 = (wl/1000.)**2 + n = np.sqrt(1.0 + (B[0]*w2 / (w2-C[0])) + (B[1]*w2 / (w2-C[1])) + (B[2]*w2 / (w2-C[2]))) + cache[wl] = n + return cache[wl] + + def transmissionCurve(self, glass): + data = self.data[glass] + keys = [int(x[7:]) for x in data.keys() if 'TAUI25' in x] + keys.sort() + curve = np.empty((2,len(keys))) + for i in range(len(keys)): + curve[0][i] = keys[i] + key = 'TAUI25/%d' % keys[i] + val = data[key] + if val == '': + val = 0 + else: + val = float(val) + curve[1][i] = val + return curve + + +GLASSDB = GlassDB() + + +def wlPen(wl): + """Return a pen representing the given wavelength""" + l1 = 400 + l2 = 700 + hue = np.clip(((l2-l1) - (wl-l1)) * 0.8 / (l2-l1), 0, 0.8) + val = 1.0 + if wl > 700: + val = 1.0 * (((700-wl)/700.) + 1) + elif wl < 400: + val = wl * 1.0/400. + #print hue, val + color = pg.hsvColor(hue, 1.0, val) + pen = pg.mkPen(color) + return pen + + +class ParamObj: + # Just a helper for tracking parameters and responding to changes + def __init__(self): + self.__params = {} + + def __setitem__(self, item, val): + self.setParam(item, val) + + def setParam(self, param, val): + self.setParams(**{param:val}) + + def setParams(self, **params): + """Set parameters for this optic. This is a good function to override for subclasses.""" + self.__params.update(params) + self.paramStateChanged() + + def paramStateChanged(self): + pass + + def __getitem__(self, item): + return self.getParam(item) + + def getParam(self, param): + return self.__params[param] + + +class Optic(pg.GraphicsObject, ParamObj): + + sigStateChanged = QtCore.Signal() + + + def __init__(self, gitem, **params): + ParamObj.__init__(self) + pg.GraphicsObject.__init__(self) #, [0,0], [1,1]) + + self.gitem = gitem + self.surfaces = gitem.surfaces + gitem.setParentItem(self) + + self.roi = pg.ROI([0,0], [1,1]) + self.roi.addRotateHandle([1, 1], [0.5, 0.5]) + self.roi.setParentItem(self) + + defaults = { + 'pos': Point(0,0), + 'angle': 0, + } + defaults.update(params) + self._ior_cache = {} + self.roi.sigRegionChanged.connect(self.roiChanged) + self.setParams(**defaults) + + def updateTransform(self): + self.resetTransform() + self.setPos(0, 0) + self.translate(Point(self['pos'])) + self.rotate(self['angle']) + + def setParam(self, param, val): + ParamObj.setParam(self, param, val) + + def paramStateChanged(self): + """Some parameters of the optic have changed.""" + # Move graphics item + self.gitem.setPos(Point(self['pos'])) + self.gitem.resetTransform() + self.gitem.rotate(self['angle']) + + # Move ROI to match + try: + self.roi.sigRegionChanged.disconnect(self.roiChanged) + br = self.gitem.boundingRect() + o = self.gitem.mapToParent(br.topLeft()) + self.roi.setAngle(self['angle']) + self.roi.setPos(o) + self.roi.setSize([br.width(), br.height()]) + finally: + self.roi.sigRegionChanged.connect(self.roiChanged) + + self.sigStateChanged.emit() + + def roiChanged(self, *args): + pos = self.roi.pos() + # rotate gitem temporarily so we can decide where it will need to move + self.gitem.resetTransform() + self.gitem.rotate(self.roi.angle()) + br = self.gitem.boundingRect() + o1 = self.gitem.mapToParent(br.topLeft()) + self.setParams(angle=self.roi.angle(), pos=pos + (self.gitem.pos() - o1)) + + def boundingRect(self): + return QtCore.QRectF() + + def paint(self, p, *args): + pass + + def ior(self, wavelength): + return GLASSDB.ior(self['glass'], wavelength) + + + +class Lens(Optic): + def __init__(self, **params): + defaults = { + 'dia': 25.4, ## diameter of lens + 'r1': 50., ## positive means convex, use 0 for planar + 'r2': 0, ## negative means convex + 'd': 4.0, + 'glass': 'N-BK7', + 'reflect': False, + } + defaults.update(params) + d = defaults.pop('d') + defaults['x1'] = -d/2. + defaults['x2'] = d/2. + + gitem = CircularSolid(brush=(100, 100, 130, 100), **defaults) + Optic.__init__(self, gitem, **defaults) + + def propagateRay(self, ray): + """Refract, reflect, absorb, and/or scatter ray. This function may create and return new rays""" + + """ + NOTE:: We can probably use this to compute refractions faster: (from GLSL 120 docs) + + For the incident vector I and surface normal N, and the + ratio of indices of refraction eta, return the refraction + vector. The result is computed by + k = 1.0 - eta * eta * (1.0 - dot(N, I) * dot(N, I)) + if (k < 0.0) + return genType(0.0) + else + return eta * I - (eta * dot(N, I) + sqrt(k)) * N + The input parameters for the incident vector I and the + surface normal N must already be normalized to get the + desired results. eta == ratio of IORs + + + For reflection: + For the incident vector I and surface orientation N, + returns the reflection direction: + I – 2 ∗ dot(N, I) ∗ N + N must already be normalized in order to achieve the + desired result. + """ + + + + iors = [self.ior(ray['wl']), 1.0] + for i in [0,1]: + surface = self.surfaces[i] + ior = iors[i] + p1, ai = surface.intersectRay(ray) + #print "surface intersection:", p1, ai*180/3.14159 + #trans = self.sceneTransform().inverted()[0] * surface.sceneTransform() + #p1 = trans.map(p1) + if p1 is None: + ray.setEnd(None) + break + p1 = surface.mapToItem(ray, p1) + + #print "adjusted position:", p1 + #ior = self.ior(ray['wl']) + rd = ray['dir'] + a1 = np.arctan2(rd[1], rd[0]) + ar = a1 - ai + np.arcsin((np.sin(ai) * ray['ior'] / ior)) + #print [x for x in [a1, ai, (np.sin(ai) * ray['ior'] / ior), ar]] + #print ai, np.sin(ai), ray['ior'], ior + ray.setEnd(p1) + dp = Point(np.cos(ar), np.sin(ar)) + #p2 = p1+dp + #p1p = self.mapToScene(p1) + #p2p = self.mapToScene(p2) + #dpp = Point(p2p-p1p) + ray = Ray(parent=ray, ior=ior, dir=dp) + return [ray] + + +class Mirror(Optic): + def __init__(self, **params): + defaults = { + 'r1': 0, + 'r2': 0, + 'd': 0.01, + } + defaults.update(params) + d = defaults.pop('d') + defaults['x1'] = -d/2. + defaults['x2'] = d/2. + gitem = CircularSolid(brush=(100,100,100,255), **defaults) + Optic.__init__(self, gitem, **defaults) + + def propagateRay(self, ray): + """Refract, reflect, absorb, and/or scatter ray. This function may create and return new rays""" + + surface = self.surfaces[0] + p1, ai = surface.intersectRay(ray) + if p1 is not None: + p1 = surface.mapToItem(ray, p1) + rd = ray['dir'] + a1 = np.arctan2(rd[1], rd[0]) + ar = a1 + np.pi - 2*ai + ray.setEnd(p1) + dp = Point(np.cos(ar), np.sin(ar)) + ray = Ray(parent=ray, dir=dp) + else: + ray.setEnd(None) + return [ray] + + +class CircularSolid(pg.GraphicsObject, ParamObj): + """GraphicsObject with two circular or flat surfaces.""" + def __init__(self, pen=None, brush=None, **opts): + """ + Arguments for each surface are: + x1,x2 - position of center of _physical surface_ + r1,r2 - radius of curvature + d1,d2 - diameter of optic + """ + defaults = dict(x1=-2, r1=100, d1=25.4, x2=2, r2=100, d2=25.4) + defaults.update(opts) + ParamObj.__init__(self) + self.surfaces = [CircleSurface(defaults['r1'], defaults['d1']), CircleSurface(-defaults['r2'], defaults['d2'])] + pg.GraphicsObject.__init__(self) + for s in self.surfaces: + s.setParentItem(self) + + if pen is None: + self.pen = pg.mkPen((220,220,255,200), width=1, cosmetic=True) + else: + self.pen = pg.mkPen(pen) + + if brush is None: + self.brush = pg.mkBrush((230, 230, 255, 30)) + else: + self.brush = pg.mkBrush(brush) + + self.setParams(**defaults) + + def paramStateChanged(self): + self.updateSurfaces() + + def updateSurfaces(self): + self.surfaces[0].setParams(self['r1'], self['d1']) + self.surfaces[1].setParams(-self['r2'], self['d2']) + self.surfaces[0].setPos(self['x1'], 0) + self.surfaces[1].setPos(self['x2'], 0) + + self.path = QtGui.QPainterPath() + self.path.connectPath(self.surfaces[0].path.translated(self.surfaces[0].pos())) + self.path.connectPath(self.surfaces[1].path.translated(self.surfaces[1].pos()).toReversed()) + self.path.closeSubpath() + + def boundingRect(self): + return self.path.boundingRect() + + def shape(self): + return self.path + + def paint(self, p, *args): + p.setRenderHints(p.renderHints() | p.Antialiasing) + p.setPen(self.pen) + p.fillPath(self.path, self.brush) + p.drawPath(self.path) + + +class CircleSurface(pg.GraphicsObject): + def __init__(self, radius=None, diameter=None): + """center of physical surface is at 0,0 + radius is the radius of the surface. If radius is None, the surface is flat. + diameter is of the optic's edge.""" + pg.GraphicsObject.__init__(self) + + self.r = radius + self.d = diameter + self.mkPath() + + def setParams(self, r, d): + self.r = r + self.d = d + self.mkPath() + + def mkPath(self): + self.prepareGeometryChange() + r = self.r + d = self.d + h2 = d/2. + self.path = QtGui.QPainterPath() + if r == 0: ## flat surface + self.path.moveTo(0, h2) + self.path.lineTo(0, -h2) + else: + ## half-height of surface can't be larger than radius + h2 = min(h2, abs(r)) + + #dx = abs(r) - (abs(r)**2 - abs(h2)**2)**0.5 + #p.moveTo(-d*w/2.+ d*dx, d*h2) + arc = QtCore.QRectF(0, -r, r*2, r*2) + #self.surfaces.append((arc.center(), r, h2)) + a1 = np.arcsin(h2/r) * 180. / np.pi + a2 = -2*a1 + a1 += 180. + self.path.arcMoveTo(arc, a1) + self.path.arcTo(arc, a1, a2) + #if d == -1: + #p1 = QtGui.QPainterPath() + #p1.addRect(arc) + #self.paths.append(p1) + self.h2 = h2 + + def boundingRect(self): + return self.path.boundingRect() + + def paint(self, p, *args): + return ## usually we let the optic draw. + #p.setPen(pg.mkPen('r')) + #p.drawPath(self.path) + + def intersectRay(self, ray): + ## return the point of intersection and the angle of incidence + #print "intersect ray" + h = self.h2 + r = self.r + p, dir = ray.currentState(relativeTo=self) # position and angle of ray in local coords. + #print " ray: ", p, dir + p = p - Point(r, 0) ## move position so center of circle is at 0,0 + #print " adj: ", p, r + + if r == 0: + #print " flat" + if dir[0] == 0: + y = 0 + else: + y = p[1] - p[0] * dir[1]/dir[0] + if abs(y) > h: + return None, None + else: + return (Point(0, y), np.arctan2(dir[1], dir[0])) + else: + #print " curve" + ## find intersection of circle and line (quadratic formula) + dx = dir[0] + dy = dir[1] + dr = (dx**2 + dy**2) ** 0.5 + D = p[0] * (p[1]+dy) - (p[0]+dx) * p[1] + idr2 = 1.0 / dr**2 + disc = r**2 * dr**2 - D**2 + if disc < 0: + return None, None + disc2 = disc**0.5 + if dy < 0: + sgn = -1 + else: + sgn = 1 + + + br = self.path.boundingRect() + x1 = (D*dy + sgn*dx*disc2) * idr2 + y1 = (-D*dx + abs(dy)*disc2) * idr2 + if br.contains(x1+r, y1): + pt = Point(x1, y1) + else: + x2 = (D*dy - sgn*dx*disc2) * idr2 + y2 = (-D*dx - abs(dy)*disc2) * idr2 + pt = Point(x2, y2) + if not br.contains(x2+r, y2): + return None, None + raise Exception("No intersection!") + + norm = np.arctan2(pt[1], pt[0]) + if r < 0: + norm += np.pi + #print " norm:", norm*180/3.1415 + dp = p - pt + #print " dp:", dp + ang = np.arctan2(dp[1], dp[0]) + #print " ang:", ang*180/3.1415 + #print " ai:", (ang-norm)*180/3.1415 + + #print " intersection:", pt + return pt + Point(r, 0), ang-norm + + +class Ray(pg.GraphicsObject, ParamObj): + """Represents a single straight segment of a ray""" + + sigStateChanged = QtCore.Signal() + + def __init__(self, **params): + ParamObj.__init__(self) + defaults = { + 'ior': 1.0, + 'wl': 500, + 'end': None, + 'dir': Point(1,0), + } + self.params = {} + pg.GraphicsObject.__init__(self) + self.children = [] + parent = params.get('parent', None) + if parent is not None: + defaults['start'] = parent['end'] + defaults['wl'] = parent['wl'] + self['ior'] = parent['ior'] + self['dir'] = parent['dir'] + parent.addChild(self) + + defaults.update(params) + defaults['dir'] = Point(defaults['dir']) + self.setParams(**defaults) + self.mkPath() + + def clearChildren(self): + for c in self.children: + c.clearChildren() + c.setParentItem(None) + self.scene().removeItem(c) + self.children = [] + + def paramStateChanged(self): + pass + + def addChild(self, ch): + self.children.append(ch) + ch.setParentItem(self) + + def currentState(self, relativeTo=None): + pos = self['start'] + dir = self['dir'] + if relativeTo is None: + return pos, dir + else: + trans = self.itemTransform(relativeTo)[0] + p1 = trans.map(pos) + p2 = trans.map(pos + dir) + return Point(p1), Point(p2-p1) + + + def setEnd(self, end): + self['end'] = end + self.mkPath() + + def boundingRect(self): + return self.path.boundingRect() + + def paint(self, p, *args): + #p.setPen(pg.mkPen((255,0,0, 150))) + p.setRenderHints(p.renderHints() | p.Antialiasing) + p.setCompositionMode(p.CompositionMode_Plus) + p.setPen(wlPen(self['wl'])) + p.drawPath(self.path) + + def mkPath(self): + self.prepareGeometryChange() + self.path = QtGui.QPainterPath() + self.path.moveTo(self['start']) + if self['end'] is not None: + self.path.lineTo(self['end']) + else: + self.path.lineTo(self['start']+500*self['dir']) + + +def trace(rays, optics): + if len(optics) < 1 or len(rays) < 1: + return + for r in rays: + r.clearChildren() + o = optics[0] + r2 = o.propagateRay(r) + trace(r2, optics[1:]) + +class Tracer(QtCore.QObject): + """ + Simple ray tracer. + + Initialize with a list of rays and optics; + calling trace() will cause rays to be extended by propagating them through + each optic in sequence. + """ + def __init__(self, rays, optics): + QtCore.QObject.__init__(self) + self.optics = optics + self.rays = rays + for o in self.optics: + o.sigStateChanged.connect(self.trace) + self.trace() + + def trace(self): + trace(self.rays, self.optics) + diff --git a/examples/optics/schott_glasses.csv.gz b/examples/optics/schott_glasses.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..8df4ae14a38b916d5591749d049804097f8e86b8 GIT binary patch literal 37232 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 4*np.pi: + m1['angle'] = 315 + 1.5*np.sin(phase) + m1a['angle'] = 315 + 1.5*np.sin(phase) + else: + m2['angle'] = 135 + 1.5*np.sin(phase) + m2a['angle'] = 135 + 1.5*np.sin(phase) + phase += 0.2 + +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(40) + + + + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/relativity b/examples/relativity new file mode 160000 index 00000000..876a0a80 --- /dev/null +++ b/examples/relativity @@ -0,0 +1 @@ +Subproject commit 876a0a80b705dad71e5b1addab9b859cfc292f20 diff --git a/examples/relativity_demo.py b/examples/relativity_demo.py new file mode 100644 index 00000000..24a1f476 --- /dev/null +++ b/examples/relativity_demo.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +""" +Special relativity simulation + + + +""" +import initExample ## Add path to library (just for examples; you do not need this) +import pyqtgraph as pg +from relativity import RelativityGUI + +pg.mkQApp() +win = RelativityGUI() +win.setWindowTitle("Relativity!") +win.resize(1100,700) +win.show() +win.loadPreset(None, 'Twin Paradox (grid)') + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(pg.QtCore, 'PYQT_VERSION'): + pg.QtGui.QApplication.instance().exec_() diff --git a/examples/verlet_chain/__init__.py b/examples/verlet_chain/__init__.py new file mode 100644 index 00000000..abd9e103 --- /dev/null +++ b/examples/verlet_chain/__init__.py @@ -0,0 +1 @@ +from chain import ChainSim \ No newline at end of file diff --git a/examples/verlet_chain/chain.py b/examples/verlet_chain/chain.py new file mode 100644 index 00000000..9671e0a5 --- /dev/null +++ b/examples/verlet_chain/chain.py @@ -0,0 +1,110 @@ +import pyqtgraph as pg +import numpy as np +import time +from relax import relax + + +class ChainSim(pg.QtCore.QObject): + + stepped = pg.QtCore.Signal() + relaxed = pg.QtCore.Signal() + + def __init__(self): + pg.QtCore.QObject.__init__(self) + + self.damping = 0.1 # 0=full damping, 1=no damping + self.relaxPerStep = 10 + self.maxTimeStep = 0.01 + + self.pos = None # (Npts, 2) float + self.mass = None # (Npts) float + self.fixed = None # (Npts) bool + self.links = None # (Nlinks, 2), uint + self.lengths = None # (Nlinks), float + self.push = None # (Nlinks), bool + self.pull = None # (Nlinks), bool + + self.initialized = False + self.lasttime = None + self.lastpos = None + + def init(self): + if self.initialized: + return + + assert None not in [self.pos, self.mass, self.links, self.lengths] + + if self.fixed is None: + self.fixed = np.zeros(self.pos.shape[0], dtype=bool) + if self.push is None: + self.push = np.ones(self.links.shape[0], dtype=bool) + if self.pull is None: + self.pull = np.ones(self.links.shape[0], dtype=bool) + + + # precompute relative masses across links + l1 = self.links[:,0] + l2 = self.links[:,1] + m1 = self.mass[l1] + m2 = self.mass[l2] + self.mrel1 = (m1 / (m1+m2))[:,np.newaxis] + self.mrel1[self.fixed[l1]] = 1 # fixed point constraint + self.mrel1[self.fixed[l2]] = 0 + self.mrel2 = 1.0 - self.mrel1 + + for i in range(100): + self.relax(n=10) + + self.initialized = True + + def makeGraph(self): + #g1 = pg.GraphItem(pos=self.pos, adj=self.links[self.rope], pen=0.2, symbol=None) + brushes = np.where(self.fixed, pg.mkBrush(0,0,0,255), pg.mkBrush(50,50,200,255)) + g2 = pg.GraphItem(pos=self.pos, adj=self.links[self.push & self.pull], pen=0.5, brush=brushes, symbol='o', size=(self.mass**0.33), pxMode=False) + p = pg.ItemGroup() + #p.addItem(g1) + p.addItem(g2) + return p + + def update(self): + # approximate physics with verlet integration + + now = pg.ptime.time() + if self.lasttime is None: + dt = 0 + else: + dt = now - self.lasttime + self.lasttime = now + + if self.lastpos is None: + self.lastpos = self.pos + + # remember fixed positions + fixedpos = self.pos[self.fixed] + + while dt > 0: + dt1 = min(self.maxTimeStep, dt) + dt -= dt1 + + # compute motion since last timestep + dx = self.pos - self.lastpos + self.lastpos = self.pos + + # update positions for gravity and inertia + acc = np.array([[0, -5]]) * dt1 + inertia = dx * (self.damping**(dt1/self.mass))[:,np.newaxis] # with mass-dependent damping + self.pos = self.pos + inertia + acc + + self.pos[self.fixed] = fixedpos # fixed point constraint + + # correct for link constraints + self.relax(self.relaxPerStep) + self.stepped.emit() + + + def relax(self, n=50): + # speed up with C magic + relax(self.pos, self.links, self.mrel1, self.mrel2, self.lengths, self.push, self.pull, n) + self.relaxed.emit() + + diff --git a/examples/verlet_chain/make b/examples/verlet_chain/make new file mode 100755 index 00000000..78503f2a --- /dev/null +++ b/examples/verlet_chain/make @@ -0,0 +1,3 @@ +gcc -fPIC -c relax.c +gcc -shared -o maths.so relax.o + diff --git a/examples/verlet_chain/maths.so b/examples/verlet_chain/maths.so new file mode 100755 index 0000000000000000000000000000000000000000..62aff3214ec02e8d87bef57c290d33d7efc7f472 GIT binary patch literal 8017 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 +#include + +void relax( + double* pos, + long* links, + double* mrel1, + double* mrel2, + double* lengths, + char* push, + char* pull, + int nlinks, + int iters) + { + int i, l, p1, p2; + double x1, x2, y1, y2, dx, dy, dist, change; +// printf("%d, %d\n", iters, nlinks); + for( i=0; i lengths[l] ) + dist = lengths[l]; + + change = (lengths[l]-dist) / dist; + dx *= change; + dy *= change; + + pos[p1] -= mrel2[l] * dx; + pos[p1+1] -= mrel2[l] * dy; + pos[p2] += mrel1[l] * dx; + pos[p2+1] += mrel1[l] * dy; + } + } +} \ No newline at end of file diff --git a/examples/verlet_chain/relax.py b/examples/verlet_chain/relax.py new file mode 100644 index 00000000..95a2b7b3 --- /dev/null +++ b/examples/verlet_chain/relax.py @@ -0,0 +1,23 @@ +import ctypes +import os + +so = os.path.join(os.path.dirname(__file__), 'maths.so') +lib = ctypes.CDLL(so) + +lib.relax.argtypes = [ + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_int, + ctypes.c_int, + ] + +def relax(pos, links, mrel1, mrel2, lengths, push, pull, iters): + nlinks = links.shape[0] + lib.relax(pos.ctypes, links.ctypes, mrel1.ctypes, mrel2.ctypes, lengths.ctypes, push.ctypes, pull.ctypes, nlinks, iters) + + diff --git a/examples/verlet_chain_demo.py b/examples/verlet_chain_demo.py new file mode 100644 index 00000000..6ed97d48 --- /dev/null +++ b/examples/verlet_chain_demo.py @@ -0,0 +1,111 @@ +""" +Mechanical simulation of a chain using verlet integration. + + + +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +from verlet_chain import ChainSim + +sim = ChainSim() + + +chlen1 = 80 +chlen2 = 60 +npts = chlen1 + chlen2 + +sim.mass = np.ones(npts) +sim.mass[chlen1-15] = 100 +sim.mass[chlen1-1] = 500 +sim.mass[npts-1] = 200 + +sim.fixed = np.zeros(npts, dtype=bool) +sim.fixed[0] = True +sim.fixed[chlen1] = True + +sim.pos = np.empty((npts, 2)) +sim.pos[:chlen1, 0] = 0 +sim.pos[chlen1:, 0] = 10 +sim.pos[:chlen1, 1] = np.arange(chlen1) +sim.pos[chlen1:, 1] = np.arange(chlen2) + +links1 = [(j, i+j+1) for i in range(chlen1) for j in range(chlen1-i-1)] +links2 = [(j, i+j+1) for i in range(chlen2) for j in range(chlen2-i-1)] +sim.links = np.concatenate([np.array(links1), np.array(links2)+chlen1, np.array([[chlen1-1, npts-1]])]) + +p1 = sim.pos[sim.links[:,0]] +p2 = sim.pos[sim.links[:,1]] +dif = p2-p1 +sim.lengths = (dif**2).sum(axis=1) ** 0.5 +sim.lengths[(chlen1-1):len(links1)] *= 1.05 # let auxiliary links stretch a little +sim.lengths[(len(links1)+chlen2-1):] *= 1.05 +sim.lengths[-1] = 7 + +push1 = np.ones(len(links1), dtype=bool) +push1[chlen1:] = False +push2 = np.ones(len(links2), dtype=bool) +push2[chlen2:] = False +sim.push = np.concatenate([push1, push2, np.array([True], dtype=bool)]) + +sim.pull = np.ones(sim.links.shape[0], dtype=bool) +sim.pull[-1] = False + +mousepos = sim.pos[0] + + +def display(): + global view, sim + view.clear() + view.addItem(sim.makeGraph()) + +def relaxed(): + global app + display() + app.processEvents() + +def mouse(pos): + global mousepos + pos = view.mapSceneToView(pos) + mousepos = np.array([pos.x(), pos.y()]) + +def update(): + global mousepos + #sim.pos[0] = sim.pos[0] * 0.9 + mousepos * 0.1 + s = 0.9 + sim.pos[0] = sim.pos[0] * s + mousepos * (1.0-s) + sim.update() + +app = pg.mkQApp() +win = pg.GraphicsLayoutWidget() +win.show() +view = win.addViewBox() +view.setAspectLocked(True) +view.setXRange(-100, 100) +#view.autoRange() + +view.scene().sigMouseMoved.connect(mouse) + +#display() +#app.processEvents() + +sim.relaxed.connect(relaxed) +sim.init() +sim.relaxed.disconnect(relaxed) + +sim.stepped.connect(display) + +timer = pg.QtCore.QTimer() +timer.timeout.connect(update) +timer.start(16) + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() From 6e9d5c3cfb4cdc1ef14c6f87a7ff06670cbb4c8f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 22 May 2014 01:30:15 -0400 Subject: [PATCH 33/50] Python 3 fixes for new demos --- examples/optics/__init__.py | 2 +- examples/optics/pyoptic.py | 6 +++--- examples/verlet_chain/__init__.py | 2 +- examples/verlet_chain/chain.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/optics/__init__.py b/examples/optics/__init__.py index 577c24da..b3d31cd0 100644 --- a/examples/optics/__init__.py +++ b/examples/optics/__init__.py @@ -1 +1 @@ -from pyoptic import * \ No newline at end of file +from .pyoptic import * \ No newline at end of file diff --git a/examples/optics/pyoptic.py b/examples/optics/pyoptic.py index 486f653d..dc493568 100644 --- a/examples/optics/pyoptic.py +++ b/examples/optics/pyoptic.py @@ -14,7 +14,7 @@ class GlassDB: def __init__(self, fileName='schott_glasses.csv'): path = os.path.dirname(__file__) fh = gzip.open(os.path.join(path, 'schott_glasses.csv.gz'), 'rb') - r = csv.reader(fh.readlines()) + r = csv.reader(map(str, fh.readlines())) lines = [x for x in r] self.data = {} header = lines[0] @@ -47,8 +47,8 @@ class GlassDB: info = self.data[glass] cache = info['ior_cache'] if wl not in cache: - B = map(float, [info['B1'], info['B2'], info['B3']]) - C = map(float, [info['C1'], info['C2'], info['C3']]) + B = list(map(float, [info['B1'], info['B2'], info['B3']])) + C = list(map(float, [info['C1'], info['C2'], info['C3']])) w2 = (wl/1000.)**2 n = np.sqrt(1.0 + (B[0]*w2 / (w2-C[0])) + (B[1]*w2 / (w2-C[1])) + (B[2]*w2 / (w2-C[2]))) cache[wl] = n diff --git a/examples/verlet_chain/__init__.py b/examples/verlet_chain/__init__.py index abd9e103..f473190f 100644 --- a/examples/verlet_chain/__init__.py +++ b/examples/verlet_chain/__init__.py @@ -1 +1 @@ -from chain import ChainSim \ No newline at end of file +from .chain import ChainSim \ No newline at end of file diff --git a/examples/verlet_chain/chain.py b/examples/verlet_chain/chain.py index 9671e0a5..896505ac 100644 --- a/examples/verlet_chain/chain.py +++ b/examples/verlet_chain/chain.py @@ -1,7 +1,7 @@ import pyqtgraph as pg import numpy as np import time -from relax import relax +from .relax import relax class ChainSim(pg.QtCore.QObject): From 374b5a33ed1167dc949bced2fdf74caeef823a41 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 22 May 2014 01:51:17 -0400 Subject: [PATCH 34/50] Added dialog to hdf5 example prompting user to generate sample data --- examples/hdf5.py | 49 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/examples/hdf5.py b/examples/hdf5.py index 3e239d9f..b43ae24a 100644 --- a/examples/hdf5.py +++ b/examples/hdf5.py @@ -18,14 +18,10 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np import h5py - import sys, os -if len(sys.argv) > 1: - fileName = sys.argv[1] -else: - fileName = 'test.hdf5' - if not os.path.isfile(fileName): - raise Exception("No suitable HDF5 file found. Use createFile() to generate an example file.") + +pg.mkQApp() + plt = pg.plot() plt.setWindowTitle('pyqtgraph example: HDF5 big data') @@ -101,10 +97,6 @@ class HDF5Plot(pg.PlotCurveItem): self.scale(scale, 1) # scale to match downsampling -f = h5py.File(fileName, 'r') -curve = HDF5Plot() -curve.setHDF5(f['data']) -plt.addItem(curve) def createFile(finalSize=2000000000): @@ -118,17 +110,42 @@ def createFile(finalSize=2000000000): f.create_dataset('data', data=chunk, chunks=True, maxshape=(None,)) data = f['data'] - for i in range(finalSize // (chunk.size * chunk.itemsize)): - newshape = [data.shape[0] + chunk.shape[0]] - data.resize(newshape) - data[-chunk.shape[0]:] = chunk - + nChunks = finalSize // (chunk.size * chunk.itemsize) + with pg.ProgressDialog("Generating test.hdf5...", 0, nChunks) as dlg: + for i in range(nChunks): + newshape = [data.shape[0] + chunk.shape[0]] + data.resize(newshape) + data[-chunk.shape[0]:] = chunk + dlg += 1 + if dlg.wasCanceled(): + f.close() + os.remove('test.hdf5') + sys.exit() + dlg += 1 f.close() +if len(sys.argv) > 1: + fileName = sys.argv[1] +else: + fileName = 'test.hdf5' + if not os.path.isfile(fileName): + size, ok = QtGui.QInputDialog.getDouble(None, "Create HDF5 Dataset?", "This demo requires a large HDF5 array. To generate a file, enter the array size (in GB) and press OK.", 2.0) + if not ok: + sys.exit(0) + else: + createFile(int(size*1e9)) + #raise Exception("No suitable HDF5 file found. Use createFile() to generate an example file.") + +f = h5py.File(fileName, 'r') +curve = HDF5Plot() +curve.setHDF5(f['data']) +plt.addItem(curve) ## Start Qt event loop unless running in interactive mode or using pyside. if __name__ == '__main__': + + import sys if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): QtGui.QApplication.instance().exec_() From d004b133cdead10f3b0653e453beb734ca56ec49 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 27 May 2014 18:46:34 -0400 Subject: [PATCH 35/50] Removed unnecessary file allocation from functions.interpolateArray --- pyqtgraph/functions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 2325186c..77643c99 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -538,7 +538,6 @@ def interpolateArray(data, x, default=0.0): prof = debug.Profiler() - result = np.empty(x.shape[:-1] + data.shape, dtype=data.dtype) nd = data.ndim md = x.shape[-1] From 35856ccaee679ace59572774b7c814bae696d144 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 6 Jun 2014 15:53:17 -0600 Subject: [PATCH 36/50] Added cx_freeze example (thanks Jerry!) --- examples/cx_freeze/plotTest.py | 20 +++++++++++++++++++ examples/cx_freeze/setup.py | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 examples/cx_freeze/plotTest.py create mode 100644 examples/cx_freeze/setup.py diff --git a/examples/cx_freeze/plotTest.py b/examples/cx_freeze/plotTest.py new file mode 100644 index 00000000..1a53a984 --- /dev/null +++ b/examples/cx_freeze/plotTest.py @@ -0,0 +1,20 @@ +import sys +from PyQt4 import QtGui +import pyqtgraph as pg +from pyqtgraph.graphicsItems import TextItem +# For packages that require scipy, these may be needed: +# from scipy.stats import futil +# from scipy.sparse.csgraph import _validation + +from pyqtgraph import setConfigOption +pg.setConfigOption('background','w') +pg.setConfigOption('foreground','k') +app = QtGui.QApplication(sys.argv) + +pw = pg.plot(x = [0, 1, 2, 4], y = [4, 5, 9, 6]) +pw.showGrid(x=True,y=True) +text = pg.TextItem(html='
%s
' % "here",anchor=(0.0, 0.0)) +text.setPos(1.0, 5.0) +pw.addItem(text) +status = app.exec_() +sys.exit(status) diff --git a/examples/cx_freeze/setup.py b/examples/cx_freeze/setup.py new file mode 100644 index 00000000..bdace733 --- /dev/null +++ b/examples/cx_freeze/setup.py @@ -0,0 +1,36 @@ +# Build with `python setup.py build_exe` +from cx_Freeze import setup, Executable + +import shutil +from glob import glob +# Remove the build folder +shutil.rmtree("build", ignore_errors=True) +shutil.rmtree("dist", ignore_errors=True) +import sys + +includes = ['PyQt4.QtCore', 'PyQt4.QtGui', 'sip', 'pyqtgraph.graphicsItems', + 'numpy', 'atexit'] +excludes = ['cvxopt','_gtkagg', '_tkagg', 'bsddb', 'curses', 'email', 'pywin.debugger', + 'pywin.debugger.dbgcon', 'pywin.dialogs', 'tcl','tables', + 'Tkconstants', 'Tkinter', 'zmq','PySide','pysideuic','scipy','matplotlib'] + +if sys.version[0] == '2': + # causes syntax error on py2 + excludes.append('PyQt4.uic.port_v3') + +base = None +if sys.platform == "win32": + base = "Win32GUI" + +build_exe_options = {'excludes': excludes, + 'includes':includes, 'include_msvcr':True, + 'compressed':True, 'copy_dependent_files':True, 'create_shared_zip':True, + 'include_in_shared_zip':True, 'optimize':2} + +setup(name = "cx_freeze plot test", + version = "0.1", + description = "cx_freeze plot test", + options = {"build_exe": build_exe_options}, + executables = [Executable("plotTest.py", base=base)]) + + From 04f1b0e6776d9c6c1d72749b903875432ed5ec66 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 13 Jun 2014 14:19:10 -0600 Subject: [PATCH 37/50] Added scrolling plot examples --- examples/__main__.py | 1 + examples/scrollingPlots.py | 118 ++++++++++++++++++++++++ pyqtgraph/graphicsItems/PlotDataItem.py | 2 +- 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 examples/scrollingPlots.py diff --git a/examples/__main__.py b/examples/__main__.py index 6c2e8b97..ea948bf4 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -31,6 +31,7 @@ examples = OrderedDict([ ('Histograms', 'histogram.py'), ('Auto-range', 'PlotAutoRange.py'), ('Remote Plotting', 'RemoteSpeedTest.py'), + ('Scrolling plots', 'scrollingPlots.py'), ('HDF5 big data', 'hdf5.py'), ('Demos', OrderedDict([ ('Optics', 'optics_demos.py'), diff --git a/examples/scrollingPlots.py b/examples/scrollingPlots.py new file mode 100644 index 00000000..623b9ab1 --- /dev/null +++ b/examples/scrollingPlots.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +""" +Various methods of drawing scrolling plots. +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +win = pg.GraphicsWindow() +win.setWindowTitle('pyqtgraph example: Scrolling Plots') + + +# 1) Simplest approach -- update data in the array such that plot appears to scroll +# In these examples, the array size is fixed. +p1 = win.addPlot() +p2 = win.addPlot() +data1 = np.random.normal(size=300) +curve1 = p1.plot(data1) +curve2 = p2.plot(data1) +ptr1 = 0 +def update1(): + global data1, curve1, ptr1 + data1[:-1] = data1[1:] # shift data in the array one sample left + # (see also: np.roll) + data1[-1] = np.random.normal() + curve1.setData(data1) + + ptr1 += 1 + curve2.setData(data1) + curve2.setPos(ptr1, 0) + + +# 2) Allow data to accumulate. In these examples, the array doubles in length +# whenever it is full. +win.nextRow() +p3 = win.addPlot() +p4 = win.addPlot() +# Use automatic downsampling and clipping to reduce the drawing load +p3.setDownsampling(mode='peak') +p4.setDownsampling(mode='peak') +p3.setClipToView(True) +p4.setClipToView(True) +p3.setRange(xRange=[-100, 0]) +p3.setLimits(xMax=0) +curve3 = p3.plot() +curve4 = p4.plot() + +data3 = np.empty(100) +ptr3 = 0 + +def update2(): + global data3, ptr3 + data3[ptr3] = np.random.normal() + ptr3 += 1 + if ptr3 >= data3.shape[0]: + tmp = data3 + data3 = np.empty(data3.shape[0] * 2) + data3[:tmp.shape[0]] = tmp + curve3.setData(data3[:ptr3]) + curve3.setPos(-ptr3, 0) + curve4.setData(data3[:ptr3]) + + +# 3) Plot in chunks, adding one new plot curve for every 100 samples +chunkSize = 100 +# Remove chunks after we have 10 +maxChunks = 10 +startTime = pg.ptime.time() +win.nextRow() +p5 = win.addPlot(colspan=2) +p5.setLabel('bottom', 'Time', 's') +p5.setXRange(-10, 0) +curves = [] +data5 = np.empty((chunkSize+1,2)) +ptr5 = 0 + +def update3(): + global p5, data5, ptr5, curves + now = pg.ptime.time() + for c in curves: + c.setPos(-(now-startTime), 0) + + i = ptr5 % chunkSize + if i == 0: + curve = p5.plot() + curves.append(curve) + last = data5[-1] + data5 = np.empty((chunkSize+1,2)) + data5[0] = last + while len(curves) > maxChunks: + c = curves.pop(0) + p5.removeItem(c) + else: + curve = curves[-1] + data5[i+1,0] = now - startTime + data5[i+1,1] = np.random.normal() + curve.setData(x=data5[:i+2, 0], y=data5[:i+2, 1]) + ptr5 += 1 + + +# update all plots +def update(): + update1() + update2() + update3() +timer = pg.QtCore.QTimer() +timer.timeout.connect(update) +timer.start(50) + + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 3e760ce1..befc5783 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -546,7 +546,7 @@ class PlotDataItem(GraphicsObject): if view is None or not view.autoRangeEnabled()[0]: # this option presumes that x-values have uniform spacing range = self.viewRect() - if range is not None: + if range is not None and len(x) > 1: 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) From ba4f4e51058c6af1554e8a5bfc5bd15a045ae4d5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 13 Jun 2014 18:02:39 -0600 Subject: [PATCH 38/50] Added image analysis example --- examples/__main__.py | 1 + examples/imageAnalysis.py | 98 +++++++++++++++++++++++++ pyqtgraph/graphicsItems/IsocurveItem.py | 6 +- 3 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 examples/imageAnalysis.py diff --git a/examples/__main__.py b/examples/__main__.py index ea948bf4..cb1b87a1 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -26,6 +26,7 @@ examples = OrderedDict([ ('Crosshair / Mouse interaction', 'crosshair.py'), ('Data Slicing', 'DataSlicing.py'), ('Plot Customization', 'customPlot.py'), + ('Image Analysis', 'imageAnalysis.py'), ('Dock widgets', 'dockarea.py'), ('Console', 'ConsoleWidget.py'), ('Histograms', 'histogram.py'), diff --git a/examples/imageAnalysis.py b/examples/imageAnalysis.py new file mode 100644 index 00000000..8283144e --- /dev/null +++ b/examples/imageAnalysis.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +""" +Demonstrates common image analysis tools. + +Many of the features demonstrated here are already provided by the ImageView +widget, but here we present a lower-level approach that provides finer control +over the user interface. +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +pg.mkQApp() + +win = pg.GraphicsLayoutWidget() +win.setWindowTitle('pyqtgraph example: Image Analysis') + +# A plot area (ViewBox + axes) for displaying the image +p1 = win.addPlot() + +# Item for displaying image data +img = pg.ImageItem() +p1.addItem(img) + +# Custom ROI for selecting an image region +roi = pg.ROI([-8, 14], [6, 5]) +roi.addScaleHandle([0.5, 1], [0.5, 0.5]) +roi.addScaleHandle([0, 0.5], [0.5, 0.5]) +p1.addItem(roi) +roi.setZValue(10) # make sure ROI is drawn above image + +# Isocurve drawing +iso = pg.IsocurveItem(level=0.8, pen='g') +iso.setParentItem(img) +iso.setZValue(5) + +# Contrast/color control +hist = pg.HistogramLUTItem() +hist.setImageItem(img) +win.addItem(hist) + +# Draggable line for setting isocurve level +isoLine = pg.InfiniteLine(angle=0, movable=True, pen='g') +hist.vb.addItem(isoLine) +hist.vb.setMouseEnabled(y=False) # makes user interaction a little easier +isoLine.setValue(0.8) +isoLine.setZValue(1000) # bring iso line above contrast controls + +# Another plot area for displaying ROI data +win.nextRow() +p2 = win.addPlot(colspan=2) +p2.setMaximumHeight(250) +win.resize(800, 800) +win.show() + + +# Generate image data +data = np.random.normal(size=(100, 200)) +data[20:80, 20:80] += 2. +data = pg.gaussianFilter(data, (3, 3)) +data += np.random.normal(size=(100, 200)) * 0.1 +img.setImage(data) +hist.setLevels(data.min(), data.max()) + +# build isocurves from smoothed data +iso.setData(pg.gaussianFilter(data, (2, 2))) + +# set position and scale of image +img.scale(0.2, 0.2) +img.translate(-50, 0) + +# zoom to fit imageo +p1.autoRange() + + +# Callbacks for handling user interaction +def updatePlot(): + global img, roi, data, p2 + selected = roi.getArrayRegion(data, img) + p2.plot(selected.mean(axis=1), clear=True) + +roi.sigRegionChanged.connect(updatePlot) +updatePlot() + +def updateIsocurve(): + global isoLine, iso + iso.setLevel(isoLine.value()) + +isoLine.sigDragged.connect(updateIsocurve) + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/pyqtgraph/graphicsItems/IsocurveItem.py b/pyqtgraph/graphicsItems/IsocurveItem.py index 897df999..4474e29a 100644 --- a/pyqtgraph/graphicsItems/IsocurveItem.py +++ b/pyqtgraph/graphicsItems/IsocurveItem.py @@ -35,11 +35,6 @@ class IsocurveItem(GraphicsObject): self.setPen(pen) self.setData(data, level) - - - #if data is not None and level is not None: - #self.updateLines(data, level) - def setData(self, data, level=None): """ @@ -65,6 +60,7 @@ class IsocurveItem(GraphicsObject): """Set the level at which the isocurve is drawn.""" self.level = level self.path = None + self.prepareGeometryChange() self.update() From 274c76559415a1786742a3c4c950968a2bc2ea13 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 14 Jun 2014 11:35:00 -0600 Subject: [PATCH 39/50] Fixed relativity example --- examples/relativity | 1 - examples/relativity/__init__.py | 1 + .../relativity/presets/Grid Expansion.cfg | 411 ++++++++++ .../presets/Twin Paradox (grid).cfg | 667 +++++++++++++++ examples/relativity/presets/Twin Paradox.cfg | 538 ++++++++++++ examples/relativity/relativity.py | 773 ++++++++++++++++++ 6 files changed, 2390 insertions(+), 1 deletion(-) delete mode 160000 examples/relativity create mode 100644 examples/relativity/__init__.py create mode 100644 examples/relativity/presets/Grid Expansion.cfg create mode 100644 examples/relativity/presets/Twin Paradox (grid).cfg create mode 100644 examples/relativity/presets/Twin Paradox.cfg create mode 100644 examples/relativity/relativity.py diff --git a/examples/relativity b/examples/relativity deleted file mode 160000 index 876a0a80..00000000 --- a/examples/relativity +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 876a0a80b705dad71e5b1addab9b859cfc292f20 diff --git a/examples/relativity/__init__.py b/examples/relativity/__init__.py new file mode 100644 index 00000000..093806ef --- /dev/null +++ b/examples/relativity/__init__.py @@ -0,0 +1 @@ +from relativity import * diff --git a/examples/relativity/presets/Grid Expansion.cfg b/examples/relativity/presets/Grid Expansion.cfg new file mode 100644 index 00000000..0ab77795 --- /dev/null +++ b/examples/relativity/presets/Grid Expansion.cfg @@ -0,0 +1,411 @@ +name: 'params' +strictNaming: False +default: None +renamable: False +enabled: True +value: None +visible: True +readonly: False +removable: False +type: 'group' +children: + Load Preset..: + name: 'Load Preset..' + limits: ['', 'Twin Paradox (grid)', 'Twin Paradox'] + strictNaming: False + default: None + renamable: False + enabled: True + value: 'Twin Paradox (grid)' + visible: True + readonly: False + values: [] + removable: False + type: 'list' + children: + Duration: + name: 'Duration' + limits: [0.1, None] + strictNaming: False + default: 10.0 + renamable: False + enabled: True + readonly: False + value: 20.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Reference Frame: + name: 'Reference Frame' + limits: ['Grid00', 'Grid01', 'Grid02', 'Grid03', 'Grid04'] + strictNaming: False + default: None + renamable: False + enabled: True + value: 'Grid02' + visible: True + readonly: False + values: [] + removable: False + type: 'list' + children: + Animate: + name: 'Animate' + strictNaming: False + default: True + renamable: False + enabled: True + value: True + visible: True + readonly: False + removable: False + type: 'bool' + children: + Animation Speed: + name: 'Animation Speed' + limits: [0.0001, None] + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + dec: True + type: 'float' + children: + Recalculate Worldlines: + name: 'Recalculate Worldlines' + strictNaming: False + default: None + renamable: False + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'action' + children: + Save: + name: 'Save' + strictNaming: False + default: None + renamable: False + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'action' + children: + Load: + name: 'Load' + strictNaming: False + default: None + renamable: False + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'action' + children: + Objects: + name: 'Objects' + strictNaming: False + default: None + renamable: False + addText: 'Add New..' + enabled: True + value: None + visible: True + readonly: False + removable: False + type: None + children: + Grid: + name: 'Grid' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: 'Grid' + autoIncrementName: True + children: + Number of Clocks: + name: 'Number of Clocks' + limits: [1, None] + strictNaming: False + default: 5 + renamable: False + enabled: True + value: 5 + visible: True + readonly: False + removable: False + type: 'int' + children: + Spacing: + name: 'Spacing' + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + ClockTemplate: + name: 'ClockTemplate' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: 'Clock' + autoIncrementName: True + children: + Initial Position: + name: 'Initial Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: -2.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: None + renamable: False + addText: 'Add Command..' + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'AccelerationGroup' + children: + Command: + name: 'Command' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + value: 1.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.5 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command2: + name: 'Command2' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 2.0 + renamable: False + enabled: True + value: 3.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command3: + name: 'Command3' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 4.0 + renamable: False + enabled: True + value: 11.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: -0.5 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command4: + name: 'Command4' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 8.0 + renamable: False + enabled: True + value: 13.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Rest Mass: + name: 'Rest Mass' + limits: [1e-09, None] + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Color: + name: 'Color' + strictNaming: False + default: (100, 100, 150) + renamable: False + enabled: True + value: (100, 100, 150, 255) + visible: True + readonly: False + removable: False + type: 'color' + children: + Size: + name: 'Size' + strictNaming: False + default: 0.5 + renamable: False + enabled: True + value: 0.5 + visible: True + readonly: False + removable: False + type: 'float' + children: + Vertical Position: + name: 'Vertical Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + addList: ['Clock', 'Grid'] diff --git a/examples/relativity/presets/Twin Paradox (grid).cfg b/examples/relativity/presets/Twin Paradox (grid).cfg new file mode 100644 index 00000000..ebe366bf --- /dev/null +++ b/examples/relativity/presets/Twin Paradox (grid).cfg @@ -0,0 +1,667 @@ +name: 'params' +strictNaming: False +default: None +renamable: False +enabled: True +value: None +visible: True +readonly: False +removable: False +type: 'group' +children: + Load Preset..: + name: 'Load Preset..' + limits: ['', 'Twin Paradox (grid)', 'Twin Paradox'] + strictNaming: False + default: None + renamable: False + enabled: True + value: 'Twin Paradox (grid)' + visible: True + readonly: False + values: [] + removable: False + type: 'list' + children: + Duration: + name: 'Duration' + limits: [0.1, None] + strictNaming: False + default: 10.0 + renamable: False + enabled: True + readonly: False + value: 27.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Reference Frame: + name: 'Reference Frame' + limits: ['Grid00', 'Grid01', 'Grid02', 'Grid03', 'Grid04', 'Grid05', 'Grid06', 'Grid07', 'Grid08', 'Grid09', 'Grid10', 'Alice', 'Bob'] + strictNaming: False + default: None + renamable: False + enabled: True + value: 'Alice' + visible: True + readonly: False + values: [] + removable: False + type: 'list' + children: + Animate: + name: 'Animate' + strictNaming: False + default: True + renamable: False + enabled: True + value: True + visible: True + readonly: False + removable: False + type: 'bool' + children: + Animation Speed: + name: 'Animation Speed' + limits: [0.0001, None] + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + dec: True + type: 'float' + children: + Recalculate Worldlines: + name: 'Recalculate Worldlines' + strictNaming: False + default: None + renamable: False + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'action' + children: + Save: + name: 'Save' + strictNaming: False + default: None + renamable: False + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'action' + children: + Load: + name: 'Load' + strictNaming: False + default: None + renamable: False + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'action' + children: + Objects: + name: 'Objects' + strictNaming: False + default: None + renamable: False + addText: 'Add New..' + enabled: True + value: None + visible: True + readonly: False + removable: False + type: None + children: + Grid: + name: 'Grid' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: 'Grid' + autoIncrementName: True + children: + Number of Clocks: + name: 'Number of Clocks' + limits: [1, None] + strictNaming: False + default: 5 + renamable: False + enabled: True + value: 11 + visible: True + readonly: False + removable: False + type: 'int' + children: + Spacing: + name: 'Spacing' + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 2.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + ClockTemplate: + name: 'ClockTemplate' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: 'Clock' + autoIncrementName: True + children: + Initial Position: + name: 'Initial Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: -10.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: None + renamable: False + addText: 'Add Command..' + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'AccelerationGroup' + children: + Rest Mass: + name: 'Rest Mass' + limits: [1e-09, None] + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Color: + name: 'Color' + strictNaming: False + default: (100, 100, 150) + renamable: False + enabled: True + value: (77, 77, 77, 255) + visible: True + readonly: False + removable: False + type: 'color' + children: + Size: + name: 'Size' + strictNaming: False + default: 0.5 + renamable: False + enabled: True + value: 1.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Vertical Position: + name: 'Vertical Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: -2.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Alice: + name: 'Alice' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: 'Clock' + autoIncrementName: True + children: + Initial Position: + name: 'Initial Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: None + renamable: False + addText: 'Add Command..' + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'AccelerationGroup' + children: + Command: + name: 'Command' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + value: 1.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.5 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command2: + name: 'Command2' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 2.0 + renamable: False + enabled: True + value: 3.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command3: + name: 'Command3' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 3.0 + renamable: False + enabled: True + value: 8.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: -0.5 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command4: + name: 'Command4' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 4.0 + renamable: False + enabled: True + value: 12.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command5: + name: 'Command5' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 6.0 + renamable: False + enabled: True + value: 17.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.5 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command6: + name: 'Command6' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 7.0 + renamable: False + enabled: True + value: 19.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Rest Mass: + name: 'Rest Mass' + limits: [1e-09, None] + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Color: + name: 'Color' + strictNaming: False + default: (100, 100, 150) + renamable: False + enabled: True + value: (82, 123, 44, 255) + visible: True + readonly: False + removable: False + type: 'color' + children: + Size: + name: 'Size' + strictNaming: False + default: 0.5 + renamable: False + enabled: True + value: 1.5 + visible: True + readonly: False + removable: False + type: 'float' + children: + Vertical Position: + name: 'Vertical Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 3.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Bob: + name: 'Bob' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: 'Clock' + autoIncrementName: True + children: + Initial Position: + name: 'Initial Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: None + renamable: False + addText: 'Add Command..' + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'AccelerationGroup' + children: + Rest Mass: + name: 'Rest Mass' + limits: [1e-09, None] + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Color: + name: 'Color' + strictNaming: False + default: (100, 100, 150) + renamable: False + enabled: True + value: (69, 69, 126, 255) + visible: True + readonly: False + removable: False + type: 'color' + children: + Size: + name: 'Size' + strictNaming: False + default: 0.5 + renamable: False + enabled: True + value: 1.5 + visible: True + readonly: False + removable: False + type: 'float' + children: + Vertical Position: + name: 'Vertical Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + addList: ['Clock', 'Grid'] diff --git a/examples/relativity/presets/Twin Paradox.cfg b/examples/relativity/presets/Twin Paradox.cfg new file mode 100644 index 00000000..569c3a04 --- /dev/null +++ b/examples/relativity/presets/Twin Paradox.cfg @@ -0,0 +1,538 @@ +name: 'params' +strictNaming: False +default: None +renamable: False +enabled: True +value: None +visible: True +readonly: False +removable: False +type: 'group' +children: + Load Preset..: + name: 'Load Preset..' + limits: ['', 'Twin Paradox', 'test'] + strictNaming: False + default: None + renamable: False + enabled: True + value: 'Twin Paradox' + visible: True + readonly: False + values: [] + removable: False + type: 'list' + children: + Duration: + name: 'Duration' + limits: [0.1, None] + strictNaming: False + default: 10.0 + renamable: False + enabled: True + readonly: False + value: 27.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Reference Frame: + name: 'Reference Frame' + limits: ['Alice', 'Bob'] + strictNaming: False + default: None + renamable: False + enabled: True + value: 'Alice' + visible: True + readonly: False + values: [] + removable: False + type: 'list' + children: + Animate: + name: 'Animate' + strictNaming: False + default: True + renamable: False + enabled: True + value: True + visible: True + readonly: False + removable: False + type: 'bool' + children: + Animation Speed: + name: 'Animation Speed' + limits: [0.0001, None] + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + dec: True + type: 'float' + children: + Recalculate Worldlines: + name: 'Recalculate Worldlines' + strictNaming: False + default: None + renamable: False + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'action' + children: + Save: + name: 'Save' + strictNaming: False + default: None + renamable: False + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'action' + children: + Load: + name: 'Load' + strictNaming: False + default: None + renamable: False + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'action' + children: + Objects: + name: 'Objects' + strictNaming: False + default: None + renamable: False + addText: 'Add New..' + enabled: True + value: None + visible: True + readonly: False + removable: False + type: None + children: + Alice: + name: 'Alice' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: 'Clock' + autoIncrementName: True + children: + Initial Position: + name: 'Initial Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: None + renamable: False + addText: 'Add Command..' + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'AccelerationGroup' + children: + Command: + name: 'Command' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + value: 1.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.5 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command2: + name: 'Command2' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 2.0 + renamable: False + enabled: True + value: 3.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command3: + name: 'Command3' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 3.0 + renamable: False + enabled: True + value: 8.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: -0.5 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command4: + name: 'Command4' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 4.0 + renamable: False + enabled: True + value: 12.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command5: + name: 'Command5' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 6.0 + renamable: False + enabled: True + value: 17.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.5 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Command6: + name: 'Command6' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: None + autoIncrementName: True + children: + Proper Time: + name: 'Proper Time' + strictNaming: False + default: 7.0 + renamable: False + enabled: True + value: 19.0 + visible: True + readonly: False + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Rest Mass: + name: 'Rest Mass' + limits: [1e-09, None] + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Color: + name: 'Color' + strictNaming: False + default: (100, 100, 150) + renamable: False + enabled: True + value: (82, 123, 44, 255) + visible: True + readonly: False + removable: False + type: 'color' + children: + Size: + name: 'Size' + strictNaming: False + default: 0.5 + renamable: False + enabled: True + value: 0.5 + visible: True + readonly: False + removable: False + type: 'float' + children: + Vertical Position: + name: 'Vertical Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.5 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Bob: + name: 'Bob' + strictNaming: False + default: None + renamable: True + enabled: True + value: None + visible: True + readonly: False + removable: True + type: 'Clock' + autoIncrementName: True + children: + Initial Position: + name: 'Initial Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Acceleration: + name: 'Acceleration' + strictNaming: False + default: None + renamable: False + addText: 'Add Command..' + enabled: True + value: None + visible: True + readonly: False + removable: False + type: 'AccelerationGroup' + children: + Rest Mass: + name: 'Rest Mass' + limits: [1e-09, None] + strictNaming: False + default: 1.0 + renamable: False + enabled: True + readonly: False + value: 1.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + Color: + name: 'Color' + strictNaming: False + default: (100, 100, 150) + renamable: False + enabled: True + value: (69, 69, 126, 255) + visible: True + readonly: False + removable: False + type: 'color' + children: + Size: + name: 'Size' + strictNaming: False + default: 0.5 + renamable: False + enabled: True + value: 0.5 + visible: True + readonly: False + removable: False + type: 'float' + children: + Vertical Position: + name: 'Vertical Position' + strictNaming: False + default: 0.0 + renamable: False + enabled: True + readonly: False + value: 0.0 + visible: True + step: 0.1 + removable: False + type: 'float' + children: + addList: ['Clock', 'Grid'] diff --git a/examples/relativity/relativity.py b/examples/relativity/relativity.py new file mode 100644 index 00000000..80a56d64 --- /dev/null +++ b/examples/relativity/relativity.py @@ -0,0 +1,773 @@ +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.parametertree import Parameter, ParameterTree +from pyqtgraph.parametertree import types as pTypes +import pyqtgraph.configfile +import numpy as np +import user +import collections +import sys, os + + + +class RelativityGUI(QtGui.QWidget): + def __init__(self): + QtGui.QWidget.__init__(self) + + self.animations = [] + self.animTimer = QtCore.QTimer() + self.animTimer.timeout.connect(self.stepAnimation) + self.animTime = 0 + self.animDt = .016 + self.lastAnimTime = 0 + + self.setupGUI() + + self.objectGroup = ObjectGroupParam() + + self.params = Parameter.create(name='params', type='group', children=[ + dict(name='Load Preset..', type='list', values=[]), + #dict(name='Unit System', type='list', values=['', 'MKS']), + dict(name='Duration', type='float', value=10.0, step=0.1, limits=[0.1, None]), + dict(name='Reference Frame', type='list', values=[]), + dict(name='Animate', type='bool', value=True), + dict(name='Animation Speed', type='float', value=1.0, dec=True, step=0.1, limits=[0.0001, None]), + dict(name='Recalculate Worldlines', type='action'), + dict(name='Save', type='action'), + dict(name='Load', type='action'), + self.objectGroup, + ]) + self.tree.setParameters(self.params, showTop=False) + self.params.param('Recalculate Worldlines').sigActivated.connect(self.recalculate) + self.params.param('Save').sigActivated.connect(self.save) + self.params.param('Load').sigActivated.connect(self.load) + self.params.param('Load Preset..').sigValueChanged.connect(self.loadPreset) + self.params.sigTreeStateChanged.connect(self.treeChanged) + + ## read list of preset configs + presetDir = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'presets') + if os.path.exists(presetDir): + presets = [os.path.splitext(p)[0] for p in os.listdir(presetDir)] + self.params.param('Load Preset..').setLimits(['']+presets) + + + + + def setupGUI(self): + self.layout = QtGui.QVBoxLayout() + self.layout.setContentsMargins(0,0,0,0) + self.setLayout(self.layout) + self.splitter = QtGui.QSplitter() + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.layout.addWidget(self.splitter) + + self.tree = ParameterTree(showHeader=False) + self.splitter.addWidget(self.tree) + + self.splitter2 = QtGui.QSplitter() + self.splitter2.setOrientation(QtCore.Qt.Vertical) + self.splitter.addWidget(self.splitter2) + + self.worldlinePlots = pg.GraphicsLayoutWidget() + self.splitter2.addWidget(self.worldlinePlots) + + self.animationPlots = pg.GraphicsLayoutWidget() + self.splitter2.addWidget(self.animationPlots) + + self.splitter2.setSizes([int(self.height()*0.8), int(self.height()*0.2)]) + + self.inertWorldlinePlot = self.worldlinePlots.addPlot() + self.refWorldlinePlot = self.worldlinePlots.addPlot() + + self.inertAnimationPlot = self.animationPlots.addPlot() + self.inertAnimationPlot.setAspectLocked(1) + self.refAnimationPlot = self.animationPlots.addPlot() + self.refAnimationPlot.setAspectLocked(1) + + self.inertAnimationPlot.setXLink(self.inertWorldlinePlot) + self.refAnimationPlot.setXLink(self.refWorldlinePlot) + + def recalculate(self): + ## build 2 sets of clocks + clocks1 = collections.OrderedDict() + clocks2 = collections.OrderedDict() + for cl in self.params.param('Objects'): + clocks1.update(cl.buildClocks()) + clocks2.update(cl.buildClocks()) + + ## Inertial simulation + dt = self.animDt * self.params['Animation Speed'] + sim1 = Simulation(clocks1, ref=None, duration=self.params['Duration'], dt=dt) + sim1.run() + sim1.plot(self.inertWorldlinePlot) + self.inertWorldlinePlot.autoRange(padding=0.1) + + ## reference simulation + ref = self.params['Reference Frame'] + dur = clocks1[ref].refData['pt'][-1] ## decide how long to run the reference simulation + sim2 = Simulation(clocks2, ref=clocks2[ref], duration=dur, dt=dt) + sim2.run() + sim2.plot(self.refWorldlinePlot) + self.refWorldlinePlot.autoRange(padding=0.1) + + + ## create animations + self.refAnimationPlot.clear() + self.inertAnimationPlot.clear() + self.animTime = 0 + + self.animations = [Animation(sim1), Animation(sim2)] + self.inertAnimationPlot.addItem(self.animations[0]) + self.refAnimationPlot.addItem(self.animations[1]) + + ## create lines representing all that is visible to a particular reference + #self.inertSpaceline = Spaceline(sim1, ref) + #self.refSpaceline = Spaceline(sim2) + self.inertWorldlinePlot.addItem(self.animations[0].items[ref].spaceline()) + self.refWorldlinePlot.addItem(self.animations[1].items[ref].spaceline()) + + + + + def setAnimation(self, a): + if a: + self.lastAnimTime = pg.ptime.time() + self.animTimer.start(self.animDt*1000) + else: + self.animTimer.stop() + + def stepAnimation(self): + now = pg.ptime.time() + dt = (now-self.lastAnimTime) * self.params['Animation Speed'] + self.lastAnimTime = now + self.animTime += dt + if self.animTime > self.params['Duration']: + self.animTime = 0 + for a in self.animations: + a.restart() + + for a in self.animations: + a.stepTo(self.animTime) + + + def treeChanged(self, *args): + clocks = [] + for c in self.params.param('Objects'): + clocks.extend(c.clockNames()) + #for param, change, data in args[1]: + #if change == 'childAdded': + self.params.param('Reference Frame').setLimits(clocks) + self.setAnimation(self.params['Animate']) + + def save(self): + fn = str(pg.QtGui.QFileDialog.getSaveFileName(self, "Save State..", "untitled.cfg", "Config Files (*.cfg)")) + if fn == '': + return + state = self.params.saveState() + pg.configfile.writeConfigFile(state, fn) + + def load(self): + fn = str(pg.QtGui.QFileDialog.getOpenFileName(self, "Save State..", "", "Config Files (*.cfg)")) + if fn == '': + return + state = pg.configfile.readConfigFile(fn) + self.loadState(state) + + def loadPreset(self, param, preset): + if preset == '': + return + path = os.path.abspath(os.path.dirname(__file__)) + fn = os.path.join(path, 'presets', preset+".cfg") + state = pg.configfile.readConfigFile(fn) + self.loadState(state) + + def loadState(self, state): + if 'Load Preset..' in state['children']: + del state['children']['Load Preset..']['limits'] + del state['children']['Load Preset..']['value'] + self.params.param('Objects').clearChildren() + self.params.restoreState(state, removeChildren=False) + self.recalculate() + + +class ObjectGroupParam(pTypes.GroupParameter): + def __init__(self): + pTypes.GroupParameter.__init__(self, name="Objects", addText="Add New..", addList=['Clock', 'Grid']) + + def addNew(self, typ): + if typ == 'Clock': + self.addChild(ClockParam()) + elif typ == 'Grid': + self.addChild(GridParam()) + +class ClockParam(pTypes.GroupParameter): + def __init__(self, **kwds): + defs = dict(name="Clock", autoIncrementName=True, renamable=True, removable=True, children=[ + dict(name='Initial Position', type='float', value=0.0, step=0.1), + #dict(name='V0', type='float', value=0.0, step=0.1), + AccelerationGroup(), + + dict(name='Rest Mass', type='float', value=1.0, step=0.1, limits=[1e-9, None]), + dict(name='Color', type='color', value=(100,100,150)), + dict(name='Size', type='float', value=0.5), + dict(name='Vertical Position', type='float', value=0.0, step=0.1), + ]) + #defs.update(kwds) + pTypes.GroupParameter.__init__(self, **defs) + self.restoreState(kwds, removeChildren=False) + + def buildClocks(self): + x0 = self['Initial Position'] + y0 = self['Vertical Position'] + color = self['Color'] + m = self['Rest Mass'] + size = self['Size'] + prog = self.param('Acceleration').generate() + c = Clock(x0=x0, m0=m, y0=y0, color=color, prog=prog, size=size) + return {self.name(): c} + + def clockNames(self): + return [self.name()] + +pTypes.registerParameterType('Clock', ClockParam) + +class GridParam(pTypes.GroupParameter): + def __init__(self, **kwds): + defs = dict(name="Grid", autoIncrementName=True, renamable=True, removable=True, children=[ + dict(name='Number of Clocks', type='int', value=5, limits=[1, None]), + dict(name='Spacing', type='float', value=1.0, step=0.1), + ClockParam(name='ClockTemplate'), + ]) + #defs.update(kwds) + pTypes.GroupParameter.__init__(self, **defs) + self.restoreState(kwds, removeChildren=False) + + def buildClocks(self): + clocks = {} + template = self.param('ClockTemplate') + spacing = self['Spacing'] + for i in range(self['Number of Clocks']): + c = template.buildClocks().values()[0] + c.x0 += i * spacing + clocks[self.name() + '%02d' % i] = c + return clocks + + def clockNames(self): + return [self.name() + '%02d' % i for i in range(self['Number of Clocks'])] + +pTypes.registerParameterType('Grid', GridParam) + +class AccelerationGroup(pTypes.GroupParameter): + def __init__(self, **kwds): + defs = dict(name="Acceleration", addText="Add Command..") + pTypes.GroupParameter.__init__(self, **defs) + self.restoreState(kwds, removeChildren=False) + + def addNew(self): + nextTime = 0.0 + if self.hasChildren(): + nextTime = self.children()[-1]['Proper Time'] + 1 + self.addChild(Parameter.create(name='Command', autoIncrementName=True, type=None, renamable=True, removable=True, children=[ + dict(name='Proper Time', type='float', value=nextTime), + dict(name='Acceleration', type='float', value=0.0, step=0.1), + ])) + + def generate(self): + prog = [] + for cmd in self: + prog.append((cmd['Proper Time'], cmd['Acceleration'])) + return prog + +pTypes.registerParameterType('AccelerationGroup', AccelerationGroup) + + +class Clock(object): + nClocks = 0 + + def __init__(self, x0=0.0, y0=0.0, m0=1.0, v0=0.0, t0=0.0, color=None, prog=None, size=0.5): + Clock.nClocks += 1 + self.pen = pg.mkPen(color) + self.brush = pg.mkBrush(color) + self.y0 = y0 + self.x0 = x0 + self.v0 = v0 + self.m0 = m0 + self.t0 = t0 + self.prog = prog + self.size = size + + def init(self, nPts): + ## Keep records of object from inertial frame as well as reference frame + self.inertData = np.empty(nPts, dtype=[('x', float), ('t', float), ('v', float), ('pt', float), ('m', float), ('f', float)]) + self.refData = np.empty(nPts, dtype=[('x', float), ('t', float), ('v', float), ('pt', float), ('m', float), ('f', float)]) + + ## Inertial frame variables + self.x = self.x0 + self.v = self.v0 + self.m = self.m0 + self.t = 0.0 ## reference clock always starts at 0 + self.pt = self.t0 ## proper time starts at t0 + + ## reference frame variables + self.refx = None + self.refv = None + self.refm = None + self.reft = None + + self.recordFrame(0) + + def recordFrame(self, i): + f = self.force() + self.inertData[i] = (self.x, self.t, self.v, self.pt, self.m, f) + self.refData[i] = (self.refx, self.reft, self.refv, self.pt, self.refm, f) + + def force(self, t=None): + if len(self.prog) == 0: + return 0.0 + if t is None: + t = self.pt + + ret = 0.0 + for t1,f in self.prog: + if t >= t1: + ret = f + return ret + + def acceleration(self, t=None): + return self.force(t) / self.m0 + + def accelLimits(self): + ## return the proper time values which bound the current acceleration command + if len(self.prog) == 0: + return -np.inf, np.inf + t = self.pt + ind = -1 + for i, v in enumerate(self.prog): + t1,f = v + if t >= t1: + ind = i + + if ind == -1: + return -np.inf, self.prog[0][0] + elif ind == len(self.prog)-1: + return self.prog[-1][0], np.inf + else: + return self.prog[ind][0], self.prog[ind+1][0] + + + def getCurve(self, ref=True): + + if ref is False: + data = self.inertData + else: + data = self.refData[1:] + + x = data['x'] + y = data['t'] + + curve = pg.PlotCurveItem(x=x, y=y, pen=self.pen) + #x = self.data['x'] - ref.data['x'] + #y = self.data['t'] + + step = 1.0 + #mod = self.data['pt'] % step + #inds = np.argwhere(abs(mod[1:] - mod[:-1]) > step*0.9) + inds = [0] + pt = data['pt'] + for i in range(1,len(pt)): + diff = pt[i] - pt[inds[-1]] + if abs(diff) >= step: + inds.append(i) + inds = np.array(inds) + + #t = self.data['t'][inds] + #x = self.data['x'][inds] + pts = [] + for i in inds: + x = data['x'][i] + y = data['t'][i] + if i+1 < len(data): + dpt = data['pt'][i+1]-data['pt'][i] + dt = data['t'][i+1]-data['t'][i] + else: + dpt = 1 + + if dpt > 0: + c = pg.mkBrush((0,0,0)) + else: + c = pg.mkBrush((200,200,200)) + pts.append({'pos': (x, y), 'brush': c}) + + points = pg.ScatterPlotItem(pts, pen=self.pen, size=7) + + return curve, points + + +class Simulation: + def __init__(self, clocks, ref, duration, dt): + self.clocks = clocks + self.ref = ref + self.duration = duration + self.dt = dt + + @staticmethod + def hypTStep(dt, v0, x0, tau0, g): + ## Hyperbolic step. + ## If an object has proper acceleration g and starts at position x0 with speed v0 and proper time tau0 + ## as seen from an inertial frame, then return the new v, x, tau after time dt has elapsed. + if g == 0: + return v0, x0 + v0*dt, tau0 + dt * (1. - v0**2)**0.5 + v02 = v0**2 + g2 = g**2 + + tinit = v0 / (g * (1 - v02)**0.5) + + B = (1 + (g2 * (dt+tinit)**2))**0.5 + + v1 = g * (dt+tinit) / B + + dtau = (np.arcsinh(g * (dt+tinit)) - np.arcsinh(g * tinit)) / g + + tau1 = tau0 + dtau + + x1 = x0 + (1.0 / g) * ( B - 1. / (1.-v02)**0.5 ) + + return v1, x1, tau1 + + + @staticmethod + def tStep(dt, v0, x0, tau0, g): + ## Linear step. + ## Probably not as accurate as hyperbolic step, but certainly much faster. + gamma = (1. - v0**2)**-0.5 + dtau = dt / gamma + return v0 + dtau * g, x0 + v0*dt, tau0 + dtau + + @staticmethod + def tauStep(dtau, v0, x0, t0, g): + ## linear step in proper time of clock. + ## If an object has proper acceleration g and starts at position x0 with speed v0 at time t0 + ## as seen from an inertial frame, then return the new v, x, t after proper time dtau has elapsed. + + + ## Compute how much t will change given a proper-time step of dtau + gamma = (1. - v0**2)**-0.5 + if g == 0: + dt = dtau * gamma + else: + v0g = v0 * gamma + dt = (np.sinh(dtau * g + np.arcsinh(v0g)) - v0g) / g + + #return v0 + dtau * g, x0 + v0*dt, t0 + dt + v1, x1, t1 = Simulation.hypTStep(dt, v0, x0, t0, g) + return v1, x1, t0+dt + + @staticmethod + def hypIntersect(x0r, t0r, vr, x0, t0, v0, g): + ## given a reference clock (seen from inertial frame) has rx, rt, and rv, + ## and another clock starts at x0, t0, and v0, with acceleration g, + ## compute the intersection time of the object clock's hyperbolic path with + ## the reference plane. + + ## I'm sure we can simplify this... + + if g == 0: ## no acceleration, path is linear (and hyperbola is undefined) + #(-t0r + t0 v0 vr - vr x0 + vr x0r)/(-1 + v0 vr) + + t = (-t0r + t0 *v0 *vr - vr *x0 + vr *x0r)/(-1 + v0 *vr) + return t + + gamma = (1.0-v0**2)**-0.5 + sel = (1 if g>0 else 0) + (1 if vr<0 else 0) + sel = sel%2 + if sel == 0: + #(1/(g^2 (-1 + vr^2)))(-g^2 t0r + g gamma vr + g^2 t0 vr^2 - + #g gamma v0 vr^2 - g^2 vr x0 + + #g^2 vr x0r + \[Sqrt](g^2 vr^2 (1 + gamma^2 (v0 - vr)^2 - vr^2 + + #2 g gamma (v0 - vr) (-t0 + t0r + vr (x0 - x0r)) + + #g^2 (t0 - t0r + vr (-x0 + x0r))^2))) + + t = (1./(g**2 *(-1. + vr**2)))*(-g**2 *t0r + g *gamma *vr + g**2 *t0 *vr**2 - g *gamma *v0 *vr**2 - g**2 *vr *x0 + g**2 *vr *x0r + np.sqrt(g**2 *vr**2 *(1. + gamma**2 *(v0 - vr)**2 - vr**2 + 2 *g *gamma *(v0 - vr)* (-t0 + t0r + vr *(x0 - x0r)) + g**2 *(t0 - t0r + vr* (-x0 + x0r))**2))) + + else: + + #-(1/(g^2 (-1 + vr^2)))(g^2 t0r - g gamma vr - g^2 t0 vr^2 + + #g gamma v0 vr^2 + g^2 vr x0 - + #g^2 vr x0r + \[Sqrt](g^2 vr^2 (1 + gamma^2 (v0 - vr)^2 - vr^2 + + #2 g gamma (v0 - vr) (-t0 + t0r + vr (x0 - x0r)) + + #g^2 (t0 - t0r + vr (-x0 + x0r))^2))) + + t = -(1./(g**2 *(-1. + vr**2)))*(g**2 *t0r - g *gamma* vr - g**2 *t0 *vr**2 + g *gamma *v0 *vr**2 + g**2* vr* x0 - g**2 *vr *x0r + np.sqrt(g**2* vr**2 *(1. + gamma**2 *(v0 - vr)**2 - vr**2 + 2 *g *gamma *(v0 - vr) *(-t0 + t0r + vr *(x0 - x0r)) + g**2 *(t0 - t0r + vr *(-x0 + x0r))**2))) + return t + + def run(self): + nPts = int(self.duration/self.dt)+1 + for cl in self.clocks.itervalues(): + cl.init(nPts) + + if self.ref is None: + self.runInertial(nPts) + else: + self.runReference(nPts) + + def runInertial(self, nPts): + clocks = self.clocks + dt = self.dt + tVals = np.linspace(0, dt*(nPts-1), nPts) + for cl in self.clocks.itervalues(): + for i in xrange(1,nPts): + nextT = tVals[i] + while True: + tau1, tau2 = cl.accelLimits() + x = cl.x + v = cl.v + tau = cl.pt + g = cl.acceleration() + + v1, x1, tau1 = self.hypTStep(dt, v, x, tau, g) + if tau1 > tau2: + dtau = tau2-tau + cl.v, cl.x, cl.t = self.tauStep(dtau, v, x, cl.t, g) + cl.pt = tau2 + else: + cl.v, cl.x, cl.pt = v1, x1, tau1 + cl.t += dt + + if cl.t >= nextT: + cl.refx = cl.x + cl.refv = cl.v + cl.reft = cl.t + cl.recordFrame(i) + break + + + def runReference(self, nPts): + clocks = self.clocks + ref = self.ref + dt = self.dt + dur = self.duration + + ## make sure reference clock is not present in the list of clocks--this will be handled separately. + clocks = clocks.copy() + for k,v in clocks.iteritems(): + if v is ref: + del clocks[k] + break + + ref.refx = 0 + ref.refv = 0 + ref.refm = ref.m0 + + ## These are the set of proper times (in the reference frame) that will be simulated + ptVals = np.linspace(ref.pt, ref.pt + dt*(nPts-1), nPts) + + for i in xrange(1,nPts): + + ## step reference clock ahead one time step in its proper time + nextPt = ptVals[i] ## this is where (when) we want to end up + while True: + tau1, tau2 = ref.accelLimits() + dtau = min(nextPt-ref.pt, tau2-ref.pt) ## do not step past the next command boundary + g = ref.acceleration() + v, x, t = Simulation.tauStep(dtau, ref.v, ref.x, ref.t, g) + ref.pt += dtau + ref.v = v + ref.x = x + ref.t = t + ref.reft = ref.pt + if ref.pt >= nextPt: + break + #else: + #print "Stepped to", tau2, "instead of", nextPt + ref.recordFrame(i) + + ## determine plane visible to reference clock + ## this plane goes through the point ref.x, ref.t and has slope = ref.v + + + ## update all other clocks + for cl in clocks.itervalues(): + while True: + g = cl.acceleration() + tau1, tau2 = cl.accelLimits() + ##Given current position / speed of clock, determine where it will intersect reference plane + #t1 = (ref.v * (cl.x - cl.v * cl.t) + (ref.t - ref.v * ref.x)) / (1. - cl.v) + t1 = Simulation.hypIntersect(ref.x, ref.t, ref.v, cl.x, cl.t, cl.v, g) + dt1 = t1 - cl.t + + ## advance clock by correct time step + v, x, tau = Simulation.hypTStep(dt1, cl.v, cl.x, cl.pt, g) + + ## check to see whether we have gone past an acceleration command boundary. + ## if so, we must instead advance the clock to the boundary and start again + if tau < tau1: + dtau = tau1 - cl.pt + cl.v, cl.x, cl.t = Simulation.tauStep(dtau, cl.v, cl.x, cl.t, g) + cl.pt = tau1-0.000001 + continue + if tau > tau2: + dtau = tau2 - cl.pt + cl.v, cl.x, cl.t = Simulation.tauStep(dtau, cl.v, cl.x, cl.t, g) + cl.pt = tau2 + continue + + ## Otherwise, record the new values and exit the loop + cl.v = v + cl.x = x + cl.pt = tau + cl.t = t1 + cl.m = None + break + + ## transform position into reference frame + x = cl.x - ref.x + t = cl.t - ref.t + gamma = (1.0 - ref.v**2) ** -0.5 + vg = -ref.v * gamma + + cl.refx = gamma * (x - ref.v * t) + cl.reft = ref.pt # + gamma * (t - ref.v * x) # this term belongs here, but it should always be equal to 0. + cl.refv = (cl.v - ref.v) / (1.0 - cl.v * ref.v) + cl.refm = None + cl.recordFrame(i) + + t += dt + + def plot(self, plot): + plot.clear() + for cl in self.clocks.itervalues(): + c, p = cl.getCurve() + plot.addItem(c) + plot.addItem(p) + +class Animation(pg.ItemGroup): + def __init__(self, sim): + pg.ItemGroup.__init__(self) + self.sim = sim + self.clocks = sim.clocks + + self.items = {} + for name, cl in self.clocks.items(): + item = ClockItem(cl) + self.addItem(item) + self.items[name] = item + + #self.timer = timer + #self.timer.timeout.connect(self.step) + + #def run(self, run): + #if not run: + #self.timer.stop() + #else: + #self.timer.start(self.dt) + + def restart(self): + for cl in self.items.values(): + cl.reset() + + def stepTo(self, t): + for i in self.items.values(): + i.stepTo(t) + + +class ClockItem(pg.ItemGroup): + def __init__(self, clock): + pg.ItemGroup.__init__(self) + self.size = clock.size + self.item = QtGui.QGraphicsEllipseItem(QtCore.QRectF(0, 0, self.size, self.size)) + self.item.translate(-self.size*0.5, -self.size*0.5) + self.item.setPen(pg.mkPen(100,100,100)) + self.item.setBrush(clock.brush) + self.hand = QtGui.QGraphicsLineItem(0, 0, 0, self.size*0.5) + self.hand.setPen(pg.mkPen('w')) + self.hand.setZValue(10) + self.flare = QtGui.QGraphicsPolygonItem(QtGui.QPolygonF([ + QtCore.QPointF(0, -self.size*0.25), + QtCore.QPointF(0, self.size*0.25), + QtCore.QPointF(self.size*1.5, 0), + QtCore.QPointF(0, -self.size*0.25), + ])) + self.flare.setPen(pg.mkPen('y')) + self.flare.setBrush(pg.mkBrush(255,150,0)) + self.flare.setZValue(-10) + self.addItem(self.hand) + self.addItem(self.item) + self.addItem(self.flare) + + self.clock = clock + self.i = 1 + + self._spaceline = None + + + def spaceline(self): + if self._spaceline is None: + self._spaceline = pg.InfiniteLine() + self._spaceline.setPen(self.clock.pen) + return self._spaceline + + def stepTo(self, t): + data = self.clock.refData + + while self.i < len(data)-1 and data['t'][self.i] < t: + self.i += 1 + while self.i > 1 and data['t'][self.i-1] >= t: + self.i -= 1 + + self.setPos(data['x'][self.i], self.clock.y0) + + t = data['pt'][self.i] + self.hand.setRotation(-0.25 * t * 360.) + + self.resetTransform() + v = data['v'][self.i] + gam = (1.0 - v**2)**0.5 + self.scale(gam, 1.0) + + f = data['f'][self.i] + self.flare.resetTransform() + if f < 0: + self.flare.translate(self.size*0.4, 0) + else: + self.flare.translate(-self.size*0.4, 0) + + self.flare.scale(-f * (0.5+np.random.random()*0.1), 1.0) + + if self._spaceline is not None: + self._spaceline.setPos(pg.Point(data['x'][self.i], data['t'][self.i])) + self._spaceline.setAngle(data['v'][self.i] * 45.) + + + def reset(self): + self.i = 1 + + +#class Spaceline(pg.InfiniteLine): + #def __init__(self, sim, frame): + #self.sim = sim + #self.frame = frame + #pg.InfiniteLine.__init__(self) + #self.setPen(sim.clocks[frame].pen) + + #def stepTo(self, t): + #self.setAngle(0) + + #pass + +if __name__ == '__main__': + pg.mkQApp() + #import pyqtgraph.console + #cw = pyqtgraph.console.ConsoleWidget() + #cw.show() + #cw.catchNextException() + win = RelativityGUI() + win.setWindowTitle("Relativity!") + win.show() + win.resize(1100,700) + + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() + + + #win.params.param('Objects').restoreState(state, removeChildren=False) + From 9a5e526c616762b5d4a0d6a39fc64d4ec1b1cbaa Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 16 Jun 2014 12:41:36 -0600 Subject: [PATCH 40/50] Update CONTRIB with instructions on checking style --- CONTRIBUTING.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index f0ab3416..0b4b1beb 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -28,6 +28,9 @@ Please use the following guidelines when preparing changes: * PyQtGraph prefers PEP8 for most style issues, but this is not enforced rigorously as long as the code is clean and readable. + * Use `python setup.py style` to see whether your code follows + the mandatory style guidelines checked by flake8. + * Exception 1: All variable names should use camelCase rather than underscore_separation. This is done for consistency with Qt From 3c2d1d4a0dcee2ad315b20448d2d4fa36c1dc2ec Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 20 Jun 2014 23:04:21 -0400 Subject: [PATCH 41/50] Added GLVolumeItem.setData --- CHANGELOG | 1 + pyqtgraph/opengl/items/GLVolumeItem.py | 27 +++++++++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1a1ba126..3f5ddb07 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -53,6 +53,7 @@ pyqtgraph-0.9.9 [unreleased] - Added ViewBox.invertX() - Docks now have optional close button - Added InfiniteLine.setHoverPen + - Added GLVolumeItem.setData Bugfixes: - PlotCurveItem now has correct clicking behavior--clicks within a few px diff --git a/pyqtgraph/opengl/items/GLVolumeItem.py b/pyqtgraph/opengl/items/GLVolumeItem.py index 84f23e12..cbe22db9 100644 --- a/pyqtgraph/opengl/items/GLVolumeItem.py +++ b/pyqtgraph/opengl/items/GLVolumeItem.py @@ -2,6 +2,7 @@ from OpenGL.GL import * from .. GLGraphicsItem import GLGraphicsItem from ...Qt import QtGui import numpy as np +from ... import debug __all__ = ['GLVolumeItem'] @@ -25,13 +26,22 @@ class GLVolumeItem(GLGraphicsItem): self.sliceDensity = sliceDensity self.smooth = smooth - self.data = data + self.data = None + self._needUpload = False + self.texture = None GLGraphicsItem.__init__(self) self.setGLOptions(glOptions) + self.setData(data) - def initializeGL(self): + def setData(self, data): + self.data = data + self._needUpload = True + self.update() + + def _uploadData(self): glEnable(GL_TEXTURE_3D) - self.texture = glGenTextures(1) + if self.texture is None: + self.texture = glGenTextures(1) glBindTexture(GL_TEXTURE_3D, self.texture) if self.smooth: glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) @@ -60,9 +70,16 @@ class GLVolumeItem(GLGraphicsItem): glNewList(l, GL_COMPILE) self.drawVolume(ax, d) glEndList() - - + + self._needUpload = False + def paint(self): + if self.data is None: + return + + if self._needUpload: + self._uploadData() + self.setupGLState() glEnable(GL_TEXTURE_3D) From b20dc0cf6f0f5ee28576c39251f42225a3a25d05 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 22 Jun 2014 21:27:48 -0400 Subject: [PATCH 42/50] Added methods for saving / restoring state of PloyLineROI --- CHANGELOG | 1 + pyqtgraph/graphicsItems/ROI.py | 49 ++++++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3f5ddb07..f9f9466f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -54,6 +54,7 @@ pyqtgraph-0.9.9 [unreleased] - Docks now have optional close button - Added InfiniteLine.setHoverPen - Added GLVolumeItem.setData + - Added PolyLineROI.setPoints, clearPoints, saveState, setState Bugfixes: - PlotCurveItem now has correct clicking behavior--clicks within a few px diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index d51f75df..f3ebd992 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1804,13 +1804,56 @@ class PolyLineROI(ROI): self.segments = [] ROI.__init__(self, pos, size=[1,1], **args) - for p in positions: - self.addFreeHandle(p) + self.setPoints(positions) + #for p in positions: + #self.addFreeHandle(p) + #start = -1 if self.closed else 0 + #for i in range(start, len(self.handles)-1): + #self.addSegment(self.handles[i]['item'], self.handles[i+1]['item']) + + def setPoints(self, points, closed=None): + """ + Set the complete sequence of points displayed by this ROI. + + ============= ========================================================= + **Arguments** + points List of (x,y) tuples specifying handle locations to set. + closed If bool, then this will set whether the ROI is closed + (the last point is connected to the first point). If + None, then the closed mode is left unchanged. + ============= ========================================================= + + """ + if closed is not None: + self.closed = closed + + for p in points: + self.addFreeHandle(p) + start = -1 if self.closed else 0 for i in range(start, len(self.handles)-1): self.addSegment(self.handles[i]['item'], self.handles[i+1]['item']) + + + def clearPoints(self): + """ + Remove all handles and segments. + """ + while len(self.handles) > 0: + self.removeHandle(self.handles[0]['item']) + def saveState(self): + state = ROI.saveState(self) + state['closed'] = self.closed + state['points'] = [tuple(h.pos()) for h in self.getHandles()] + return state + + def setState(self, state): + ROI.setState(self, state) + self.clearPoints() + self.setPoints(state['points'], closed=state['closed']) + def addSegment(self, h1, h2, index=None): seg = LineSegmentROI(handles=(h1, h2), pen=self.pen, parent=self, movable=False) if index is None: @@ -1936,6 +1979,8 @@ class PolyLineROI(ROI): for seg in self.segments: seg.setPen(*args, **kwds) + + class LineSegmentROI(ROI): """ ROI subclass with two freely-moving handles defining a line. From 8b0a866ad9a0a4807c4836c577ce65ecac5fd283 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 27 Jun 2014 10:55:55 -0400 Subject: [PATCH 43/50] Add ErrorBarItem.setData --- CHANGELOG | 1 + examples/ErrorBarItem.py | 2 +- pyqtgraph/graphicsItems/ErrorBarItem.py | 35 +++++++++++++++++-------- pyqtgraph/graphicsItems/GraphicsItem.py | 2 ++ 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f9f9466f..e574e479 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -55,6 +55,7 @@ pyqtgraph-0.9.9 [unreleased] - Added InfiniteLine.setHoverPen - Added GLVolumeItem.setData - Added PolyLineROI.setPoints, clearPoints, saveState, setState + - Added ErrorBarItem.setData Bugfixes: - PlotCurveItem now has correct clicking behavior--clicks within a few px diff --git a/examples/ErrorBarItem.py b/examples/ErrorBarItem.py index 3bbf06d1..cd576d51 100644 --- a/examples/ErrorBarItem.py +++ b/examples/ErrorBarItem.py @@ -7,7 +7,7 @@ Demonstrates basic use of ErrorBarItem import initExample ## Add path to library (just for examples; you do not need this) import pyqtgraph as pg -from pyqtgraph.Qt import QtGui +from pyqtgraph.Qt import QtGui, QtCore import numpy as np import pyqtgraph as pg diff --git a/pyqtgraph/graphicsItems/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py index 7b681389..d7cb06db 100644 --- a/pyqtgraph/graphicsItems/ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/ErrorBarItem.py @@ -8,15 +8,7 @@ __all__ = ['ErrorBarItem'] class ErrorBarItem(GraphicsObject): def __init__(self, **opts): """ - Valid keyword options are: - x, y, height, width, top, bottom, left, right, beam, pen - - x and y must be numpy arrays specifying the coordinates of data points. - height, width, top, bottom, left, right, and beam may be numpy arrays, - single values, or None to disable. All values should be positive. - - If height is specified, it overrides top and bottom. - If width is specified, it overrides left and right. + All keyword arguments are passed to setData(). """ GraphicsObject.__init__(self) self.opts = dict( @@ -31,14 +23,35 @@ class ErrorBarItem(GraphicsObject): beam=None, pen=None ) - self.setOpts(**opts) + self.setData(**opts) + + def setData(self, **opts): + """ + Update the data in the item. All arguments are optional. - def setOpts(self, **opts): + Valid keyword options are: + x, y, height, width, top, bottom, left, right, beam, pen + + * x and y must be numpy arrays specifying the coordinates of data points. + * height, width, top, bottom, left, right, and beam may be numpy arrays, + single values, or None to disable. All values should be positive. + * top, bottom, left, and right specify the lengths of bars extending + in each direction. + * If height is specified, it overrides top and bottom. + * If width is specified, it overrides left and right. + * beam specifies the width of the beam at the end of each bar. + * pen may be any single argument accepted by pg.mkPen(). + """ self.opts.update(opts) self.path = None self.update() + self.prepareGeometryChange() self.informViewBoundsChanged() + def setOpts(self, **opts): + # for backward compatibility + self.setData(**opts) + def drawPath(self): p = QtGui.QPainterPath() diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 9fa323e2..c1a96a62 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -318,6 +318,8 @@ class GraphicsItem(object): vt = self.deviceTransform() if vt is None: return None + if isinstance(obj, QtCore.QPoint): + obj = QtCore.QPointF(obj) vt = fn.invertQTransform(vt) return vt.map(obj) From d7a1ae1d5265b1d3cbc1d94871ea490050f2c67f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 27 Jun 2014 10:57:52 -0400 Subject: [PATCH 44/50] Note about version method was added --- pyqtgraph/graphicsItems/ErrorBarItem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/graphicsItems/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py index d7cb06db..986c5140 100644 --- a/pyqtgraph/graphicsItems/ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/ErrorBarItem.py @@ -41,6 +41,8 @@ class ErrorBarItem(GraphicsObject): * If width is specified, it overrides left and right. * beam specifies the width of the beam at the end of each bar. * pen may be any single argument accepted by pg.mkPen(). + + This method was added in version 0.9.9. For prior versions, use setOpts. """ self.opts.update(opts) self.path = None From 26730ad94757554ffb55411ada41f92cfa175c61 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 27 Jun 2014 11:05:05 -0400 Subject: [PATCH 45/50] Link ErrorBarItem in documentation --- doc/source/graphicsItems/errorbaritem.rst | 8 ++++++++ doc/source/graphicsItems/index.rst | 1 + 2 files changed, 9 insertions(+) create mode 100644 doc/source/graphicsItems/errorbaritem.rst diff --git a/doc/source/graphicsItems/errorbaritem.rst b/doc/source/graphicsItems/errorbaritem.rst new file mode 100644 index 00000000..be68a5dd --- /dev/null +++ b/doc/source/graphicsItems/errorbaritem.rst @@ -0,0 +1,8 @@ +ErrorBarItem +============ + +.. autoclass:: pyqtgraph.ErrorBarItem + :members: + + .. automethod:: pyqtgraph.ErrorBarItem.__init__ + diff --git a/doc/source/graphicsItems/index.rst b/doc/source/graphicsItems/index.rst index 970e9500..7042d27e 100644 --- a/doc/source/graphicsItems/index.rst +++ b/doc/source/graphicsItems/index.rst @@ -23,6 +23,7 @@ Contents: isocurveitem axisitem textitem + errorbaritem arrowitem fillbetweenitem curvepoint From 6e4b2f4ff4240df2bcad1f9cd8cc75c7d92b5ae6 Mon Sep 17 00:00:00 2001 From: Thomas Feldmann Date: Sat, 5 Jul 2014 17:00:53 +0200 Subject: [PATCH 46/50] Fixed typo in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2dff1031..990664c0 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Installation Methods used as a git subtree by cloning the git-core repository from github. * To install system-wide from source distribution: `$ python setup.py install` - * For instalation packages, see the website (pyqtgraph.org) + * For installation packages, see the website (pyqtgraph.org) * On debian-like systems, pyqtgraph requires the following packages: python-numpy, python-qt4 | python-pyside For 3D support: python-opengl, python-qt4-gl | python-pyside.qtopengl From ed0b95602adf9322564be63f782c495b98b2dea6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 6 Jul 2014 11:00:38 -0400 Subject: [PATCH 47/50] Correct LegendItem addItem to use next available row --- pyqtgraph/graphicsItems/LegendItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index ea6798fb..20d6416e 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -75,7 +75,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): sample = item else: sample = ItemSample(item) - row = len(self.items) + row = self.layout.rowCount() self.items.append((sample, label)) self.layout.addItem(sample, row, 0) self.layout.addItem(label, row, 1) From 8268ccfa655feef7e2f6b5128b012f53201474d1 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 6 Jul 2014 11:44:26 -0400 Subject: [PATCH 48/50] Added GLImageItem.setData() --- pyqtgraph/opengl/items/GLImageItem.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/opengl/items/GLImageItem.py b/pyqtgraph/opengl/items/GLImageItem.py index 2cab23a3..59ddaf6f 100644 --- a/pyqtgraph/opengl/items/GLImageItem.py +++ b/pyqtgraph/opengl/items/GLImageItem.py @@ -25,13 +25,21 @@ class GLImageItem(GLGraphicsItem): """ self.smooth = smooth - self.data = data + self._needUpdate = False GLGraphicsItem.__init__(self) + self.setData(data) self.setGLOptions(glOptions) def initializeGL(self): glEnable(GL_TEXTURE_2D) self.texture = glGenTextures(1) + + def setData(self, data): + self.data = data + self._needUpdate = True + self.update() + + def _updateTexture(self): glBindTexture(GL_TEXTURE_2D, self.texture) if self.smooth: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) @@ -63,7 +71,8 @@ class GLImageItem(GLGraphicsItem): def paint(self): - + if self._needUpdate: + self._updateTexture() glEnable(GL_TEXTURE_2D) glBindTexture(GL_TEXTURE_2D, self.texture) From 74b5ba6f7e97d9b6d1b64dfe384a26999fa2e072 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 25 Jul 2014 07:52:58 -0400 Subject: [PATCH 49/50] Add AxisItem.setTickSpacing() --- pyqtgraph/graphicsItems/AxisItem.py | 57 +++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 5eef4ae0..ededed56 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -55,6 +55,8 @@ class AxisItem(GraphicsWidget): ], 'showValues': showValues, 'tickLength': maxTickLength, + 'maxTickLevel': 2, + 'maxTextLevel': 2, } self.textWidth = 30 ## Keeps track of maximum width / height of tick text @@ -68,6 +70,7 @@ class AxisItem(GraphicsWidget): self.tickFont = None self._tickLevels = None ## used to override the automatic ticking system with explicit ticks + self._tickSpacing = None # used to override default tickSpacing method self.scale = 1.0 self.autoSIPrefix = True self.autoSIPrefixScale = 1.0 @@ -517,6 +520,37 @@ class AxisItem(GraphicsWidget): self.picture = None self.update() + def setTickSpacing(self, major=None, minor=None, levels=None): + """ + Explicitly determine the spacing of major and minor ticks. This + overrides the default behavior of the tickSpacing method, and disables + the effect of setTicks(). Arguments may be either *major* and *minor*, + or *levels* which is a list of (spacing, offset) tuples for each + tick level desired. + + If no arguments are given, then the default behavior of tickSpacing + is enabled. + + Examples:: + + # two levels, all offsets = 0 + axis.setTickSpacing(5, 1) + # three levels, all offsets = 0 + axis.setTickSpacing([(3, 0), (1, 0), (0.25, 0)]) + # reset to default + axis.setTickSpacing() + """ + + if levels is None: + if major is None: + levels = None + else: + levels = [(major, 0), (minor, 0)] + self._tickSpacing = levels + self.picture = None + self.update() + + def tickSpacing(self, minVal, maxVal, size): """Return values describing the desired spacing and offset of ticks. @@ -532,6 +566,10 @@ class AxisItem(GraphicsWidget): ... ] """ + # First check for override tick spacing + if self._tickSpacing is not None: + return self._tickSpacing + dif = abs(maxVal - minVal) if dif == 0: return [] @@ -557,12 +595,13 @@ class AxisItem(GraphicsWidget): #(intervals[minorIndex], 0) ## Pretty, but eats up CPU ] - ## decide whether to include the last level of ticks - minSpacing = min(size / 20., 30.) - maxTickCount = size / minSpacing - if dif / intervals[minorIndex] <= maxTickCount: - levels.append((intervals[minorIndex], 0)) - return levels + if self.style['maxTickLevel'] >= 2: + ## decide whether to include the last level of ticks + minSpacing = min(size / 20., 30.) + maxTickCount = size / minSpacing + if dif / intervals[minorIndex] <= maxTickCount: + levels.append((intervals[minorIndex], 0)) + return levels @@ -588,8 +627,6 @@ class AxisItem(GraphicsWidget): #(intervals[intIndexes[0]], 0) #] - - def tickValues(self, minVal, maxVal, size): """ Return the values and spacing of ticks to draw:: @@ -763,8 +800,6 @@ class AxisItem(GraphicsWidget): values.append(val) strings.append(strn) - textLevel = 1 ## draw text at this scale level - ## determine mapping between tick values and local coordinates dif = self.range[1] - self.range[0] if dif == 0: @@ -853,7 +888,7 @@ class AxisItem(GraphicsWidget): if not self.style['showValues']: return (axisSpec, tickSpecs, textSpecs) - for i in range(len(tickLevels)): + for i in range(min(len(tickLevels), self.style['maxTextLevel']+1)): ## Get the list of strings to display for this level if tickStrings is None: spacing, values = tickLevels[i] From 55a07b0bec998adc2f63a87befeab0435679961e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 29 Jul 2014 23:57:34 -0400 Subject: [PATCH 50/50] Correction in GraphicsItem.deviceTransform() during export. This fixes some (all?) issues with exporting ScatterPlotItem. --- pyqtgraph/graphicsItems/GraphicsItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index c1a96a62..2ca35193 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -102,7 +102,7 @@ class GraphicsItem(object): Extends deviceTransform to automatically determine the viewportTransform. """ if self._exportOpts is not False and 'painter' in self._exportOpts: ## currently exporting; device transform may be different. - return self._exportOpts['painter'].deviceTransform() + return self._exportOpts['painter'].deviceTransform() * self.sceneTransform() if viewportTransform is None: view = self.getViewWidget()