class BaseAnnotationItem(ResizableGraphicsItem): handler_cache = {} styles = ['font-family', 'font-size', 'text-bold', 'text-italic', 'text-underline', 'text-color', 'color-border', 'color-background'] minSize = ANNOTATION_MINIMUM_QSIZE def __init__(self, position=None, *args, **kwargs): super(BaseAnnotationItem, self).__init__(*args, **kwargs) # Config for each annotation item, holding the settings (styles, etc) # update-control via the toolbar using add_handler linking self.config = ConfigManager() self.config.updated.connect(self.applyStyleConfig) self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemIsSelectable) self.setFlag(QGraphicsItem.ItemIsFocusable) if position: self.setPos(position) self.setZValue(-1) def delete(self): self.prepareGeometryChange() self.scene().annotations.remove(self) self.scene().removeItem(self) self.removeHandlers() def keyPressEvent(self, e): if e.key() == Qt.Key_Backspace and e.modifiers() == Qt.ControlModifier: self.delete() else: return super(BaseAnnotationItem, self).keyPressEvent(e) def importStyleConfig(self, config): for k in self.styles: self.config.set(k, config.get(k)) def addHandlers(self): m = self.scene().views()[0].m # Hack; need to switch to importing this for k in self.styles: self.config.add_handler(k, m.styletoolbarwidgets[k]) def removeHandlers(self): for k in self.styles: self.config.remove_handler(k) def itemChange(self, change, value): if change == QGraphicsItem.ItemSelectedChange: if not value: self.removeHandlers() elif change == QGraphicsItem.ItemSelectedHasChanged: if value: self.addHandlers() return super(BaseAnnotationItem, self).itemChange(change, value)
class QGraphicsSceneExtend(QGraphicsScene): def __init__(self, parent, *args, **kwargs): super(QGraphicsSceneExtend, self).__init__(parent, *args, **kwargs) self.m = parent.m self.config = ConfigManager() # These config settings are transient (ie. not stored between sessions) self.config.set_defaults({ 'mode': EDITOR_MODE_NORMAL, 'font-family': 'Arial', 'font-size': '12', 'text-bold': False, 'text-italic': False, 'text-underline': False, 'text-color': '#000000', 'color-border': None, # '#000000', 'color-background': None, }) # Pre-set these values (will be used by default) self.config.set('color-background', '#5555ff') self.background_image = QImage(os.path.join(utils.scriptdir, 'icons', 'grid100.png')) if settings.get('Editor/Show_grid'): self.showGrid() else: self.hideGrid() self.mode = EDITOR_MODE_NORMAL self.mode_current_object = None self.annotations = [] def mousePressEvent(self, e): if self.config.get('mode') != EDITOR_MODE_NORMAL: for i in self.selectedItems(): i.setSelected(False) if self.config.get('mode') == EDITOR_MODE_TEXT: tw = AnnotationTextItem(position=e.scenePos()) elif self.config.get('mode') == EDITOR_MODE_REGION: tw = AnnotationRegionItem(position=e.scenePos()) elif self.config.get('mode') == EDITOR_MODE_ARROW: tw = AnnotationRegionItem(position=e.scenePos()) self.addItem(tw) self.mode_current_object = tw tw._createFromMousePressEvent(e) tw.importStyleConfig(self.config) self.annotations.append(tw) else: for i in self.selectedItems(): i.setSelected(False) super(QGraphicsSceneExtend, self).mousePressEvent(e) def mouseMoveEvent(self, e): if self.config.get('mode') == EDITOR_MODE_TEXT and self.mode_current_object: self.mode_current_object._resizeFromMouseMoveEvent(e) elif self.config.get('mode') == EDITOR_MODE_REGION and self.mode_current_object: self.mode_current_object._resizeFromMouseMoveEvent(e) else: super(QGraphicsSceneExtend, self).mouseMoveEvent(e) def mouseReleaseEvent(self, e): if self.config.get('mode'): self.mode_current_object.setSelected(True) self.mode_current_object.setFocus() self.config.set('mode', EDITOR_MODE_NORMAL) self.mode_current_object = None super(QGraphicsSceneExtend, self).mouseReleaseEvent(e) def showGrid(self): self.setBackgroundBrush(QBrush(self.background_image)) def hideGrid(self): self.setBackgroundBrush(QBrush(None)) def onSaveAsImage(self): filename, _ = QFileDialog.getSaveFileName(self.m, 'Save current figure', '', "Tagged Image File Format (*.tif);;\ Portable Network Graphics (*.png)") if filename: self.saveAsImage(filename) def saveAsImage(self, f): self.image = QImage(self.sceneRect().size().toSize(), QImage.Format_ARGB32) self.image.fill(Qt.white) painter = QPainter(self.image) self.render(painter) self.image.save(f) def addApp(self, app, position=None): i = ToolItem(self, app, position=position) self.addItem(i) #i.onShow() return i def removeApp(self, app): i = app.editorItem i.hide() self.removeItem(i) app.editorItem = None def dragEnterEvent(self, e): if e.mimeData().hasFormat('application/x-pathomx-app') or e.mimeData().hasFormat('text/uri-list'): e.accept() else: e.ignore() def dragMoveEvent(self, e): e.accept() def dropEvent(self, e): scenePos = e.scenePos() - QPointF(32, 32) if e.mimeData().hasFormat('application/x-pathomx-app'): try: app_id = str(e.mimeData().data('application/x-pathomx-app'), 'utf-8') # Python 3 except: app_id = str(e.mimeData().data('application/x-pathomx-app')) # Python 2 e.setDropAction(Qt.CopyAction) a = app_launchers[app_id](self.m, position=scenePos, auto_focus=False) #self.centerOn(a.editorItem) e.accept() elif e.mimeData().hasFormat('text/uri-list'): for ufn in e.mimeData().urls(): fn = ufn.path() fnn, ext = os.path.splitext(fn) ext = ext.strip('.') if ext in file_handlers: a = file_handlers[ext](position=scenePos, auto_focus=False, filename=fn) self.centerOn(a.editorItem) e.accept() def getXMLAnnotations(self, root): # Iterate over the entire set (in order) creating a XML representation of the MatchDef and Style for annotation in self.annotations: ase = et.SubElement(root, "Annotation") ase.set('type', type(annotation).__name__) ase.set('x', str(annotation.x())) ase.set('y', str(annotation.y())) ase.set('width', str(annotation.rect().width())) ase.set('height', str(annotation.rect().height())) if hasattr(annotation, 'text'): text = et.SubElement(ase, "Text") text.text = annotation.text.toPlainText() ase = annotation.config.getXMLConfig(ase) return root def setXMLAnnotations(self, root): ANNOTATION_TYPES = { 'AnnotationTextItem': AnnotationTextItem, 'AnnotationRegionItem': AnnotationRegionItem, } for ase in root.findall('Annotation'): # Validate the class definition before creating it if ase.get('type') in ANNOTATION_TYPES: pos = QPointF(float(ase.get('x')), float(ase.get('y'))) aobj = ANNOTATION_TYPES[ase.get('type')](position=pos) aobj.setRect(QRectF(0, 0, float(ase.get('width')), float(ase.get('height')))) to = ase.find('Text') if to is not None: aobj.text.setPlainText(to.text) self.addItem(aobj) self.annotations.append(aobj) aobj.config.setXMLConfig(ase) aobj.applyStyleConfig()
class AnnotatePeaks(GenericDialog): def __init__(self, parent, config=None, *args, **kwargs): super(AnnotatePeaks, self).__init__(parent, *args, **kwargs) self.setWindowTitle('Annotate Peaks') if config: # Copy in starting state self.config = ConfigManager() self.config.hooks.update(custom_pyqtconfig_hooks.items()) self.config.set_defaults(config) self.fwd_map_cache = {} # Correlation variables gb = QGroupBox('Peaks') vbox = QVBoxLayout() # Populate the list boxes self.lw_peaks = QListWidgetAddRemove() self.lw_peaks.setSelectionMode(QAbstractItemView.ExtendedSelection) vbox.addWidget(self.lw_peaks) vboxh = QHBoxLayout() self.add_label = QLineEdit() self.add_start = QDoubleSpinBox() self.add_start.setRange(-1, 12) self.add_start.setDecimals(3) self.add_start.setSuffix('ppm') self.add_start.setSingleStep(0.001) self.add_end = QDoubleSpinBox() self.add_end.setRange(-1, 12) self.add_end.setDecimals(3) self.add_end.setSuffix('ppm') self.add_end.setSingleStep(0.001) addc = QPushButton('Add') addc.clicked.connect(self.onPeakAdd) remc = QPushButton('Remove selected') remc.clicked.connect(self.onPeakAdd) loadc = QPushButton("Import from file") loadc.setIcon( QIcon( os.path.join(utils.scriptdir, 'icons', 'folder-open-document.png'))) loadc.clicked.connect(self.onPeakImport) metabh = QPushButton("Auto match via MetaboHunter") metabh.setIcon( QIcon(os.path.join(utils.scriptdir, 'icons', 'metabohunter.png'))) metabh.clicked.connect(self.onPeakImportMetabohunter) vboxh.addWidget(self.add_label) vboxh.addWidget(self.add_start) vboxh.addWidget(self.add_end) vboxh.addWidget(addc) vbox.addWidget(remc) vbox.addLayout(vboxh) vbox.addWidget(loadc) vbox.addWidget(metabh) gb.setLayout(vbox) self.layout.addWidget(gb) self.config.add_handler('annotation/peaks', self.lw_peaks, (self.map_list_fwd, self.map_list_rev)) self.dialogFinalise() def onPeakAdd(self): c = self.config.get( 'annotation/peaks' )[:] # Create new list to force refresh on reassign c.append((self.add_label.text(), float(self.add_start.value()), float(self.add_end.value()))) self.config.set('annotation/peaks', c) def onPeakRemove(self): i = self.lw_peaks.removeItemAt(self.lw_peaks.currentRow()) # c = self.map_list_fwd(i.text()) def onPeakImport(self): filename, _ = QFileDialog.getOpenFileName( self.parent(), 'Load peak annotations from file', '', "All compatible files (*.csv *.txt *.tsv);;Comma Separated Values (*.csv);;Plain Text Files (*.txt);;Tab Separated Values (*.tsv);;All files (*.*)" ) if filename: c = self.config.get( 'annotation/peaks' )[:] # Create new list to force refresh on reassign with open(filename, 'rU') as f: reader = csv.reader(f, delimiter=b',', dialect='excel') for row in reader: if row not in c: c.append(row[0], float(row[1]), float(row[2])) self.config.set('annotation/peaks', c) def onPeakImportMetabohunter(self): c = self.config.get( 'annotation/peaks' )[:] # Create new list to force refresh on reassign t = self.parent().current_tool dlg = MetaboHunter(self) if dlg.exec_(): if 'spc' in t.data: # We have a spectra; calcuate mean; reduce size if required spc = t.data['spc'] n = spc.data.shape[1] ppm = spc.ppm spcd = np.mean(spc.data, axis=0) # Set a hard limit on the size of data we submit to be nice. if n > 3000: # Calculate the division required to be under the limit d = np.ceil(float(n) / 3000) # Trim axis to multiple of divisor trim = (n // d) * d spcd = spcd[:trim] ppm = ppm[:trim] # Mean d shape spcd = np.mean(spcd.reshape(-1, d), axis=1) ppm = np.mean(ppm.reshape(-1, d), axis=1) # Submit with settings hmdbs = metabohunter.request( ppm, spcd, metabotype=dlg.config.get('Metabotype'), database=dlg.config.get('Database Source'), ph=dlg.config.get('Sample pH'), solvent=dlg.config.get('Solvent'), frequency=dlg.config.get('Frequency'), method=dlg.config.get('Method'), noise=dlg.config.get('Noise Threshold'), confidence=dlg.config.get('Confidence Threshold'), tolerance=dlg.config.get('Tolerance')) ha = np.array(hmdbs) unique_hmdbs = set(hmdbs) if None in unique_hmdbs: unique_hmdbs.remove(None) # Extract out regions for hmdb in unique_hmdbs: hb = np.diff(ha == hmdb) # These are needed to ensure markers are there for objects starting and ending on array edge if ha[0] == hmdb: hb[0] == True if ha[-1] == hmdb: hb[-1] == True idx = np.nonzero(hb)[0] idx = idx.reshape(-1, 2) if dlg.config.get( 'convert_hmdb_ids_to_names' ) and hmdb in METABOHUNTER_HMDB_NAME_MAP.keys(): label = METABOHUNTER_HMDB_NAME_MAP[hmdb] else: label = hmdb # Now we have an array of all start, stop positions for this item for start, stop in idx: c.append((label, ppm[start], ppm[stop])) self.config.set('annotation/peaks', c) def map_list_fwd(self, s): " Receive text name, return the indexes " return self.fwd_map_cache[s] def map_list_rev(self, x): " Receive the indexes, return the label" s = "%s\t%.2f\t%.2f" % tuple(x) self.fwd_map_cache[s] = x return s
class AnnotateClasses(GenericDialog): def __init__(self, parent, config=None, *args, **kwargs): super(AnnotateClasses, self).__init__(parent, *args, **kwargs) self.setWindowTitle('Annotate Classes') if config: # Copy in starting state self.config = ConfigManager() self.config.hooks.update(custom_pyqtconfig_hooks.items()) self.config.set_defaults(config) self.fwd_map_cache = {} # Correlation variables gb = QGroupBox('Sample classes') vbox = QVBoxLayout() # Populate the list boxes self.lw_classes = QListWidgetAddRemove() self.lw_classes.setSelectionMode(QAbstractItemView.ExtendedSelection) vbox.addWidget(self.lw_classes) vboxh = QHBoxLayout() self.add_label = QLineEdit() self.add_class = QLineEdit() addc = QPushButton('Add') addc.clicked.connect(self.onClassAdd) remc = QPushButton('Remove selected') remc.clicked.connect(self.onClassAdd) loadc = QPushButton('Import from file') loadc.setIcon( QIcon( os.path.join(utils.scriptdir, 'icons', 'folder-open-document.png'))) loadc.clicked.connect(self.onClassImport) vboxh.addWidget(self.add_label) vboxh.addWidget(self.add_class) vboxh.addWidget(addc) vbox.addWidget(remc) vbox.addLayout(vboxh) vboxh.addWidget(loadc) gb.setLayout(vbox) self.layout.addWidget(gb) self.config.add_handler('annotation/sample_classes', self.lw_classes, (self.map_list_fwd, self.map_list_rev)) self.dialogFinalise() def onClassAdd(self): c = self.config.get( 'annotation/sample_classes' )[:] # Create new list to force refresh on reassign c.append((self.add_label.text(), self.add_class.text())) self.config.set('annotation/sample_classes', c) def onClassRemove(self): i = self.lw_classes.removeItemAt(self.lw_classes.currentRow()) # c = self.map_list_fwd(i.text()) def onClassImport(self): filename, _ = QFileDialog.getOpenFileName( self.parent(), 'Load classifications from file', '', "All compatible files (*.csv *.txt *.tsv);;Comma Separated Values (*.csv);;Plain Text Files (*.txt);;Tab Separated Values (*.tsv);;All files (*.*)" ) if filename: c = self.config.get( 'annotation/sample_classes' )[:] # Create new list to force refresh on reassign with open(filename, 'rU') as f: reader = csv.reader(f, delimiter=b',', dialect='excel') for row in reader: if row not in c: c.append(row[:2]) self.config.set('annotation/sample_classes', c) def map_list_fwd(self, s): " Receive text name, return the indexes " return self.fwd_map_cache[s] def map_list_rev(self, x): " Receive the indexes, return the label" s = "%s\t%s" % tuple(x) self.fwd_map_cache[s] = x return s
class BaseAnnotationItem(ResizableGraphicsItem): handler_cache = {} styles = [ 'font-family', 'font-size', 'text-bold', 'text-italic', 'text-underline', 'text-color', 'color-border', 'color-background' ] minSize = ANNOTATION_MINIMUM_QSIZE def __init__(self, position=None, *args, **kwargs): super(BaseAnnotationItem, self).__init__(*args, **kwargs) # Config for each annotation item, holding the settings (styles, etc) # update-control via the toolbar using add_handler linking self.config = ConfigManager() self.config.updated.connect(self.applyStyleConfig) self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemIsSelectable) self.setFlag(QGraphicsItem.ItemIsFocusable) if position: self.setPos(position) self.setZValue(-1) def delete(self): self.prepareGeometryChange() self.scene().annotations.remove(self) self.scene().removeItem(self) self.removeHandlers() def keyPressEvent(self, e): if e.key() == Qt.Key_Backspace and e.modifiers() == Qt.ControlModifier: self.delete() else: return super(BaseAnnotationItem, self).keyPressEvent(e) def importStyleConfig(self, config): for k in self.styles: self.config.set(k, config.get(k)) def addHandlers(self): m = self.scene().views()[0].m # Hack; need to switch to importing this for k in self.styles: self.config.add_handler(k, m.styletoolbarwidgets[k]) def removeHandlers(self): for k in self.styles: self.config.remove_handler(k) def itemChange(self, change, value): if change == QGraphicsItem.ItemSelectedChange: if not value: self.removeHandlers() elif change == QGraphicsItem.ItemSelectedHasChanged: if value: self.addHandlers() return super(BaseAnnotationItem, self).itemChange(change, value)
class QGraphicsSceneExtend(QGraphicsScene): def __init__(self, parent, *args, **kwargs): super(QGraphicsSceneExtend, self).__init__(parent, *args, **kwargs) self.m = parent.m self.config = ConfigManager() # These config settings are transient (ie. not stored between sessions) self.config.set_defaults({ 'mode': EDITOR_MODE_NORMAL, 'font-family': 'Arial', 'font-size': '12', 'text-bold': False, 'text-italic': False, 'text-underline': False, 'text-color': '#000000', 'color-border': None, # '#000000', 'color-background': None, }) # Pre-set these values (will be used by default) self.config.set('color-background', '#5555ff') self.background_image = QImage( os.path.join(utils.scriptdir, 'icons', 'grid100.png')) if settings.get('Editor/Show_grid'): self.showGrid() else: self.hideGrid() self.mode = EDITOR_MODE_NORMAL self.mode_current_object = None self.annotations = [] def mousePressEvent(self, e): if self.config.get('mode') != EDITOR_MODE_NORMAL: for i in self.selectedItems(): i.setSelected(False) if self.config.get('mode') == EDITOR_MODE_TEXT: tw = AnnotationTextItem(position=e.scenePos()) elif self.config.get('mode') == EDITOR_MODE_REGION: tw = AnnotationRegionItem(position=e.scenePos()) elif self.config.get('mode') == EDITOR_MODE_ARROW: tw = AnnotationRegionItem(position=e.scenePos()) self.addItem(tw) self.mode_current_object = tw tw._createFromMousePressEvent(e) tw.importStyleConfig(self.config) self.annotations.append(tw) else: super(QGraphicsSceneExtend, self).mousePressEvent(e) def mouseMoveEvent(self, e): if self.config.get( 'mode') == EDITOR_MODE_TEXT and self.mode_current_object: self.mode_current_object._resizeFromMouseMoveEvent(e) elif self.config.get( 'mode') == EDITOR_MODE_REGION and self.mode_current_object: self.mode_current_object._resizeFromMouseMoveEvent(e) else: super(QGraphicsSceneExtend, self).mouseMoveEvent(e) def mouseReleaseEvent(self, e): if self.config.get('mode'): self.mode_current_object.setSelected(True) self.mode_current_object.setFocus() self.config.set('mode', EDITOR_MODE_NORMAL) self.mode_current_object = None super(QGraphicsSceneExtend, self).mouseReleaseEvent(e) def showGrid(self): self.setBackgroundBrush(QBrush(self.background_image)) def hideGrid(self): self.setBackgroundBrush(QBrush(None)) def onSaveAsImage(self): filename, _ = QFileDialog.getSaveFileName( self.m, 'Save current figure', '', "Tagged Image File Format (*.tif);;\ Portable Network Graphics (*.png)" ) if filename: self.saveAsImage(filename) def saveAsImage(self, f): self.image = QImage(self.sceneRect().size().toSize(), QImage.Format_ARGB32) self.image.fill(Qt.white) painter = QPainter(self.image) self.render(painter) self.image.save(f) def addApp(self, app, position=None): i = ToolItem(self, app, position=position) self.addItem(i) return i def removeApp(self, app): i = app.editorItem i.hide() self.removeItem(i) app.editorItem = None def dragEnterEvent(self, e): if e.mimeData().hasFormat('application/x-pathomx-app') or e.mimeData( ).hasFormat('text/uri-list'): e.accept() else: e.ignore() def dragMoveEvent(self, e): e.accept() def dropEvent(self, e): scenePos = e.scenePos() - QPointF(32, 32) if e.mimeData().hasFormat('application/x-pathomx-app'): try: app_id = str(e.mimeData().data('application/x-pathomx-app'), 'utf-8') # Python 3 except: app_id = str( e.mimeData().data('application/x-pathomx-app')) # Python 2 e.setDropAction(Qt.CopyAction) a = app_launchers[app_id](self.m, position=scenePos, auto_focus=False) #self.centerOn(a.editorItem) e.accept() elif e.mimeData().hasFormat('text/uri-list'): for ufn in e.mimeData().urls(): fn = ufn.path() fnn, ext = os.path.splitext(fn) ext = ext.strip('.') if ext in file_handlers: a = file_handlers[ext](position=scenePos, auto_focus=False, filename=fn) self.centerOn(a.editorItem) e.accept() def getXMLAnnotations(self, root): # Iterate over the entire set (in order) creating a XML representation of the MatchDef and Style for annotation in self.annotations: ase = et.SubElement(root, "Annotation") ase.set('type', type(annotation).__name__) ase.set('x', str(annotation.x())) ase.set('y', str(annotation.y())) ase.set('width', str(annotation.rect().width())) ase.set('height', str(annotation.rect().height())) if hasattr(annotation, 'text'): text = et.SubElement(ase, "Text") text.text = annotation.text.toPlainText() ase = annotation.config.getXMLConfig(ase) return root def setXMLAnnotations(self, root): ANNOTATION_TYPES = { 'AnnotationTextItem': AnnotationTextItem, 'AnnotationRegionItem': AnnotationRegionItem, } for ase in root.findall('Annotation'): # Validate the class definition before creating it if ase.get('type') in ANNOTATION_TYPES: pos = QPointF(float(ase.get('x')), float(ase.get('y'))) aobj = ANNOTATION_TYPES[ase.get('type')](position=pos) aobj.setRect( QRectF(0, 0, float(ase.get('width')), float(ase.get('height')))) to = ase.find('Text') if to is not None: aobj.text.setPlainText(to.text) self.addItem(aobj) self.annotations.append(aobj) aobj.config.setXMLConfig(ase) aobj.applyStyleConfig()
class ToolBase(QObject): ''' Base tool definition for inclusion in the UI. Define specific config settings; attach a panel widget for configuration. ''' is_manual_runnable = True is_auto_runnable = True is_auto_rerunnable = True is_disableable = True progress = pyqtSignal(float) status = pyqtSignal(str) config_panel_size = 250 view_widget = 'SpectraViewer' def __init__(self, parent, *args, **kwargs): super(ToolBase, self).__init__(parent, *args, **kwargs) self.config = ConfigManager() self.config.hooks.update(custom_pyqtconfig_hooks.items()) self.config.set_defaults({ 'is_active': True, 'auto_run_on_config_change': True }) self.config.updated.connect(self.auto_run_on_config_change) self.buttonBar = QWidget() self.configPanels = QWidget() self.configLayout = QVBoxLayout() self.configLayout.setContentsMargins(0, 0, 0, 0) self.configPanels.setLayout(self.configLayout) self._previous_config_backup_ = {} self._worker_thread_ = None self._worker_thread_lock_ = False self.data = { 'spc': None, } self.current_status = 'ready' self.current_progress = 0 self.progress.connect(self.progress_callback) self.status.connect(self.status_callback) def addConfigPanel(self, panel): self.configLayout.addWidget(panel(self)) def addButtonBar(self, buttons): ''' Create a button bar Supplied with a list of QPushButton objects (already created using helper stubs; see below) :param buttons: :return: ''' btnlayout = QHBoxLayout() btnlayout.addSpacerItem( QSpacerItem(250, 1, QSizePolicy.Maximum, QSizePolicy.Maximum)) for btn in buttons: btnlayout.addWidget(btn) self.configLayout.addLayout(btnlayout) btnlayout.addSpacerItem( QSpacerItem(250, 1, QSizePolicy.Maximum, QSizePolicy.Maximum)) def run_manual(self): pass def disable(self): self.status.emit('inactive') self.config.set('is_active', False) self.item.setFlags(Qt.NoItemFlags) def reset(self): self.config.set_many(self.config.defaults) def undo(self): self.config.set_many(self._config_backup_) def deftaultButtons(self): buttons = [] if self.is_disableable: disable = QPushButton( QIcon(os.path.join(utils.scriptdir, 'icons', 'cross.png')), 'Disable') disable.setToolTip('Disable this tool') disable.pressed.connect(self.disable) buttons.append(disable) reset = QPushButton( QIcon( os.path.join(utils.scriptdir, 'icons', 'arrow-turn-180-left.png')), 'Reset to defaults') reset.setToolTip('Reset to defaults') reset.pressed.connect(self.reset) buttons.append(reset) undo = QPushButton( QIcon( os.path.join(utils.scriptdir, 'icons', 'arrow-turn-180-left.png')), 'Undo') undo.setToolTip('Undo recent changes') undo.pressed.connect(self.undo) buttons.append(undo) if self.is_auto_runnable: auto = QPushButton( QIcon(os.path.join(utils.scriptdir, 'icons', 'lightning.png')), 'Auto') auto.setToolTip('Auto-update spectra when settings change') auto.setCheckable(True) auto.pressed.connect(self.run_manual) self.config.add_handler('auto_run_on_config_change', auto) buttons.append(auto) if self.is_manual_runnable: apply = QPushButton( QIcon(os.path.join(utils.scriptdir, 'icons', 'play.png')), 'Apply') apply.setToolTip('Apply current settings to spectra') apply.pressed.connect(self.run_manual) buttons.append(apply) return buttons def enable(self): if self.current_status == 'inactive': self.status.emit('ready') self.item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) self.config.set('is_active', True) def activate(self): self.parent().current_tool = self self.enable() self._config_backup_ = self.config.as_dict() self._refresh_plot_timer_ = QTimer.singleShot(0, self.plot) self.parent().viewStack.setCurrentWidget(self.parent().spectraViewer) self.parent().configstack.setCurrentWidget(self.configPanels) self.parent().configstack.setMaximumHeight(self.config_panel_size) def set_active(self, active): self.config.set('is_active', active) def get_previous_tool(self): # Get the previous ACTIVE tool in the tool table n = self.parent().tools.index(self) for tool in self.parent().tools[n - 1::-1]: if tool.current_status != 'inactive': return tool else: return None def get_previous_spc(self): t = self.get_previous_tool() if t: return t.data['spc'] else: return None def plot(self, **kwargs): if 'spc' in self.data: self.parent().spectraViewer.plot(self.data['spc'], **kwargs) def get_plotitem(self): return self.parent().spectraViewer.spectraViewer.plotItem def auto_run_on_config_change(self): pass #if self.is_auto_runnable and self.config.get('is_active') and self.config.get('auto_run_on_config_change'): # self.run_manual() def run(self, fn): ''' Run the target function, passing in the current spectra, and config settings (as dict) :param fn: :return: ''' if self._worker_thread_lock_: return False # Can't run self.progress.emit(0) self.status.emit('active') spc = self.get_previous_spc() self._worker_thread_lock_ = True print(self.config.as_dict()) self._worker_thread_ = Worker(fn=fn, **{ 'spc': deepcopy(spc), 'config': self.config.as_dict(), 'progress_callback': self.progress.emit, }) self._worker_thread_.signals.finished.connect(self.finished) self._worker_thread_.signals.result.connect(self.result) self._worker_thread_.signals.error.connect(self.error) self.parent().threadpool.start(self._worker_thread_) def error(self, error): self.progress.emit(1.0) self.status.emit('error') logging.error(error) self._worker_thread_lock_ = False def result(self, result): self.progress.emit(1) self.status.emit('complete') # Apply post-processing if 'spc' in result: result['spc'] = self.post_process_spc(result['spc']) self.data = result self.plot() def finished(self): # Cleanup self._worker_thread_lock_ = False def progress_callback(self, progress): self.current_progress = progress self.item.setData(Qt.UserRole + 2, progress) def status_callback(self, status): self.current_status = status self.item.setData(Qt.UserRole + 3, status) def post_process_spc(self, spc): ''' Apply post-processing to the spectra before loading into the data store, e.g. for outlier detection, stats etc. :param spc: :return: ''' # Outliers def identify_outliers(data, m=2): return abs(data - np.mean(data, axis=0)) < (m * np.std(data, axis=0)) # Identify outliers on a point by point basis. Count up 'outliers' and score ratio of points that are # outliers for each specra > 5% (make this configurable) is an outlier. spc.outliers = np.sum(~identify_outliers(spc.data), axis=1) / float( spc.data.shape[1]) return spc
class ToolBase(QObject): ''' Base tool definition for inclusion in the UI. Define specific config settings; attach a panel widget for configuration. ''' is_manual_runnable = True is_auto_runnable = False is_auto_rerunnable = True is_disableable = True progress = pyqtSignal(float) status = pyqtSignal(str) complete = pyqtSignal() config_panel_size = 150 view = None def __init__(self, parent, *args, **kwargs): super(ToolBase, self).__init__(parent, *args, **kwargs) self.config = ConfigManager() self.config.hooks.update(custom_pyqtconfig_hooks.items()) self.config.set_defaults({ 'auto_run_on_config_change': True }) self.current_status = 'inactive' self.config.updated.connect(self.auto_run_on_config_change) self.buttonBar = QWidget() self.configPanels = QWidget() self.configLayout = QVBoxLayout() self.configLayout.setContentsMargins(0,0,0,0) self.configPanels.setLayout(self.configLayout) self._previous_config_backup_ = {} self._worker_thread_ = None self._worker_thread_lock_ = False self.view = MplView(self.parent()) self.setup() self.current_progress = 0 self.progress.connect(self.progress_callback) self.status.connect(self.status_callback) def setup(self): self.data = { 'data': None, } self.view.figure.clear() self.view.redraw() self.status.emit('inactive' if self.current_status == 'inactive' else 'ready') def addConfigPanel(self, panel): self.configLayout.addWidget( panel(self) ) def addButtonBar(self, buttons): ''' Create a button bar Supplied with a list of QPushButton objects (already created using helper stubs; see below) :param buttons: :return: ''' btnlayout = QHBoxLayout() btnlayout.addSpacerItem(QSpacerItem(250, 1, QSizePolicy.Maximum, QSizePolicy.Maximum)) for btn in buttons: btnlayout.addWidget(btn) self.configLayout.addLayout(btnlayout) btnlayout.addSpacerItem(QSpacerItem(250, 1, QSizePolicy.Maximum, QSizePolicy.Maximum)) def run_manual(self): pass def disable(self): self.status.emit('inactive') self.item.setFlags(Qt.NoItemFlags) def reset(self): self.config.set_many( self.config.defaults ) def undo(self): self.config.set_many(self._config_backup_) def defaultButtons(self): buttons = [] reset = QPushButton(QIcon(os.path.join(utils.scriptdir, 'icons', 'arrow-turn-180-left.png')), 'Reset to defaults') reset.setToolTip('Reset to defaults') reset.pressed.connect(self.reset) buttons.append(reset) apply = QPushButton(QIcon(os.path.join(utils.scriptdir, 'icons', 'play.png')), 'Apply') apply.setToolTip('Apply current settings to spectra') apply.pressed.connect(self.run_manual) buttons.append(apply) return buttons def enable(self): if self.current_status == 'inactive': self.current_status = 'ready' self.status.emit('ready') self.item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) def activate(self): self.parent().current_tool = self self.enable() self._config_backup_ = self.config.as_dict() self._refresh_plot_timer_ = QTimer.singleShot(0, self.plot) if self.view: self.parent().viewStack.setCurrentWidget(self.view) self.parent().configstack.setCurrentWidget(self.configPanels) self.parent().configstack.setMaximumHeight(self.config_panel_size) def set_active(self, active): self.config.set('is_active', active) def get_previous_tool(self): # Get the previous ACTIVE tool in the tool table n = self.parent().tools.index(self) for tool in self.parent().tools[n-1::-1]: if tool.current_status != 'inactive': return tool else: return None def get_next_tool(self): # Get the previous ACTIVE tool in the tool table n = self.parent().tools.index(self) for tool in self.parent().tools[n+1:]: if tool.current_status != 'inactive': return tool else: return None def get_previous_data(self): t = self.get_previous_tool() if t and 'data' in t.data: return t.data['data'] else: return None def plot(self, **kwargs): pass #if 'spc' in self.data: # self.parent().spectraViewer.plot(self.data['spc'], **kwargs) def get_plotitem(self): return self.parent().spectraViewer.spectraViewer.plotItem def auto_run_on_config_change(self): if self.is_auto_runnable and self.current_status == 'ready' and self.config.get('auto_run_on_config_change'): self.run_manual() def run(self, fn): ''' Run the target function, passing in the current spectra, and config settings (as dict) :param fn: :return: ''' if self._worker_thread_lock_: return False # Can't run self.progress.emit(0) self.status.emit('active') data = self.get_previous_data() self._worker_thread_lock_ = True kwargs = { 'config': self.config.as_dict(), 'progress_callback': self.progress.emit, } if data: kwargs.update(deepcopy(data)) # Close any open figures; ensure new axes. plt.close() self._worker_thread_ = Worker(fn = fn, **kwargs) self._worker_thread_.signals.finished.connect(self.finished) self._worker_thread_.signals.result.connect(self.result) self._worker_thread_.signals.error.connect(self.error) self.parent().threadpool.start(self._worker_thread_) def error(self, error): self.progress.emit(1.0) self.status.emit('error') logging.error(error) self._worker_thread_lock_ = False def result(self, result): # Apply post-processing if 'fig' in result and result['fig']: fig = result['fig'] fig.set_size_inches(self.view.get_size_inches(fig.get_dpi())) fig.set_tight_layout(False) fig.tight_layout(pad=0.5, rect=[0.25, 0.10, 0.75, 0.90]) self.view.figure = fig self.view.redraw() self.data = result self.plot() self.progress.emit(1) self.status.emit('complete') def finished(self): # Cleanup self._worker_thread_lock_ = False self.complete.emit() def progress_callback(self, progress): self.current_progress = progress self.item.setData(Qt.UserRole + 2, progress) def status_callback(self, status): self.current_status = status self.item.setData(Qt.UserRole + 3, status)
class AutomatonDialog(GenericDialog): mode_options = { 'Manual': MODE_MANUAL, 'Watch files': MODE_WATCH_FILES, 'Watch folder': MODE_WATCH_FOLDER, 'Timer': MODE_TIMER, } def __init__(self, parent, **kwargs): super(AutomatonDialog, self).__init__(parent, **kwargs) self.setWindowTitle("Edit Automaton") self.config = ConfigManager() gb = QGroupBox('IPython notebook(s) (*.ipynb)') grid = QGridLayout() notebook_path_le = QLineEdit() self.config.add_handler('notebook_paths', notebook_path_le, mapper=(lambda x: x.split(";"), lambda x: ";".join(x))) grid.addWidget(notebook_path_le, 0, 0, 1, 2) notebook_path_btn = QToolButton() notebook_path_btn.setIcon( QIcon( os.path.join(utils.scriptdir, 'icons', 'document-attribute-i.png'))) notebook_path_btn.clicked.connect( lambda: self.onNotebookBrowse(notebook_path_le)) grid.addWidget(notebook_path_btn, 0, 2, 1, 1) gb.setLayout(grid) self.layout.addWidget(gb) gb = QGroupBox('Automaton mode') grid = QGridLayout() mode_cb = QComboBox() mode_cb.addItems(self.mode_options.keys()) mode_cb.currentIndexChanged.connect(self.onChangeMode) self.config.add_handler('mode', mode_cb, mapper=self.mode_options) grid.addWidget(QLabel('Mode'), 0, 0) grid.addWidget(mode_cb, 0, 1) grid.addWidget(QLabel('Hold trigger'), 1, 0) fwatcher_hold_sb = QSpinBox() fwatcher_hold_sb.setRange(0, 60) fwatcher_hold_sb.setSuffix(' secs') self.config.add_handler('trigger_hold', fwatcher_hold_sb) grid.addWidget(fwatcher_hold_sb, 1, 1) gb.setLayout(grid) self.layout.addWidget(gb) self.watchfile_gb = QGroupBox('Watch files') grid = QGridLayout() watched_path_le = QLineEdit() grid.addWidget(watched_path_le, 0, 0, 1, 2) self.config.add_handler('watched_files', watched_path_le, mapper=(lambda x: x.split(";"), lambda x: ";".join(x))) watched_path_btn = QToolButton() watched_path_btn.setIcon( QIcon(os.path.join(utils.scriptdir, 'icons', 'document-copy.png'))) watched_path_btn.setStatusTip('Add file(s)') watched_path_btn.clicked.connect( lambda: self.onFilesBrowse(watched_path_le)) grid.addWidget(watched_path_btn, 0, 2, 1, 1) grid.addWidget(QLabel('Watch window'), 1, 0) watch_window_sb = QSpinBox() watch_window_sb.setRange(0, 60) watch_window_sb.setSuffix(' secs') self.config.add_handler('watch_window', watch_window_sb) grid.addWidget(watch_window_sb, 1, 1) self.watchfile_gb.setLayout(grid) self.layout.addWidget(self.watchfile_gb) self.watchfolder_gb = QGroupBox('Watch folder') grid = QGridLayout() watched_path_le = QLineEdit() grid.addWidget(watched_path_le, 0, 0, 1, 3) self.config.add_handler('watched_folder', watched_path_le) watched_path_btn = QToolButton() watched_path_btn.setIcon( QIcon( os.path.join(utils.scriptdir, 'icons', 'folder-horizontal-open.png'))) watched_path_btn.setStatusTip('Add folder') watched_path_btn.clicked.connect( lambda: self.onFolderBrowse(watched_path_le)) grid.addWidget(watched_path_btn, 0, 3, 1, 1) grid.addWidget(QLabel('Iterate files in folder'), 3, 0) loop_folder_sb = QCheckBox() self.config.add_handler('iterate_watched_folder', loop_folder_sb) grid.addWidget(loop_folder_sb, 3, 1) loop_wildcard_le = QLineEdit() self.config.add_handler('iterate_wildcard', loop_wildcard_le) grid.addWidget(loop_wildcard_le, 3, 2) self.watchfolder_gb.setLayout(grid) self.layout.addWidget(self.watchfolder_gb) self.timer_gb = QGroupBox('Timer') grid = QGridLayout() grid.addWidget(QLabel('Run every'), 0, 0) watch_timer_sb = QSpinBox() watch_timer_sb.setRange(0, 60) watch_timer_sb.setSuffix(' secs') self.config.add_handler('timer_seconds', watch_timer_sb) grid.addWidget(watch_timer_sb, 0, 1) self.timer_gb.setLayout(grid) self.layout.addWidget(self.timer_gb) self.manual_gb = QGroupBox('Manual') # No show grid = QGridLayout() grid.addWidget(QLabel('No configuration'), 0, 0) self.manual_gb.setLayout(grid) self.layout.addWidget(self.manual_gb) gb = QGroupBox('Output') grid = QGridLayout() output_path_le = QLineEdit() self.config.add_handler('output_path', output_path_le) grid.addWidget(output_path_le, 0, 0, 1, 2) notebook_path_btn = QToolButton() notebook_path_btn.setIcon( QIcon( os.path.join(utils.scriptdir, 'icons', 'folder-horizontal-open.png'))) notebook_path_btn.clicked.connect( lambda: self.onFolderBrowse(notebook_path_le)) grid.addWidget(notebook_path_btn, 0, 2, 1, 1) export_cb = QComboBox() export_cb.addItems(IPyexporter_map.keys()) self.config.add_handler('output_format', export_cb) grid.addWidget(QLabel('Notebook output format'), 1, 0) grid.addWidget(export_cb, 1, 1) gb.setLayout(grid) self.layout.addWidget(gb) self.layout.addStretch() self.finalise() self.onChangeMode(mode_cb.currentIndex()) def onNotebookBrowse(self, t): global _w filenames, _ = QFileDialog.getOpenFileNames( _w, "Load IPython notebook(s)", '', "IPython Notebooks (*.ipynb);;All files (*.*)") if filenames: self.config.set('notebook_paths', filenames) def onFolderBrowse(self, t): global _w filename = QFileDialog.getExistingDirectory(_w, "Select folder to watch") if filename: self.config.set('watched_folder', filename) def onFilesBrowse(self, t): global _w filenames, _ = QFileDialog.getOpenFileNames(_w, "Select file(s) to watch") if filenames: self.config.set('watched_files', filenames) def onChangeMode(self, i): for m, gb in { MODE_MANUAL: self.manual_gb, MODE_WATCH_FILES: self.watchfile_gb, MODE_WATCH_FOLDER: self.watchfolder_gb, MODE_TIMER: self.timer_gb }.items(): if m == list(self.mode_options.items())[i][1]: gb.show() else: gb.hide() def sizeHint(self): return QSize(400, 200)
class AutomatonDialog(GenericDialog): mode_options = { 'Manual': MODE_MANUAL, 'Watch files': MODE_WATCH_FILES, 'Watch folder': MODE_WATCH_FOLDER, 'Timer': MODE_TIMER, } def __init__(self, parent, **kwargs): super(AutomatonDialog, self).__init__(parent, **kwargs) self.setWindowTitle("Edit Automaton") self.config = ConfigManager() gb = QGroupBox('IPython notebook(s) (*.ipynb)') grid = QGridLayout() notebook_path_le = QLineEdit() self.config.add_handler('notebook_paths', notebook_path_le, mapper=(lambda x: x.split(";"), lambda x: ";".join(x))) grid.addWidget(notebook_path_le, 0, 0, 1, 2) notebook_path_btn = QToolButton() notebook_path_btn.setIcon(QIcon(os.path.join(utils.scriptdir, 'icons', 'document-attribute-i.png'))) notebook_path_btn.clicked.connect(lambda: self.onNotebookBrowse(notebook_path_le)) grid.addWidget(notebook_path_btn, 0, 2, 1, 1) gb.setLayout(grid) self.layout.addWidget(gb) gb = QGroupBox('Automaton mode') grid = QGridLayout() mode_cb = QComboBox() mode_cb.addItems(self.mode_options.keys()) mode_cb.currentIndexChanged.connect(self.onChangeMode) self.config.add_handler('mode', mode_cb, mapper=self.mode_options) grid.addWidget(QLabel('Mode'), 0, 0) grid.addWidget(mode_cb, 0, 1) grid.addWidget(QLabel('Hold trigger'), 1, 0) fwatcher_hold_sb = QSpinBox() fwatcher_hold_sb.setRange(0, 60) fwatcher_hold_sb.setSuffix(' secs') self.config.add_handler('trigger_hold', fwatcher_hold_sb) grid.addWidget(fwatcher_hold_sb, 1, 1) gb.setLayout(grid) self.layout.addWidget(gb) self.watchfile_gb = QGroupBox('Watch files') grid = QGridLayout() watched_path_le = QLineEdit() grid.addWidget(watched_path_le, 0, 0, 1, 2) self.config.add_handler('watched_files', watched_path_le, mapper=(lambda x: x.split(";"), lambda x: ";".join(x))) watched_path_btn = QToolButton() watched_path_btn.setIcon(QIcon(os.path.join(utils.scriptdir, 'icons', 'document-copy.png'))) watched_path_btn.setStatusTip('Add file(s)') watched_path_btn.clicked.connect(lambda: self.onFilesBrowse(watched_path_le)) grid.addWidget(watched_path_btn, 0, 2, 1, 1) grid.addWidget(QLabel('Watch window'), 1, 0) watch_window_sb = QSpinBox() watch_window_sb.setRange(0, 60) watch_window_sb.setSuffix(' secs') self.config.add_handler('watch_window', watch_window_sb) grid.addWidget(watch_window_sb, 1, 1) self.watchfile_gb.setLayout(grid) self.layout.addWidget(self.watchfile_gb) self.watchfolder_gb = QGroupBox('Watch folder') grid = QGridLayout() watched_path_le = QLineEdit() grid.addWidget(watched_path_le, 0, 0, 1, 3) self.config.add_handler('watched_folder', watched_path_le) watched_path_btn = QToolButton() watched_path_btn.setIcon(QIcon(os.path.join(utils.scriptdir, 'icons', 'folder-horizontal-open.png'))) watched_path_btn.setStatusTip('Add folder') watched_path_btn.clicked.connect(lambda: self.onFolderBrowse(watched_path_le)) grid.addWidget(watched_path_btn, 0, 3, 1, 1) grid.addWidget(QLabel('Iterate files in folder'), 3, 0) loop_folder_sb = QCheckBox() self.config.add_handler('iterate_watched_folder', loop_folder_sb) grid.addWidget(loop_folder_sb, 3, 1) loop_wildcard_le = QLineEdit() self.config.add_handler('iterate_wildcard', loop_wildcard_le) grid.addWidget(loop_wildcard_le, 3, 2) self.watchfolder_gb.setLayout(grid) self.layout.addWidget(self.watchfolder_gb) self.timer_gb = QGroupBox('Timer') grid = QGridLayout() grid.addWidget(QLabel('Run every'), 0, 0) watch_timer_sb = QSpinBox() watch_timer_sb.setRange(0, 60) watch_timer_sb.setSuffix(' secs') self.config.add_handler('timer_seconds', watch_timer_sb) grid.addWidget(watch_timer_sb, 0, 1) self.timer_gb.setLayout(grid) self.layout.addWidget(self.timer_gb) self.manual_gb = QGroupBox('Manual') # No show grid = QGridLayout() grid.addWidget(QLabel('No configuration'), 0, 0) self.manual_gb.setLayout(grid) self.layout.addWidget(self.manual_gb) gb = QGroupBox('Output') grid = QGridLayout() output_path_le = QLineEdit() self.config.add_handler('output_path', output_path_le) grid.addWidget(output_path_le, 0, 0, 1, 2) notebook_path_btn = QToolButton() notebook_path_btn.setIcon(QIcon(os.path.join(utils.scriptdir, 'icons', 'folder-horizontal-open.png'))) notebook_path_btn.clicked.connect(lambda: self.onFolderBrowse(notebook_path_le)) grid.addWidget(notebook_path_btn, 0, 2, 1, 1) export_cb = QComboBox() export_cb.addItems(IPyexporter_map.keys()) self.config.add_handler('output_format', export_cb) grid.addWidget(QLabel('Notebook output format'), 1, 0) grid.addWidget(export_cb, 1, 1) gb.setLayout(grid) self.layout.addWidget(gb) self.layout.addStretch() self.finalise() self.onChangeMode(mode_cb.currentIndex()) def onNotebookBrowse(self, t): global _w filenames, _ = QFileDialog.getOpenFileNames(_w, "Load IPython notebook(s)", '', "IPython Notebooks (*.ipynb);;All files (*.*)") if filenames: self.config.set('notebook_paths', filenames) def onFolderBrowse(self, t): global _w filename = QFileDialog.getExistingDirectory(_w, "Select folder to watch") if filename: self.config.set('watched_folder', filename) def onFilesBrowse(self, t): global _w filenames, _ = QFileDialog.getOpenFileNames(_w, "Select file(s) to watch") if filenames: self.config.set('watched_files', filenames) def onChangeMode(self, i): for m, gb in {MODE_MANUAL: self.manual_gb, MODE_WATCH_FILES: self.watchfile_gb, MODE_WATCH_FOLDER: self.watchfolder_gb, MODE_TIMER: self.timer_gb}.items(): if m == list(self.mode_options.items())[i][1]: gb.show() else: gb.hide() def sizeHint(self): return QSize(400, 200)
class AnnotatePeaks(GenericDialog): def __init__(self, parent, config=None, *args, **kwargs): super(AnnotatePeaks, self).__init__(parent, *args, **kwargs) self.setWindowTitle('Annotate Peaks') if config: # Copy in starting state self.config = ConfigManager() self.config.hooks.update(custom_pyqtconfig_hooks.items()) self.config.set_defaults(config) self.fwd_map_cache = {} # Correlation variables gb = QGroupBox('Peaks') vbox = QVBoxLayout() # Populate the list boxes self.lw_peaks = QListWidgetAddRemove() self.lw_peaks.setSelectionMode(QAbstractItemView.ExtendedSelection) vbox.addWidget(self.lw_peaks) vboxh = QHBoxLayout() self.add_label = QLineEdit() self.add_start = QDoubleSpinBox() self.add_start.setRange(-1, 12) self.add_start.setDecimals(3) self.add_start.setSuffix('ppm') self.add_start.setSingleStep(0.001) self.add_end = QDoubleSpinBox() self.add_end.setRange(-1, 12) self.add_end.setDecimals(3) self.add_end.setSuffix('ppm') self.add_end.setSingleStep(0.001) addc = QPushButton('Add') addc.clicked.connect(self.onPeakAdd) remc = QPushButton('Remove selected') remc.clicked.connect(self.onPeakAdd) loadc = QPushButton("Import from file") loadc.setIcon(QIcon(os.path.join(utils.scriptdir, 'icons', 'folder-open-document.png'))) loadc.clicked.connect(self.onPeakImport) metabh = QPushButton("Auto match via MetaboHunter") metabh.setIcon(QIcon(os.path.join(utils.scriptdir, 'icons', 'metabohunter.png'))) metabh.clicked.connect(self.onPeakImportMetabohunter) vboxh.addWidget(self.add_label) vboxh.addWidget(self.add_start) vboxh.addWidget(self.add_end) vboxh.addWidget(addc) vbox.addWidget(remc) vbox.addLayout(vboxh) vbox.addWidget(loadc) vbox.addWidget(metabh) gb.setLayout(vbox) self.layout.addWidget(gb) self.config.add_handler('annotation/peaks', self.lw_peaks, (self.map_list_fwd, self.map_list_rev)) self.dialogFinalise() def onPeakAdd(self): c = self.config.get('annotation/peaks')[:] # Create new list to force refresh on reassign c.append( (self.add_label.text(), float(self.add_start.value()), float(self.add_end.value()) ) ) self.config.set('annotation/peaks', c) def onPeakRemove(self): i = self.lw_peaks.removeItemAt(self.lw_peaks.currentRow()) # c = self.map_list_fwd(i.text()) def onPeakImport(self): filename, _ = QFileDialog.getOpenFileName(self.parent(), 'Load peak annotations from file', '', "All compatible files (*.csv *.txt *.tsv);;Comma Separated Values (*.csv);;Plain Text Files (*.txt);;Tab Separated Values (*.tsv);;All files (*.*)") if filename: c = self.config.get('annotation/peaks')[:] # Create new list to force refresh on reassign with open(filename, 'rU') as f: reader = csv.reader(f, delimiter=b',', dialect='excel') for row in reader: if row not in c: c.append( row[0], float(row[1]), float(row[2]) ) self.config.set('annotation/peaks', c) def onPeakImportMetabohunter(self): c = self.config.get('annotation/peaks')[:] # Create new list to force refresh on reassign t = self.parent().current_tool dlg = MetaboHunter(self) if dlg.exec_(): if 'spc' in t.data: # We have a spectra; calcuate mean; reduce size if required spc = t.data['spc'] n = spc.data.shape[1] ppm = spc.ppm spcd = np.mean(spc.data, axis=0) # Set a hard limit on the size of data we submit to be nice. if n > 3000: # Calculate the division required to be under the limit d = np.ceil(float(n)/3000) # Trim axis to multiple of divisor trim = (n//d)*d spcd = spcd[:trim] ppm = ppm[:trim] # Mean d shape spcd = np.mean( spcd.reshape(-1,d), axis=1) ppm = np.mean( ppm.reshape(-1,d), axis=1) # Submit with settings hmdbs = metabohunter.request(ppm, spcd, metabotype=dlg.config.get('Metabotype'), database=dlg.config.get('Database Source'), ph=dlg.config.get('Sample pH'), solvent=dlg.config.get('Solvent'), frequency=dlg.config.get('Frequency'), method=dlg.config.get('Method'), noise=dlg.config.get('Noise Threshold'), confidence=dlg.config.get('Confidence Threshold'), tolerance=dlg.config.get('Tolerance') ) ha = np.array(hmdbs) unique_hmdbs = set(hmdbs) if None in unique_hmdbs: unique_hmdbs.remove(None) # Extract out regions for hmdb in unique_hmdbs: hb = np.diff(ha == hmdb) # These are needed to ensure markers are there for objects starting and ending on array edge if ha[0] == hmdb: hb[0] == True if ha[-1] == hmdb: hb[-1] == True idx = np.nonzero(hb)[0] idx = idx.reshape(-1,2) if dlg.config.get('convert_hmdb_ids_to_names') and hmdb in METABOHUNTER_HMDB_NAME_MAP.keys(): label = METABOHUNTER_HMDB_NAME_MAP[hmdb] else: label = hmdb # Now we have an array of all start, stop positions for this item for start, stop in idx: c.append( (label, ppm[start], ppm[stop]) ) self.config.set('annotation/peaks', c) def map_list_fwd(self, s): " Receive text name, return the indexes " return self.fwd_map_cache[s] def map_list_rev(self, x): " Receive the indexes, return the label" s = "%s\t%.2f\t%.2f" % tuple(x) self.fwd_map_cache[s] = x return s
class AnnotateClasses(GenericDialog): def __init__(self, parent, config=None, *args, **kwargs): super(AnnotateClasses, self).__init__(parent, *args, **kwargs) self.setWindowTitle('Annotate Classes') if config: # Copy in starting state self.config = ConfigManager() self.config.hooks.update(custom_pyqtconfig_hooks.items()) self.config.set_defaults(config) self.fwd_map_cache = {} # Correlation variables gb = QGroupBox('Sample classes') vbox = QVBoxLayout() # Populate the list boxes self.lw_classes = QListWidgetAddRemove() self.lw_classes.setSelectionMode(QAbstractItemView.ExtendedSelection) vbox.addWidget(self.lw_classes) vboxh = QHBoxLayout() self.add_label = QLineEdit() self.add_class = QLineEdit() addc = QPushButton('Add') addc.clicked.connect(self.onClassAdd) remc = QPushButton('Remove selected') remc.clicked.connect(self.onClassAdd) loadc = QPushButton('Import from file') loadc.setIcon(QIcon(os.path.join(utils.scriptdir, 'icons', 'folder-open-document.png'))) loadc.clicked.connect(self.onClassImport) vboxh.addWidget(self.add_label) vboxh.addWidget(self.add_class) vboxh.addWidget(addc) vbox.addWidget(remc) vbox.addLayout(vboxh) vboxh.addWidget(loadc) gb.setLayout(vbox) self.layout.addWidget(gb) self.config.add_handler('annotation/sample_classes', self.lw_classes, (self.map_list_fwd, self.map_list_rev)) self.dialogFinalise() def onClassAdd(self): c = self.config.get('annotation/sample_classes')[:] # Create new list to force refresh on reassign c.append( (self.add_label.text(), self.add_class.text()) ) self.config.set('annotation/sample_classes', c) def onClassRemove(self): i = self.lw_classes.removeItemAt(self.lw_classes.currentRow()) # c = self.map_list_fwd(i.text()) def onClassImport(self): filename, _ = QFileDialog.getOpenFileName(self.parent(), 'Load classifications from file', '', "All compatible files (*.csv *.txt *.tsv);;Comma Separated Values (*.csv);;Plain Text Files (*.txt);;Tab Separated Values (*.tsv);;All files (*.*)") if filename: c = self.config.get('annotation/sample_classes')[:] # Create new list to force refresh on reassign with open(filename, 'rU') as f: reader = csv.reader(f, delimiter=b',', dialect='excel') for row in reader: if row not in c: c.append(row[:2]) self.config.set('annotation/sample_classes', c) def map_list_fwd(self, s): " Receive text name, return the indexes " return self.fwd_map_cache[s] def map_list_rev(self, x): " Receive the indexes, return the label" s = "%s\t%s" % tuple(x) self.fwd_map_cache[s] = x return s
class ToolBase(QObject): ''' Base tool definition for inclusion in the UI. Define specific config settings; attach a panel widget for configuration. ''' is_manual_runnable = True is_auto_runnable = True is_auto_rerunnable = True is_disableable = True progress = pyqtSignal(float) status = pyqtSignal(str) config_panel_size = 250 view_widget = 'SpectraViewer' def __init__(self, parent, *args, **kwargs): super(ToolBase, self).__init__(parent, *args, **kwargs) self.config = ConfigManager() self.config.hooks.update(custom_pyqtconfig_hooks.items()) self.config.set_defaults({ 'is_active': True, 'auto_run_on_config_change': True }) self.config.updated.connect(self.auto_run_on_config_change) self.buttonBar = QWidget() self.configPanels = QWidget() self.configLayout = QVBoxLayout() self.configLayout.setContentsMargins(0,0,0,0) self.configPanels.setLayout(self.configLayout) self._previous_config_backup_ = {} self._worker_thread_ = None self._worker_thread_lock_ = False self.data = { 'spc': None, } self.current_status = 'ready' self.current_progress = 0 self.progress.connect(self.progress_callback) self.status.connect(self.status_callback) def addConfigPanel(self, panel): self.configLayout.addWidget( panel(self) ) def addButtonBar(self, buttons): ''' Create a button bar Supplied with a list of QPushButton objects (already created using helper stubs; see below) :param buttons: :return: ''' btnlayout = QHBoxLayout() btnlayout.addSpacerItem(QSpacerItem(250, 1, QSizePolicy.Maximum, QSizePolicy.Maximum)) for btn in buttons: btnlayout.addWidget(btn) self.configLayout.addLayout(btnlayout) btnlayout.addSpacerItem(QSpacerItem(250, 1, QSizePolicy.Maximum, QSizePolicy.Maximum)) def run_manual(self): pass def disable(self): self.status.emit('inactive') self.config.set('is_active', False) self.item.setFlags(Qt.NoItemFlags) def reset(self): self.config.set_many( self.config.defaults ) def undo(self): self.config.set_many(self._config_backup_) def deftaultButtons(self): buttons = [] if self.is_disableable: disable = QPushButton(QIcon(os.path.join(utils.scriptdir, 'icons', 'cross.png')), 'Disable') disable.setToolTip('Disable this tool') disable.pressed.connect(self.disable) buttons.append(disable) reset = QPushButton(QIcon(os.path.join(utils.scriptdir, 'icons', 'arrow-turn-180-left.png')), 'Reset to defaults') reset.setToolTip('Reset to defaults') reset.pressed.connect(self.reset) buttons.append(reset) undo = QPushButton(QIcon(os.path.join(utils.scriptdir, 'icons', 'arrow-turn-180-left.png')), 'Undo') undo.setToolTip('Undo recent changes') undo.pressed.connect(self.undo) buttons.append(undo) if self.is_auto_runnable: auto = QPushButton(QIcon(os.path.join(utils.scriptdir, 'icons', 'lightning.png')), 'Auto') auto.setToolTip('Auto-update spectra when settings change') auto.setCheckable(True) auto.pressed.connect(self.run_manual) self.config.add_handler('auto_run_on_config_change', auto) buttons.append(auto) if self.is_manual_runnable: apply = QPushButton(QIcon(os.path.join(utils.scriptdir, 'icons', 'play.png')), 'Apply') apply.setToolTip('Apply current settings to spectra') apply.pressed.connect(self.run_manual) buttons.append(apply) return buttons def enable(self): if self.current_status == 'inactive': self.status.emit('ready') self.item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) self.config.set('is_active', True) def activate(self): self.parent().current_tool = self self.enable() self._config_backup_ = self.config.as_dict() self._refresh_plot_timer_ = QTimer.singleShot(0, self.plot) self.parent().viewStack.setCurrentWidget(self.parent().spectraViewer) self.parent().configstack.setCurrentWidget(self.configPanels) self.parent().configstack.setMaximumHeight(self.config_panel_size) def set_active(self, active): self.config.set('is_active', active) def get_previous_tool(self): # Get the previous ACTIVE tool in the tool table n = self.parent().tools.index(self) for tool in self.parent().tools[n-1::-1]: if tool.current_status != 'inactive': return tool else: return None def get_previous_spc(self): t = self.get_previous_tool() if t: return t.data['spc'] else: return None def plot(self, **kwargs): if 'spc' in self.data: self.parent().spectraViewer.plot(self.data['spc'], **kwargs) def get_plotitem(self): return self.parent().spectraViewer.spectraViewer.plotItem def auto_run_on_config_change(self): pass #if self.is_auto_runnable and self.config.get('is_active') and self.config.get('auto_run_on_config_change'): # self.run_manual() def run(self, fn): ''' Run the target function, passing in the current spectra, and config settings (as dict) :param fn: :return: ''' if self._worker_thread_lock_: return False # Can't run self.progress.emit(0) self.status.emit('active') spc = self.get_previous_spc() self._worker_thread_lock_ = True print(self.config.as_dict()) self._worker_thread_ = Worker(fn = fn, **{ 'spc': deepcopy(spc), 'config': self.config.as_dict(), 'progress_callback': self.progress.emit, }) self._worker_thread_.signals.finished.connect(self.finished) self._worker_thread_.signals.result.connect(self.result) self._worker_thread_.signals.error.connect(self.error) self.parent().threadpool.start(self._worker_thread_) def error(self, error): self.progress.emit(1.0) self.status.emit('error') logging.error(error) self._worker_thread_lock_ = False def result(self, result): self.progress.emit(1) self.status.emit('complete') # Apply post-processing if 'spc' in result: result['spc'] = self.post_process_spc(result['spc']) self.data = result self.plot() def finished(self): # Cleanup self._worker_thread_lock_ = False def progress_callback(self, progress): self.current_progress = progress self.item.setData(Qt.UserRole + 2, progress) def status_callback(self, status): self.current_status = status self.item.setData(Qt.UserRole + 3, status) def post_process_spc(self, spc): ''' Apply post-processing to the spectra before loading into the data store, e.g. for outlier detection, stats etc. :param spc: :return: ''' # Outliers def identify_outliers(data, m=2): return abs(data - np.mean(data, axis=0)) < (m * np.std(data,axis=0)) # Identify outliers on a point by point basis. Count up 'outliers' and score ratio of points that are # outliers for each specra > 5% (make this configurable) is an outlier. spc.outliers = np.sum( ~identify_outliers(spc.data), axis=1 ) / float(spc.data.shape[1]) return spc