# default.py --- # # Filename: default.py # Description: # Author: Subhasis Ray # Maintainer: # Created: Tue Nov 13 15:58:31 2012 (+0530) # Version: # Last-Updated: Thu Jul 18 10:35:00 2013 (+0530) # By: subha # Update #: 2244 # URL: # Keywords: # Compatibility: # # # Commentary: # # The default placeholder plugin for MOOSE # # # Change log: # # # # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 3, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; see the file COPYING. If not, write to # the Free Software Foundation, Inc., 51 Franklin Street, Fifth # Floor, Boston, MA 02110-1301, USA. # # # Code: import sys import config import pickle import os from collections import defaultdict import numpy as np from PyQt4 import QtGui, QtCore from PyQt4.Qt import Qt import moose from moose import utils import mtree from mtoolbutton import MToolButton from msearch import SearchWidget from checkcombobox import CheckComboBox from mplugin import MoosePluginBase, EditorBase, EditorWidgetBase, PlotBase, RunBase #from defaultToolPanel import DefaultToolPanel #from DataTable import DataTable from matplotlib import rcParams rcParams.update({'figure.autolayout': True}) from matplotlib.lines import Line2D from PlotWidgetContainer import PlotWidgetContainer from PyQt4 import QtCore, QtGui from PyQt4.QtGui import QDoubleValidator from kkitUtil import getColor from Runner import Runner # from Runner import Runner # from __future__ import print_function from PyQt4 import QtGui, QtCore from PyQt4.QtGui import QToolBar from PyQt4.QtGui import QToolButton from PyQt4.QtGui import QLabel from PyQt4.QtGui import QIcon from PyQt4.QtGui import QLineEdit from PyQt4.QtGui import QErrorMessage from PyQt4.QtGui import QSizeGrip from PyQt4.QtGui import QIcon from PyQt4.QtGui import QPixmap from PyQt4.QtGui import QAction from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar #from EventBlocker import EventBlocker # from PlotNavigationToolbar import PlotNavigationToolbar from global_constants import preferences from setsolver import * ELECTRICAL_MODEL = 0 CHEMICAL_MODEL = 1 class MoosePlugin(MoosePluginBase): """Default plugin for MOOSE GUI""" def __init__(self, root, mainwindow): MoosePluginBase.__init__(self, root, mainwindow) #print "mplugin ",self.getRunView() #self.connect(self, QtCore.SIGNAL("tableCreated"),self.getRunView().getCentralWidget().plotAllData) def getPreviousPlugin(self): return None def getNextPlugin(self): return None def getAdjacentPlugins(self): return [] def getViews(self): return self._views def getCurrentView(self): return self.currentView def getEditorView(self): if not hasattr(self, 'editorView'): self.editorView = MooseEditorView(self) #signal to objecteditor from default plugin self.editorView.getCentralWidget().editObject.connect(self.mainWindow.objectEditSlot) self.currentView = self.editorView return self.editorView def getPlotView(self): if not hasattr(self, 'plotView'): self.plotView = PlotView(self) return self.plotView def getRunView(self): if not hasattr(self, 'runView') or self.runView is None: self.runView = RunView(self.modelRoot, self) return self.runView def getMenus(self): """Create a custom set of menus.""" return self._menus class MooseEditorView(EditorBase): """Default editor. """ def __init__(self, plugin): EditorBase.__init__(self, plugin) self.__initMenus() self.__initToolBars() def __initMenus(self): editMenu = QtGui.QMenu('&Edit') for menu in self.getCentralWidget().getMenus(): editMenu.addMenu(menu) self._menus.append(editMenu) def __initToolBars(self): for toolbar in self.getCentralWidget().getToolBars(): self._toolBars.append(toolbar) def getToolPanes(self): return super(MooseEditorView, self).getToolPanes() def getLibraryPane(self): return super(MooseEditorView, self).getLibraryPane() def getOperationsWidget(self): return super(MooseEditorView, self).getOperationsPane() def getCentralWidget(self): """Retrieve or initialize the central widget. Note that we call the widget's setModelRoot() function explicitly with the plugin's modelRoot as the argument. This enforces an update of the widget display with the current modelRoot. This function should be overridden by any derived class as it has the editor widget class hard coded into it. """ if self._centralWidget is None: self._centralWidget = DefaultEditorWidget() if hasattr(self._centralWidget, 'init'): self._centralWidget.init() self._centralWidget.setModelRoot(self.plugin.modelRoot) return self._centralWidget class MooseTreeEditor(mtree.MooseTreeWidget): """Subclass of MooseTreeWidget to implement drag and drop events. It creates an element under the drop location using the dropped mime data as class name. """ def __init__(self, *args): mtree.MooseTreeWidget.__init__(self, *args) def dragEnterEvent(self, event): if event.mimeData().hasFormat('text/plain'): event.acceptProposedAction() def dragMoveEvent(self, event): if event.mimeData().hasFormat('text/plain'): event.acceptProposedAction() def dropEvent(self, event): """Insert an element of the specified class in drop location""" if not event.mimeData().hasFormat('text/plain'): return pos = event.pos() item = self.itemAt(pos) try: self.insertChildElement(item, str(event.mimeData().text())) event.acceptProposedAction() except NameError: return class DefaultEditorWidget(EditorWidgetBase): """Editor widget for default plugin. Plugin-writers should code there own editor widgets derived from EditorWidgetBase. It adds a toolbar for inserting moose objects into the element tree. The toolbar contains MToolButtons for moose classes. Signals: editObject - inherited from EditorWidgetBase , emitted with currently selected element's path as argument. Should be connected to whatever slot is responsible for firing the object editor in top level. """ def __init__(self, *args): EditorWidgetBase.__init__(self, *args) layout = QtGui.QHBoxLayout() self.setLayout(layout) self.tree = MooseTreeEditor() self.tree.setAcceptDrops(True) self.getTreeMenu() self.layout().addWidget(self.tree) def getTreeMenu(self): try: return self.treeMenu except AttributeError: self.treeMenu = QtGui.QMenu() self.tree.setContextMenuPolicy(Qt.CustomContextMenu) self.tree.customContextMenuRequested.connect(lambda : self.treeMenu.exec_(QtGui.QCursor.pos()) ) # Inserting a child element self.insertMenu = QtGui.QMenu('Insert') self._menus.append(self.insertMenu) self.treeMenu.addMenu(self.insertMenu) self.insertMapper = QtCore.QSignalMapper(self) ignored_bases = ['ZPool', 'Msg', 'Panel', 'SolverBase', 'none'] ignored_classes = ['ZPool','ZReac','ZMMenz','ZEnz','CplxEnzBase'] classlist = [ch[0].name for ch in moose.element('/classes').children if (ch[0].baseClass not in ignored_bases) and (ch[0].name not in (ignored_bases + ignored_classes)) and not ch[0].name.startswith('Zombie') and not ch[0].name.endswith('Base') ] insertMapper, actions = self.getInsertActions(classlist) for action in actions: self.insertMenu.addAction(action) self.connect(insertMapper, QtCore.SIGNAL('mapped(const QString&)'), self.tree.insertElementSlot) self.editAction = QtGui.QAction('Edit', self.treeMenu) self.editAction.triggered.connect(self.editCurrentObjectSlot) self.tree.elementInserted.connect(self.elementInsertedSlot) self.treeMenu.addAction(self.editAction) return self.treeMenu def updateModelView(self): self.tree.recreateTree(root=self.modelRoot) # if current in self.tree.odict: # self.tree.setCurrentItem(current) def updateItemSlot(self, mobj): """This should be overridden by derived classes to connect appropriate slot for updating the display item. """ self.tree.updateItemSlot(mobj) def editCurrentObjectSlot(self): """Emits an `editObject(str)` signal with moose element path of currently selected tree item as argument """ mobj = self.tree.currentItem().mobj self.editObject.emit(mobj.path) def sizeHint(self): return QtCore.QSize(400, 300) def getToolBars(self): if not hasattr(self, '_insertToolBar'): self._insertToolBar = QtGui.QToolBar('Insert') return self._toolBars for action in self.insertMenu.actions(): button = MToolButton() button.setDefaultAction(action) self._insertToolBar.addWidget(button) self._toolBars.append(self._insertToolBar) return self._toolBars ############################################################ # # View for running a simulation and runtime visualization # ############################################################ from mplot import CanvasWidget class RunView(RunBase): """A default runtime view implementation. This should be sufficient for most common usage. canvas: widget for plotting dataRoot: location of data tables """ def __init__(self, modelRoot, *args, **kwargs): RunBase.__init__(self, *args, **kwargs) self.modelRoot = modelRoot if modelRoot != "/": self.dataRoot = modelRoot + '/data' else: self.dataRoot = "/data" self.setModelRoot(moose.Neutral(self.plugin.modelRoot).path) self.setDataRoot(moose.Neutral('/data').path) self.setDataRoot(moose.Neutral(self.plugin.modelRoot).path) self.plugin.modelRootChanged.connect(self.setModelRoot) self.plugin.dataRootChanged.connect(self.setDataRoot) # self.getCentralWidget() self._menus += self.getCentralWidget().getMenus() def getCentralWidget(self): """TODO: replace this with an option for multiple canvas tabs""" if self._centralWidget is None: self._centralWidget = PlotWidgetContainer(self.modelRoot) return self._centralWidget def setDataRoot(self, path): self.dataRoot = path def setModelRoot(self, path): self.modelRoot = path def getDataTablesPane(self): """This should create a tree widget with dataRoot as the root to allow visual selection of data tables for plotting.""" raise NotImplementedError() def plotAllData(self): """This is wrapper over the same function in PlotWidget.""" self.centralWidget.plotAllData() def getToolPanes(self): return [] if not self._toolPanes: self._toolPanes = [self.getSchedulingDockWidget()] return self._toolPanes def getSchedulingDockWidget(self): """Create and/or return a widget for schduling""" if hasattr(self, 'schedulingDockWidget') and self.schedulingDockWidget is not None: return self.schedulingDockWidget self.schedulingDockWidget = QtGui.QDockWidget('Scheduling') self.schedulingDockWidget.setFeatures( QtGui.QDockWidget.NoDockWidgetFeatures); self.schedulingDockWidget.setWindowFlags(Qt.CustomizeWindowHint) titleWidget = QtGui.QWidget(); self.schedulingDockWidget.setTitleBarWidget(titleWidget) widget = SchedulingWidget() widget.setDataRoot(self.dataRoot) widget.setModelRoot(self.modelRoot) self.schedulingDockWidget.setWidget(widget) widget.runner.simulationStarted.connect(self._centralWidget.extendXAxes) widget.runner.simulationProgressed.connect(self._centralWidget.updatePlots) widget.runner.simulationFinished.connect(self._centralWidget.rescalePlots) # widget.runner.simulationContinued.connect(self._centralWidget.extendXAxes) widget.runner.simulationReset.connect(self._centralWidget.plotAllData) self._toolBars += widget.getToolBars() return self.schedulingDockWidget ''' class MooseRunner(QtCore.QObject): """Helper class to control simulation execution See: http://doc.qt.digia.com/qq/qq27-responsive-guis.html : 'Solving a Problem Step by Step' for design details. """ resetAndRun = QtCore.pyqtSignal(name='resetAndRun') update = QtCore.pyqtSignal(name='update') currentTime = QtCore.pyqtSignal(float, name='currentTime') finished = QtCore.pyqtSignal(name='finished') def __init__( self , runTime , updateInterval ): QtCore.QObject.__init__(self) # if (MooseRunner.inited): # return self.runTime = runTime self.updateInterval = updateInterval self._updateInterval = 100e-3 self._simtime = 0.0 self._clock = moose.Clock('/clock') self._pause = False self.dataRoot = '/data' self.modelRoot = '/model' #MooseRunner.inited = True def doResetAndRun(self, tickDtMap, tickTargetMap, simtime, updateInterval): self._pause = False self._updateInterval = 0.1 #updateInterval self._simtime = simtime utils.updateTicks(tickDtMap) utils.assignTicks(tickTargetMap) self.resetAndRun.emit() moose.reinit() QtCore.QTimer.singleShot(0, self.run) def run(self): """Run simulation for a small interval.""" print("simtime => ", self._simtime) print("update interval => ", self._updateInterval) print("current time => ", self._clock.currentTime) print("Base dt => ", self._clock.baseDt) if self._clock.currentTime >= self._simtime: self.finished.emit() return if self._pause: return toRun = self._simtime - self._clock.currentTime if toRun > self._updateInterval: toRun = self._updateInterval if toRun < self._clock.baseDt: return moose.start(toRun) self.update.emit() self.currentTime.emit(self._clock.currentTime) QtCore.QTimer.singleShot(0, self.run) def continueRun(self, simtime, updateInterval): """Continue running without reset for `simtime`.""" self._simtime = simtime self._updateInterval = updateInterval self._pause = False QtCore.QTimer.singleShot(0, self.run) def stop(self): """Pause simulation""" self._pause = True ''' class SchedulingWidget(QtGui.QWidget): """Widget for scheduling. Important member fields: runner - object to run/pause/continue simulation. Whenever `updateInterval` time has been simulated this object sends an `update()` signal. This can be connected to other objects to update their data. SIGNALS: resetAndRun(tickDt, tickTargets, simtime, updateInterval) tickDt: dict mapping tick nos to dt tickTargets: dict mapping ticks to target paths simtime: total simulation runtime updateInterval: interval between update signals are to be emitted. simtimeExtended(simtime) emitted when simulation time is increased by user. """ resetAndRun = QtCore.pyqtSignal(dict, dict, float, float, name='resetAndRun') simtimeExtended = QtCore.pyqtSignal(float, name='simtimeExtended') continueRun = QtCore.pyqtSignal(float, float, name='continueRun') def __init__(self, *args, **kwargs): QtGui.QWidget.__init__(self, *args, **kwargs) self.simulationInterval = None self.updateInterval = None self.runTime = None # if not self.advanceOptiondisplayed: # self.advancedOptionsWidget.hide() # self.__getUpdateIntervalWidget() #layout.addWidget(self.__getUpdateIntervalWidget()) # spacerItem = QtGui.QSpacerItem(450, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) # layout.addItem(spacerItem) # self.setLayout(layout) # self._toolBars.append( self.modelRoot = None self.dataRoot = None self.runner = Runner() self.resetAndRunAction = None self.stopAction = None self.continueAction = None self.preferences = preferences self.currentSimulationRuntime = None self.modelType = None self.simulationRuntime = None self.schedulerToolBar = self.getSchedulerToolBar() self.runner.simulationProgressed.connect(self.updateCurrentSimulationRuntime) self.continueFlag = False self.preferences.applyChemicalSettings.connect(self.resetSimulation) # self.resetAndRunButton.clicked.connect(self.resetAndRunSlot) # self.continueButton.clicked.connect(self.doContinueRun) # self.continueRun.connect(self.runner.continueRun) # self.stopButton.clicked.connect(self.runner.stop) def updateCurrentSimulationRuntime(self, time): self.currentSimulationRuntime.setText(str(time)) def getToolBars(self): return [self.schedulerToolBar] def getSchedulerToolBar(self): bar = QToolBar("Run", self) self.resetAction = bar.addAction( QIcon('icons/arrow_undo.png') , 'Reset' , self.resetSimulation ) self.resetAction.setToolTip('Reset simulation.') self.runAction = bar.addAction( QIcon('icons/run.png') , 'Run' , self.runSimulation ) self.runAction.setToolTip('Run simulation.') self.stopAction = bar.addAction( QIcon('icons/stop.png') , 'Stop' , self.runner.togglePauseSimulation ) self.stopAction.setToolTip('Stop simulation.') bar.addSeparator() runtimeLabel = QLabel('Run for') self.simulationRuntime = QLineEdit() self.simulationRuntime.setValidator(QDoubleValidator()) self.simulationRuntime.setFixedWidth(75) bar.addWidget(runtimeLabel) bar.addWidget(self.simulationRuntime) bar.addWidget(QLabel(' (s)')) bar.addSeparator() #: current time # spacer.setSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Preferred) # self._runToolBar.addWidget(spacer) # self._runToolBar.addWidget(QtGui.QLabel('Current time')) self.currentSimulationRuntime = QLineEdit() # 6 digits self.currentSimulationRuntime.setToolTip('Current simulation runtime.') self.currentSimulationRuntime.setFixedWidth(75) self.currentSimulationRuntime.setValidator(QDoubleValidator()) self.currentSimulationRuntime.setText("0.0") self.currentSimulationRuntime.setReadOnly(True) # self.runner.currentTime.connect(self.currentTimeWidget.display) bar.addWidget(QLabel("Current Time : ")) bar.addWidget(self.currentSimulationRuntime) bar.addWidget(QLabel(" (s)")) # self._runToolBar.addWidget(self.()) bar.addSeparator() self.preferencesButton = QToolButton() self.preferencesButton.setText("Preferences") self.preferencesButton.clicked.connect(self.preferencesToggler) bar.addWidget(self.preferencesButton) return bar # def updateTickswidget(self): # if self.advanceOptiondisplayed: # self.advancedOptionsWidget.hide() # self.advanceOptiondisplayed = False # else: # self.advancedOptionsWidget.show() # self.advanceOptiondisplayed = True def continueSimulation(self): self.runner.continueSimulation( self.runTime , self.updateInterval , self.simulationInterval ) self.simulationRuntime.setText(str(float(self.simulationRuntime.text()) + self.runTime)) def resetSimulation(self): self.setParameters() try: self.runtime = float(runtime) except: self.runtime = 100.0 # print(self.runTime) # print(self.updateInterval) # print(self.simulationInterval) self.currentSimulationRuntime.setText("0.0") self.checkConsistency() # self.preferences.setChemicalClocks() self.simulationRuntime.setText(str(self.runTime)) self.runner.resetSimulation( self.runTime , self.updateInterval , self.simulationInterval ) self.continueFlag = False def runSimulation(self): if self.modelType == CHEMICAL_MODEL: compt = moose.wildcardFind(self.modelRoot+'/##[ISA=ChemCompt]') if not moose.exists(compt[0].path+'/stoich'): chemPref = self.preferences.getChemicalPreferences() solver = chemPref["simulation"]["solver"] addSolver(self.modelRoot,solver) status = self.solverStatus() #print "status ",status # if status != 0 or status == -1: # return if status == None or int(status) == -1 or int(status) == 0: #allow the model to Run pass else: # if something is dangling or solver is not set then return return runtime = str(self.simulationRuntime.text()) try: self.runtime = float(runtime) except: self.runtime = 100.0 self.simulationRuntime.setText("100.0") self.checkConsistency() self.continueSimulation = True self.runner.runSimulation(self.runtime) # return # if self.continueFlag: # self.continueSimulation() # else: # self.runner.runSimulation() # self.continueFlag = True def setParameters(self): if self.modelType == ELECTRICAL_MODEL: self.setElectricalParameters() elif self.modelType == CHEMICAL_MODEL: self.setChemicalParameters() def setChemicalParameters(self): chemicalPreferences = self.preferences.getChemicalPreferences() self.preferences.initializeChemicalClocks() self.updateInterval = chemicalPreferences["simulation"]["gui-update-interval"] self.simulationInterval = chemicalPreferences["simulation"]["simulation-dt"] if str(self.simulationRuntime.text()) == "": self.simulationRuntime.setText(str(chemicalPreferences["simulation"]["default-runtime"])) self.runTime = float(self.simulationRuntime.text()) self.solver = chemicalPreferences["simulation"]["solver"] #print(self.solver) def setElectricalParameters(self): electricalPreferences = self.preferences.getElectricalPreferences() self.preferences.initializeElectricalClocks() self.updateInterval = electricalPreferences["simulation"]["gui-update-interval"] self.simulationInterval = electricalPreferences["simulation"]["simulation-dt"] if str(self.simulationRuntime.text()) == "": self.simulationRuntime.setText(str(electricalPreferences["simulation"]["default-runtime"])) self.runTime = float(self.simulationRuntime.text()) self.solver = electricalPreferences["simulation"]["solver"] #print(self.solver) def checkConsistency(self): if self.updateInterval < self.simulationInterval : self.updateInterval = self.simulationInterval # print("Hello") # dialog = QErrorMessage() # dialog.showMessage( # """GUI Update interval should be greater than Simulation Interval. # Please update these values in Edit > Preferences.""" # ) # return False if self.runTime < self.updateInterval : self.runTime = self.updateInterval # dialog = QErrorMessage() # dialog.showMessage( # """Simulation runtime should greater than GUI Update interval. # Please update the runtime in the Scheduling Toolbar""" # ) # return False return True def solverStatus(self): compt = moose.wildcardFind(self.modelRoot+'/##[ISA=ChemCompt]') if not moose.exists(compt[0].path+'/stoich'): return None else: stoich = moose.Stoich(compt[0].path+'/stoich') status = int(stoich.status) # print("Status =>", status) if status == -1: QtGui.QMessageBox.warning(None,"Could not Run the model","Warning: Reaction path not yet assigned.\n ") return -1 if status == 1: QtGui.QMessageBox.warning(None,"Could not Run the model","Warning: Missing a reactant in a Reac or Enz.\n ") return 1 elif status == 2: QtGui.QMessageBox.warning(None,"Could not Run the model","Warning: Missing a substrate in an MMenz.\n ") return 2 elif status == 3: QtGui.QMessageBox.warning(None,"Could not Run the model","Warning: Missing substrates as well as reactants.\n ") return 3 elif status == 4: QtGui.QMessageBox.warning(None,"Could not Run the model"," Warning: Compartment not defined.\n ") return 4 elif status == 8: QtGui.QMessageBox.warning(None,"Could not Run the model","Warning: Neither Ksolve nor Dsolve defined.\n ") return 8 elif status == 16: QtGui.QMessageBox.warning(None,"Could not Run the model","Warning: No objects found on path.\n ") return 16 elif status == 0: print "Successfully built stoichiometry matrix.\n " # moose.reinit() return 0 # def setElectricalParameters(self): # chemicalPreferences = self.preferences.getChemicalPreferences() # self.updateInterval = chemicalPreferences["guiUpdateInterval"] # self.simulationInterval = chemicalPreferences["simulationInterval"] # chemicalPreferences["diffusionInterval"] # chemicalPreferences # self. chemicalPreferences # self. chemicalPreferences # self. chemicalPreferences # self. runTime = float(self.simulationRuntime.text()) def __getAdvanceOptionsButton(self): icon = QtGui.QIcon(os.path.join(config.settings[config.KEY_ICON_DIR],'arrow.png')) # self.advancedOptionsButton.setIcon(QtGui.QIcon(icon)) # self.advancedOptionsButton.setToolButtonStyle( Qt.ToolButtonTextBesideIcon ); return self.advancedOptionsButton def preferencesToggler(self): visibility = not self.preferences.getView().isVisible() self.preferences.getView().setVisible(visibility) def continueSlot(self): pass def updateCurrentTime(self): sys.stdout.flush() self.currentTimeWidget.dispay(str(moose.Clock('/clock').currentTime)) def updateTextFromTick(self, tickNo): tick = moose.vector('/clock/tick')[tickNo] widget = self.tickListWidget.layout().itemAtPosition(tickNo + 1, 1).widget() if widget is not None and isinstance(widget, QtGui.QLineEdit): widget.setText(str(tick.dt)) def updateFromMoose(self): """Update the tick dt from the tick objects""" ticks = moose.vector('/clock/tick') # Items at position 0 are the column headers, hence ii+1 for ii in range(ticks[0].localNumField): self.updateTextFromTick(ii) self.updateCurrentTime() def getSimTime(self): try: time = float(str(self.simtimeEdit.text())) return time except ValueError, e: QtGui.QMessageBox.warning(self, 'Invalid value', 'Specified runtime was meaningless.') return 0 def setDataRoot(self, root='/data'): self.dataRoot = moose.element(root).path def setModelRoot(self, root='/model'): self.modelRoot = moose.element(root).path self.setModelType() def setModelType(self): if moose.exists(self.modelRoot + "/model/cells"): self.modelType = ELECTRICAL_MODEL else: self.modelType = CHEMICAL_MODEL self.resetSimulation() from collections import namedtuple # Keeps track of data sources for a plot. 'x' can be a table path or # '/clock' to indicate time points from moose simulations (which will # be created from currentTime field of the `/clock` element and the # number of dat points in 'y'. 'y' should be a table. 'z' can be empty # string or another table or something else. Will not be used most of # the time (unless 3D or heatmap plotting). PlotDataSource = namedtuple('PlotDataSource', ['x', 'y', 'z'], verbose=False) event = None legend = None canvas = None from PyQt4.QtGui import QWidget from PyQt4.QtGui import QSizeGrip from PyQt4.QtGui import QLayout from PyQt4.QtGui import QScrollArea from PyQt4.QtGui import QMenu from PyQt4.QtCore import pyqtSlot,SIGNAL,SLOT, Signal, pyqtSignal class PlotWidget(QWidget): """A wrapper over CanvasWidget to handle additional MOOSE-specific stuff. modelRoot - path to the entire model our plugin is handling dataRoot - path to the container of data tables. TODO: do we really need this separation or should we go for standardizing location of data with respect to model root. pathToLine - map from moose path to Line2D objects in plot. Can one moose table be plotted multiple times? Maybe yes (e.g., when you want multiple other tables to be compared with the same data). lineToDataSource - map from Line2D objects to moose paths """ widgetClosedSignal = pyqtSignal(object) addGraph = pyqtSignal(object) def __init__(self, model, graph, index, parentWidget, *args, **kwargs): super(PlotWidget, self).__init__() self.model = model self.graph = graph self.index = index self.menu = self.getContextMenu() self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.connect( self , SIGNAL("customContextMenuRequested(QPoint)") , self , SLOT("contextMenuRequested(QPoint)") ) self.canvas = CanvasWidget(self.model, self.graph, self.index) self.canvas.setParent(self) self.navToolbar = NavigationToolbar(self.canvas, self) self.hackNavigationToolbar() self.canvas.mpl_connect('pick_event',self.togglePlot) # self.canvas. # self.navToolbar.addSeparator() layout = QtGui.QGridLayout() # canvasScrollArea = QScrollArea() # canvasScrollArea.setWidget(self.canvas) layout.addWidget(self.navToolbar, 0, 0) layout.addWidget(self.canvas, 1, 0) self.setLayout(layout) # self.setAcceptDrops(True) #self.modelRoot = '/' self.pathToLine = defaultdict(set) self.lineToDataSource = {} self.axesRef = self.canvas.addSubplot(1, 1) # box = self.axesRef.get_position() # self.axesRef.set_position([box.x0, box.y0, box.width * 0.8, box.height]) self.legend = None # layout.setSizeConstraint( QLayout.SetNoConstraint ) # self.setSizePolicy( QtGui.QSizePolicy.Expanding # , QtGui.QSizePolicy.Expanding # ) desktop = QtGui.QApplication.desktop() # print("**********************") # print(desktop.screenGeometry()) # print("***********************") self.setMinimumSize(desktop.screenGeometry().width() / 4, desktop.screenGeometry().height() / 3) # self.setMaximumSize(desktop.screenGeometry().width() / 2, desktop.screenGeometry().height() / 2) # self.setMinimumSize(self.width(), self.height()) # self.setMaximumSize(2 * self.width(), 2* self.height()) # QtCore.QObject.connect(utils.tableEmitter,QtCore.SIGNAL("tableCreated()"),self.plotAllData) self.canvas.updateSignal.connect(self.plotAllData) self.plotAllData() # self.plotView = PlotView(model, graph, index, self) #self.dataTable = DataTable() #utils.tableCreated.connect(plotAllData) # self.plotAllData() # self.setSizePolicy(QtGui.QSizePolicy.Fixed, # QtGui.QSizePolicy.Expanding) def hackNavigationToolbar(self): # ADD Graph Action pixmap = QPixmap("icons/add_graph.png") icon = QIcon(pixmap) action = QAction(icon, "Add a graph", self.navToolbar) # self.navToolbar.addAction(action) action.triggered.connect(self.addGraph.emit) self.navToolbar.insertAction(self.navToolbar.actions()[0], action) # Delete Graph Action pixmap = QPixmap("icons/delete_graph.png") icon = QIcon(pixmap) action = QAction(icon, "Delete this graph", self.navToolbar) # self.navToolbar.addAction(action) action.triggered.connect(self.delete) self.navToolbar.insertAction(self.navToolbar.actions()[1], action) #Toggle Grid Action pixmap = QPixmap("icons/grid.png") icon = QIcon(pixmap) action = QAction(icon, "Toggle Grid", self.navToolbar) # self.navToolbar.addAction(action) action.triggered.connect(self.canvas.toggleGrid) self.navToolbar.insertAction(self.navToolbar.actions()[2], action) self.navToolbar.insertSeparator(self.navToolbar.actions()[3]) @property def plotAll(self): return len(self.pathToLine) == 0 def toggleLegend(self): if self.legend is not None: self.legend.set_visible(not self.legend.get_visible()) self.canvas.draw() def getContextMenu(self): menu = QMenu() # closeAction = menu.addAction("Delete") exportCsvAction = menu.addAction("Export to CSV") exportCsvAction.triggered.connect(self.saveAllCsv) toggleLegendAction = menu.addAction("Toggle legend") toggleLegendAction.triggered.connect(self.toggleLegend) self.removeSubmenu = menu.addMenu("Remove") # configureAction.triggered.connect(self.configure) # self.connect(,SIGNAL("triggered()"), # self,SLOT("slotShow500x500()")) # self.connect(action1,SIGNAL("triggered()"), # self,SLOT("slotShow100x100()")) return menu def deleteGraph(self): print("Deleting " + self.graph.path) moose.delete(self.graph.path) def delete(self, event): print("Deleting PlotWidget") self.deleteGraph() self.close() self.widgetClosedSignal.emit(self) def configure(self, event): print("Displaying configure view!") self.plotView.getCentralWidget().show() @pyqtSlot(QtCore.QPoint) def contextMenuRequested(self,point): # menu = QtGui.QMenu() # # action1 = menu.addAction("Set Size 100x100") # # action2 = menu.addAction("Set Size 500x500") # # self.connect(action2,SIGNAL("triggered()"), # # self,SLOT("slotShow500x500()")) # # self.connect(action1,SIGNAL("triggered()"), # # self,SLOT("slotShow100x100()")) self.menu.exec_(self.mapToGlobal(point)) def setModelRoot(self, path): self.modelRoot = path def setDataRoot(self, path): self.dataRoot = path #plotAllData() def genColorMap(self,tableObject): #print "tableObject in colorMap ",tableObject species = tableObject+'/info' colormap_file = open(os.path.join(config.settings[config.KEY_COLORMAP_DIR], 'rainbow2.pkl'),'rb') self.colorMap = pickle.load(colormap_file) colormap_file.close() hexchars = "0123456789ABCDEF" color = 'white' #Genesis model exist the path and color will be set but not xml file so bypassing #print "here genColorMap ",moose.exists(species) if moose.exists(species): color = moose.element(species).getField('color') if ((not isinstance(color,(list,tuple)))): if color.isdigit(): tc = int(color) tc = (tc * 2 ) r,g,b = self.colorMap[tc] color = "#"+ hexchars[r / 16] + hexchars[r % 16] + hexchars[g / 16] + hexchars[g % 16] + hexchars[b / 16] + hexchars[b % 16] else: color = 'white' return color def removePlot(self, table): print("removePlot =>", table) moose.delete(table) self.plotAllData() def makeRemovePlotAction(self, label, table): action = self.removeSubmenu.addAction(label) action.triggered.connect(lambda: self.removePlot(table)) return action def plotAllData(self): """Plot data from existing tables""" self.axesRef.lines = [] self.pathToLine.clear() self.removeSubmenu.clear() if self.legend is not None: self.legend.set_visible(False) path = self.model.path modelroot = self.model.path time = moose.Clock('/clock').currentTime tabList = [] #for tabId in moose.wildcardFind('%s/##[TYPE=Table]' % (path)): #harsha: policy graphs will be under /model/modelName need to change in kkit #for tabId in moose.wildcardFind('%s/##[TYPE=Table]' % (modelroot)): plotTables = list(moose.wildcardFind(self.graph.path + '/##[TYPE=Table]')) plotTables.extend(moose.wildcardFind(self.graph.path + '/##[TYPE=Table2]')) if len (plotTables) > 0: for tabId in plotTables: tab = moose.Table(tabId) #print("Table =>", tab) line_list=[] tableObject = tab.neighbors['requestOut'] # Not a good way #tableObject.msgOut[0] if len(tableObject) > 0: # This is the default case: we do not plot the same # table twice. But in special cases we want to have # multiple variations of the same table on different # axes. # #Harsha: Adding color to graph for signalling model, check if given path has cubemesh or cylmesh color = '#FFFFFF' if moose.exists(tableObject[0].path + '/info'): color = getColor(tableObject[0].path + '/info') color = str(color[1].name()).upper() lines = self.pathToLine[tab.path] if len(lines) == 0: #Harsha: pass color for plot if exist and not white else random color #print "tab in plotAllData ",tab, tab.path,tab.name field = tab.path.rpartition(".")[-1] if field.endswith("[0]") or field.endswith("_0_"): field = field[:-3] # label = ( tableObject[0].path.partition(self.model.path + "/model[0]/")[-1] # + "." # + field # ) label = ( tableObject[0].path.rpartition("/")[-1] + "." + field ) self.makeRemovePlotAction(label, tab) if (color != '#FFFFFF'): newLines = self.addTimeSeries(tab, label=label,color=color) else: newLines = self.addTimeSeries(tab, label=label) self.pathToLine[tab.path].update(newLines) for line in newLines: self.lineToDataSource[line] = PlotDataSource(x='/clock', y=tab.path, z='') else: for line in lines: dataSrc = self.lineToDataSource[line] xSrc = moose.element(dataSrc.x) ySrc = moose.element(dataSrc.y) if isinstance(xSrc, moose.Clock): ts = np.linspace(0, time, len(tab.vector)) elif isinstance(xSrc, moose.Table): ts = xSrc.vector.copy() line.set_data(ts, tab.vector.copy()) tabList.append(tab) # if len(tabList) > 0: self.legend = self.canvas.callAxesFn( 'legend' , loc='upper right' , prop= {'size' : 10 } # , bbox_to_anchor=(1.0, 0.5) , fancybox = True , shadow=False , ncol=1 ) if self.legend is not None: self.legend.draggable() self.legend.get_frame().set_alpha(0.5) self.legend.set_visible(True) self.canvas.draw() # # leg = self.canvas.callAxesFn( 'legend' # # , loc ='upper right' # # , prop = {'size' : 10 } # # # , bbox_to_anchor = (0.5, -0.03) # # , fancybox = False # # # , shadow = True # # , ncol = 1 # # ) # # leg.draggable(False) # # print(leg.get_window_extent()) # #leg = self.canvas.callAxesFn('legend') # #leg = self.canvas.callAxesFn('legend',loc='upper left', fancybox=True, shadow=True) # #global legend # #legend =leg # for legobj in leg.legendHandles: # legobj.set_linewidth(5.0) # legobj.set_picker(True) # else: # print "returning as len tabId is zero ",tabId, " tableObject ",tableObject, " len ",len(tableObject) def togglePlot(self, event): #print "onclick",event1.artist.get_label() #harsha:To workout with double-event-registered on onclick event #http://stackoverflow.com/questions/16278358/double-event-registered-on-mouse-click-if-legend-is-outside-axes legline = event.artist for line in self.axesRef.lines: if line.get_label() == event.artist.get_label(): vis = not line.get_visible() line.set_visible(vis) if vis: legline.set_alpha(1.0) else: legline.set_alpha(0.2) break self.canvas.draw() def addTimeSeries(self, table, *args, **kwargs): ts = np.linspace(0, moose.Clock('/clock').currentTime, len(table.vector)) return self.canvas.plot(ts, table.vector, *args, **kwargs) def addRasterPlot(self, eventtable, yoffset=0, *args, **kwargs): """Add raster plot of events in eventtable. yoffset - offset along Y-axis. """ y = np.ones(len(eventtable.vector)) * yoffset return self.canvas.plot(eventtable.vector, y, '|') def updatePlots(self): for path, lines in self.pathToLine.items(): element = moose.element(path) if isinstance(element, moose.Table2): tab = moose.Table2(path) else: tab = moose.Table(path) data = tab.vector ts = np.linspace(0, moose.Clock('/clock').currentTime, len(data)) for line in lines: line.set_data(ts, data) self.canvas.draw() def extendXAxes(self, xlim): for axes in self.canvas.axes.values(): # axes.autoscale(False, axis='x', tight=True) axes.set_xlim(right=xlim) axes.autoscale_view(tight=True, scalex=True, scaley=True) self.canvas.draw() def rescalePlots(self): """This is to rescale plots at the end of simulation. ideally we should set xlim from simtime. """ for axes in self.canvas.axes.values(): axes.autoscale(True, tight=True) axes.relim() axes.autoscale_view(tight=True,scalex=True,scaley=True) self.canvas.draw() def saveCsv(self, line, directory): """Save selected plot data in CSV file""" src = self.lineToDataSource[line] xSrc = moose.element(src.x) ySrc = moose.element(src.y) y = ySrc.vector.copy() if isinstance(xSrc, moose.Clock): x = np.linspace(0, xSrc.currentTime, len(y)) elif isinstance(xSrc, moose.Table): x = xSrc.vector.copy() filename = str(directory)+'/'+'%s.csv' % (ySrc.name) np.savetxt(filename, np.vstack((x, y)).transpose()) print 'Saved data from %s and %s in %s' % (xSrc.path, ySrc.path, filename) def saveAllCsv(self): """Save data for all currently plotted lines""" #Harsha: Plots were saved in GUI folder instead provided QFileDialog box to save to #user choose fileDialog2 = QtGui.QFileDialog(self) fileDialog2.setFileMode(QtGui.QFileDialog.Directory) fileDialog2.setWindowTitle('Select Directory to save plots') fileDialog2.setOptions(QtGui.QFileDialog.ShowDirsOnly) fileDialog2.setLabelText(QtGui.QFileDialog.Accept, self.tr("Save")) targetPanel = QtGui.QFrame(fileDialog2) targetPanel.setLayout(QtGui.QVBoxLayout()) layout = fileDialog2.layout() layout.addWidget(targetPanel) if fileDialog2.exec_(): directory = fileDialog2.directory().path() for line in self.lineToDataSource.keys(): self.saveCsv(line,directory) def getMenus(self): if not hasattr(self, '_menus'): self._menus = [] self.plotAllAction = QtGui.QAction('Plot all data', self) self.plotAllAction.triggered.connect(self.plotAllData) self.plotMenu = QtGui.QMenu('Plot') self.plotMenu.addAction(self.plotAllAction) self.saveAllCsvAction = QtGui.QAction('Save all data in CSV files', self) self.saveAllCsvAction.triggered.connect(self.saveAllCsv) self.plotMenu.addAction(self.saveAllCsvAction) self._menus.append(self.plotMenu) return self._menus # def resizeEvent(self, event): # print("Here", event) # self.canvas.figure.subplots_adjust(bottom=0.2)#, left = 0.18) ################################################### # # Plot view - select fields to record # ################################################### ''' class PlotView(PlotBase): """View for selecting fields on elements to plot.""" def __init__(self, model, graph, index, *args): PlotBase.__init__(self, *args) self.model = model self.graph = graph self.index = index # self.plugin.modelRootChanged.connect(self.getSelectionPane().setSearchRoot) # self.plugin.dataRootChanged.connect(self.setDataRoot) # self.dataRoot = self.plugin.dataRoot def setDataRoot(self, root): self.dataRoot = moose.element(root).path def getToolPanes(self): return (self.getFieldSelectionDock(), ) def getSelectionPane(self): """Creates a widget to select elements and fields for plotting. search-root, field-name, comparison operator , value """ if not hasattr(self, '_selectionPane'): self._searchWidget = SearchWidget() self._searchWidget.setSearchRoot(self.model.path) self._fieldLabel = QtGui.QLabel('Field to plot') self._fieldEdit = QtGui.QLineEdit() self._fieldEdit.returnPressed.connect(self._searchWidget.searchSlot) self._selectionPane = QtGui.QWidget() layout = QtGui.QHBoxLayout() layout.addWidget(self._fieldLabel) layout.addWidget(self._fieldEdit) self._searchWidget.layout().addLayout(layout) self._selectionPane = self._searchWidget self._selectionPane.layout().addStretch(1) return self._selectionPane def getOperationsPane(self): """TODO: complete this""" if hasattr(self, 'operationsPane'): return self.operationsPane self.operationsPane = QtGui.QWidget() self._createTablesButton = QtGui.QPushButton('Create tables for recording selected fields', self.operationsPane) self._createTablesButton.clicked.connect(self.setupRecording) layout = QtGui.QVBoxLayout() self.operationsPane.setLayout(layout) layout.addWidget(self._createTablesButton) return self.operationsPane def getFieldSelectionDock(self): if not hasattr(self, '_fieldSelectionDock'): self._fieldSelectionDock = QtGui.QDockWidget('Search and select elements') self._fieldSelectionWidget = QtGui.QWidget() layout = QtGui.QVBoxLayout() self._fieldSelectionWidget.setLayout(layout) layout.addWidget(self.getSelectionPane()) layout.addWidget(self.getOperationsPane()) self._fieldSelectionDock.setWidget(self._fieldSelectionWidget) return self._fieldSelectionDock def getCentralWidget(self): if not hasattr(self, '_centralWidget') or self._centralWidget is None: self._centralWidget = PlotSelectionWidget(self.model, self.graph) self.getSelectionPane().executed.connect(self.selectElements) return self._centralWidget def selectElements(self, elements): """Refines the selection. Currently checks if _fieldEdit has an entry and if so, selects only elements which have that field, and ticks the same in the PlotSelectionWidget. """ field = str(self._fieldEdit.text()).strip() if len(field) == 0: self.getCentralWidget().setSelectedElements(elements) return classElementDict = defaultdict(list) for epath in elements: el = moose.element(epath) classElementDict[el.className].append(el) refinedList = [] elementFieldList = [] for className, elist in classElementDict.items(): if field in elist[0].getFieldNames('valueFinfo'): refinedList +=elist elementFieldList += [(el, field) for el in elist] self.getCentralWidget().setSelectedElements(refinedList) self.getCentralWidget().setSelectedFields(elementFieldList) def setupRecording(self): """Create the tables for recording selected data and connect them.""" for element, field in self.getCentralWidget().getSelectedFields(): #createRecordingTable(element, field, self._recordingDict, self._reverseDict, self.dataRoot) #harsha:CreateRecordingTable function is moved to python/moose/utils.py file as create function #as this is required when I drop table on to the plot utils.create(self.plugin.modelRoot,moose.element(element),field,"Table2") #self.dataTable.create(self.plugin.modelRoot, moose.element(element), field) #self.updateCallback() def createRecordingTable(self, element, field): """Create table to record `field` from element `element` Tables are created under `dataRoot`, the names are generally created by removing `/model` in the beginning of `elementPath` and replacing `/` with `_`. If this conflicts with an existing table, the id value of the target element (elementPath) is appended to the name. """ if len(field) == 0 or ((element, field) in self._recordingDict): return # The table path is not foolproof - conflict is # possible: e.g. /model/test_object and # /model/test/object will map to same table. So we # check for existing table without element field # path in recording dict. relativePath = element.path.partition('/model[0]/')[-1] if relativePath.startswith('/'): relativePath = relativePath[1:] #Convert to camelcase if field == "concInit": field = "ConcInit" elif field == "conc": field = "Conc" elif field == "nInit": field = "NInit" elif field == "n": field = "N" elif field == "volume": field = "Volume" elif field == "diffConst": field ="DiffConst" tablePath = relativePath.replace('/', '_') + '.' + field tablePath = re.sub('.', lambda m: {'[':'_', ']':'_'}.get(m.group(), m.group()),tablePath) tablePath = self.dataRoot + '/' +tablePath if moose.exists(tablePath): tablePath = '%s_%d' % (tablePath, element.getId().value) if not moose.exists(tablePath): table = moose.Table(tablePath) print 'Created', table.path, 'for plotting', '%s.%s' % (element.path, field) target = element moose.connect(table, 'requestOut', target, 'get%s' % (field)) self._recordingDict[(target, field)] = table self._reverseDict[table] = (target, field) ''' class PlotSelectionWidget(QtGui.QScrollArea): """Widget showing the fields of specified elements and their plottable fields. User can select any number of fields for plotting and click a button to generate the tables for recording data. The data tables are by default created under /data. One can call setDataRoot with a path to specify alternate location. """ def __init__(self, model, graph, *args): QtGui.QScrollArea.__init__(self, *args) self.model = moose.element(model.path + "/model") self.modelRoot = self.model.path self.setLayout(QtGui.QVBoxLayout(self)) self.layout().addWidget(self.getPlotListWidget()) self.setDataRoot(self.model.path) self._elementWidgetsDict = {} # element path to corresponding qlabel and fields combo def getPlotListWidget(self): """An internal widget to display the list of elements and their plottable fields in comboboxes.""" if not hasattr(self, '_plotListWidget'): self._plotListWidget = QtGui.QWidget(self) layout = QtGui.QGridLayout(self._plotListWidget) self._plotListWidget.setLayout(layout) layout.addWidget(QtGui.QLabel('<h1>Elements matching search criterion will be listed here</h1>'), 0, 0) return self._plotListWidget def setSelectedElements(self, elementlist): """Create a grid of widgets displaying paths of elements in `elementlist` if it has at least one plottable field (a field with a numeric value). The numeric fields are listed in a combobox next to the element path and can be selected for plotting by the user. """ for ii in range(self.getPlotListWidget().layout().count()): item = self.getPlotListWidget().layout().itemAt(ii) if item is None: continue self.getPlotListWidget().layout().removeItem(item) w = item.widget() w.hide() del w del item self._elementWidgetsDict.clear() label = QtGui.QLabel('Element') label.setSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) self.getPlotListWidget().layout().addWidget(label, 0, 0, 1, 2) self.getPlotListWidget().layout().addWidget(QtGui.QLabel('Fields to plot'), 0, 2, 1, 1) for ii, entry in enumerate(elementlist): el = moose.element(entry) plottableFields = [] for field, dtype in moose.getFieldDict(el.className, 'valueFinfo').items(): if dtype == 'double': plottableFields.append(field) if len(plottableFields) == 0: continue elementLabel = QtGui.QLabel(el.path) fieldsCombo = CheckComboBox(self) fieldsCombo.addItem('') for item in plottableFields: fieldsCombo.addItem(item) self.getPlotListWidget().layout().addWidget(elementLabel, ii+1, 0, 1, 2) self.getPlotListWidget().layout().addWidget(fieldsCombo, ii+1, 2, 1, 1) self._elementWidgetsDict[el] = (elementLabel, fieldsCombo) def setModelRoot(self, root): pass def setDataRoot(self, path): """The tables will be created under dataRoot""" pass self.dataRoot = path def getSelectedFields(self): """Returns a list containing (element, field) for all selected fields""" ret = [] for el, widgets in self._elementWidgetsDict.items(): combo = widgets[1] for ii in range(combo.count()): field = str(combo.itemText(ii)).strip() if len(field) == 0: continue checked, success = combo.itemData(ii, Qt.CheckStateRole).toInt() if success and checked == Qt.Checked: ret.append((el, field)) return ret def setSelectedFields(self, elementFieldList): """Set the checked fields for each element in elementFieldList. elementFieldList: ((element1, field1), (element2, field2), ...) """ for el, field in elementFieldList: combo = self._elementWidgetsDict[el][1] idx = combo.findText(field) if idx >= 0: combo.setItemData(idx, QtCore.QVariant(Qt.Checked), Qt.CheckStateRole) combo.setCurrentIndex(idx) # # default.py ends here