Cleaned up some broken flowchart nodes

This commit is contained in:
Luke Campagnola 2012-04-22 13:06:39 -04:00
parent f8758dba39
commit 394f4d788a
3 changed files with 70 additions and 188 deletions

View File

@ -199,6 +199,7 @@ class EvalNode(Node):
self.addOutBtn = QtGui.QPushButton('+Output')
self.text = QtGui.QTextEdit()
self.text.setTabStopWidth(30)
self.text.setPlainText("# Access inputs as args['input_name']\nreturn {'output': None} ## one key per output terminal")
self.layout.addWidget(self.addInBtn, 0, 0)
self.layout.addWidget(self.addOutBtn, 0, 1)
self.layout.addWidget(self.text, 1, 0, 1, 2)
@ -208,7 +209,7 @@ class EvalNode(Node):
self.addInBtn.clicked.connect(self.addInput)
#QtCore.QObject.connect(self.addOutBtn, QtCore.SIGNAL('clicked()'), self.addOutput)
self.addOutBtn.clicked.connect(self.addOutput)
self.ui.focusOutEvent = lambda ev: self.focusOutEvent(ev)
self.text.focusOutEvent = self.focusOutEvent
self.lastText = None
def ctrlWidget(self):
@ -226,6 +227,7 @@ class EvalNode(Node):
self.lastText = text
print "eval node update"
self.update()
return QtGui.QTextEdit.focusOutEvent(self.text, ev)
def process(self, display=True, **args):
l = locals()

View File

@ -1,187 +0,0 @@
# -*- coding: utf-8 -*-
from ..Node import Node
import functions
from common import *
class Threshold(CtrlNode):
"""Absolute threshold detection filter. Returns indexes where data crosses threshold."""
nodeName = 'ThresholdDetect'
uiTemplate = [
('direction', 'combo', {'values': ['rising', 'falling'], 'index': 0}),
('threshold', 'spin', {'value': 0, 'step': 1, 'minStep': 1e-12, 'dec': True, 'range': [None, None], 'siPrefix': True}),
]
def __init__(self, name, **opts):
CtrlNode.__init__(self, name, self.uiTemplate)
def processData(self, data):
s = self.stateGroup.state()
if s['direction'] == 'rising':
d = 1
else:
d = -1
return functions.threshold(data, s['threshold'], d)
class StdevThreshold(CtrlNode):
"""Relative threshold event detection. Finds regions in data greater than threshold*stdev.
Returns a record array with columns: index, length, sum, peak.
This function is only useful for data with its baseline removed."""
nodeName = 'StdevThreshold'
uiTemplate = [
('threshold', 'spin', {'value': 0, 'step': 1, 'minStep': 0.1, 'dec': True, 'range': [None, None], 'siPrefix': True}),
]
def __init__(self, name, **opts):
CtrlNode.__init__(self, name, self.uiTemplate)
def processData(self, data):
s = self.stateGroup.state()
return functions.stdevThresholdEvents(data, s['threshold'])
class ZeroCrossingEvents(CtrlNode):
"""Detects events in a waveform by splitting the data up into chunks separated by zero-crossings,
then keeping only the ones that meet certain criteria."""
nodeName = 'ZeroCrossing'
uiTemplate = [
('minLength', 'intSpin', {'value': 0, 'min': 0, 'max': 100000}),
('minSum', 'spin', {'value': 0, 'step': 1, 'minStep': 0.1, 'dec': True, 'range': [None, None], 'siPrefix': True}),
('minPeak', 'spin', {'value': 0, 'step': 1, 'minStep': 0.1, 'dec': True, 'range': [None, None], 'siPrefix': True}),
('eventLimit', 'intSpin', {'value': 400, 'min': 1, 'max': 1e9}),
]
def __init__(self, name, **opts):
CtrlNode.__init__(self, name, self.uiTemplate)
def processData(self, data):
s = self.stateGroup.state()
events = functions.zeroCrossingEvents(data, minLength=s['minLength'], minPeak=s['minPeak'], minSum=s['minSum'])
events = events[:s['eventLimit']]
return events
class ThresholdEvents(CtrlNode):
"""Detects regions of a waveform that cross a threshold (positive or negative) and returns the time, length, sum, and peak of each event."""
nodeName = 'ThresholdEvents'
uiTemplate = [
('threshold', 'spin', {'value': 1e-12, 'step': 1, 'minStep': 0.1, 'dec': True, 'range': [None, None], 'siPrefix': True, 'tip': 'Events are detected only if they cross this threshold.'}),
('adjustTimes', 'check', {'value': True, 'tip': 'If False, then event times are reported where the trace crosses threshold. If True, the event time is adjusted to estimate when the trace would have crossed 0.'}),
#('index', 'combo', {'values':['start','peak'], 'index':0}),
('minLength', 'intSpin', {'value': 0, 'min': 0, 'max': 1e9, 'tip': 'Events must contain this many samples to be detected.'}),
('minSum', 'spin', {'value': 0, 'step': 1, 'minStep': 0.1, 'dec': True, 'range': [None, None], 'siPrefix': True}),
('minPeak', 'spin', {'value': 0, 'step': 1, 'minStep': 0.1, 'dec': True, 'range': [None, None], 'siPrefix': True, 'tip': 'Events must reach this threshold to be detected.'}),
('eventLimit', 'intSpin', {'value': 100, 'min': 1, 'max': 1e9, 'tip': 'Limits the number of events that may be detected in a single trace. This prevents runaway processes due to over-sensitive detection criteria.'}),
('deadTime', 'spin', {'value': 0, 'step': 1, 'minStep': 1e-4, 'range': [0,None], 'siPrefix': True, 'suffix': 's', 'tip': 'Ignore events that occur too quickly following another event.'}),
('reverseTime', 'spin', {'value': 0, 'step': 1, 'minStep': 1e-4, 'range': [0,None], 'siPrefix': True, 'suffix': 's', 'tip': 'Ignore events that 1) have the opposite sign of the event immediately prior and 2) occur within the given time window after the prior event. This is useful for ignoring rebound signals.'}),
]
def __init__(self, name, **opts):
CtrlNode.__init__(self, name, self.uiTemplate)
#self.addOutput('plot')
#self.remotePlot = None
#def connected(self, term, remote):
#CtrlNode.connected(self, term, remote)
#if term is not self.plot:
#return
#node = remote.node()
#node.sigPlotChanged.connect(self.connectToPlot)
#self.connectToPlot(node)
#def connectToPlot(self, node):
#if self.remotePlot is not None:
#self.remotePlot = None
#if node.plot is None:
#return
#plot = self.plot.
#def disconnected(self, term, remote):
#CtrlNode.disconnected(self, term, remote)
#if term is not self.plot:
#return
#remote.node().sigPlotChanged.disconnect(self.connectToPlot)
#self.disconnectFromPlot()
#def disconnectFromPlot(self):
#if self.remotePlot is None:
#return
#for l in self.lines:
#l.scene().removeItem(l)
#self.lines = []
def processData(self, data):
s = self.stateGroup.state()
events = functions.thresholdEvents(data, s['threshold'], s['adjustTimes'])
## apply first round of filters
mask = events['len'] >= s['minLength']
mask *= abs(events['sum']) >= s['minSum']
mask *= abs(events['peak']) >= s['minPeak']
events = events[mask]
## apply deadtime filter
mask = np.ones(len(events), dtype=bool)
last = 0
dt = s['deadTime']
rt = s['reverseTime']
for i in xrange(1, len(events)):
tdiff = events[i]['time'] - events[last]['time']
if tdiff < dt: ## check dead time
mask[i] = False
elif tdiff < rt and (events[i]['peak'] * events[last]['peak'] < 0): ## check reverse time
mask[i] = False
else:
last = i
#mask[1:] *= (events[1:]['time']-events[:-1]['time']) >= s['deadTime']
events = events[mask]
## limit number of events
events = events[:s['eventLimit']]
return events
class SpikeDetector(CtrlNode):
"""Very simple spike detector. Returns the indexes of sharp spikes by comparing each sample to its neighbors."""
nodeName = "SpikeDetect"
uiTemplate = [
('radius', 'intSpin', {'value': 1, 'min': 1, 'max': 100000}),
('minDiff', 'spin', {'value': 0, 'step': 1, 'minStep': 1e-12, 'dec': True, 'siPrefix': True}),
]
def __init__(self, name, **opts):
CtrlNode.__init__(self, name, self.uiTemplate)
def processData(self, data):
s = self.stateGroup.state()
radius = s['radius']
d1 = data.view(np.ndarray)
d2 = data[radius:] - data[:-radius] #a derivative
mask1 = d2 > s['minDiff'] #where derivative is large and positive
mask2 = d2 < -s['minDiff'] #where derivative is large and negative
maskpos = mask1[:-radius] * mask2[radius:] #both need to be true
maskneg = mask1[radius:] * mask2[:-radius]
mask = maskpos + maskneg ## All regions that match criteria
## now reduce consecutive hits to a single hit.
hits = (mask[1:] - mask[:-1]) > 0
sHits = np.argwhere(hits)[:,0]+(radius+2)
## convert to record array with 'index' column
ret = np.empty(len(sHits), dtype=[('index', int), ('time', float)])
ret['index'] = sHits
ret['time'] = data.xvals('Time')[sHits]
return ret
def processBypassed(self, args):
return {'Out': np.empty(0, dtype=[('index', int), ('time', float)])}

View File

@ -218,3 +218,70 @@ def histogramDetrend(data, window=500, bins=50, threshold=3.0):
return MetaArray(d3, info=data.infoCopy())
return d3
def concatenateColumns(data):
"""Returns a single record array with columns taken from the elements in data.
data should be a list of elements, which can be either record arrays or tuples (name, type, data)
"""
## first determine dtype
dtype = []
names = set()
maxLen = 0
for element in data:
if isinstance(element, np.ndarray):
## use existing columns
for i in range(len(element.dtype)):
name = element.dtype.names[i]
dtype.append((name, element.dtype[i]))
maxLen = max(maxLen, len(element))
else:
name, type, d = element
if type is None:
type = suggestDType(d)
dtype.append((name, type))
if isinstance(d, list) or isinstance(d, np.ndarray):
maxLen = max(maxLen, len(d))
if name in names:
raise Exception('Name "%s" repeated' % name)
names.add(name)
## create empty array
out = np.empty(maxLen, dtype)
## fill columns
for element in data:
if isinstance(element, np.ndarray):
for i in range(len(element.dtype)):
name = element.dtype.names[i]
try:
out[name] = element[name]
except:
print "Column:", name
print "Input shape:", element.shape, element.dtype
print "Output shape:", out.shape, out.dtype
raise
else:
name, type, d = element
out[name] = d
return out
def suggestDType(x):
"""Return a suitable dtype for x"""
if isinstance(x, list) or isinstance(x, tuple):
if len(x) == 0:
raise Exception('can not determine dtype for empty list')
x = x[0]
if hasattr(x, 'dtype'):
return x.dtype
elif isinstance(x, float):
return float
elif isinstance(x, int) or isinstance(x, long):
return int
#elif isinstance(x, basestring): ## don't try to guess correct string length; use object instead.
#return '<U%d' % len(x)
else:
return object