def parent(self, item): if not item.isValid(): return QtCore.QModelIndex() item = item.internalPointer() parent_item = item.parent() if (parent_item == self.root_item): return QtCore.QModelIndex() index = self.createIndex(parent_item.row(), 0, parent_item) parent_item.index = index return index
def parent(self, index): if not index.isValid(): return QtCore.QModelIndex() tp, idx = index.internalPointer() if tp == 'group': return QtCore.QModelIndex() elif tp == 'cluster': # Get groupidx from clusteridx (=idx). groupidx = self.cluster_groups[idx] return self.createIndex(groupidx, 0, ('group', groupidx)) return QtCore.QModelIndex()
class SimilarityMatrixTask(QtCore.QObject): correlationMatrixComputed = QtCore.pyqtSignal(np.ndarray, object, np.ndarray, np.ndarray, object) # def __init__(self, parent=None): # super(SimilarityMatrixTask, self).__init__(parent) def compute(self, features, clusters, cluster_groups, masks, clusters_selected, target_next=None, similarity_measure=None): log.debug("Computing correlation for clusters {0:s}.".format( str(list(clusters_selected)))) if len(clusters_selected) == 0: return {} correlations = compute_correlations(features, clusters, masks, clusters_selected, similarity_measure=similarity_measure) return correlations def compute_done(self, features, clusters, cluster_groups, masks, clusters_selected, target_next=None, similarity_measure=None, _result=None): correlations = _result self.correlationMatrixComputed.emit(np.array(clusters_selected), correlations, get_array(clusters, copy=True), get_array(cluster_groups, copy=True), target_next)
class OpenTask(QtCore.QObject): dataOpened = QtCore.pyqtSignal() dataSaved = QtCore.pyqtSignal() dataOpenFailed = QtCore.pyqtSignal(str) def open(self, loader, path): try: loader.close() loader.open(path) self.dataOpened.emit() except Exception as e: self.dataOpenFailed.emit(traceback.format_exc()) def save(self, loader): loader.save() self.dataSaved.emit()
def index(self, row, column, parent=None): if parent is None: parent = self.root_item.index if not self.hasIndex(row, column, parent): return QtCore.QModelIndex() if not parent.isValid(): parent_item = self.root_item else: parent_item = parent.internalPointer() child_item = parent_item.child(row) if child_item: index = self.createIndex(row, column, child_item) child_item.index = index return index else: return QtCore.QModelIndex()
def start_delayed(fun, delay): _timer_master = QtCore.QTimer() _timer_master.setSingleShot(True) _timer_master.setInterval(int(delay * 1000)) _timer_master.timeout.connect(wrap(fun)) _timer_master.start() return _timer_master
class OutLog(QtCore.QObject): # We use signals so that this stream object can write from another thread # and the QTextEdit widget will be updated in the main thread through # a slot connected in the main window. writeRequested = QtCore.pyqtSignal(str) def write(self, m): self.writeRequested.emit(m)
def get_application(): """Get the current QApplication, or create a new one.""" app_created = False app = QtCore.QCoreApplication.instance() if app is None: app = QtCore.QCoreApplication(sys.argv) app_created = True return app, app_created
class Buffer(QtCore.QObject): accepted = QtCore.pyqtSignal(object) def __init__(self, parent=None, delay_timer=None, delay_buffer=None): """Create a new buffer. The user can request an item at any time. The buffer will respond to the requests only after some delay where no request happened. This allows users to handle time-consuming requests smoothly, when the latency is not critical. * delay_timer: time interval during two visits. * delay_buffer: minimum time interval between two accepted requests. """ super(Buffer, self).__init__(parent) self.delay_timer = delay_timer self.delay_buffer = delay_buffer # Internal methods. # ----------------- def _accept(self): # log.debug("Accept") self.accepted.emit(self._buffer.pop()) self._last_accepted = time() self._buffer = [] def _visit(self): delay = time() - self._last_request n = len(self._buffer) # log.debug("Visit {0:d} {1:.5f}".format(n, delay)) # Only accept items that have been put after a sufficiently long # idle time. if ((n == 1 and (delay >= self.delay_buffer / 2)) or ((n >= 2) and (delay >= self.delay_buffer))): self._accept() # Public methods. # --------------- def start(self): self._buffer = [] self._last_request = 0 self._last_accepted = 0 self.timer = QtCore.QTimer(self) self.timer.setInterval(int(self.delay_timer * 1000)) self.timer.timeout.connect(self._visit) self.timer.start() def stop(self): self.timer.stop() def request(self, item): self._buffer.append(item) n = len(self._buffer) self._last_request = time()
def __init__(self, parent=None, data=None): """data is an OrderedDict""" self.parent_item = parent self.index = QtCore.QModelIndex() self.children = [] # by default: root if data is None: data = OrderedDict(name='root') self.item_data = data
def start(self): self._buffer = [] self._last_request = 0 self._last_accepted = 0 self.timer = QtCore.QTimer(self) self.timer.setInterval(int(self.delay_timer * 1000)) self.timer.timeout.connect(self._visit) self.timer.start()
def rowCount(self, parent=None): if parent is None: parent = QtCore.QModelIndex() if parent.column() > 0: return 0 if not parent.isValid(): parent_item = self.root_item else: parent_item = parent.internalPointer() return parent_item.rowCount()
def index(self, row, column, parent=None): if parent is None: parent = QtCore.QModelIndex() data = parent.internalPointer() if data is None: return self.createIndex(row, column, ('group', row)) else: tp, idx = data assert tp == 'cluster' return self.createIndex(row, column, ('cluster', idx))
class ReclusterTask(QtCore.QObject): reclusterDone = QtCore.pyqtSignal(int, object, object, object, object) def recluster(self, exp, channel_group=0, clusters=None, wizard=None): spikes, clu = run_klustakwik(exp, channel_group=channel_group, clusters=clusters) return spikes, clu def recluster_done(self, exp, channel_group=0, clusters=None, wizard=None, _result=None): spikes, clu = _result self.reclusterDone.emit(channel_group, clusters, spikes, clu, wizard)
class SelectionTask(QtCore.QObject): selectionDone = QtCore.pyqtSignal(object, bool, int) def set_loader(self, loader): self.loader = loader def select(self, clusters, wizard, channel_group=0): self.loader.select(clusters=clusters) def select_done(self, clusters, wizard, channel_group=0, _result=None): self.selectionDone.emit(clusters, wizard, channel_group)
class TestWindow(QtGui.QMainWindow): operatorStarted = QtCore.pyqtSignal(int) def __init__(self): super(TestWindow, self).__init__() self.setFocusPolicy(QtCore.Qt.WheelFocus) self.setMouseTracking(True) self.setWindowTitle("KlustaViewa") self.view = view_class(self, getfocus=False) self.view.set_data(**kwargs) self.setCentralWidget(self.view) self.move(100, 100) self.show() # Start "operator" asynchronously in the main thread. if operators: self.operator_list = operators self.operatorStarted.connect(self.operator) self._thread = threading.Thread(target=self._run_operator) self._thread.start() def _run_operator(self): for i in xrange(len(self.operator_list)): # Call asynchronously operation #i, after a given delay. if type(self.operator_list[i]) == tuple: dt = self.operator_list[i][1] else: # Default delay. dt = USERPREF['test_operator_delay'] or .1 time.sleep(dt) self.operatorStarted.emit(i) def operator(self, i): # Execute operation #i. if type(self.operator_list[i]) == tuple: fun = self.operator_list[i][0] else: fun = self.operator_list[i] fun(self) def keyPressEvent(self, e): super(TestWindow, self).keyPressEvent(e) self.view.keyPressEvent(e) if e.key() == QtCore.Qt.Key_Q: self.close() def keyReleaseEvent(self, e): super(TestWindow, self).keyReleaseEvent(e) self.view.keyReleaseEvent(e) def closeEvent(self, e): if operators: self._thread.join() return super(TestWindow, self).closeEvent(e)
def __init__(self, group_sizes={}, cluster_groups=None): """ Arguments: * group_sizes: a dictionary {groupidx: nclusters} that gives the number of clusters in each group. * cluster_groups: an object such that cluster_groups[clusteridx] returns the groupidx that contains that cluster. """ QtCore.QAbstractItemModel.__init__(self) self.group_sizes = group_sizes self.cluster_groups = cluster_groups self.beginInsertRows(QtCore.QModelIndex(), 0, len(self.group_sizes)) self.endInsertRows()
def test_buffer(): app, app_created = get_application() test = BufferTest() # Launch the application. timer = QtCore.QTimer() timer.setSingleShot(True) timer.setInterval(100) timer.start() timer.timeout.connect(test.main) app.exec_() # print test.accepted_list assert test.accepted_list[0] == 0 assert test.accepted_list[-1] == 13
def create_feature_widget(self, coord=0): # coord => (channel, feature) self.projection = [(0, 0), (0, 1)] hbox = QtGui.QHBoxLayout() hbox.setSpacing(0) # HACK: pyside does not have this function if hasattr(hbox, 'setMargin'): hbox.setMargin(0) # channel selection comboBox = QtGui.QComboBox(self) comboBox.setEditable(True) comboBox.setMaximumWidth(100) comboBox.setInsertPolicy(QtGui.QComboBox.NoInsert) comboBox.addItems(["%d" % i for i in self.channels]) comboBox.addItems(["Extra %d" % i for i in xrange(self.nextrafet)]) comboBox.editTextChanged.connect(partial(self.select_channel, coord)) # comboBox.setFocusPolicy(QtCore.Qt.ClickFocus) self.channel_box[coord] = comboBox hbox.addWidget(comboBox) # create 3 buttons for selecting the feature widths = [30] * self.fetdim labels = ['PC%d' % i for i in xrange(1, self.fetdim + 1)] hbox.addSpacing(10) # ensure exclusivity of the group of buttons pushButtonGroup = QtGui.QButtonGroup(self) for i in xrange(len(labels)): # selecting feature i pushButton = QtGui.QPushButton(labels[i], self) pushButton.setCheckable(True) if coord == i: pushButton.setChecked(True) pushButton.setMaximumSize(QtCore.QSize(widths[i], 20)) pushButton.clicked.connect(partial(self.select_feature, coord, i)) pushButtonGroup.addButton(pushButton, i) self.feature_buttons[coord][i] = pushButton hbox.addWidget(pushButton) return hbox
def create_widget(self): box = QtGui.QHBoxLayout() if hasattr(box, 'setMargin'): box.setContentsMargins(QtCore.QMargins(10, 2, 10, 2)) # box.addSpacing(10) # coord => channel combo box self.channel_box = [None, None] # coord => (butA, butB, butC) self.feature_buttons = [[None] * self.fetdim, [None] * self.fetdim] # add feature widget self.feature_widget1 = self.create_feature_widget(0) box.addLayout(self.feature_widget1) box.addSpacing(10) # Switch button. # button = QtGui.QPushButton('Flip', self) button = QtGui.QPushButton(self) button.setIcon(get_icon('flip')) button.setMaximumWidth(40) button.clicked.connect(self.flip_projections_callback) box.addWidget(button) box.addSpacing(10) # add feature widget self.feature_widget2 = self.create_feature_widget(1) box.addLayout(self.feature_widget2) # box.addSpacing(10) self.setTabOrder(self.channel_box[0], self.channel_box[1]) # self.setMaximumWidth(300) # self.setMaximumHeight(80) return box
class CorrelogramsTask(QtCore.QObject): correlogramsComputed = QtCore.pyqtSignal(np.ndarray, object, int, float, object) # def __init__(self, parent=None): # super(CorrelogramsTask, self).__init__(parent) def compute(self, spiketimes, clusters, clusters_to_update=None, clusters_selected=None, ncorrbins=None, corrbin=None, wizard=None): log.debug("Computing correlograms for clusters {0:s}.".format( str(list(clusters_to_update)))) if len(clusters_to_update) == 0: return {} clusters_to_update = np.array(clusters_to_update, dtype=np.int32) correlograms = compute_correlograms( spiketimes, clusters, clusters_to_update=clusters_to_update, ncorrbins=ncorrbins, corrbin=corrbin) return correlograms def compute_done(self, spiketimes, clusters, clusters_to_update=None, clusters_selected=None, ncorrbins=None, corrbin=None, wizard=None, _result=None): correlograms = _result self.correlogramsComputed.emit(np.array(clusters_selected), correlograms, ncorrbins, corrbin, wizard)
class TestTasks(QtCore.QObject): squareDone = QtCore.pyqtSignal(int) # Constructor # ----------- def __init__(self, parent=None): super(TestTasks, self).__init__(parent) # Square task # ----------- def square(self, x): time.sleep(.1) return x * x def square_done(self, x, _result=None): self.squareDone.emit(_result) # Operation task # -------------- def operation(self, x, y, coeff=1): time.sleep(.1) return coeff * (x + y)
class ViewDockWidget(QtGui.QDockWidget): closed = QtCore.pyqtSignal(object) def __init__(self, parent): super(ViewDockWidget, self).__init__(parent) self.setFocusPolicy(QtCore.Qt.WheelFocus) def closeEvent(self, e): self.closed.emit(self) super(ViewDockWidget, self).closeEvent(e) def keyPressEvent(self, e): super(ViewDockWidget, self).keyPressEvent(e) # Notify the main window of the key events when the dock widget # is floating. if self.isFloating(): self.parent().keyPressEvent(e) def keyReleaseEvent(self, e): super(ViewDockWidget, self).keyReleaseEvent(e) # Notify the main window of the key events when the dock widget # is floating. if self.isFloating(): self.parent().keyReleaseEvent(e)
def minimumSizeHint(self): return QtCore.QSize(50, 24)
class ChannelViewModel(TreeModel): headers = ['Channel', 'Color'] channelsMoved = QtCore.pyqtSignal(np.ndarray, int) def __init__(self, **kwargs): """Initialize the tree model. Arguments: * channels: a Nspikes long array with the channel index for each spike. * channels_info: an Info object with fields names, colors, groups_info. """ super(ChannelViewModel, self).__init__(self.headers) self.background = {} self.load(**kwargs) # I/O methods # ----------- def load(self, channel_colors=None, channel_groups=None, channel_names=None, group_colors=None, group_names=None, background={}): if group_names is None or channel_colors is None: return # Create the tree. # go through all groups for groupidx, groupname in group_names.iteritems(): groupitem = self.add_group_node(groupidx=groupidx, name=groupname, color=select( group_colors, groupidx)) # go through all channels for channelidx, color in channel_colors.iteritems(): # add channel bgcolor = background.get(channelidx, None) channelitem = self.add_channel(name=channel_names[channelidx], channelidx=channelidx, color=color, bgcolor=None, parent=self.get_group( select(channel_groups, channelidx))) # Data methods # ------------ def headerData(self, section, orientation, role): if (orientation == QtCore.Qt.Horizontal) and (role == QtCore.Qt.DisplayRole): return self.headers[section] def data(self, index, role): item = index.internalPointer() col = index.column() # group item if type(item) == GroupItem: if col == 0: if role == QtCore.Qt.DisplayRole: return str(item.name()) # color elif col == self.columnCount() - 1: if role == QtCore.Qt.BackgroundRole: if item.color() >= 0: color = np.array(COLORMAP[item.color()]) * 255 return QtGui.QColor(*color) elif role == QtCore.Qt.DisplayRole: return "" # channel item if type(item) == ChannelItem: # name if col == 0: if role == QtCore.Qt.DisplayRole: return str(item.name()) elif role == QtCore.Qt.BackgroundRole: if item.bgcolor is None: return elif item.bgcolor == 'candidate': color = np.array(COLORMAP[item.color()]) * 255 return QtGui.QColor(color[0], color[1], color[2], 90) elif item.bgcolor == 'target': # return QtGui.QColor(177, 177, 177, 255) color = np.array(COLORMAP[item.color()]) * 255 return QtGui.QColor(color[0], color[1], color[2], 255) elif role == QtCore.Qt.ForegroundRole: if item.bgcolor is None: return QtGui.QColor(177, 177, 177, 255) elif item.bgcolor == 'target': return QtGui.QColor(0, 0, 0, 255) # color elif col == self.columnCount() - 1: if role == QtCore.Qt.BackgroundRole: color = np.array(COLORMAP[item.color()]) * 255 return QtGui.QColor(*color) # default if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: return item.data(col) if role == QtCore.Qt.ForegroundRole: return QtGui.QColor(177, 177, 177, 255) def setData(self, index, data, role=None): if role is None: role = QtCore.Qt.EditRole if index.isValid() and role == QtCore.Qt.EditRole: item = index.internalPointer() if index.column() == 0: item.item_data['name'] = data elif index.column() == 1: item.item_data['color'] = data self.dataChanged.emit(index, index) return True def flags(self, index): if not index.isValid(): return QtCore.Qt.ItemIsEnabled return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | \ QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled def set_background(self, background=None): """Set the background of some channels. The argument is a dictionary channelidx ==> color index.""" if background is not None: # Record the changed channels. self.background.update(background) # Get all channels to update. keys = self.background.keys() # Reset the keys if not background: self.background = {} # If background is None, use the previous one. else: background = self.background keys = self.background.keys() for channelidx in keys: bgcolor = background.get(channelidx, None) groupidx = self.get_groupidx(channelidx) # If the channel does not exist yet in the view, just discard it. if groupidx is None: continue group = self.get_group(groupidx) channel = self.get_channel(channelidx) index = self.index(channel.row(), 0, parent=group.index) index1 = self.index(channel.row(), 1, parent=group.index) if index.isValid(): item = index.internalPointer() # bgcolor = True means using the same color # if bgcolor is True: # bgcolor = item.color() item.bgcolor = bgcolor self.dataChanged.emit(index, index1) # Action methods # -------------- def add_group(self, name, color): """Add a group.""" groupidx = max([group.groupidx() for group in self.get_groups()]) + 1 # Add the group in the tree. groupitem = self.add_group_node(groupidx=groupidx, name=name, color=color) return groupitem def add_group_node(self, **kwargs): return self.add_node(item_class=GroupItem, **kwargs) def remove_group(self, group): """Remove an empty group. Raise an error if the group is not empty.""" groupidx = group.groupidx() # check that the group is empty if self.get_channels_in_group(groupidx): raise ValueError("group %d is not empty, unable to delete it" % \ groupidx) groups = [g for g in self.get_groups() if g.groupidx() == groupidx] if groups: group = groups[0] self.remove_node(group) else: log.warn("Group %d does not exist0" % groupidx) def add_channel(self, parent=None, **kwargs): channel = self.add_node(item_class=ChannelItem, parent=parent, **kwargs) return channel def move_channels(self, sources, target): # Get the groupidx if the target is a group, if type(target) == GroupItem: groupidx = target.groupidx() target = None # else, if it is a channel, take the corresponding group. elif type(target) == ChannelItem: groupidx = self.get_groupidx(target.channelidx()) else: return None # Move channels. target_group = self.get_group(groupidx) for node in sources: self._move_channel(node, target_group, target) self.update_group_sizes() def _move_channel(self, channel, parent_target, child_target=None): row = channel.row() parent_source = channel.parent() # Find the row where the channel needs to be inserted. if child_target is not None: child_target_row = child_target.row() else: child_target_row = parent_target.rowCount() # Begin moving the row. canmove = self.beginMoveRows(parent_source.index, row, row, parent_target.index, child_target_row) if canmove: # Create a new channel, clone of the old one. channel_new = ChannelItem(parent=parent_target, channelidx=channel.channelidx(), name=channel.name(), color=channel.color()) # Create the index. channel_new.index = self.createIndex(child_target_row, 0, channel_new) # Insert the new channel. parent_target.insertChild(channel_new, child_target_row) # Remove the old channel. if parent_target == parent_source: if child_target_row < row: row += 1 parent_source.removeChildAt(row) else: parent_source.removeChild(channel) self.endMoveRows() # Drag and drop for moving channels # --------------------------------- def drag(self, target, sources): # Get source ChannelItem nodes. source_items = [ node for node in self.all_nodes() if (str(node) in sources and type(node) == ChannelItem) ] # Find the target group. if type(target) == GroupItem: groupidx = target.groupidx() # else, if it is a channel, take the corresponding group. elif type(target) == ChannelItem: groupidx = self.get_groupidx(target.channelidx()) # Emit internal signal to let TreeView emit a public signal, and to # effectively move the channels. self.channelsMoved.emit( np.array([channel.channelidx() for channel in source_items]), groupidx) def rename_channel_group(self, group, name): self.setData(self.index(group.row(), 0), name) def rename_channel(self, channel, name): groupidx = self.get_groupidx(channel.channelidx()) group = self.get_group(groupidx) self.setData(self.index(channel.row(), 0, parent=group.index), name) def change_group_color(self, group, color): self.setData(self.index(group.row(), 2), color) def change_channel_color(self, channel, color): groupidx = self.get_groupidx(channel.channelidx()) group = self.get_group(groupidx) self.setData(self.index(channel.row(), 2, parent=group.index), color) # Getter methods # -------------- def get_groups(self): return [group for group in self.get_descendants(self.root_item) \ if (type(group) == GroupItem)] def get_group(self, groupidx): return [group for group in self.get_descendants(self.root_item) \ if (type(group) == GroupItem) and \ (group.groupidx() == groupidx)][0] def get_channels(self): return [channel for channel in self.get_descendants(self.root_item) \ if (type(channel) == ChannelItem)] def get_channel(self, channelidx): l = [channel for channel in self.get_descendants(self.root_item) \ if (type(channel) == ChannelItem) and \ (channel.channelidx() == channelidx)] if l: return l[0] def get_channels_in_group(self, groupidx): group = self.get_group(groupidx) return [channel for channel in self.get_descendants(group) \ if (type(channel) == ChannelItem)] def get_groupidx(self, channelidx): """Return the group index currently assigned to the specifyed channel index.""" for group in self.get_groups(): channelindices = [channel.channelidx() \ for channel in self.get_channels_in_group(group.groupidx())] if channelidx in channelindices: return group.groupidx() return None
def qInitResources(): QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
def qCleanupResources(): QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
class Loader(QtCore.QObject): progressReported = QtCore.pyqtSignal(int, int) saveProgressReported = QtCore.pyqtSignal(int, int) # Progress report # --------------- def report_progress(self, index, count): self.progressReported.emit(index, count) def report_progress_save(self, index, count): self.saveProgressReported.emit(index, count) # Initialization methods # ---------------------- def __init__(self, parent=None, filename=None, userpref=None): """Initialize a Loader object for loading Klusters-formatted files. Arguments: * filename: the full path of any file belonging to the same dataset. """ super(Loader, self).__init__() self.spikes_selected = None self.clusters_selected = None self.override_color = False if not userpref: # HACK: if no UserPref is given in argument to the loader, # use a mock dictionary returning None all the time. class MockDict(object): def __getitem__(self, name): return None userpref = MockDict() self.userpref = userpref if filename: self.filename = filename self.open(self.filename) def open(self, filename=None): """Open everything.""" pass def open_spikes(self): """Open just spike-related information.""" def open_traces(self): """Open just trace information.""" def open_aesthetic(self): """Open aesthetic visualization-related information.""" # Input-Output methods # -------------------- def read(self): pass def save(self): pass def close(self): pass # Access to the data: spikes # -------------------------- def select(self, spikes=None, clusters=None): if clusters is not None: spikes = get_spikes_in_clusters(clusters, self.clusters) self.spikes_selected = spikes self.clusters_selected = clusters def unselect(self): self.select(spikes=None, clusters=None) def get_clusters_selected(self): return self.clusters_selected def has_selection(self): return self.clusters_selected is not None and len(self.clusters_selected) > 0 def get_clusters_unique(self): return self.clusters_unique def get_features(self, spikes=None, clusters=None): if clusters is not None: spikes = get_spikes_in_clusters(clusters, self.clusters) if spikes is None: spikes = self.spikes_selected return select(self.features, spikes) def get_features_background(self): return self.features def get_some_features(self, clusters=None): """Return the features for a subset of all spikes: a large number of spikes from any cluster, and a controlled subset of the selected clusters.""" if clusters is None: clusters = self.clusters_selected if clusters is not None: spikes_background = get_some_spikes(self.clusters, nspikes_max=self.userpref['features_nspikes_background_max'],) spikes_clusters = get_some_spikes_in_clusters( clusters, self.clusters, counter=self.counter, nspikes_max_expected=self.userpref[ 'features_nspikes_selection_max'], nspikes_per_cluster_min=self.userpref[ 'features_nspikes_per_cluster_min']) spikes = np.union1d(spikes_background, spikes_clusters) else: spikes = self.spikes_selected return select(self.features, spikes) def get_spiketimes(self, spikes=None, clusters=None): if clusters is not None: spikes = get_spikes_in_clusters(clusters, self.clusters) if spikes is None: spikes = self.spikes_selected spiketimes = getattr(self, 'spiketimes', getattr(self, 'spiketimes_res', None)) return select(spiketimes, spikes) def get_clusters(self, spikes=None, clusters=None): if clusters is not None: spikes = get_spikes_in_clusters(clusters, self.clusters) if spikes is None: spikes = self.spikes_selected return select(self.clusters, spikes) def get_masks(self, spikes=None, full=None, clusters=None): if clusters is not None: spikes = get_spikes_in_clusters(clusters, self.clusters) if spikes is None: spikes = self.spikes_selected if not full: masks = self.masks else: masks = self.masks_full return select(masks, spikes) def get_waveforms(self, spikes=None, clusters=None): if spikes is not None: return select(self.waveforms, spikes) else: if clusters is None: clusters = self.clusters_selected if clusters is not None: spikes = get_some_spikes_in_clusters(clusters, self.clusters, counter=self.counter, nspikes_max_expected=self.userpref['waveforms_nspikes_max_expected'], nspikes_per_cluster_min=self.userpref['waveforms_nspikes_per_cluster_min']) else: spikes = self.spikes_selected return select(self.waveforms, spikes) def get_dat(self): return self.dat def get_spikes(self, clusters=None): if clusters is None: clusters = self.clusters_selected return get_indices(self.get_clusters(clusters=clusters)) def get_duration(self): return self.duration # Access to the data: clusters # ---------------------------- def get_cluster_colors(self, clusters=None, can_override=True, ): if clusters is None: clusters = self.clusters_selected if can_override and self.override_color: group_colors = get_array(self.get_group_colors('all')) groups = get_array(self.get_cluster_groups('all')) colors = pd.Series(group_colors[groups], index=self.get_clusters_unique()) else: colors = self.cluster_colors return select(colors, clusters) def get_cluster_color(self, cluster): try: return select(self.cluster_colors, cluster) except IndexError: return 0 def get_cluster_groups(self, clusters=None): if clusters is None: clusters = self.clusters_selected return select(self.cluster_groups, clusters) def get_group_colors(self, groups=None): return select(self.group_colors, groups) def get_group_names(self, groups=None): return select(self.group_names, groups) def get_cluster_sizes(self, clusters=None): if clusters is None: clusters = self.clusters_selected # counter = Counter(self.clusters) sizes = pd.Series(self.counter, dtype=np.int32) return select(sizes, clusters) # Access to the data: channels # ---------------------------- def get_channel_colors(self, channels=None, can_override=True, ): if channels is None: channels = self.channels_selected if can_override and self.override_color: channel_group_colors = get_array(self.get_channel_group_colors('all')) channel_groups = get_array(self.get_channel_groups('all')) colors = pd.Series(channel_group_colors[channel_groups], index=self.channels) else: colors = self.channel_colors return select(colors, channels) def get_channel_color(self, channel): try: return select(self.channel_colors, channel) except IndexError: return 0 def get_channel_names(self, channels=None): return select(self.channel_names, channels) def get_channels_visible(self, channels=None): return select(self.channels_visible, channels) def get_channel_groups(self, channels=None): if channels is None: channels = self.channels_selected return select(self.channel_groups, channels) def get_channel_groups_visible(self, channel_groups=None): return select(self.channel_groups_visible, channel_groups) def get_channel_group_colors(self, channel_groups=None): return select(self.channel_group_colors, channel_groups) def get_channel_group_names(self, channel_groups=None): return select(self.channel_group_names, channel_groups) # Access to the data: channel traces # ---------------------------------- def get_traces(self): return select(self.raw) def get_freq(self): return select(self.freq) # Access to the data: misc # ------------------------ def get_probe(self): return self.probe def get_probe_geometry(self): return self.probe def get_new_clusters(self, n=1): return self.clusters.max() + np.arange(1, n + 1, dtype=np.int32) def get_next_cluster(self, cluster): cluster_groups = self.get_cluster_groups('all') group = get_array(self.get_cluster_groups(cluster)) clusters = get_indices(cluster_groups) cluster_groups = get_array(cluster_groups) samegroup = (cluster_groups == group) & (clusters > cluster) i = np.nonzero(samegroup)[0] if len(i) > 0: return clusters[i[0]] else: return cluster def get_new_group(self): groups = get_indices(self.group_names).values if len(groups) > 0: return groups.max() + 1 else: return 0 def set_override_color(self, override_color): self.override_color = override_color # Control methods # --------------- def _update_data(self,): """Update internal variables.""" clusters_array = get_array(self.clusters) self.clusters_unique = np.unique(clusters_array) self.nclusters = len(self.clusters_unique) bincount = np.bincount(clusters_array) self.counter = {key: bincount[key] for key in np.nonzero(bincount)[0]} # Set. def set_cluster(self, spikes, cluster): self.clusters.ix[spikes] = cluster self._update_data() def set_cluster_groups(self, clusters, group): self.cluster_groups.ix[clusters] = group def set_cluster_colors(self, clusters, color): self.cluster_colors.ix[clusters] = color def set_group_names(self, groups, name): self.group_names.ix[groups] = name def set_group_colors(self, groups, color): self.group_colors.ix[groups] = color # Add. def add_cluster(self, cluster, group, color): if cluster not in self.cluster_groups.index: self.cluster_groups = self.cluster_groups.append( pd.Series([group], index=[cluster])).sort_index() if cluster not in self.cluster_colors.index: self.cluster_colors = self.cluster_colors.append( pd.Series([color], index=[cluster])).sort_index() def add_clusters(self, clusters, groups, colors): for cluster, group, color in zip(clusters, groups, colors): self.add_cluster(cluster, group, color) def add_group(self, group, name, color): if group not in self.group_colors.index: self.group_colors = self.group_colors.append( pd.Series([color], index=[group])).sort_index() if group not in self.group_names.index: self.group_names = self.group_names.append( pd.Series([name], index=[group])).sort_index() # Remove. def remove_cluster(self, cluster): if np.any(np.in1d(cluster, self.clusters)): raise ValueError(("Cluster {0:d} is not empty and cannot " "be removed.").format(cluster)) if cluster in self.cluster_groups.index: self.cluster_groups = self.cluster_groups.drop(cluster) if cluster in self.cluster_colors.index: self.cluster_colors = self.cluster_colors.drop(cluster) def remove_group(self, group): if np.any(np.in1d(group, self.cluster_groups)): raise ValueError(("Group {0:d} is not empty and cannot " "be removed.").format(group)) if group in self.group_colors.index: self.group_colors = self.group_colors.drop(group) if group in self.group_names.index: self.group_names = self.group_names.drop(group) def remove_empty_clusters(self): clusters_all = self.cluster_groups.index clusters_in_data = self.clusters_unique clusters_empty = sorted(set(clusters_all) - set(clusters_in_data)) if len(clusters_empty) > 0: debug("Removing empty clusters {0:s}.". format(str(clusters_empty))) for cluster in clusters_empty: self.remove_cluster(cluster) return clusters_empty # Cluster and group info. def update_cluster_info(self): cluster_info = { 'color': self.cluster_colors, 'group': self.cluster_groups, } self.cluster_info = pd.DataFrame(cluster_info, dtype=np.int32) def update_group_info(self): group_info = { 'color': self.group_colors, 'name': self.group_names, } self.group_info = pd.DataFrame(group_info) # Renumber. def renumber(self): self.clusters_renumbered, self.cluster_info_renumbered = \ renumber_clusters(self.clusters, self.cluster_info)
class FeatureProjectionView(QtGui.QWidget): spikesHighlighted = QtCore.pyqtSignal(np.ndarray) spikesSelected = QtCore.pyqtSignal(np.ndarray) projectionChanged = QtCore.pyqtSignal(int, int, int) def __init__(self, parent, getfocus=None): super(FeatureProjectionView, self).__init__(parent) # Focus policy. if getfocus: self.setFocusPolicy(QtCore.Qt.WheelFocus) else: self.setFocusPolicy(QtCore.Qt.NoFocus) self.setWindowTitle('FeatureView') self.create_layout() self.show() def create_layout(self): self.projection_view = ProjectionView(self, getfocus=False) self.feature_view = FeatureView(self, getfocus=False) self.set_data() # Connect the FeatureView signal to the top-level widget signals. self.feature_view.spikesHighlighted.connect(self.spikesHighlighted) self.feature_view.spikesSelected.connect(self.spikesSelected) # Connect the bottom-level projectionChanged signals to the top-level # widget signals. self.feature_view.projectionChanged.connect(self.projectionChanged) self.projection_view.projectionChanged.connect(self.projectionChanged) # Interconnect the projectionChanged between the two bottom-level widgets. self.projection_view.projectionChanged.connect( self.projection_changed_projection_callback) self.feature_view.projectionChanged.connect( self.projection_changed_feature_callback) box = QtGui.QVBoxLayout() # HACK: pyside does not have this function if hasattr(box, 'setMargin'): box.setMargin(0) box.addWidget(self.projection_view) box.addWidget(self.feature_view) self.setLayout(box) def set_data(self, *args, **kwargs): fetdim = kwargs.get('fetdim', 3) nchannels = kwargs.get('nchannels', 1) nextrafet = kwargs.get('nextrafet', 0) self.projection_view.set_data(fetdim=fetdim, nchannels=nchannels, nextrafet=nextrafet) self.feature_view.set_data(*args, **kwargs) def projection_changed_projection_callback(self, *args): self.feature_view.set_projection(*args, do_emit=False) def projection_changed_feature_callback(self, *args): self.projection_view.set_projection(*args, do_emit=False) # FeatureView methods # ------------------- def set_wizard_pair(self, *args, **kwargs): return self.feature_view.set_wizard_pair(*args, **kwargs) def highlight_spikes(self, *args, **kwargs): return self.feature_view.highlight_spikes(*args, **kwargs) def select_spikes(self, *args, **kwargs): return self.feature_view.select_spikes(*args, **kwargs) def toggle_mask(self, *args, **kwargs): return self.feature_view.toggle_mask(*args, **kwargs) # ProjectionView methods # ---------------------- def select_feature(self, *args, **kwargs): return self.projection_view.select_feature(*args, **kwargs) def select_channel(self, *args, **kwargs): return self.projection_view.select_channel(*args, **kwargs) def get_projection(self, *args, **kwargs): return self.projection_view.get_projection(*args, **kwargs) def set_projection(self, coord, channel, feature, do_emit=True): if feature == -1: feature = self.feature_view.projection_manager.get_smart_feature( coord, channel) self.projection_view.set_projection(coord, channel, feature, do_emit=do_emit) self.feature_view.set_projection(coord, channel, feature, do_emit=do_emit) # Event methods # ------------- def keyPressEvent(self, e): super(FeatureProjectionView, self).keyPressEvent(e) [ view.keyPressEvent(e) for view in (self.feature_view, self.projection_view) ] def keyReleaseEvent(self, e): super(FeatureProjectionView, self).keyReleaseEvent(e) [ view.keyReleaseEvent(e) for view in (self.feature_view, self.projection_view) ]
class ChannelView(QtGui.QTreeView): # Signals # ------- # Selection. # The boolean indicates whether the selection has been initiated externally # or not (internally by clicking on items in the view). channelsSelected = QtCore.pyqtSignal(np.ndarray, bool) # groupsSelected = QtCore.pyqtSignal(np.ndarray) # Channel and group info. channelColorChanged = QtCore.pyqtSignal(int, int) groupColorChanged = QtCore.pyqtSignal(int, int) groupRenamed = QtCore.pyqtSignal(int, object) channelRenamed = QtCore.pyqtSignal(int, object) channelsMoved = QtCore.pyqtSignal(np.ndarray, int) groupRemoved = QtCore.pyqtSignal(int) groupAdded = QtCore.pyqtSignal(int, str, int) class ChannelDelegate(QtGui.QStyledItemDelegate): def paint(self, painter, option, index): """Disable the color column so that the color remains the same even when it is selected.""" # deactivate all columns except the first one, so that selection # is only possible in the first column if index.column() >= 1: if option.state and QtGui.QStyle.State_Selected: option.state = option.state and QtGui.QStyle.State_Off super(ChannelView.ChannelDelegate, self).paint(painter, option, index) def __init__(self, parent, getfocus=None): super(ChannelView, self).__init__(parent) # Current item. self.current_item = None self.wizard = False self.channels_selected_previous = [] # Focus policy. if getfocus: self.setFocusPolicy(QtCore.Qt.WheelFocus) else: self.setFocusPolicy(QtCore.Qt.NoFocus) self.setDragDropMode(QtGui.QAbstractItemView.InternalMove) self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) self.setAllColumnsShowFocus(True) # self.setFirstColumnSpanned(0, QtCore.QModelIndex(), True) # select full rows self.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) self.setMaximumWidth(300) # self.setRootIsDecorated(False) self.setItemDelegate(self.ChannelDelegate()) # Create menu. (hidden because it doesn't work - yet!) self.create_actions() self.create_context_menu() self.restore_geometry() # Data methods # ------------ def set_data(self, **kwargs): # if channel_colors is None: if kwargs.get('channel_colors', None) is None: return self.model = ChannelViewModel(**kwargs) self.setModel(self.model) self.expandAll() # set spkcount column size self.header().resizeSection(1, 40) self.header().resizeSection(2, 60) # # set color column size # self.header().resizeSection(3, 40) # HACK: drag is triggered in the model, so connect it to move_channels # in this function self.model.channelsMoved.connect(self.move_channels) def clear(self): self.setModel(ChannelViewModel()) # Public methods # -------------- def select(self, channels, groups=None, wizard=False): """Select multiple channels from their indices.""" self.wizard = wizard if channels is None: channels = [] if groups is None: groups = [] if isinstance(channels, (int, long, np.integer)): channels = [channels] if isinstance(groups, (int, long, np.integer)): groups = [groups] if len(channels) == len(groups) == 0: return # Convert to lists. channels = list(channels) groups = list(groups) selection_model = self.selectionModel() selection = QtGui.QItemSelection() # Select groups. for groupidx in groups: group = self.model.get_group(groupidx) if group is not None: selection.select(group.index, group.index) # Select channels. for channelidx in channels: channel = self.model.get_channel(channelidx) if channel is not None: selection.select(channel.index, channel.index) # Process selection. selection_model.select( selection, selection_model.Clear | selection_model.Current | selection_model.Select | selection_model.Rows) if len(channels) > 0: channel = self.model.get_channel(channels[-1]) if channel is not None: # Set current index in the selection. selection_model.setCurrentIndex( channel.index, QtGui.QItemSelectionModel.NoUpdate) # Scroll to that channel. self.scrollTo(channel.index) def unselect(self): self.selectionModel().clear() def move_channels(self, channels, groupidx): if not hasattr(channels, '__len__'): channels = [channels] if len(channels) == 0: return # Signal. log.debug("Moving channels {0:s} to group {1:d}.".format( str(channels), groupidx)) self.channelsMoved.emit(np.array(channels), groupidx) def add_group(self, name, channels=[]): color = random_color() group = self.model.add_group(name, color) groupidx = group.groupidx() # Signal. log.debug("Adding group {0:s}.".format(name)) self.groupAdded.emit(groupidx, name, color) # Move the selected channels to the new group. if channels: self.move_channels(channels, groupidx) self.expandAll() return groupidx def rename_channel_group(self, groupidx, name): self.model.rename_channel_group(self.model.get_group(groupidx), name) # Signal. log.debug("Rename group {0:d} to {1:s}.".format(groupidx, name)) self.groupRenamed.emit(groupidx, name) def rename_channel(self, channelidx, name): self.model.rename_channel(self.model.get_channel(channelidx), name) # Signal. log.debug("Rename channel {0:d} to {1:s}.".format(channelidx, name)) self.channelRenamed.emit(channelidx, name) def remove_group(self, groupidx): self.model.remove_group(self.model.get_group(groupidx)) # Signal. log.debug("Removed group {0:d}.".format(groupidx)) self.groupRemoved.emit(groupidx) def change_channel_color(self, channelidx, color): self.model.change_channel_color(self.model.get_channel(channelidx), color) # Signal. log.debug("Changed color of channel {0:d} to {1:d}.".format( channelidx, color)) self.channelColorChanged.emit(channelidx, color) def change_group_color(self, groupidx, color): self.model.change_group_color(self.model.get_group(groupidx), color) # Signal. log.debug("Changed color of group {0:d} to {1:d}.".format( groupidx, color)) self.groupColorChanged.emit(groupidx, color) def set_background(self, background=None): self.model.set_background(background) # Menu methods # ------------ def create_color_dialog(self): self.color_dialog = QtGui.QColorDialog(self) self.color_dialog.setOptions(QtGui.QColorDialog.DontUseNativeDialog) for i in xrange(48): if i < len(COLORMAP): rgb = COLORMAP[i] * 255 else: rgb = (255, 255, 255) # rgb = (1., 1., 1.) k = 6 * (np.mod(i, 8)) + i // 8 self.color_dialog.setStandardColor(k, QtGui.qRgb(*rgb)) def create_actions(self): self.change_color_action = QtGui.QAction("Change &color", self) self.change_color_action.triggered.connect(self.change_color_callback) self.add_group_action = QtGui.QAction("&Add group", self) self.add_group_action.triggered.connect(self.add_group_callback) self.rename_channel_group_action = QtGui.QAction("Re&name group", self) self.rename_channel_group_action.setShortcut("F2") self.rename_channel_group_action.triggered.connect( self.rename_channel_group_callback) self.rename_channel_action = QtGui.QAction("Rename c&hannel", self) self.rename_channel_action.setShortcut("F3") self.rename_channel_action.triggered.connect( self.rename_channel_callback) self.remove_group_action = QtGui.QAction("&Remove group", self) self.remove_group_action.triggered.connect(self.remove_group_callback) # Add actions to the widget. self.addAction(self.change_color_action) self.addAction(self.add_group_action) self.addAction(self.rename_channel_group_action) self.addAction(self.rename_channel_action) self.addAction(self.remove_group_action) def create_context_menu(self): self.create_color_dialog() self.context_menu = QtGui.QMenu(self) self.context_menu.addAction(self.change_color_action) self.context_menu.addSeparator() self.context_menu.addAction(self.add_group_action) self.context_menu.addAction(self.rename_channel_group_action) self.context_menu.addAction(self.rename_channel_action) self.context_menu.addAction(self.remove_group_action) def update_actions(self): channels = self.selected_channels() groups = self.selected_groups() if len(groups) > 0: self.rename_channel_group_action.setEnabled(True) # First three groups are not removable (noise and MUA and good). if 0 not in groups and 1 not in groups and 2 not in groups: self.remove_group_action.setEnabled(True) else: self.remove_group_action.setEnabled(False) else: self.rename_channel_group_action.setEnabled(False) self.remove_group_action.setEnabled(False) if len(channels) > 0: self.rename_channel_action.setEnabled(True) else: self.rename_channel_action.setEnabled(False) def contextMenuEvent(self, event): action = self.context_menu.exec_(self.mapToGlobal(event.pos())) def currentChanged(self, index, previous): self.current_item = index.internalPointer() # Callback # -------- def change_color_callback(self, checked=None): item = self.current_item initial_color = item.color() if initial_color >= 0: initial_color = 255 * COLORMAP[initial_color] initial_color = QtGui.QColor(*initial_color) color = QtGui.QColorDialog.getColor(initial_color) else: color = QtGui.QColorDialog.getColor() # return if the user canceled if not color.isValid(): return # get the RGB values of the chosen color rgb = np.array(color.getRgbF()[:3]).reshape((1, -1)) # take the closest color in the palette color = np.argmin(np.abs(COLORMAP[1:, :] - rgb).sum(axis=1)) + 1 # Change the color and emit the signal. if isinstance(item, ChannelItem): self.change_channel_color(item.channelidx(), color) elif isinstance(item, GroupItem): self.change_group_color(item.groupidx(), color) def add_group_callback(self, checked=None): text, ok = QtGui.QInputDialog.getText(self, "Group name", "Name group:", QtGui.QLineEdit.Normal, "New group") if ok: self.add_group(text, self.selected_channels()) def remove_group_callback(self, checked=None): item = self.current_item if isinstance(item, GroupItem): self.remove_group(item.groupidx()) def rename_channel_group_callback(self, checked=None): group = self.current_item if isinstance(group, GroupItem): groupidx = group.groupidx() name = group.name() text, ok = QtGui.QInputDialog.getText(self, "Group name", "Rename group:", QtGui.QLineEdit.Normal, name) if ok: # Rename the group. self.rename_channel_group(groupidx, text) def rename_channel_callback(self, checked=None): channel = self.current_item if isinstance(channel, ChannelItem): channelidx = channel.channelidx() name = channel.name() text, ok = QtGui.QInputDialog.getText(self, "Channel name", "Rename channel:", QtGui.QLineEdit.Normal, name) if ok: # Rename the group. self.rename_channel(channelidx, text) # Get methods # ----------- def get_channel_indices(self): return [channel.channelidx() for channel in self.model.get_channels()] def get_group_indices(self): return [group.groupidx() for group in self.model.get_groups()] def get_channel_indices_in_group(self, groupidx): return [ channel.channelidx() for channel in self.model.get_channels_in_group(groupidx) ] # Selection methods # ----------------- def selectionChanged(self, selected, deselected): super(ChannelView, self).selectionChanged(selected, deselected) selected_channels = self.selected_channels() selected_groups = self.selected_groups() # All channels in selected groups minus selected channels. channels = [ channel for group in selected_groups for channel in self.get_channel_indices_in_group(group) if channel not in selected_channels ] # Add selected channels not in selected groups. channels.extend([ channel for channel in selected_channels if (channel not in channels and self.model.get_groupidx(channel) not in selected_groups) ]) # Selected groups. group_indices = [ self.model.get_group(groupidx) for groupidx in selected_groups ] # log.debug("Selected {0:d} channels.".format(len(channels))) # log.debug("Selected channels {0:s}.".format(str(channels))) self.channelsSelected.emit(np.array(channels, dtype=np.int32), self.wizard) # if group_indices: # self.scrollTo(group_indices[-1].index) if len(self.channels_selected_previous) <= 1: if len(channels) == 1: self.scrollTo(self.model.get_channel(channels[0]).index) elif len(group_indices) == 1: self.scrollTo(group_indices[0].index) self.wizard = False self.channels_selected_previous = channels self.update_actions() # Selected items # -------------- def selected_items(self): """Return the list of selected channel indices.""" return [(v.internalPointer()) \ for v in self.selectedIndexes() \ if v.column() == 0] def selected_channels(self): """Return the list of selected channel indices.""" return [(v.internalPointer().channelidx()) \ for v in self.selectedIndexes() \ if v.column() == 0 and \ type(v.internalPointer()) == ChannelItem] def selected_groups(self): """Return the list of selected groups.""" return [(v.internalPointer().groupidx()) \ for v in self.selectedIndexes() \ if v.column() == 0 and \ type(v.internalPointer()) == GroupItem] # Event methods # ------------- def keyPressEvent(self, e): key = e.key() modif = e.modifiers() ctrl = modif & QtCore.Qt.ControlModifier shift = modif & QtCore.Qt.ShiftModifier alt = modif & QtCore.Qt.AltModifier if (ctrl and key == QtCore.Qt.Key_A): self.select(self.get_channel_indices()) else: return super(ChannelView, self).keyPressEvent(e) # Save and restore geometry # ------------------------- def save_geometry(self): SETTINGS['channel_widget.geometry'] = encode_bytearray( self.saveGeometry()) SETTINGS['channel_widget.header'] = encode_bytearray( self.header().saveState()) def restore_geometry(self): g = SETTINGS['channel_widget.geometry'] h = SETTINGS['channel_widget.header'] if g: self.restoreGeometry(decode_bytearray(g)) if h: self.header().restoreState(decode_bytearray(h)) def closeEvent(self, e): # Save the window geometry when closing the software. self.save_geometry() return super(ChannelView, self).closeEvent(e)