Added deprecation warning for Node.__getattr__
Expanded flowchart.Node docstrings Added custom node example
This commit is contained in:
parent
4cbc012474
commit
5786a627b5
@ -21,21 +21,24 @@ import pyqtgraph.metaarray as metaarray
|
||||
|
||||
app = QtGui.QApplication([])
|
||||
|
||||
|
||||
## Create main window with grid layout
|
||||
win = QtGui.QMainWindow()
|
||||
cw = QtGui.QWidget()
|
||||
win.setCentralWidget(cw)
|
||||
layout = QtGui.QGridLayout()
|
||||
cw.setLayout(layout)
|
||||
|
||||
## Create flowchart, define input/output terminals
|
||||
fc = Flowchart(terminals={
|
||||
'dataIn': {'io': 'in'},
|
||||
'dataOut': {'io': 'out'}
|
||||
})
|
||||
w = fc.widget()
|
||||
|
||||
## Add flowchart control panel to the main window
|
||||
layout.addWidget(fc.widget(), 0, 0, 2, 1)
|
||||
|
||||
## Add two plot widgets
|
||||
pw1 = pg.PlotWidget()
|
||||
pw2 = pg.PlotWidget()
|
||||
layout.addWidget(pw1, 0, 1)
|
||||
@ -43,14 +46,17 @@ layout.addWidget(pw2, 1, 1)
|
||||
|
||||
win.show()
|
||||
|
||||
|
||||
## generate signal data to pass through the flowchart
|
||||
data = np.random.normal(size=1000)
|
||||
data[200:300] += 1
|
||||
data += np.sin(np.linspace(0, 100, 1000))
|
||||
data = metaarray.MetaArray(data, info=[{'name': 'Time', 'values': np.linspace(0, 1.0, len(data))}, {}])
|
||||
|
||||
## Feed data into the input terminal of the flowchart
|
||||
fc.setInput(dataIn=data)
|
||||
|
||||
## populate the flowchart with a basic set of processing nodes.
|
||||
## (usually we let the user do this)
|
||||
pw1Node = fc.createNode('PlotWidget', pos=(0, -150))
|
||||
pw1Node.setPlot(pw1)
|
||||
|
||||
@ -59,42 +65,12 @@ pw2Node.setPlot(pw2)
|
||||
|
||||
fNode = fc.createNode('GaussianFilter', pos=(0, 0))
|
||||
fNode.ctrls['sigma'].setValue(5)
|
||||
fc.connectTerminals(fc.dataIn, fNode.In)
|
||||
fc.connectTerminals(fc.dataIn, pw1Node.In)
|
||||
fc.connectTerminals(fNode.Out, pw2Node.In)
|
||||
fc.connectTerminals(fNode.Out, fc.dataOut)
|
||||
fc.connectTerminals(fc['dataIn'], fNode['In'])
|
||||
fc.connectTerminals(fc['dataIn'], pw1Node['In'])
|
||||
fc.connectTerminals(fNode['Out'], pw2Node['In'])
|
||||
fc.connectTerminals(fNode['Out'], fc['dataOut'])
|
||||
|
||||
|
||||
#n1 = fc.createNode('Add', pos=(0,-80))
|
||||
#n2 = fc.createNode('Subtract', pos=(140,-10))
|
||||
#n3 = fc.createNode('Abs', pos=(0, 80))
|
||||
#n4 = fc.createNode('Add', pos=(140,100))
|
||||
|
||||
#fc.connectTerminals(fc.dataIn, n1.A)
|
||||
#fc.connectTerminals(fc.dataIn, n1.B)
|
||||
#fc.connectTerminals(fc.dataIn, n2.A)
|
||||
#fc.connectTerminals(n1.Out, n4.A)
|
||||
#fc.connectTerminals(n1.Out, n2.B)
|
||||
#fc.connectTerminals(n2.Out, n3.In)
|
||||
#fc.connectTerminals(n3.Out, n4.B)
|
||||
#fc.connectTerminals(n4.Out, fc.dataOut)
|
||||
|
||||
|
||||
#def process(**kargs):
|
||||
#return fc.process(**kargs)
|
||||
|
||||
|
||||
#print process(dataIn=7)
|
||||
|
||||
#fc.setInput(dataIn=3)
|
||||
|
||||
#s = fc.saveState()
|
||||
#fc.clear()
|
||||
|
||||
#fc.restoreState(s)
|
||||
|
||||
#fc.setInput(dataIn=3)
|
||||
|
||||
|
||||
## Start Qt event loop unless running in interactive mode or using pyside.
|
||||
if __name__ == '__main__':
|
||||
|
144
examples/FlowchartCustomNode.py
Normal file
144
examples/FlowchartCustomNode.py
Normal file
@ -0,0 +1,144 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This example demonstrates writing a custom Node subclass for use with flowcharts.
|
||||
|
||||
We implement a couple of simple image processing nodes.
|
||||
"""
|
||||
import initExample ## Add path to library (just for examples; you do not need this)
|
||||
|
||||
from pyqtgraph.flowchart import Flowchart, Node
|
||||
import pyqtgraph.flowchart.library as fclib
|
||||
from pyqtgraph.flowchart.library.common import CtrlNode
|
||||
from pyqtgraph.Qt import QtGui, QtCore
|
||||
import pyqtgraph as pg
|
||||
import numpy as np
|
||||
import scipy.ndimage
|
||||
|
||||
app = QtGui.QApplication([])
|
||||
|
||||
## Create main window with a grid layout inside
|
||||
win = QtGui.QMainWindow()
|
||||
cw = QtGui.QWidget()
|
||||
win.setCentralWidget(cw)
|
||||
layout = QtGui.QGridLayout()
|
||||
cw.setLayout(layout)
|
||||
|
||||
## Create an empty flowchart with a single input and output
|
||||
fc = Flowchart(terminals={
|
||||
'dataIn': {'io': 'in'},
|
||||
'dataOut': {'io': 'out'}
|
||||
})
|
||||
w = fc.widget()
|
||||
|
||||
layout.addWidget(fc.widget(), 0, 0, 2, 1)
|
||||
|
||||
## Create two ImageView widgets to display the raw and processed data with contrast
|
||||
## and color control.
|
||||
v1 = pg.ImageView()
|
||||
v2 = pg.ImageView()
|
||||
layout.addWidget(v1, 0, 1)
|
||||
layout.addWidget(v2, 1, 1)
|
||||
|
||||
win.show()
|
||||
|
||||
## generate random input data
|
||||
data = np.random.normal(size=(100,100))
|
||||
data = 25 * scipy.ndimage.gaussian_filter(data, (5,5))
|
||||
data += np.random.normal(size=(100,100))
|
||||
data[40:60, 40:60] += 15.0
|
||||
data[30:50, 30:50] += 15.0
|
||||
#data += np.sin(np.linspace(0, 100, 1000))
|
||||
#data = metaarray.MetaArray(data, info=[{'name': 'Time', 'values': np.linspace(0, 1.0, len(data))}, {}])
|
||||
|
||||
## Set the raw data as the input value to the flowchart
|
||||
fc.setInput(dataIn=data)
|
||||
|
||||
|
||||
## At this point, we need some custom Node classes since those provided in the library
|
||||
## are not sufficient. Each node will define a set of input/output terminals, a
|
||||
## processing function, and optionally a control widget (to be displayed in the
|
||||
## flowchart control panel)
|
||||
|
||||
class ImageViewNode(Node):
|
||||
"""Node that displays image data in an ImageView widget"""
|
||||
nodeName = 'ImageView'
|
||||
|
||||
def __init__(self, name):
|
||||
self.view = None
|
||||
## Initialize node with only a single input terminal
|
||||
Node.__init__(self, name, terminals={'data': {'io':'in'}})
|
||||
|
||||
def setView(self, view): ## setView must be called by the program
|
||||
self.view = view
|
||||
|
||||
def process(self, data, display=True):
|
||||
## if process is called with display=False, then the flowchart is being operated
|
||||
## in batch processing mode, so we should skip displaying to improve performance.
|
||||
|
||||
if display and self.view is not None:
|
||||
## the 'data' argument is the value given to the 'data' terminal
|
||||
if data is None:
|
||||
self.view.setImage(np.zeros((1,1))) # give a blank array to clear the view
|
||||
else:
|
||||
self.view.setImage(data)
|
||||
|
||||
## register the class so it will appear in the menu of node types.
|
||||
## It will appear in the 'display' sub-menu.
|
||||
fclib.registerNodeType(ImageViewNode, [('Display',)])
|
||||
|
||||
## We will define an unsharp masking filter node as a subclass of CtrlNode.
|
||||
## CtrlNode is just a convenience class that automatically creates its
|
||||
## control widget based on a simple data structure.
|
||||
class UnsharpMaskNode(CtrlNode):
|
||||
"""Return the input data passed through scipy.ndimage.gaussian_filter."""
|
||||
nodeName = "UnsharpMask"
|
||||
uiTemplate = [
|
||||
('sigma', 'spin', {'value': 1.0, 'step': 1.0, 'range': [0.0, None]}),
|
||||
('strength', 'spin', {'value': 1.0, 'dec': True, 'step': 0.5, 'minStep': 0.01, 'range': [0.0, None]}),
|
||||
]
|
||||
def __init__(self, name):
|
||||
## Define the input / output terminals available on this node
|
||||
terminals = {
|
||||
'dataIn': dict(io='in'), # each terminal needs at least a name and
|
||||
'dataOut': dict(io='out'), # to specify whether it is input or output
|
||||
} # other more advanced options are available
|
||||
# as well..
|
||||
|
||||
CtrlNode.__init__(self, name, terminals=terminals)
|
||||
|
||||
def process(self, dataIn, display=True):
|
||||
# CtrlNode has created self.ctrls, which is a dict containing {ctrlName: widget}
|
||||
sigma = self.ctrls['sigma'].value()
|
||||
strength = self.ctrls['strength'].value()
|
||||
output = dataIn - (strength * scipy.ndimage.gaussian_filter(dataIn, (sigma,sigma)))
|
||||
return {'dataOut': output}
|
||||
|
||||
## register the class so it will appear in the menu of node types.
|
||||
## It will appear in a new 'image' sub-menu.
|
||||
fclib.registerNodeType(UnsharpMaskNode, [('Image',)])
|
||||
|
||||
|
||||
|
||||
## Now we will programmatically add nodes to define the function of the flowchart.
|
||||
## Normally, the user will do this manually or by loading a pre-generated
|
||||
## flowchart file.
|
||||
|
||||
v1Node = fc.createNode('ImageView', pos=(0, -150))
|
||||
v1Node.setView(v1)
|
||||
|
||||
v2Node = fc.createNode('ImageView', pos=(150, -150))
|
||||
v2Node.setView(v2)
|
||||
|
||||
fNode = fc.createNode('UnsharpMask', pos=(0, 0))
|
||||
fc.connectTerminals(fc['dataIn'], fNode['dataIn'])
|
||||
fc.connectTerminals(fc['dataIn'], v1Node['data'])
|
||||
fc.connectTerminals(fNode['dataOut'], v2Node['data'])
|
||||
fc.connectTerminals(fNode['dataOut'], fc['dataOut'])
|
||||
|
||||
|
||||
|
||||
## 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_()
|
@ -13,6 +13,18 @@ def strDict(d):
|
||||
return dict([(str(k), v) for k, v in d.items()])
|
||||
|
||||
class Node(QtCore.QObject):
|
||||
"""
|
||||
Node represents the basic processing unit of a flowchart.
|
||||
A Node subclass implements at least:
|
||||
|
||||
1) A list of input / ouptut terminals and their properties
|
||||
2) a process() function which takes the names of input terminals as keyword arguments and returns a dict with the names of output terminals as keys.
|
||||
|
||||
A flowchart thus consists of multiple instances of Node subclasses, each of which is connected
|
||||
to other by wires between their terminals. A flowchart is, itself, also a special subclass of Node.
|
||||
This allows Nodes within the flowchart to connect to the input/output nodes of the flowchart itself.
|
||||
|
||||
Optionally, a node class can implement the ctrlWidget() method, which must return a QWidget (usually containing other widgets) that will be displayed in the flowchart control panel. Some nodes implement fairly complex control widgets, but most nodes follow a simple form-like pattern: a list of parameter names and a single value (represented as spin box, check box, etc..) for each parameter. To make this easier, the CtrlNode subclass allows you to instead define a simple data structure that CtrlNode will use to automatically generate the control widget. """
|
||||
|
||||
sigOutputChanged = QtCore.Signal(object) # self
|
||||
sigClosed = QtCore.Signal(object)
|
||||
@ -23,6 +35,31 @@ class Node(QtCore.QObject):
|
||||
|
||||
|
||||
def __init__(self, name, terminals=None, allowAddInput=False, allowAddOutput=False, allowRemove=True):
|
||||
"""
|
||||
============== ============================================================
|
||||
Arguments
|
||||
name The name of this specific node instance. It can be any
|
||||
string, but must be unique within a flowchart. Usually,
|
||||
we simply let the flowchart decide on a name when calling
|
||||
Flowchart.addNode(...)
|
||||
terminals Dict-of-dicts specifying the terminals present on this Node.
|
||||
Terminal specifications look like::
|
||||
|
||||
'inputTerminalName': {'io': 'in'}
|
||||
'outputTerminalName': {'io': 'out'}
|
||||
|
||||
There are a number of optional parameters for terminals:
|
||||
multi, pos, renamable, removable, multiable, bypass. See
|
||||
the Terminal class for more information.
|
||||
allowAddInput bool; whether the user is allowed to add inputs by the
|
||||
context menu.
|
||||
allowAddOutput bool; whether the user is allowed to add outputs by the
|
||||
context menu.
|
||||
allowRemove bool; whether the user is allowed to remove this node by the
|
||||
context menu.
|
||||
============== ============================================================
|
||||
|
||||
"""
|
||||
QtCore.QObject.__init__(self)
|
||||
self._name = name
|
||||
self._bypass = False
|
||||
@ -52,15 +89,25 @@ class Node(QtCore.QObject):
|
||||
return name2
|
||||
|
||||
def addInput(self, name="Input", **args):
|
||||
"""Add a new input terminal to this Node with the given name. Extra
|
||||
keyword arguments are passed to Terminal.__init__.
|
||||
|
||||
This is a convenience function that just calls addTerminal(io='in', ...)"""
|
||||
#print "Node.addInput called."
|
||||
return self.addTerminal(name, io='in', **args)
|
||||
|
||||
def addOutput(self, name="Output", **args):
|
||||
"""Add a new output terminal to this Node with the given name. Extra
|
||||
keyword arguments are passed to Terminal.__init__.
|
||||
|
||||
This is a convenience function that just calls addTerminal(io='out', ...)"""
|
||||
return self.addTerminal(name, io='out', **args)
|
||||
|
||||
def removeTerminal(self, term):
|
||||
## term may be a terminal or its name
|
||||
"""Remove the specified terminal from this Node. May specify either the
|
||||
terminal's name or the terminal itself.
|
||||
|
||||
Causes sigTerminalRemoved to be emitted."""
|
||||
if isinstance(term, Terminal):
|
||||
name = term.name()
|
||||
else:
|
||||
@ -80,7 +127,9 @@ class Node(QtCore.QObject):
|
||||
|
||||
|
||||
def terminalRenamed(self, term, oldName):
|
||||
"""Called after a terminal has been renamed"""
|
||||
"""Called after a terminal has been renamed
|
||||
|
||||
Causes sigTerminalRenamed to be emitted."""
|
||||
newName = term.name()
|
||||
for d in [self.terminals, self._inputs, self._outputs]:
|
||||
if oldName not in d:
|
||||
@ -92,6 +141,10 @@ class Node(QtCore.QObject):
|
||||
self.sigTerminalRenamed.emit(term, oldName)
|
||||
|
||||
def addTerminal(self, name, **opts):
|
||||
"""Add a new terminal to this Node with the given name. Extra
|
||||
keyword arguments are passed to Terminal.__init__.
|
||||
|
||||
Causes sigTerminalAdded to be emitted."""
|
||||
name = self.nextTerminalName(name)
|
||||
term = Terminal(self, name, **opts)
|
||||
self.terminals[name] = term
|
||||
@ -105,38 +158,60 @@ class Node(QtCore.QObject):
|
||||
|
||||
|
||||
def inputs(self):
|
||||
"""Return dict of all input terminals.
|
||||
Warning: do not modify."""
|
||||
return self._inputs
|
||||
|
||||
def outputs(self):
|
||||
"""Return dict of all output terminals.
|
||||
Warning: do not modify."""
|
||||
return self._outputs
|
||||
|
||||
def process(self, **kargs):
|
||||
"""Process data through this node. Each named argument supplies data to the corresponding terminal."""
|
||||
"""Process data through this node. This method is called any time the flowchart
|
||||
wants the node to process data. It will be called with one keyword argument
|
||||
corresponding to each input terminal, and must return a dict mapping the name
|
||||
of each output terminal to its new value.
|
||||
|
||||
This method is also called with a 'display' keyword argument, which indicates
|
||||
whether the node should update its display (if it implements any) while processing
|
||||
this data. This is primarily used to disable expensive display operations
|
||||
during batch processing.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def graphicsItem(self):
|
||||
"""Return a (the?) graphicsitem for this node"""
|
||||
#print "Node.graphicsItem called."
|
||||
"""Return the GraphicsItem for this node. Subclasses may re-implement
|
||||
this method to customize their appearance in the flowchart."""
|
||||
if self._graphicsItem is None:
|
||||
#print "Creating NodeGraphicsItem..."
|
||||
self._graphicsItem = NodeGraphicsItem(self)
|
||||
#print "Node.graphicsItem is returning ", self._graphicsItem
|
||||
return self._graphicsItem
|
||||
|
||||
## this is just bad planning. Causes too many bugs.
|
||||
def __getattr__(self, attr):
|
||||
"""Return the terminal with the given name"""
|
||||
if attr not in self.terminals:
|
||||
raise AttributeError(attr)
|
||||
else:
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
print("Warning: use of node.terminalName is deprecated; use node['terminalName'] instead.")
|
||||
return self.terminals[attr]
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
#return getattr(self, item)
|
||||
"""Return the terminal with the given name"""
|
||||
if item not in self.terminals:
|
||||
raise KeyError(item)
|
||||
else:
|
||||
return self.terminals[item]
|
||||
|
||||
def name(self):
|
||||
"""Return the name of this node."""
|
||||
return self._name
|
||||
|
||||
def rename(self, name):
|
||||
"""Rename this node. This will cause sigRenamed to be emitted."""
|
||||
oldName = self._name
|
||||
self._name = name
|
||||
#self.emit(QtCore.SIGNAL('renamed'), self, oldName)
|
||||
@ -154,15 +229,25 @@ class Node(QtCore.QObject):
|
||||
return "<Node %s @%x>" % (self.name(), id(self))
|
||||
|
||||
def ctrlWidget(self):
|
||||
"""Return this Node's control widget."""
|
||||
return None
|
||||
|
||||
def bypass(self, byp):
|
||||
"""Set whether this node should be bypassed.
|
||||
|
||||
When bypassed, a Node's process() method is never called. In some cases,
|
||||
data is automatically copied directly from specific input nodes to
|
||||
output nodes instead (see the bypass argument to Terminal.__init__).
|
||||
This is usually called when the user disables a node from the flowchart
|
||||
control panel.
|
||||
"""
|
||||
self._bypass = byp
|
||||
if self.bypassButton is not None:
|
||||
self.bypassButton.setChecked(byp)
|
||||
self.update()
|
||||
|
||||
def isBypassed(self):
|
||||
"""Return True if this Node is currently bypassed."""
|
||||
return self._bypass
|
||||
|
||||
def setInput(self, **args):
|
||||
@ -179,12 +264,14 @@ class Node(QtCore.QObject):
|
||||
self.update()
|
||||
|
||||
def inputValues(self):
|
||||
"""Return a dict of all input values currently assigned to this node."""
|
||||
vals = {}
|
||||
for n, t in self.inputs().items():
|
||||
vals[n] = t.value()
|
||||
return vals
|
||||
|
||||
def outputValues(self):
|
||||
"""Return a dict of all output values currently generated by this node."""
|
||||
vals = {}
|
||||
for n, t in self.outputs().items():
|
||||
vals[n] = t.value()
|
||||
@ -195,11 +282,15 @@ class Node(QtCore.QObject):
|
||||
pass
|
||||
|
||||
def disconnected(self, localTerm, remoteTerm):
|
||||
"""Called whenever one of this node's terminals is connected elsewhere."""
|
||||
"""Called whenever one of this node's terminals is disconnected from another."""
|
||||
pass
|
||||
|
||||
def update(self, signal=True):
|
||||
"""Collect all input values, attempt to process new output values, and propagate downstream."""
|
||||
"""Collect all input values, attempt to process new output values, and propagate downstream.
|
||||
Subclasses should call update() whenever thir internal state has changed
|
||||
(such as when the user interacts with the Node's control widget). Update
|
||||
is automatically called when the inputs to the node are changed.
|
||||
"""
|
||||
vals = self.inputValues()
|
||||
#print " inputs:", vals
|
||||
try:
|
||||
@ -227,6 +318,9 @@ class Node(QtCore.QObject):
|
||||
self.sigOutputChanged.emit(self) ## triggers flowchart to propagate new data
|
||||
|
||||
def processBypassed(self, args):
|
||||
"""Called when the flowchart would normally call Node.process, but this node is currently bypassed.
|
||||
The default implementation looks for output terminals with a bypass connection and returns the
|
||||
corresponding values. Most Node subclasses will _not_ need to reimplement this method."""
|
||||
result = {}
|
||||
for term in list(self.outputs().values()):
|
||||
byp = term.bypassValue()
|
||||
@ -266,6 +360,13 @@ class Node(QtCore.QObject):
|
||||
self.graphicsItem().setPen(QtGui.QPen(QtGui.QColor(150, 0, 0), 3))
|
||||
|
||||
def saveState(self):
|
||||
"""Return a dictionary representing the current state of this node
|
||||
(excluding input / output values). This is used for saving/reloading
|
||||
flowcharts. The default implementation returns this Node's position,
|
||||
bypass state, and information about each of its terminals.
|
||||
|
||||
Subclasses may want to extend this method, adding extra keys to the returned
|
||||
dict."""
|
||||
pos = self.graphicsItem().pos()
|
||||
state = {'pos': (pos.x(), pos.y()), 'bypass': self.isBypassed()}
|
||||
termsEditable = self._allowAddInput | self._allowAddOutput
|
||||
@ -276,6 +377,8 @@ class Node(QtCore.QObject):
|
||||
return state
|
||||
|
||||
def restoreState(self, state):
|
||||
"""Restore the state of this node from a structure previously generated
|
||||
by saveState(). """
|
||||
pos = state.get('pos', (0,0))
|
||||
self.graphicsItem().setPos(*pos)
|
||||
self.bypass(state.get('bypass', False))
|
||||
|
@ -24,6 +24,8 @@ class Terminal(object):
|
||||
renamable (bool) Whether the terminal can be renamed by the user
|
||||
removable (bool) Whether the terminal can be removed by the user
|
||||
multiable (bool) Whether the user may toggle the *multi* option for this terminal
|
||||
bypass (str) Name of the terminal from which this terminal's value is derived
|
||||
when the Node is in bypass mode.
|
||||
============== =================================================================================
|
||||
"""
|
||||
self._io = io
|
||||
|
Loading…
Reference in New Issue
Block a user