From fd5bfe0b1715d5849e8b0fd2bcd20ac1a257a48d Mon Sep 17 00:00:00 2001 From: Federico Montesino Pouzols <federico.montesino-pouzols@stfc.ac.uk> Date: Fri, 14 Nov 2014 06:14:46 +0000 Subject: [PATCH] mantidplot dir renamed to pymantidplot, avoid name conflict, re #8912 --- Code/Mantid/MantidPlot/CMakeLists.txt | 10 +- Code/Mantid/MantidPlot/mantidplot.py | 11 + .../MantidPlot/pymantidplot/__init__.py | 1034 +++++++++++++++ .../pymantidplot/future/__init__.py | 0 .../MantidPlot/pymantidplot/future/pyplot.py | 1180 +++++++++++++++++ .../Mantid/MantidPlot/pymantidplot/proxies.py | 907 +++++++++++++ 6 files changed, 3137 insertions(+), 5 deletions(-) create mode 100644 Code/Mantid/MantidPlot/mantidplot.py create mode 100644 Code/Mantid/MantidPlot/pymantidplot/__init__.py create mode 100644 Code/Mantid/MantidPlot/pymantidplot/future/__init__.py create mode 100644 Code/Mantid/MantidPlot/pymantidplot/future/pyplot.py create mode 100644 Code/Mantid/MantidPlot/pymantidplot/proxies.py diff --git a/Code/Mantid/MantidPlot/CMakeLists.txt b/Code/Mantid/MantidPlot/CMakeLists.txt index 0488bd82125..a34141cea76 100644 --- a/Code/Mantid/MantidPlot/CMakeLists.txt +++ b/Code/Mantid/MantidPlot/CMakeLists.txt @@ -852,8 +852,8 @@ set( MTDPLOTPY_FILES proxies.py ) copy_files_to_dir ( "${MTDPLOTPY_FILES}" - ${CMAKE_CURRENT_SOURCE_DIR}/mantidplot - ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${CMAKE_CFG_INTDIR}/mantidplot + ${CMAKE_CURRENT_SOURCE_DIR}/pymantidplot + ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${CMAKE_CFG_INTDIR}/pymantidplot MTDPLOT_INSTALL_FILES ) set( FUTURE_FILES @@ -861,8 +861,8 @@ set( FUTURE_FILES pyplot.py ) copy_files_to_dir ( "${FUTURE_FILES}" - ${CMAKE_CURRENT_SOURCE_DIR}/mantidplot/future - ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${CMAKE_CFG_INTDIR}/mantidplot/future + ${CMAKE_CURRENT_SOURCE_DIR}/pymantidplot/future + ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${CMAKE_CFG_INTDIR}/pymantidplot/future MTDPLOT_FUTURE_INSTALL_FILES ) # IPython scripts @@ -1016,7 +1016,7 @@ install ( TARGETS MantidPlot RUNTIME DESTINATION ${BIN_DIR} # Ship required files. Explicit so some can be disabled easily if necessary install ( FILES ${PY_FILES} DESTINATION ${BIN_DIR} ) foreach(PY_FILE ${MTDPLOTPY_FILES} ) - install ( FILES mantidplot/${PY_FILE} DESTINATION ${BIN_DIR}/mantidplot ) + install ( FILES pymantidplot/${PY_FILE} DESTINATION ${BIN_DIR}/pymantidplot ) endforeach() foreach(PY_FILE ${IPY_FILES} ) install ( FILES ipython_widget/${PY_FILE} DESTINATION ${BIN_DIR}/ipython_widget ) diff --git a/Code/Mantid/MantidPlot/mantidplot.py b/Code/Mantid/MantidPlot/mantidplot.py new file mode 100644 index 00000000000..48e47f867ff --- /dev/null +++ b/Code/Mantid/MantidPlot/mantidplot.py @@ -0,0 +1,11 @@ +#------------------------------------------------------------------- +# mantidplot.py +# +# Load 'pymantidplot' which cannot be called 'mantidplot' because of +# name conflict with the MantidPlot binary (especially on osx) +#------------------------------------------------------------------- + +try: + import pymantidplot +except ImportError: + raise ImportError('Could not import mantidplot (when trying to import pymantidplot). Something is broken in this installation, please check.') diff --git a/Code/Mantid/MantidPlot/pymantidplot/__init__.py b/Code/Mantid/MantidPlot/pymantidplot/__init__.py new file mode 100644 index 00000000000..fbfa00c4976 --- /dev/null +++ b/Code/Mantid/MantidPlot/pymantidplot/__init__.py @@ -0,0 +1,1034 @@ +""" +MantidPlot module to gain access to plotting functions etc. +Requires that the main script be run from within MantidPlot +""" +# Requires MantidPlot +try: + import _qti +except ImportError: + raise ImportError('The "mantidplot" module can only be used from within MantidPlot.') + +import mantidplot.proxies as proxies +from mantidplot.proxies import threadsafe_call, new_proxy, getWorkspaceNames + +from PyQt4 import QtCore, QtGui +from PyQt4.QtCore import Qt +import os +import time +import mantid.api + +# Import into the global namespace qti classes that: +# (a) don't need a proxy & (b) can be constructed from python or (c) have enumerations within them +from _qti import (PlotSymbol, ImageSymbol, ArrowMarker, ImageMarker, + GraphOptions, InstrumentWindow, InstrumentWindowPickTab, InstrumentWindowMaskTab) + +# Make the ApplicationWindow instance accessible from the mantidplot namespace +from _qti import app + +# Alias threadsafe_call so users have a more understandable name +gui_cmd = threadsafe_call + +#---------------- Mantid Python access functions-------------------------- +# Grab a few Mantid things so that we can recognise workspace variables +def _get_analysis_data_service(): + """Returns an object that can be used to get a workspace by name from Mantid + + Returns: + A object that acts like a dictionary to retrieve workspaces + """ + import mantid + return mantid.AnalysisDataService.Instance() + +#-------------------------- Wrapped MantidPlot functions ----------------- + +def runPythonScript(code, async = False, quiet = False, redirect = True): + """ + Redirects the runPythonScript method to the app object + @param code :: A string of code to execute + @param async :: If the true the code is executed in a separate thread + @param quiet :: If true no messages reporting status are issued + @param redirect :: If true then output is redirected to MantidPlot + """ + if async and QtCore.QThread.currentThread() != QtGui.qApp.thread(): + async = False + threadsafe_call(_qti.app.runPythonScript, code, async, quiet, redirect) + +# Overload for consistency with qtiplot table(..) & matrix(..) commands +def workspace(name): + """Get a handle on a workspace. + + Args: + name: The name of the workspace in the Analysis Data Service. + """ + return _get_analysis_data_service()[name] + +def table(name): + """Get a handle on a table. + + Args: + name: The name of the table. + + Returns: + A handle to the table. + """ + return new_proxy(proxies.MDIWindow, _qti.app.table, name) + +def newTable(name=None,rows=30,columns=2): + """Create a table. + + Args: + name: The name to give to the table (if None, a unique name will be generated). + row: The number of rows in the table (default: 30). + columns: The number of columns in the table (default: 2). + + Returns: + A handle to the created table. + """ + if name is None: + return new_proxy(proxies.MDIWindow, _qti.app.newTable) + else: + return new_proxy(proxies.MDIWindow, _qti.app.newTable, name,rows,columns) + +def matrix(name): + """Get a handle on a matrix. + + Args: + name: The name of the matrix. + + Returns: + A handle to the matrix. + """ + return new_proxy(proxies.MDIWindow, _qti.app.matrix, name) + +def newMatrix(name=None,rows=32,columns=32): + """Create a matrix (N.B. This is not the same as a 'MantidMatrix'). + + Args: + name: The name to give to the matrix (if None, a unique name will be generated). + row: The number of rows in the matrix (default: 32). + columns: The number of columns in the matrix (default: 32). + + Returns: + A handle to the created matrix. + """ + if name is None: + return new_proxy(proxies.MDIWindow, _qti.app.newMatrix) + else: + return new_proxy(proxies.MDIWindow, _qti.app.newMatrix,name,rows,columns) + +def graph(name): + """Get a handle on a graph widget. + + Args: + name: The name of the graph window. + + Returns: + A handle to the graph. + """ + return new_proxy(proxies.Graph, _qti.app.graph, name) + +def newGraph(name=None,layers=1,rows=1,columns=1): + """Create a graph window. + + Args: + name: The name to give to the graph (if None, a unique name will be generated). + layers: The number of plots (a.k.a. layers) to put in the graph window (default: 1). + rows: The number of rows of to put in the graph window (default: 1). + columns: The number of columns of to put in the graph window (default: 1). + + Returns: + A handle to the created graph widget. + """ + if name is None: + return new_proxy(proxies.Graph, _qti.app.newGraph) + else: + return new_proxy(proxies.Graph, _qti.app.newGraph,name,layers,rows,columns) + +def note(name): + """Get a handle on a note. + + Args: + name: The name of the note. + + Returns: + A handle to the note. + """ + return new_proxy(proxies.MDIWindow, _qti.app.note, name) + +def newNote(name=None): + """Create a note. + + Args: + name: The name to give to the note (if None, a unique name will be generated). + + Returns: + A handle to the created note. + """ + if name is None: + return new_proxy(proxies.MDIWindow, _qti.app.newNote) + else: + return new_proxy(proxies.MDIWindow, _qti.app.newNote, name) + +def newTiledWindow(name=None): + """Create an empty tiled window. + + Args: + name: The name to give to the window (if None, a unique name will be generated). + + Returns: + A handle to the created window. + """ + if name is None: + return new_proxy(proxies.TiledWindowProxy, _qti.app.newTiledWindow) + else: + return new_proxy(proxies.TiledWindowProxy, _qti.app.newTiledWindow, name) + +#----------------------------------------------------------------------------- +# Intercept qtiplot "plot" command and forward to plotSpectrum for a workspace +def plot(source, *args, **kwargs): + """Create a new plot given a workspace, table or matrix. + + Args: + source: what to plot; if it is a Workspace, will + call plotSpectrum() + + Returns: + A handle to the created Graph widget. + """ + if hasattr(source, '_getHeldObject') and isinstance(source._getHeldObject(), QtCore.QObject): + return new_proxy(proxies.Graph,_qti.app.plot, source._getHeldObject(), *args, **kwargs) + else: + return plotSpectrum(source, *args, **kwargs) + +#---------------------------------------------------------------------------------------------------- +def plotSpectrum(source, indices, error_bars = False, type = -1, window = None, clearWindow = False): + """Open a 1D Plot of a spectrum in a workspace. + + This plots one or more spectra, with X as the bin boundaries, + and Y as the counts in each bin. + + Args: + source: workspace or name of a workspace + indices: workspace index, or tuple or list of workspace indices to plot + error_bars: bool, set to True to add error bars. + type: curve style for plot (-1: unspecified; 0: line, default; 1: scatter/dots) + window: window used for plotting. If None a new one will be created + clearWindow: if is True, the window specified will be cleared before adding new curve + Returns: + A handle to window if one was specified, otherwise a handle to the created one. None in case of error. + """ + workspace_names = getWorkspaceNames(source) + __checkPlotWorkspaces(workspace_names) + # check spectrum indices + index_list = __getWorkspaceIndices(indices) + if len(index_list) == 0: + raise ValueError("No spectrum indices given") + for idx in index_list: + if idx < 0: + raise ValueError("Wrong spectrum index (<0): %d" % idx) + for name in workspace_names: + max_spec = workspace(name).getNumberHistograms() - 1 + for idx in index_list: + if idx > max_spec: + raise ValueError("Wrong spectrum index for workspace '%s': %d, which is bigger than the" + " number of spectra in this workspace - 1 (%d)" % (name, idx, max_spec)) + + # Unwrap the window object, if any specified + if window != None: + window = window._getHeldObject() + + graph = proxies.Graph(threadsafe_call(_qti.app.mantidUI.plot1D, + workspace_names, index_list, True, error_bars, + type, window, clearWindow)) + if graph._getHeldObject() == None: + raise RuntimeError("Cannot create graph, see log for details.") + else: + return graph + +#---------------------------------------------------------------------------------------------------- +# IPython auto-complete can't handle enumerations as defaults +DEFAULT_2D_STYLE = int(_qti.Layer.ColorMap) + +def plot2D(source, style = DEFAULT_2D_STYLE, window = None): + """Open a 2D plot of the given workspace(s) + + Produces a 2D histogram for each of the given workspaces + + Args: + source: workspace or name of a workspace + style: Indicates the type of plot required. Default=ColorMap + window: window used for plotting. If None a new one will be created + Returns: + If a single workspace is specified then the handle is returned, otherwise a list + of handles for each new window is returned + """ + names = getWorkspaceNames(source) + if len(names) == 0: + raise ValueError("No workspace names given to plot") + + # Unwrap the window object, if any specified + if window != None: + window = window._getHeldObject() + + handles = [] + cfunc = _qti.app.mantidUI.drawSingleColorFillPlot + for name in names: + g = proxies.Graph(threadsafe_call(cfunc, name, style, window)) + if g: + handles.append(g) + else: + raise RuntimeError("Cannot create graph from workspace '%s'" % name) + + if len(handles) == 1: return handles[0] + else: return handles + +#---------------------------------------------------------------------------------------------------- + +# IPython couldn't correctly display complex enum value in doc pop-up, so we extract integer value +# of enum manually. +DEFAULT_MD_NORMALIZATION = int(mantid.api.MDNormalization.VolumeNormalization) + +def plotMD(source, plot_axis=-2, normalization = DEFAULT_MD_NORMALIZATION, error_bars = False, window = None, + clearWindow = False): + """Open a 1D plot of a MDWorkspace. + + Args: + source: Workspace(s) to plot + plot_axis: Index of the plot axis (defaults to auto-select) + normalization: Type of normalization required (defaults to volume) + error_bars: Flag for error bar plotting. + window: window used for plotting. If None a new one will be created + clearWindow: if is True, the window specified will be cleared before adding new curve + Returns: + A handle to the matrix containing the image data. + """ + workspace_names = getWorkspaceNames(source) + __checkPlotMDWorkspaces(workspace_names) + + for name in workspace_names: + non_integrated_dims = mantid.api.mtd[name].getNonIntegratedDimensions() + if not len(non_integrated_dims) == 1: + raise ValueError("'%s' must have a single non-integrated dimension in order to be rendered via plotMD" % name) + + # check axis index + for name in workspace_names: + ws = workspace(name) + if hasattr(ws, "axes"): + max_axis = workspace(name).axes() + # see choice in MantidQwtIMDWorkspaceData::setPlotAxisChoice, -2: auto, -1: distance + if plot_axis < -2 or plot_axis > max_axis: + raise ValueError("Incorrect axis index given for workspace '%s': %d, should be < %d" % (name, plot_axis, max_axis)) + + # Unwrap the window object, if any specified + if window != None: + window = window._getHeldObject() + + graph = proxies.Graph(threadsafe_call(_qti.app.mantidUI.plotMDList, workspace_names, plot_axis, normalization, + error_bars, window, clearWindow)) + + return graph + +def fitBrowser(): + """ + Access the fit browser. + """ + import mantidqtpython + return proxies.FitBrowserProxy(_qti.app.mantidUI.fitFunctionBrowser()) + +#----------------------------------------------------------------------------- +def plotBin(source, indices, error_bars = False, type = -1, window = None, clearWindow = False): + """Create a 1D Plot of bin count vs spectrum in a workspace. + + This puts the spectrum number as the X variable, and the + count in the particular bin # (in 'indices') as the Y value. + + If indices is a tuple or list, then several curves are created, one + for each bin index. + + Args: + source: workspace or name of a workspace + indices: bin number(s) to plot + error_bars: bool, set to True to add error bars. + type: Plot style + window: window used for plotting. If None a new one will be created + clearWindow: if is True, the window specified will be cleared before adding new curve + Returns: + A handle to window if one was specified, otherwise a handle to the created one. None in case of error. + """ + workspace_names = getWorkspaceNames(source) + __checkPlotWorkspaces(workspace_names) + index_list = __getWorkspaceIndices(indices) + if len(index_list) == 0: + raise ValueError("No indices given") + + for idx in index_list: + if idx < 0: + raise ValueError("Wrong bin index (<0): %d" % idx) + for name in workspace_names: + max_bin = workspace(name).blocksize() - 1 + for idx in index_list: + if idx > max_bin: + raise ValueError("Wrong bin index for workspace '%s': %d, which is bigger than the" + " number of bins in this workspace - 1 (%d)" % (name, idx, max_bin)) + + # Unwrap the window object, if any specified + if window != None: + window = window._getHeldObject() + + graph = proxies.Graph(threadsafe_call(_qti.app.mantidUI.plot1D, + workspace_names, index_list, False, error_bars, + type, window, clearWindow)) + if graph._getHeldObject() == None: + raise RuntimeError("Cannot create graph, see log for details.") + else: + return graph + +#----------------------------------------------------------------------------- +def stemPlot(source, index, power=None, startPoint=None, endPoint=None): + """Generate a stem-and-leaf plot from an input table column or workspace spectrum + + Args: + source: A reference to a workspace or a table. + index: For a table, the column number or name. For a workspace, the workspace index. + power: The stem unit as a power of 10. If not provided, a dialog will appear with a suggested value. + startPoint: The first point (row or bin) to use (Default: the first one). + endPoint: The last point (row or bin) to use (Default: the last one). + + Returns: + A string representation of the stem plot + """ + # Turn the optional arguments into the magic numbers that the C++ expects + if power==None: + power=1001 + if startPoint==None: + startPoint=0 + if endPoint==None: + endPoint=-1 + + if isinstance(source,proxies.QtProxyObject): + source = source._getHeldObject() + elif hasattr(source, 'getName'): + # If the source is a workspace, create a table from the specified index + wsName = source.getName() + source = threadsafe_call(_qti.app.mantidUI.workspaceToTable.wsName,wsName,[index],False,True) + # The C++ stemPlot method takes the name of the column, so get that + index = source.colName(2) + # Get column name if necessary + if isinstance(index, int): + index = source.colName(index) + # Call the C++ method + return threadsafe_call(_qti.app.stemPlot, source,index,power,startPoint,endPoint) + +#----------------------------------------------------------------------------- +def waterfallPlot(table, columns): + """Create a waterfall plot from data in a table. + + Args: + table: A reference to the table containing the data to plot + columns: A tuple of the column numbers whose data to plot + + Returns: + A handle to the created plot (Layer). + """ + return new_proxy(proxies.Graph, _qti.app.waterfallPlot, table._getHeldObject(),columns) + +#----------------------------------------------------------------------------- +def importImage(filename): + """Load an image file into a matrix. + + Args: + filename: The name of the file to load. + + Returns: + A handle to the matrix containing the image data. + """ + return new_proxy(proxies.MDIWindow, _qti.app.importImage, filename) + +#----------------------------------------------------------------------------- +def newPlot3D(): + return new_proxy(proxies.Graph3D, _qti.app.newPlot3D) + +def plot3D(*args): + if isinstance(args[0],str): + return new_proxy(proxies.Graph3D, _qti.app.plot3D, *args) + else: + return new_proxy(proxies.Graph3D, _qti.app.plot3D, args[0]._getHeldObject(),*args[1:]) + +#----------------------------------------------------------------------------- +def selectMultiPeak(source, showFitPropertyBrowser = True, xmin = None, xmax = None): + """Switch on the multi-peak selecting tool for fitting with the Fit algorithm. + + Args: + source: A reference to a MultiLayer with the data to fit. + showFitPropertyBrowser: Whether to show the FitPropertyBrowser or not. + xmin: An optionall minimum X value to select + xmax: An optionall maximum X value to select + """ + if xmin is not None and xmax is not None: + threadsafe_call(_qti.app.selectMultiPeak, source._getHeldObject(), showFitPropertyBrowser, xmin, xmax) + else: + threadsafe_call(_qti.app.selectMultiPeak, source._getHeldObject(), showFitPropertyBrowser) + +def disableTools(): + """Disable all the tools from all the graphs within MantidPlot.""" + threadsafe_call(_qti.app.disableTools) + +#------------------------------------------------------------------------------ +def setToolbarsVisible(visible): + """Show/hide MantidPlot toolbars + + Args: + visible: If True, make toolbars visible, if False - hidden + """ + threadsafe_call(_qti.app.setToolbarsVisible, visible) + +#----------------------------------------------------------------------------- +#-------------------------- Project/Folder functions ----------------------- +#----------------------------------------------------------------------------- +def windows(): + """Get a list of the open windows.""" + f = _qti.app.windows() + ret = [] + for item in f: + ret.append(proxies.MDIWindow(item)) + return ret + +def activeFolder(): + """Get a handle to the currently active folder.""" + return new_proxy(proxies.Folder, _qti.app.activeFolder) + +# These methods don't seem to work +#def appendProject(filename, parentFolder=None): +# if parentFolder is not None: +# parentFolder = parentFolder._getHeldObject() +# return proxies.Folder(_qti.app.appendProject(filename,parentFolder)) +# +#def saveFolder(folder, filename, compress=False): +# _qti.app.saveFolder(folder._getHeldObject(),filename,compress) + +def rootFolder(): + """Get a handle to the top-level folder.""" + return new_proxy(proxies.Folder, _qti.app.rootFolder) + +def addFolder(name,parentFolder=None): + """Create a new folder. + + Args: + name: The name of the folder to create. + parentFolder: If given, make the new folder a subfolder of this one. + + Returns: + A handle to the newly created folder. + """ + if parentFolder is not None: + parentFolder = parentFolder._getHeldObject() + return new_proxy(proxies.Folder, _qti.app.addFolder, name,parentFolder) + +def deleteFolder(folder): + """Delete the referenced folder""" + return threadsafe_call(_qti.app.deleteFolder, folder._getHeldObject()) + +def changeFolder(folder, force=False): + """Changes the current folder. + + Args: + folder: A reference to the folder to change to. + force: Whether to do stuff even if the new folder is already the active one (default: no). + + Returns: + True on success. + """ + return threadsafe_call(_qti.app.changeFolder, folder._getHeldObject(),force) + +def copyFolder(source, destination): + """Copy a folder (and its contents) into another. + + Returns: + True on success. + """ + return threadsafe_call(_qti.app.copyFolder, source._getHeldObject(),destination._getHeldObject()) + +def openTemplate(filename): + """Load a previously saved window template""" + return new_proxy(proxies.MDIWindow,_qti.app.openTemplate, filename) + +def saveAsTemplate(window, filename): + """Save the characteristics of the given window to file""" + threadsafe_call(_qti.app.saveAsTemplate, window._getHeldObject(), filename) + +def setWindowName(window, name): + """Set the given window to have the given name""" + threadsafe_call(_qti.app.setWindowName, window._getHeldObject(), name) + +def setPreferences(layer): + threadsafe_call(_qti.app.setPreferences, layer._getHeldObject()) + +def clone(window): + return new_proxy(proxies.MDIWindow, _qti.app.clone, window._getHeldObject()) + +def tableToMatrix(table): + return new_proxy(proxies.MDIWindow, _qti.app.tableToMatrix, table._getHeldObject()) + +def matrixToTable(matrix, conversionType=_qti.app.Direct): + return new_proxy(proxies.MDIWindow, _qti.app.matrixToTable, matrix._getHeldObject(),conversionType) + +#----------------------------------------------------------------------------- +#-------------------------- Wrapped MantidUI functions ----------------------- +#----------------------------------------------------------------------------- + +def mergePlots(graph1,graph2): + """Combine two graphs into a single plot""" + return new_proxy(proxies.Graph, _qti.app.mantidUI.mergePlots,graph1._getHeldObject(),graph2._getHeldObject()) + +def convertToWaterfall(graph): + """Convert a graph (containing a number of plotted spectra) to a waterfall plot""" + threadsafe_call(_qti.app.mantidUI.convertToWaterfall, graph._getHeldObject()) + +def getMantidMatrix(name): + """Get a handle to the named Mantid matrix""" + return new_proxy(proxies.MantidMatrix, _qti.app.mantidUI.getMantidMatrix, name) + +def getInstrumentView(name, tab=InstrumentWindow.RENDER): + """Create an instrument view window based on the given workspace. + + Args: + name: The name of the workspace. + tab: The index of the tab to display initially, (default=InstrumentWindow.RENDER) + + Returns: + A handle to the created instrument view widget. + """ + ads = _get_analysis_data_service() + if name not in ads: + raise ValueError("Workspace '%s' does not exist" % name) + return new_proxy(proxies.InstrumentWindow, _qti.app.mantidUI.getInstrumentView, name, tab) + +def importMatrixWorkspace(name, firstIndex=None, lastIndex=None, showDialog=False, visible=False): + """Create a MantidMatrix object from the named workspace. + + Args: + name: The name of the workspace. + firstIndex: The workspace index to start at (default: the first one). + lastIndex: The workspace index to stop at (default: the last one). + showDialog: Whether to bring up a dialog to allow options to be entered manually (default: no). + visible: Whether to initially show the created matrix (default: no). + + Returns: + A handle to the created matrix. + """ + # Turn the optional arguments into the magic numbers that the C++ expects + if firstIndex is None: + firstIndex = -1 + if lastIndex is None: + lastIndex = -1 + return new_proxy(proxies.MantidMatrix, _qti.app.mantidUI.importMatrixWorkspace, name, + firstIndex,lastIndex,showDialog,visible) + +def importTableWorkspace(name, visible=False): + """Create a MantidPlot table from a table workspace. + + Args: + name: The name of the workspace. + visible: Whether to initially show the created matrix (default: no). + + Returns: + A handle to the newly created table. + """ + return new_proxy(proxies.MDIWindow,_qti.app.mantidUI.importTableWorkspace, name,False,visible) + +def createScriptInputDialog(alg_name, preset_values, optional_msg, enabled, disabled): + """Raises a property input dialog for an algorithm""" + return threadsafe_call(_qti.app.mantidUI.createScriptInputDialog, alg_name, preset_values, optional_msg, enabled, disabled) + +#----------------------------------------------------------------------------- +#-------------------------- SliceViewer functions ---------------------------- +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +def plotSlice(source, label="", xydim=None, slicepoint=None, + colormin=None, colormax=None, colorscalelog=False, + normalization=1, + limits=None, **kwargs): + """Opens the SliceViewer with the given MDWorkspace(s). + + Args: + source :: one workspace, or a list of workspaces + + Optional Keyword Args: + label :: label for the window title + xydim :: indexes or names of the dimensions to plot, as an (X,Y) list or tuple. See SliceViewer::setXYDim() + slicepoint :: list with the slice point in each dimension. Must be the same length as the number of dimensions of the workspace. See SliceViewer::setSlicePoint() + colormin :: value of the minimum color in the scale. See SliceViewer::setColorScaleMin() + colormax :: value of the maximum color in the scale. See SliceViewer::setColorScaleMax() + colorscalelog :: value of the maximum color in the scale. See SliceViewer::setColorScaleLog() + limits :: list with the (xleft, xright, ybottom, ytop) limits to the view to show. See SliceViewer::setXYLimits() + normalization :: 0=none; 1=volume (default); 2=# of events. + + Returns: + a (list of) handle(s) to the SliceViewerWindow widgets that were open. + Use SliceViewerWindow.getSlicer() to get access to the functions of the + SliceViewer, e.g. setting the view and slice point. + """ + workspace_names = getWorkspaceNames(source) + try: + import mantidqtpython + except: + print "Could not find module mantidqtpython. Cannot open the SliceViewer." + return + + # Make a list of widgets to return + out = [] + for wsname in workspace_names: + window = __doSliceViewer(wsname, label=label, + xydim=xydim, slicepoint=slicepoint, colormin=colormin, + colormax=colormax, colorscalelog=colorscalelog, limits=limits, + normalization=normalization, + **kwargs) + pxy = proxies.SliceViewerWindowProxy(window) + out.append(pxy) + + # Return the widget alone if only 1 + if len(out) == 1: + return out[0] + else: + return out + + +#----------------------------------------------------------------------------- +def getSliceViewer(source, label=""): + """Retrieves a handle to a previously-open SliceViewerWindow. + This allows you to get a handle on, e.g., a SliceViewer that was open + by the MultiSlice view in VATES Simple Interface. + Will raise an exception if not found. + + Args: + source :: name of the workspace that was open + label :: additional label string that was used to identify the window. + + Returns: + a handle to the SliceViewerWindow object that was created before. + """ + import mantidqtpython + workspace_names = getWorkspaceNames(source) + if len(workspace_names) != 1: + raise Exception("Please specify only one workspace.") + else: + svw = threadsafe_call(mantidqtpython.MantidQt.Factory.WidgetFactory.Instance().getSliceViewerWindow, workspace_names[0], label) + if svw is not None: + return proxies.SliceViewerWindowProxy(svw) + else: + return None + + +#----------------------------------------------------------------------------- +def closeAllSliceViewers(): + """ + Closes all currently open SliceViewer windows. This might be useful to + clean up your desktop after opening many windows. + """ + import mantidqtpython + threadsafe_call(mantidqtpython.MantidQt.Factory.WidgetFactory.Instance().closeAllSliceViewerWindows) + +#----------------------------------------------------------------------------- +# Legacy function +plotTimeBin = plotBin + +# import 'safe' methods (i.e. no proxy required) of ApplicationWindow into the global namespace +# Only 1 at the moment! +appImports = [ + 'saveProjectAs' + ] +for name in appImports: + globals()[name] = getattr(_qti.app,name) + +# Ensure these functions are available as without the _qti.app.mantidUI prefix +MantidUIImports = [ + 'getSelectedWorkspaceName' + ] +# Update globals +for name in MantidUIImports: + globals()[name] = getattr(_qti.app.mantidUI,name) + +# Set some aliases for Layer enumerations so that old code will still work +Layer = _qti.Layer +Layer.Log10 = _qti.GraphOptions.Log10 +Layer.Linear = _qti.GraphOptions.Linear +Layer.Left = _qti.GraphOptions.Left +Layer.Right = _qti.GraphOptions.Right +Layer.Bottom = _qti.GraphOptions.Bottom +Layer.Top = _qti.GraphOptions.Top + +#----------------------------------------------------------------------------- +#--------------------------- "Private" functions ----------------------------- +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +def __doSliceViewer(wsname, label="", xydim=None, slicepoint=None, + colormin=None, colormax=None, colorscalelog=False, + limits=None, normalization=1): + """Open a single SliceViewerWindow for the workspace, and shows it + + Args: + wsname :: name of the workspace + See plotSlice() for full list of keyword parameters. + + Returns: + A handle to the created SliceViewerWindow widget + """ + import mantidqtpython + from PyQt4 import QtCore + + svw = threadsafe_call(mantidqtpython.MantidQt.Factory.WidgetFactory.Instance().createSliceViewerWindow, wsname, label) + threadsafe_call(svw.show) + + # -- Connect to main window's shut down signal --- + QtCore.QObject.connect(_qti.app, QtCore.SIGNAL("shutting_down()"), + svw, QtCore.SLOT("close()")) + + sv = threadsafe_call(svw.getSlicer) + # --- X/Y Dimensions --- + if (not xydim is None): + if len(xydim) != 2: + raise Exception("You need to specify two values in the 'xydim' parameter") + else: + threadsafe_call(sv.setXYDim, xydim[0], xydim[1]) + + # --- Slice point --- + if not slicepoint is None: + for d in xrange(len(slicepoint)): + try: + val = float(slicepoint[d]) + except ValueError: + raise ValueError("Could not convert item %d of slicepoint parameter to float (got '%s'" % (d, slicepoint[d])) + sv.setSlicePoint(d, val) + + # Set the normalization before the color scale + threadsafe_call(sv.setNormalization, normalization) + + # --- Color scale --- + if (not colormin is None) and (not colormax is None): + threadsafe_call(sv.setColorScale, colormin, colormax, colorscalelog) + else: + if (not colormin is None): threadsafe_call(sv.setColorScaleMin, colormin) + if (not colormax is None): threadsafe_call(sv.setColorScaleMax, colormax) + try: + threadsafe_call(sv.setColorScaleLog, colorscalelog) + except: + print "Log color scale not possible." + + # --- XY limits --- + if not limits is None: + threadsafe_call(sv.setXYLimits, limits[0], limits[1], limits[2], limits[3]) + + return svw + + +def get_screenshot_dir(): + """ Returns the directory for screenshots, + or NONE if not set """ + expected_env_var = 'MANTID_SCREENSHOT_REPORT' + dest = os.getenv(expected_env_var) + if not dest is None: + # Create the report directory if needed + if not os.path.exists(dest): + os.mkdir(dest) + else: + errormsg = "The expected environmental does not exist: " + expected_env_var + print errormsg + raise RuntimeError(errormsg) + return dest + + + + +#====================================================================== +def _replace_report_text(filename, section, newtext): + """ Search html report text to +replace a line <!-- Filename --> etc. +Then, the contents of that section are replaced +@param filename :: full path to .html report +@param section :: string giving the name of the section +@param newtext :: replacement contents of that section. No new lines! +@return the new contents of the entire page +""" + # Get the current report contents if any + if os.path.exists(filename): + f = open(filename, 'r') + contents = f.read() + f.close() + else: + contents = "" + + + lines = contents.splitlines() + sections = dict() + # Find the text in each section + for line in lines: + if line.startswith("<!-- "): + # All lines should go <!-- Section --> + n = line.find(" ", 5) + if n > 0: + current_section = line[5:n].strip() + current_text = line[n+4:] + sections[current_section] = current_text + + # Replace the section + sections[section] = newtext.replace("\n","") + + # Make the output + items = sections.items() + items.sort() + output = [] + for (section_name, text) in items: + output.append("<!-- %s -->%s" % (section_name, text)) + + # Save the new text + contents = os.linesep.join(output) + f = open(filename, 'w') + f.write(contents) + f.close() + + +class Screenshot(QtCore.QObject): + """ + Handles taking a screenshot while + ensuring the call takes place on the GUI + thread + """ + + def take_picture(self, widget, filename): + """ + Takes a screenshot and saves it to the + filename given, ensuring the call is processed + through a slot if the call is from a separate + thread + """ + # First save the screenshot + widget.resize(widget.size()) + QtCore.QCoreApplication.processEvents() + + pix = QtGui.QPixmap.grabWidget(widget) + pix.save(filename) + +def screenshot(widget, filename, description, png_exists=False): + """ Take a screenshot of the widget for displaying in a html report. + + The MANTID_SCREENSHOT_REPORT environment variable must be set + to the destination folder. Screenshot taking is skipped otherwise. + + :param widget: QWidget to grab. + :param filename: Save to this file (no extension!). + :param description: Short descriptive text of what the screenshot should look like. + :param png_exists: if True, then the 'filename' already exists. Don't grab a screenshot, but add to the report. + """ + dest = get_screenshot_dir() + if not dest is None: + report = os.path.join(dest, "index.html") + + if png_exists: + pass + else: + # Find the widget if handled with a proxy + if hasattr(widget, "_getHeldObject"): + widget = widget._getHeldObject() + + if widget is not None: + camera = Screenshot() + threadsafe_call(camera.take_picture, widget, os.path.join(dest, filename+".png")) + + # Modify the section in the HTML page + section_text = '<h2>%s</h2>' % filename + now = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) + section_text += '%s (%s)<br />' % (description, now) + section_text += '<img src="%s.png" alt="%s"></img>' % (filename, description) + + _replace_report_text(report, filename, section_text) + +def screenshot_to_dir(widget, filename, screenshot_dir): + """Take a screenshot_to_dir of a widget + + @param widget :: QWidget to take an image of + @param filename :: Destination filename for that image + @param screenshot_dir :: Directory to put the screenshots into. + """ + # Find the widget if handled with a proxy + if hasattr(widget, "_getHeldObject"): + widget = widget._getHeldObject() + + if widget is not None: + camera = Screenshot() + imgpath = os.path.join(screenshot_dir, filename) + threadsafe_call(camera.take_picture, widget, imgpath) + return imgpath + else: + raise RuntimeError("Unable to retrieve widget. Has it been deleted?") + + +#============================================================================= +# Helper methods +#============================================================================= + +def __getWorkspaceIndices(source): + """ + Returns a list of workspace indices from a source. + The source can be a list, a tuple, an int or a string. + """ + index_list = [] + if isinstance(source,list) or isinstance(source,tuple): + for i in source: + nums = __getWorkspaceIndices(i) + for j in nums: + index_list.append(j) + elif isinstance(source, int): + index_list.append(source) + elif isinstance(source, str): + elems = source.split(',') + for i in elems: + try: + index_list.append(int(i)) + except ValueError: + pass + else: + raise TypeError('Incorrect type passed as index argument "' + str(source) + '"') + return index_list + +# Common checks for plotSpectrum, plotMD, and plotBin: +def __checkPlotWorkspaces(workspace_names): + """Check that a list of workspaces is not empty and all the elements exist + + Throws Python-level exceptions if the list is empty or any of the spaces don't exist. + + Args: + workspace_names: list of names of workspace(s) + + Returns: + Nothing, just throws exceptions in case of error/inconsistent inputs + """ + + if len(workspace_names) == 0: + raise ValueError("No workspace names given") + for name in workspace_names: + if not mantid.api.mtd.doesExist(name): + raise ValueError("Workspace '%s' does not exist in the workspace list" % name) + if not isinstance(mantid.api.mtd[name], mantid.api.IMDWorkspace): + raise ValueError("Workspace '%s' is not an IMDWorkspace" % name) + +def __checkPlotMDWorkspaces(workspace_names): + """Check that a list of workspaces is not empty AND all the elements exist AND they are + IMDWorkspace(s). First part of the check is done based on __checkPlotWorkspaces() + + Throws Python-level exceptions if the list is empty or any of the spaces don't exist or + are not IMDWorkspace. + + Args: + workspace_names: list of names of workspace(s) + + Returns: + Nothing, just throws exceptions in case of error/inconsistent inputs + """ + __checkPlotWorkspaces(workspace_names) + for name in workspace_names: + if not isinstance(mantid.api.mtd[name], mantid.api.IMDWorkspace): + raise ValueError("Workspace '%s' is not an IMDWorkspace" % name) + +#----------------------------------------------------------------------------- diff --git a/Code/Mantid/MantidPlot/pymantidplot/future/__init__.py b/Code/Mantid/MantidPlot/pymantidplot/future/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Code/Mantid/MantidPlot/pymantidplot/future/pyplot.py b/Code/Mantid/MantidPlot/pymantidplot/future/pyplot.py new file mode 100644 index 00000000000..d6bf362f5f5 --- /dev/null +++ b/Code/Mantid/MantidPlot/pymantidplot/future/pyplot.py @@ -0,0 +1,1180 @@ +"""============================================================================ +New Python command line interface for plotting in Mantid (ala matplotlib) +============================================================================ + +The idea behind this new module is to provide a simpler, more +homogeneous command line interface (CLI) to the Mantid plotting +functionality. This new interface is meant to resemble MatPlotLib as +far as possible, and provide a more manageable, limited number of plot +options. + +The module is at a very early stage of development and provides +limited functionality. This is very much work in progress at the +moment. Feedback is very much welcome! + +To use this new functionality you first need to import the new pyplot module: + + from mantidplot.future.pyplot import * + +Please do not forget this step, otherwise you may get arcane error +messages from functions of the old mantidplot Python CLI. + +Simple plots can be created and manipulated with a handul of +commands. See the following examples. + +Plot an array (python list) +--------------------------- + + plot([0.1, 0.3, 0.2, 4]) + # The array values will be inserted in a workspace named 'array_dummy_workspace_...' + + +Plot a Mantid workspace +----------------------- + + # first, load a workspace. You can do this with a Load command or just from the GUI menus + ws=Load("/path/to/MAR11060.raw", OutputWorkspace="foo") + plot(ws, [100]) + +The list or array of values will be inserted in a workspace named +'array_dummy_workspace_...', for example: array_dummy_workspace_1, +array_dummy_workspace_2, as you create new plots. + + +The plot commands accept a list of options (kwargs) as parameters +passed by name. With these options you can modify plot properties, +such as line styles, colors, axis scale, etc. The following example +illustrates the use of a few options. You can refer to the list of +options provided further down in this document. In principle, any +combination of options is supported, as long as it makes sense! + +Plot an array with a different style +------------------------------------ + + a = [0.1, 0.3, 0.2, 4] + plot(a) + import numpy as np + y = np.sin(np.linspace(-2.28, 2.28, 1000)) + plot(y) + + +If you have used the traditional Mantid command line interface in +Python you will probably remember the plotSpectrum, plotBin and plotMD +functions. These are supported in this new interface as shown in the +following examples. + +Plot spectra using workspace objects and workspace names +-------------------------------------------------------- + + # please make sure that you use the right path and file name + mar=Load('/path/to/MAR11060.raw', OutputWorkspace="MAR11060") + plot('MAR11060', [10,100,500]) + plot(mar,[3, 500, 800]) + +Let's load one more workspace so we can see some examples with list of workspaces + + loq=Load('/path/to/LOQ48097.raw', OutputWorkspace="LOQ48097") + +The next lines are all equivalent, you can use workspace objects or +names in the list passed to plot: + + plot([mar, 'LOQ48097'], [800, 900]) + plot([mar, loq], [800, 900]) + plot(['MAR11060', loq], [800, 900]) + +Here, the plot function is making a guess and plotting the spectra of +these workspaces (instead of the bins or anything else). You can make +that choice more explicit by specifying the 'tool' argument: + + plot(['MAR11060', loq], [800, 900], tool='plot_spectrum') + +Alternatively, you can use the plot_spectrum command, which is +equivalent to the plot command with the keyword argument +tool='plot_spectrum': + + plot_spectrum(['MAR11060', loq], [800, 900]) + +Plotting bins +------------- + +To plot workspace bins you can use the keyword 'tool' with the value 'plot_bin', like this: + + ws=Load('/path/to/HRP39182.RAW', OutputWorkspace="HRP39182") + plot(ws, [1, 5, 7, 100], tool='plot_bin') + +or, alternatively, you can use the plot_bin command: + + plot_bin(ws, [1, 5, 7, 100], linewidth=4, linestyle=':') + +Plotting MD workspaces +--------------------- + +Similarly, to plot MD workspaces you can use the keyword 'tool' with the value 'plot_md', like this: + + simple_md_ws = CreateMDWorkspace(Dimensions='3',Extents='0,10,0,10,0,10',Names='x,y,z',Units='m,m,m',SplitInto='5',MaxRecursionDepth='20',OutputWorkspace=MDWWorkspaceName) + plot(simple_md_ws, tool='plot_md') + +or a specific plot_md command: + + plot_md(simple_md_wsws) + +For simplicity, these examples use a dummy MD workspace. Please refer +to the Mantid (http://www.mantidproject.org/MBC_MDWorkspaces) for a +more real example, which necessarily gets more complicated and data +intensive. + +Changing style properties +------------------------- + +You can modify the style of your plots. For example like this (for a +full list of options currently supported, see below). + + lines=plot(loq, [100, 104], tool='plot_spectrum', linestyle='-.', marker='*', color='red') + +Notice that the plot function returns a list of lines, which +correspond to the spectra lines. At present the lines have limited +functionality. Essentially, the data underlying these lines data can +be retrieved as follows: + + lines[0].get_xdata() + lines[0].get_ydata() + +If you use plot_spectrum, the number of elements in the output lines +should be equal to the number of bins in the corresponding +workspace. Conversely, if you use plot_bin, the number of elements in +the output lines should be equal to the number of spectra in the +workspace. + +Other properties can be modified using different functions, as in +matplotlib's pyplot. For example: + + title('Test plot of LOQ') + xlabel('ToF') + ylabel('Counts') + ylim(0, 8) + xlim(1e3, 4e4) + grid('on') + +You can also save the current figure into a file like this: + + savefig('example_saved_figure.png') + +where the file format is guessed from the file extension. The same +extensions as in the MantidPlot figure export dialog are supported, +including jpg, png, tif, ps, and svg. + +Additional options supported as keyword arguments (kwargs): +----------------------------------------------------------- + +There is a couple of important plot options that are set as keyword +arguments: + +============ ================ +Option name Values supported +------------ ---------------- +error_bars True, False (default) +hold on, off + +error_bars has the same meaning as in the traditional mantidplot plot +functions: it defines whether error bars should be added to the +plots. hold has the same behavior as in matplotlib and pyplot. If the +value of hold is 'on' in a plot command, the new plot will be drawn on +top of the current plot window, without clearing it. This makes it +possible to make plots incrementally. + +Multi-plot commands +------------------- + +In this version of future.pyplot there is limited support for +multi-plot commands (as in pyplot and matlab). For example, you can +type commands like the following: + +plot(ws, [100, 101], 'r', ws, [200, 201], 'b', tool='plot_spectrum') + +This command will plot spectra 100 and 101 in red and spectra 200 and +201 in blue on the same figure. You can also combine different +workspaces, for example: + +plot(ws, [100, 101], 'r', mar, [50, 41], 'b', tool='plot_spectrum') + + +Style options supported as keyword arguments +-------------------------------------------- + +Unless otherwise stated, these options are in principle supported in +all the plot variants. These options have the same (or as closed as +possible) meaning as in matplotlib. + +============ ================ +Option name Values supported +------------ ---------------- +linewidth real values +linestyle '-', '--', '-.' '.' +marker 'o', 'v', '^', '<', '>', 's', '*', 'h', '|', '_' +color color character or string ('b', 'blue', 'g', 'green', 'k', 'black', 'y', 'yellow', 'c', 'cyan', 'r', 'red'. 'm', 'magenta', etc.). RGB colors are not supported at the moment. +============ ================ + +Functions that modify plot properties +------------------------------------- + +Here is a list of the functions supported at the moment. The offer the +same functionality as their counterparts in matplotlib's pyplot. + +- title +- xlabel +- ylabel +- ylim +- xlim +- axis +- grid +- savefig + +""" + +try: + import _qti +except ImportError: + raise ImportError('The \'mantidplot\' module can only be used from within MantidPlot.') + +import numpy as np +from PyQt4 import Qt, QtGui, QtCore +from mantid.api import (IMDWorkspace as IMDWorkspace, MatrixWorkspace as MatrixWorkspace, AlgorithmManager as AlgorithmManager, AnalysisDataService as ADS) +from mantid.api import mtd +# return __is_workspace(arg) or (mantid.api.mtd.doesExist(arg) and isinstance(mantid.api.mtd[arg], mantid.api.IMDWorkspace)) +from mantid.simpleapi import CreateWorkspace as CreateWorkspace +import mantidplot + +print ("You are loading '" + __name__ + "', which is an experimental module." + +""" +Please note: this module is at a very early stage of development and +provides limited functionality. This is work in progress and feedback +is very much welcome! Please let us know any wishes and suggestions.""") + +class Line2D(): + """ + A very minimal replica of matplotlib.Line.Line2D. The true Line2D + is a sublcass of matplotlib.artist and provides tons of + functionality. At the moment this just provides get_xdata() + and get_ydata(). It also holds its Graph object and through + it it would be possible to provide additional selected + functionality. Keep in mind that providing GUI line/plot + manipulation functionality would require a proxy for this + class. + """ + + def __init__(self, graph, index, x_data, y_data, fig=None): + self._graph = graph + self._index = index # will (may) be needed to change properties of this line + self._xdata = x_data + self._ydata = y_data + self._fig = None + + def get_xdata(self): + return self._xdata + + def get_ydata(self): + return self._ydata + + def figure(self): + return self._fig + +class Figure(): + """ + A very minimal replica of matplotlib.figure.Figure. This class is + here to support manipulation of multiple figures from the command + line. + """ + + """For the moment this is just a very crude wrapper for Graph + (proxy to qti Multilayer), and it is here just to hide (the rather + obscure) Graph from users. + """ + # Holds the set of figure ids (integers) as they're being created and/or destroyed + __figures = {} + # Always increasing seq number, not necessarily the number of figures in the dict + __figures_seq = 0 + + def __init__(self, num): + if isinstance(num, int): + # normal matplotlib use, like figure(2) + missing = -1 + fig = __figures.get(num, missing) + if missing == fig: + self._graph = Figure.__empty_graph() + __figures[num] = self + if num > __figures_seq: + __figures_seq = num + else: + if None == fig._graph._getHeldObject(): + # has been destroyed! + self._graph = Figure.__empty_graph() + else: + self._graph = fig._graph + elif isinstance(num, mantidplot.proxies.Graph): + if None == num._getHeldObject(): + # deleted Graph! + self._graph = Figure.__empty_graph() + else: + self._graph = num + num = Figure.__make_new_fig_number() + Figure.__figures[num] = self + else: + raise ValueError("To create a Figure you need to specify a figure number or a Graph object." ) + + @classmethod + def fig_seq(cls): + """ Helper method, returns the current sequence number for figures""" + return cls.__figures_seq + + @classmethod + def __make_new_fig_number(cls): + """ Helper method, creates and return a new figure number""" + num = cls.__figures_seq + 1 + avail = False + while not avail: + missing = -1 + fig = cls.__figures.get(num, missing) + if missing == fig: + avail = True # break + else: + num += 1 + cls.__figures_seq = num + return num + + @staticmethod + def __empty_graph(): + """Helper method, just create a new Graph with an 'empty' plot""" + lines = plot([0]) + return lines._graph + + +def __empty_fig(): + """Helper function, for the functional interface. Makes a blank/empty figure""" + lines = plot([0]) # for the very first figure, this would generate infinite recursion! + return Figure(lines[0]._graph) + +# TODO/TOTHINK: no 'hold' function support for now. How to handle multi-plots with different types/tools? Does it make sense at all? +__hold_status = False + +__last_shown_fig = None + +def __last_fig(): + """ + Helper function, especially for the functional interface. + Avoid using it inside the plot_spectrum, plot_bin, etc. as there's risk of infinite recursion + Returns :: last figure, creating new one if there was none + """ + global __last_shown_fig + if not __last_shown_fig: + f = __empty_fig() + __last_shown_fig = f + return __last_shown_fig + +def __update_last_shown_fig(g): + """ + Helper function, especially for the functional interface. + @param g :: graph object + Returns :: new last fig + """ + global __last_shown_fig + __last_shown_fig = Figure(g) + return __last_shown_fig + +def __is_array(arg): + """ + Is the argument a python or numpy list? + @param arg :: argument + + Returns :: True if the argument is a python or numpy list + """ + return isinstance(arg, list) or isinstance(arg, np.ndarray) + +def __is_array_or_int(arg): + """ + Is the argument a valid workspace index/indices, which is to say: + Is the argument an int, or python or numpy list? + @param arg :: argument + + Returns :: True if the argument is an integer, or a python or numpy list + """ + return isinstance(arg, int) or __is_array(arg) + + +def __is_registered_workspace_name(arg): + """" + Check whether the argument passed is the name of a registered workspace + + @param arg :: argument (supposedly a workspace name) + + Returns :: True if arg is a correct workspace name + """ + return (mtd.doesExist(arg) and isinstance(mtd[arg], IMDWorkspace)) + +def __is_valid_single_workspace_arg(arg): + """" + Check whether the argument passed can be used as a workspace input. Note that this differs from + __is_workspace() in that workspace names are also accepted. Throws ValueError with informative + message if arg is not a valid workspace object or name. + + @param arg :: argument (supposedly one workspace, possibly given by name) + + Returns :: True if arg can be accepted as a workspace + """ + if __is_workspace(arg) or __is_registered_workspace_name(arg): + return True + else: + raise ValueError("This parameter is not a valid workspace: " + str(arg)) + +def __is_valid_workspaces_arg(arg): + """" + Check whether the argument passed can be used as a workspace(s) input. Note that this differs from + __is_workspace() in that lists of workspaces and workspace names are also accepted. + + @param arg :: argument (supposedly one or more workspaces, possibly given by name) + + Returns :: True if arg can be accepted as a workspace or a list of workspaces + """ + if 0 == len(arg): + return False + + if isinstance(arg, basestring): + return __is_valid_single_workspace_arg(arg) + else: + for name in arg: + # name can be a workspace name or a workspace object + try: + __is_valid_single_workspace_arg(name) + except: + raise ValueError("This parameter passed in a list of workspaces is not a valid workspace: " + str(name)) + return True + +def __is_data_pair(a, b): + """ + Are the two arguments passed (a and b) a valid data pair for plotting, like in plot(x, y) or + plot(ws, [0, 1, 2])? + @param a :: first argument passed (supposedly array or workspace(s)) + @param b :: second argument (supposedly an array of values, or indices) + + Returns :: True if the arguments can be used to plot something is an integer, or a python or numpy list + """ + res = (__is_array(a) and __is_array(a)) or (__is_valid_workspaces_arg(a) and __is_array_or_int(b)) + return res + +def __is_workspace(arg): + """ + Is the argument a Mantid MatrixWorkspace? + @param arg :: argument + + Returns :: True if the argument a MatrixWorkspace + """ + return isinstance(arg, MatrixWorkspace) + +def __is_array_of_workspaces(arg): + """ + Is the argument a sequence of Mantid MatrixWorkspaces? + @param arg :: argument + + Returns :: True if the argument is a sequence of MatrixWorkspace + """ + return __is_array(arg) and len(arg) > 0 and __is_workspace(arg[0]) + + +def __create_workspace(x, y, name="array_dummy_workspace"): + """ + Create a workspace. Also puts it in the ADS + @param x :: x array + @param y :: y array + @param name :: workspace name + + Returns :: Workspace + """ + alg = AlgorithmManager.create("CreateWorkspace") + alg.setChild(True) + alg.initialize() + # fake empty workspace (when doing plot([]), cause setProperty needs non-empty data) + if [] == x: + x = [0] + if [] == y: + y = [0] + alg.setProperty("DataX", x) + alg.setProperty("DataY", y) + name = name + "_" + str(Figure.fig_seq()) + alg.setPropertyValue("OutputWorkspace", name) + alg.execute() + ws = alg.getProperty("OutputWorkspace").value + ADS.addOrReplace(name, ws) # Cannot plot a workspace that is not in the ADS + return ws + + +def __list_of_lines_from_graph(g, first_line=0): + """ + Produces a python list of line objects, with one object per line plotted on the passed graph + Note: at the moment these objects are of class Line2D which is much simpler than matplotlib.lines.Line2D + This function should always be part of the process of creating a new figure/graph, and it guarantees + that this figure being created is registered as the last shown figure. + + @param g :: graph (with several plot layers = qti Multilayer) + @param first_line :: index to start from (useful for hold='on', multi-plots, etc.) + + Returns :: List of line objects + """ + if None == g: + raise ValueError("Got empty Graph object, cannot get its lines." ) + # assume we use a single layer + active = g.activeLayer() + res = [] + for i in range(first_line, active.numCurves()): + x_data = [] + y_data = [] + d = active.curve(i).data() + for i in range(0, active.curve(i).data().size()): + x_data.append(d.x(i)) + y_data.append(d.y(i)) + res.append(Line2D(g, i, x_data, y_data)) + + fig = __update_last_shown_fig(g) + for lines in res: + lines._fig = fig + + return res; + +def __matplotlib_defaults(l): + """ + Tries to (approximately) mimic the default plot properties of a pylab.plot() + @param l :: layer (plot) from a mantidplot Graph object + + Returns :: nothing, just modifies properties of the layer passed + """ + if None == l: + raise ValueError("Got empty Layer object, cannot modify its properties." ) + l.removeLegend() + for i in range(0, l.numCurves()): + l.setCurveLineColor(i, __color_char_to_color_idx['b']) + l.setTitle(' ') + l.setXTitle(' ') + l.setYTitle(' ') + +__marker_to_plotsymbol = { + 'o': _qti.PlotSymbol.Ellipse, 'v': _qti.PlotSymbol.DTriangle, '^': _qti.PlotSymbol.UTriangle, + '<': _qti.PlotSymbol.LTriangle, '>': _qti.PlotSymbol.RTriangle, 's': _qti.PlotSymbol.Rect, + '*': _qti.PlotSymbol.Star1, 'h': _qti.PlotSymbol.Hexagon, '|': _qti.PlotSymbol.VLine, + '_': _qti.PlotSymbol.HLine +} + +"""Contains all the supported line styles""" +__linestyle_to_qt_penstyle = { + '-': QtCore.Qt.SolidLine, '--': QtCore.Qt.DashLine, + '-.': QtCore.Qt.DashDotLine, ':': QtCore.Qt.DotLine +} # other available: Qt.DashDotDotLine, Qt.CustomDashLine + +def __apply_linestyle(graph, linestyle, first_line=0): + """ + Sets the linestyle of lines/curves of the active layer of the graph passed + + @param graph :: mantidplot graph (figure) + @param linestyle :: linestyle string + @param first_line :: index of first line to which the linestyle will apply + (useful when in hold mode / adding lines) + + Returns :: nothing, just modifies the line styles of the active layer of the graph passed + """ + global __linestyle_to_qt_penstyle + wrong = 'inexistent' + penstyle = __linestyle_to_qt_penstyle.get(linestyle, wrong) + if wrong == penstyle: + raise ValueError("Wrong linestyle given, unrecognized: " + linestyle) + l = graph.activeLayer() + for i in range(first_line, l.numCurves()): + l.setCurveLineStyle(i, penstyle) + +# beware this is not Qt.Qt.color_name (black, etc.) +__color_char_to_color_idx = { + 'k': 0, 'r': 1, 'g': 2, 'b': 3, 'c': 4, 'm': 5, 'y': 18, + 'black': 0, 'red': 1, 'green': 2, 'blue': 3, 'cyan': 4, 'magenta': 5, 'orange': 6, + 'purple': 7, 'darkGreen': 8, 'darkBlue': 9, 'brown': 10, 'gray': 17, 'yellow': 18 +} + +def __apply_line_color(graph, c, first_line=0): + """ + Sets the color of curves of the active layer of the graph passed + + @param graph :: mantidplot graph (figure) + @param c :: color string + @param first_line :: index of first line to which the color will apply + (useful when in hold mode / adding lines) + + Returns :: nothing, just modifies the line styles of the active layer of the graph passed + """ + inex = 'inexistent' + col_idx = __color_char_to_color_idx.get(c, inex) + if inex == col_idx: + col_idx = QtGui.QColor(c) + l = graph.activeLayer() + for i in range(first_line, l.numCurves()): + l.setCurveLineColor(i, col_idx) # beware this is not Qt.Qt.black, but could be faked with QtGui.QColor("orange") + +def __apply_marker(graph, marker, first_line=0): + """ + Sets the marker of curves of the active layer of the graph passed + + @param graph :: mantidplot graph (figure) + @param marker :: line marker character + @param first_line :: index of first line to which the color will apply + (useful when in hold mode / adding lines) + + Returns :: nothing + """ + wrong = 'inexistent' + sym_code = __marker_to_plotsymbol.get(marker, wrong) + if wrong == sym_code: + raise ValueError("Warning: unrecognized marker: " + str(marker)) + sym = _qti.PlotSymbol(sym_code, QtGui.QBrush(), QtGui.QPen(), QtCore.QSize(5,5)) + l = graph.activeLayer() + for idx in range(first_line, l.numCurves()): + l.setCurveSymbol(idx, sym) + +def __is_marker(char): + """ Is it a marker character + @param char :: suspected marker character coming from a linestyle string + Returns :: True if it's a marker character + """ + inex = 'inexistent' + m = __marker_to_plotsymbol.get(char, inex) + return m != inex + +__linestyle_to_qt_penstyle = { + '-': QtCore.Qt.SolidLine, '--': QtCore.Qt.DashLine, + '-.': QtCore.Qt.DashDotLine, ':': QtCore.Qt.DotLine +} # other available: Qt.DashDotDotLine, Qt.CustomDashLine + +def __is_linestyle(s, i): + """ + Check if we have a linestyle string in string s at position i + @param s :: input (style) string + @param i :: index where to start checking in string s + + Returns :: 0 if no linestyle string is identified, length of the string (1 or 2) otherwise + """ + global __linestyle_to_qt_penstyle + + if len(s) <= i: + return 0 + + if len(s) > len(s) + 1: + # can check 2 chars + wrong = 'inexistent' + penstyle = __linestyle_to_qt_penstyle.get(linestyle, wrong) + if wrong != penstyle: + return 2 + + if '-'==s[i] or ':'==s[i]: + return 1 + else: + return 0 + +def __apply_plot_args(graph, first_line, *args): + """ + Applies args, like '-r' etc. + @param graph :: a graph (or figure) that can contain multiple layers + @param first_line :: first line to which the options will apply (useful when in hold mode / adding lines) + @param args :: plot arguments + + Returns :: nothing, just uses kwargs to modify properties of the layer passed + """ + if None==graph or len(args) < 1 or ((),) == args: + return + + for a in args: + if isinstance(a, basestring): + # this will eat characters as they come, without minding much the past/previous characters + # users can chain as many modifiers as they wish. It could be modified to be more strict/picky + i = 0 + while i < len(a): + linestyle_len = __is_linestyle(a,i) + if linestyle_len > 0: + __apply_linestyle(graph, a[i:i+linestyle_len], first_line) + i += 2 + elif __is_marker(a[i]): + __apply_marker(graph, a[i:], first_line) + i += 1 + elif a[i].isalpha(): + __apply_line_color(graph, a[i], first_line) + i += 1 + else: + # TOTHINK - error here? like this? sure? or just a warning? + raise ValueError("Unrecognized character in input string: " + str(a[i])) + else: + raise ValueError("Expecting style string, but got an unrecognized input parameter: " + str(a) + ", of type: " + str(type(a))) + +def __apply_plot_kwargs(graph, first_line=0, **kwargs): + """ + Applies kwargs + @param graph :: a graph (or figure) that can contain multiple layers + + Returns :: nothing, just uses kwargs to modify properties of the layer passed + """ + if None==graph or None==kwargs or ((),) == kwargs: + return + + for key in kwargs: + if 'linestyle' == key: + __apply_linestyle(graph, kwargs[key]) + + elif 'linewidth' == key: + l = graph.activeLayer() + for i in range(first_line, l.numCurves()): + l.setCurveLineWidth(i, kwargs[key]) + + elif 'color' == key: + __apply_line_color(graph, kwargs[key], first_line) + + elif 'marker' == key: + __apply_marker(graph, kwargs[key], first_line) + +def __is_multiplot_command(*args, **kwargs): + """ + Finds out if the passed *args make a valid multi-plot command. At the same time, splits the + multi-plot command line into individual plot commands. + + @param args :: curve data and options. + @param kwargs :: plot keyword options + + Returns :: tuple: (boolean: whether it is a multiplot command, list of single plot commands as tuples) + """ + # A minimum multi-plot command would be plot(x, y, z, w) or plot(ws1, idx1, ws2, idx2) + nargs = len(args) + # this will be a list with the sequence of individual plots (a tuples, each describing a single plot) + plots_seq = [] + if nargs < 4: + return (False, []) + i = 0 + while i < nargs: + a = [] + b = [] + style = '' + if (nargs-i) >= 3: + if __is_data_pair(args[i], args[i+1]): + a = args[i] + b = args[i+1] + i += 2 + else: + return (False, []); + # can have style string, but don't get confused with single workspace name strings! + if (not __is_registered_workspace_name(args[i])) and isinstance(args[i], basestring): + style = args[i] + i += 1 + plots_seq.append((a,b,style)) + + elif (nargs-i) >= 2: + if __is_data_pair(args[i], args[i+1]): + a = args[i] + b = args[i+1] + i += 2 + else: + return (False, []) + plots_seq.append((a, b, '')) + + elif (nargs-i) > 0: + raise ValueError("Not plottable. I do not know what to do with this last parameter: " + args[i] + ", of type " + str(type(args))) + + return (i == nargs, plots_seq) + +def __process_multiplot_command(plots_seq, **kwargs): + """ + Make one plot at a time when given a multi-plot command. + + @param plots_seq :: list of individual plot parameters + @param kwargs :: plot style options + + Returns :: the list of curves included in the plot + """ + lines = [] + if len(plots_seq) >= 1: + if not 'hold' in kwargs: + kwargs['hold'] = 'off' + lines = plot(*(plots_seq[0]), **kwargs) + for i in range(1, len(plots_seq)): + kwargs['hold'] = 'on' + lines.extend(plot(*(plots_seq[i]), **kwargs)) + return lines + +def __translate_hold_kwarg(**kwargs): + """ + Helper function to translate from hold='on'/'off' kwarg to a True/False value for the + mantidplot window and window error_bars + + @param kwargs :: keyword arguments passed to a plot function, this function only cares about hold. Any + value different from 'on' will be considered as 'off' + + Returns :: tuple with a couple of values: True/False value for window, and True/False for clearWindow, + to be used with plotSpectrum, plotBin, etc. + """ + # window and clearWindow + window_val = None + clearWindow_val = False + hold_name = 'hold' + missing_off = -1 + str_val = kwargs.get(hold_name, missing_off) + if str_val != missing_off and str_val == 'on': + if None == __last_shown_fig: + window_val = None + else: + window_val = __last_fig()._graph + clearWindow_val = False + + return window_val, clearWindow_val + +def __translate_error_bars_kwarg(**kwargs): + """ + Helper function to translate from error_bars=True/False kwarg to a True/False value for the + mantidplot error_bars argument + + @param kwargs :: keyword arguments passed to a plot function. This function only cares about 'error_bars'. + Any value different from 'True' will be considered as 'False' + + Returns :: True/False value for error_bars, to be used with plotSpectrum, plotBin, etc. + + """ + # error_bars param + bars_val = False + bars_name = 'error_bars' + missing_off = -1 + str_val = kwargs.get(bars_name, missing_off) + if str_val != missing_off and str_val == 'True': + bars_val = True + + return bars_val + +def __plot_as_workspace(*args, **kwargs): + """ + plot spectrum via qti plotting framework to plot a workspace. + + @param args :: curve data and options. + @param kwargs :: plot line options + + Returns :: List of line objects + """ + return plot_spectrum(*args, **kwargs) + +def __plot_as_workspaces_list(*args, **kwargs): + """ + Plot a series of workspaces + @param args :: curve data and options. + @param kwargs :: plot line options + + Returns :: List of line objects + """ + # mantidplot.plotSpectrum can already handle 1 or more input workspaces. + return __plot_as_workspace(*args, **kwargs) + + +def __plot_as_array(*args, **kwargs): + """ + Plot from an array + @param args :: curve data and options. + @param kwargs :: plot line options + + Returns :: the list of curves (1) included in the plot + """ + y = args[0] + idx_style = len(args) # have to guess if we get plot(x,'r'), or plot(x, y, 'r') or no style string + if len(args) > 1: + if __is_array(args[1]): + ws = __create_workspace(y, args[1]) + idx_style = 2 + elif isinstance(args[1], basestring): + x = range(0, len(y), 1) # 0 to n, incremented by 1. + ws = __create_workspace(x, y) + # have to assume that args[1] is a style string + idx_style = 1 + else: + raise ValueError("Inputs are of type: " + str(type(args)) + ". Not plottable." ) + else: + x = range(0, len(y), 1) + ws = __create_workspace(x, y) + + lines = __plot_as_workspace(ws, [0], *args[idx_style:], **kwargs) + graph = None + if len(lines) > 0: + graph = lines[0]._graph + else: + raise Exception("Could not plot a workspace: " + ws) + # something to improve: if the C++ Graph class provided a plot1D that doesn't do show(), so that + # we could modify properties behind the scene and at the end do the show(). Con: do we really need + # to load the qti layer with more methods because of outer layers like here? + if 0 == len(kwargs): + __matplotlib_defaults(graph.activeLayer()) + return __list_of_lines_from_graph(graph) + +def __plot_with_tool(tool, *args, **kwargs): + bin_tool_name = 'plot_bin' + spectrum_tool_name = 'plot_spectrum' + md_tool_name = 'plot_md' + + if bin_tool_name == tool or spectrum_tool_name == tool: + if len(args) < 2: + raise ValueError("To plot using %s as a tool you need to give at least two parameters"%tool) + + if bin_tool_name == tool: + return plot_bin(args[0], args[1], *args[2:], **kwargs) + elif md_tool_name == tool: + return plot_md(args[0], *args[1:], **kwargs) + elif spectrum_tool_name == tool: + return plot_spectrum(args[0], args[1], *args[2:], **kwargs) + # here you would add slice/spectrum/instrument viewer, etc. and maybe you'll want to put them in a dict + else: + raise ValueError("Unrecognized tool specified: '" + tool + ";. Cannot plot this. ") + +def __plot_with_best_guess(*args, **kwargs): + y = args[0] + if __is_array(y): + if __is_array_of_workspaces(y): + return __plot_as_workspaces_list(*args, **kwargs) + else: + return __plot_as_array(*args, **kwargs) + else: + # mantidplot.plotSpectrum can handle workspace names (strings) + return __plot_as_workspace(*args, **kwargs) + +def plot_bin(workspaces, indices, *args, **kwargs): + """ + X-Y plot of the bin counts in a workspace. + + Plots one or more bin, selected by indices, using spectra numbers as x-axis and bin counts for + each spectrum as y-axis. + + @param workspaces :: workspace or list of workspaces (both workspace objects and names accepted) + @param indices :: indices of the bin(s) to plot + + Returns :: the list of curves included in the plot + """ + # Find optional params to plotBin + bars_val = __translate_error_bars_kwarg(**kwargs) + window_val, clearWindow_val = __translate_hold_kwarg(**kwargs) + + # to change properties on the new lines being added + first_line = 0 + if None != window_val: + first_line = window_val.activeLayer().numCurves() + + graph = mantidplot.plotBin(workspaces, indices, error_bars=bars_val, type=-1, window=window_val, clearWindow=clearWindow_val) + + __apply_plot_args(graph, first_line, *args) + __apply_plot_kwargs(graph, first_line, **kwargs) + + return __list_of_lines_from_graph(graph, first_line) + + +def plot_md(workspaces, *args, **kwargs): + """ + X-Y plot of an MDWorkspace. + + @param workspaces :: workspace or list of workspaces (both workspace objects and names accepted) + + Returns :: the list of curves included in the plot + """ + # Find optional params to plotBin + bars_val = __translate_error_bars_kwarg(**kwargs) + window_val, clearWindow_val = __translate_hold_kwarg(**kwargs) + + # to change properties on the new lines being added + first_line = 0 + if None != window_val: + first_line = window_val.activeLayer().numCurves() + + graph = mantidplot.plotMD(workspaces, normalization=mantidplot.DEFAULT_MD_NORMALIZATION, error_bars=bars_val, window=window_val, clearWindow=clearWindow_val) + + __apply_plot_args(graph, first_line, *args) + __apply_plot_kwargs(graph, first_line, **kwargs) + + return __list_of_lines_from_graph(graph, first_line) + + +def plot_spectrum(workspaces, indices, *args, **kwargs): + """X-Y Plot of spectra in a workspace. + + Plots one or more spectra, selected by indices, using bin boundaries as x-axis + and the spectra values in each bin as y-axis. + + @param workspaces :: workspace or list of workspaces (both workspace objects and names accepted) + @param indices :: indices of the spectra to plot, given as a single integer or a list of integers + + Returns :: the list of curves included in the plot + + """ + # Find optional params to plotSpectrum + bars_val = __translate_error_bars_kwarg(**kwargs) + window_val, clearWindow_val = __translate_hold_kwarg(**kwargs) + + # to change properties on the new lines being added + first_line = 0 + if None != window_val: + first_line = window_val.activeLayer().numCurves() + + graph = mantidplot.plotSpectrum(workspaces, indices, error_bars=bars_val, type=-1, window=window_val, clearWindow=clearWindow_val) + + __apply_plot_args(graph, first_line, *args) + __apply_plot_kwargs(graph, first_line, **kwargs) + + return __list_of_lines_from_graph(graph, first_line) + + +def plot(*args, **kwargs): + """Plot the data in various forms depending on what arguments are passed. Currently supported + inputs: arrays (as Python lists or numpy arrays) and workspaces (by name or workspace objects). + + @param args :: curve data and options + @param kwargs :: plot line options + + Returns :: the list of curves included in the plot + + args can take different forms depending on what you plot. You can plot: + - a python list or array (x) for example like this: + plot(x) + - a workspace (ws) for example like this: + plot(ws, [100,101]) # this will plot spectra 100 and 101 + - a list of workspaces (ws, ws2, ws3, etc.) for example like this: + plot([ws, ws2, ws3], [100,101]) + - workspaces identified by their names: + plot(['HRP39182', 'MAR11060.nxs'], [100,101]) + + You can also pass matplotlib/pyplot style strings as arguments, for example: + - plot(x, '-.') + + As keyword arguments (kwargs) you can specify multiple + parameters, for example: linewidth, linestyle, marker, color. + + An important keyword argument is tool. At the moment the + following values are supported: + - plot_spectrum (default for workspaces) + - plot_bin + - plot_md + + Please see the documentation of this module (use help()) for more details. + + """ + nargs = len(args) + if nargs < 1: + raise ValueError("You did not pass any argument. You must provide data to plot.") + + # TOTHINK: should there be an exception if it's plot_md (tool='plot_md') + (is_it, plots_seq) = __is_multiplot_command(*args, **kwargs) + if is_it: + return __process_multiplot_command(plots_seq, **kwargs) + elif len(args) > 3: + raise ValueError("Could not interpret the arguments passed. You passed more than 3 positional arguments but this does not seem to be a correct multi-plot command. Please check your command and make sure that the workspaces given are correct.") + + # normally guess; exception if e.g. a parameter tool='plot_bin' is given + try: + tool_val = kwargs['tool'] + del kwargs['tool'] + return __plot_with_tool(tool_val, *args, **kwargs) + except KeyError: + return __plot_with_best_guess(*args, **kwargs) + + +#============================================================================= +# Functions, for pyplot / old matlab style manipulation of figures +#============================================================================= + +def xlim(xmin, xmax): + """ + Set the boundaries of the x axis + + @param xmin :: minimum value + @param xmax :: maximum value + """ + l = __last_fig()._graph.activeLayer() + l.setAxisScale(2, xmin, xmax) + +def ylim(ymin, ymax): + """ + Set the boundaries of the y axis + + @param ymin :: minimum value + @param ymax :: maximum value + """ + l = __last_fig()._graph.activeLayer() + l.setAxisScale(0, ymin, ymax) + +def xlabel(lbl): + """ + Set the label or title of the x axis + + @param lbl :: x axis lbl + """ + l = __last_fig()._graph.activeLayer() + l.setXTitle(lbl) + +def ylabel(lbl): + """ + Set the label or title of the y axis + + @param lbl :: y axis lbl + """ + l = __last_fig()._graph.activeLayer() + l.setYTitle(lbl) + +def title(title): + """ + Set title of active plot + + @param title :: title string + """ + l = __last_fig()._graph.activeLayer() + l.setTitle(title) + +def axis(lims): + """ + Set the boundaries of the x and y axes + + @param lims :: list or vector specifying min x, max x, min y, max y + """ + l = __last_fig()._graph.activeLayer() + if 4 != len(lims): + raise ValueError("Error: 4 real values are required for the x and y axes limits") + l.setScale(*lims) + +def grid(opt='on'): + """ + Set title of active plot + + @param title :: title string + """ + l = __last_fig()._graph.activeLayer() + if None == opt or 'on' == opt: + l.showGrid() + elif 'off' == opt: + # TODO is there support for a 'hideGrid' in qti? Apparently not. + print "Sorry, hiding/disabling grids is currenlty not supported" + +def figure(num=None): + """ + Return Figure object for a new figure or an existing one (if there is any + with the number passed as parameter). + + @param num :: figure number (optional). If empty, a new figure is created. + """ + if not num: + return __empty_fig() + else: + if num < 0: + raise ValueError("The figure number cannot be negative") + + missing = None + fig = Figure.__figures.get(num, None) + if None == fig: + return __empty_fig() + else: + return fig + +def savefig(name): + """ + Save current plot into a file. The format is guessed from the file extension (.eps, .png, .jpg, etc.) + + @param name :: file name + """ + if not name: + raise ValueError("Error: you need to specify a non-empty file name") + l = __last_fig()._graph.activeLayer() + l.saveImage(name); diff --git a/Code/Mantid/MantidPlot/pymantidplot/proxies.py b/Code/Mantid/MantidPlot/pymantidplot/proxies.py new file mode 100644 index 00000000000..4a2e66b47f5 --- /dev/null +++ b/Code/Mantid/MantidPlot/pymantidplot/proxies.py @@ -0,0 +1,907 @@ +""" +Module containing classes that act as proxies to the various MantidPlot gui objects that are +accessible from python. They listen for the QObject 'destroyed' signal and set the wrapped +reference to None, thus ensuring that further attempts at access do not cause a crash. +""" + +from PyQt4 import QtCore, QtGui +from PyQt4.QtCore import Qt, pyqtSlot +import __builtin__ +import mantid + +#----------------------------------------------------------------------------- +#--------------------------- MultiThreaded Access ---------------------------- +#----------------------------------------------------------------------------- + +class CrossThreadCall(QtCore.QObject): + """ + Defines a dispatch call that marshals + function calls between threads + """ + __callable = None + __args = [] + __kwargs = {} + __func_return = None + __exception = None + + def __init__(self, callable): + """ Construct the object + """ + QtCore.QObject.__init__(self) + self.moveToThread(QtGui.qApp.thread()) + self.__callable = callable + self.__call__.__func__.__doc__ = callable.__doc__ + + def dispatch(self, *args, **kwargs): + """Dispatches a call to callable with + the given arguments using QMetaObject.invokeMethod + to ensure the call happens in the object's thread + """ + self.__args = args + self.__kwargs = kwargs + self.__func_return = None + self.__exception = None + return self._do_dispatch() + + def __call__(self, *args, **kwargs): + """ + Calls the dispatch method + """ + return self.dispatch(*args, **kwargs) + + @pyqtSlot() + def _do_dispatch(self): + """Perform a call to a GUI function across a + thread and return the result + """ + if QtCore.QThread.currentThread() != QtGui.qApp.thread(): + QtCore.QMetaObject.invokeMethod(self, "_do_dispatch", Qt.BlockingQueuedConnection) + else: + try: + self.__func_return = self.__callable(*self.__args, **self.__kwargs) + except Exception, exc: + self.__exception = exc + + if self.__exception is not None: + raise self.__exception # Ensures this happens in the right thread + return self.__func_return + + def _get_argtype(self, argument): + """ + Returns the argument type that will be passed to + the QMetaObject.invokeMethod call. + + Most types pass okay, but enums don't so they have + to be coerced to ints. An enum is currently detected + as a type that is not a bool and inherits from __builtin__.int + """ + argtype = type(argument) + return argtype + if isinstance(argument, __builtin__.int) and argtype != bool: + argtype = int + return argtype + +def threadsafe_call(callable, *args, **kwargs): + """ + Calls the given function with the given arguments + by passing it through the CrossThreadCall class. This + ensures that the calls to the GUI functions + happen on the correct thread. + """ + caller = CrossThreadCall(callable) + return caller.dispatch(*args, **kwargs) + +def new_proxy(classType, callable, *args, **kwargs): + """ + Calls the callable object with the given args and kwargs dealing + with possible thread-safety issues. + If the returned value is not None it is wrapped in a new proxy of type classType + + @param classType :: A new proxy class for the return type + @param callable :: A python callable object, i.e. a function/method + @param \*args :: The positional arguments passed on as given + @param \*kwargs :: The keyword arguments passed on as given + """ + obj = threadsafe_call(callable, *args, **kwargs) + if obj is None: + return obj + return classType(obj) + +#----------------------------------------------------------------------------- +#--------------------------- Proxy Objects ----------------------------------- +#----------------------------------------------------------------------------- + +class QtProxyObject(QtCore.QObject): + """Generic Proxy object for wrapping Qt C++ QObjects. + This holds the QObject internally and passes methods to it. + When the underlying object is deleted, the reference is set + to None to avoid segfaults. + """ + def __init__(self, toproxy): + QtCore.QObject.__init__(self) + self.__obj = toproxy + # Connect to track the destroyed + if self.__obj is not None: + self.connect(self.__obj, QtCore.SIGNAL("destroyed()"), + self._kill_object, Qt.DirectConnection) + + def __del__(self): + """ + Disconnect the signal + """ + self._disconnect_from_destroyed() + + def close(self): + """ + Reroute a method call to the the stored object + """ + self._disconnect_from_destroyed() + if hasattr(self.__obj, 'closeDependants'): + threadsafe_call(self.__obj.closeDependants) + if hasattr(self.__obj, 'close'): + threadsafe_call(self.__obj.close) + self._kill_object() + + def inherits(self, className): + """ + Reroute a method call to the stored object + """ + return threadsafe_call(self.__obj.inherits, className) + + def _disconnect_from_destroyed(self): + """ + Disconnects from the wrapped object's destroyed signal + """ + if self.__obj is not None: + self.disconnect(self.__obj, QtCore.SIGNAL("destroyed()"), + self._kill_object) + + def __getattr__(self, attr): + """ + Reroute a method call to the the stored object via + the threadsafe call mechanism. Essentially this guarantees + that when the method is called it wil be on the GUI thread + """ + callable = getattr(self._getHeldObject(), attr) + return CrossThreadCall(callable) + + def __dir__(self): + return dir(self._getHeldObject()) + + def __str__(self): + """ + Return a string representation of the proxied object + """ + return str(self._getHeldObject()) + + def __repr__(self): + """ + Return a string representation of the proxied object + """ + return `self._getHeldObject()` + + def _getHeldObject(self): + """ + Returns a reference to the held object + """ + return self.__obj + + def _kill_object(self): + """ + Release the stored instance + """ + self.__obj = None + + def _swap(self, obj): + """ + Swap an object so that the proxy now refers to this object + """ + self.__obj = obj + +#----------------------------------------------------------------------------- +class MDIWindow(QtProxyObject): + """Proxy for the _qti.MDIWindow object. + Also used for subclasses that do not need any methods intercepted (e.g. Table, Note, Matrix) + """ + def __init__(self, toproxy): + QtProxyObject.__init__(self,toproxy) + + def folder(self): + return new_proxy(Folder, self._getHeldObject().folder) + +#----------------------------------------------------------------------------- +class Graph(MDIWindow): + """Proxy for the _qti.Graph object. + """ + # When checking the SIP interface, remember the following name mappings (PyName): + # C++ 'Multilayer' class => Python 'Graph' class + # C++ 'Graph' class => Python 'Layer' class + + def __init__(self, toproxy): + MDIWindow.__init__(self,toproxy) + + def activeLayer(self): + """Get a handle to the presently active layer """ + return new_proxy(Layer, self._getHeldObject().activeLayer) + + def setActiveLayer(self, layer): + """Set the active layer to that specified. + + Args: + layer: A reference to the Layer to make the active one. Must belong to this Graph. + """ + threadsafe_call(self._getHeldObject().setActiveLayer, layer._getHeldObject()) + + def layer(self, num): + """ Get a handle to a specified layer + + Args: + num: The index of the required layer + """ + return new_proxy(Layer, self._getHeldObject().layer, num) + + def addLayer(self, x=0, y=0, width=None, height=None): + """Add a layer to the graph. + + Args: + x: The coordinate in pixels (from the graph's left) of the top-left of the new layer (default: 0). + y: The coordinate in pixels (from the graph's top) of the top-left of the new layer (default: 0). + width: The width of the new layer (default value if not specified). + height: The height of the new layer (default value if not specified). + + Returns: + A handle to the newly created layer. + """ + # Turn the optional arguments into the magic numbers that the C++ expects + if width is None: + width=0 + if height is None: + height=0 + return new_proxy(Layer, self._getHeldObject().addLayer, x,y,width,height) + + def insertCurve(self, graph, index): + """Add a curve from another graph to this one. + + Args: + graph: A reference to the graph from which the curve is coming (does nothing if this argument is the present Graph). + index: The index of the curve to add (counts from zero). + """ + threadsafe_call(self._getHeldObject().insertCurve, graph._getHeldObject(), index) + + +#----------------------------------------------------------------------------- +class Layer(QtProxyObject): + """Proxy for the _qti.Layer object. + """ + # These methods are used for the new matplotlib-like CLI + # These ones are provided by the C++ class Graph, which in the SIP declarations is renamed as Layer + # The only purpose of listing them here is that these will be returned by this class' __dir()__, and + # shown interactively, while the ones not listed and/or overloaded here may not be shown in ipython, etc. + additional_methods = ['logLogAxes', 'logXLinY', 'logXLinY', + 'removeLegend', 'saveImage', 'setAxisScale', 'setCurveLineColor', 'setCurveLineStyle', + 'setCurveLineWidth', 'setCurveSymbol', 'setScale', 'setTitle', 'setXTitle', 'setYTitle'] + + def __init__(self, toproxy): + QtProxyObject.__init__(self,toproxy) + + def insertCurve(self, *args): + """Add a curve from a workspace, table or another Layer to the plot + + Args: + The first argument should be a reference to a workspace, table or layer, or a workspace name. + Subsequent arguments vary according to the type of the first. + + Returns: + A boolean indicating success or failure. + """ + if isinstance(args[0],str): + return threadsafe_call(self._getHeldObject().insertCurve, *args) + elif hasattr(args[0], 'getName'): + return threadsafe_call(self._getHeldObject().insertCurve, args[0].getName(),*args[1:]) + else: + return threadsafe_call(self._getHeldObject().insertCurve, args[0]._getHeldObject(),*args[1:]) + + def addCurves(self, table, columns, style=0, lineWidth=1, symbolSize=3, startRow=0, endRow=-1): + """Add curves based on table columns to the plot. + + Args: + table: A reference to the table containing the data to plot. + columns: A tuple of column indices. + style: The curve style (default: Line). + lineWidth: The width of the curve line (default: 1). + symbolSize: The curve's symbol size (default: 3). + startRow: The first row to include in the curve's data (default: the first one) + endRow: The last row to include in the curve's data (default: the last one) + + Returns: + A boolean indicating success or failure. + """ + return threadsafe_call(self._getHeldObject().addCurves, table._getHeldObject(),columns,style,lineWidth,symbolSize,startRow,endRow) + + def addCurve(self, table, columnName, style=0, lineWidth=1, symbolSize=3, startRow=0, endRow=-1): + """Add a curve based on a table column to the plot. + + Args: + table: A reference to the table containing the data to plot. + columns: The name of the column to plot. + style: The curve style (default: Line). + lineWidth: The width of the curve line (default: 1). + symbolSize: The curve's symbol size (default: 3). + startRow: The first row to include in the curve's data (default: the first one) + endRow: The last row to include in the curve's data (default: the last one) + + Returns: + A boolean indicating success or failure. + """ + return threadsafe_call(self._getHeldObject().addCurve, table._getHeldObject(),columnName,style,lineWidth,symbolSize,startRow,endRow) + + def addErrorBars(self, yColName, errTable, errColName, type=1, width=1, cap=8, color=Qt.black, through=False, minus=True, plus=True): + """Add error bars to a plot that was created from a table column. + + Args: + yColName: The name of the column pertaining to the curve's data values. + errTable: A reference to the table holding the error values. + errColName: The name of the column containing the error values. + type: The orientation of the error bars - horizontal (0) or vertical (1, the default). + width: The line width of the error bars (default: 1). + cap: The length of the cap on the error bars (default: 8). + color: The color of error bars (default: black). + through: Whether the error bars are drawn through the symbol (default: no). + minus: Whether these errors should be shown as negative errors (default: yes). + plus: Whether these errors should be shown as positive errors (default: yes). + """ + threadsafe_call(self._getHeldObject().addErrorBars, yColName,errTable._getHeldObject(),errColName,type,width,cap,color,through,minus,plus) + + def errorBarSettings(self, curveIndex, errorBarIndex=0): + """Get a handle to the error bar settings for a specified curve. + + Args: + curveIndex: The curve to get the settings for + errorBarIndex: A curve can hold more than one set of error bars. Specify which one (default: the first). + Note that a curve plotted from a workspace can have only one set of error bars (and hence settings). + + Returns: A handle to the error bar settings object. + """ + return new_proxy(QtProxyObject, self._getHeldObject().errorBarSettings, curveIndex,errorBarIndex) + + def addHistogram(self, matrix): + """Add a matrix histogram to the graph""" + threadsafe_call(self._getHeldObject().addHistogram, matrix._getHeldObject()) + + def newLegend(self, text): + """Create a new legend. + + Args: + text: The text of the legend. + + Returns: + A handle to the newly created legend widget. + """ + return new_proxy(QtProxyObject, self._getHeldObject().newLegend, text) + + def legend(self): + """Get a handle to the layer's legend widget.""" + return new_proxy(QtProxyObject, self._getHeldObject().legend) + + def grid(self): + """Get a handle to the grid object for this layer.""" + return new_proxy(QtProxyObject, self._getHeldObject().grid) + + def spectrogram(self): + """If the layer contains a spectrogram, get a handle to the spectrogram object.""" + return new_proxy(QtProxyObject, self._getHeldObject().spectrogram) + + + def __dir__(self): + """Returns the list of attributes of this object.""" + # The first part (explicitly defined ones) are here for the traditional Mantid CLI, + # the additional ones have been added for the matplotlib-like CLI (without explicit + # declaration/documentation here in the proxies layer. + return ['insertCurve', 'addCurves', 'addCurve', 'addErrorBars', 'errorBarSettings', 'addHistogram', + 'newLegend', 'legend', 'grid', 'spectrogram' ] + self.additional_methods + + +#----------------------------------------------------------------------------- +class Graph3D(QtProxyObject): + """Proxy for the _qti.Graph3D object. + """ + def __init__(self, toproxy): + QtProxyObject.__init__(self,toproxy) + + def setData(self, table, colName, type=0): + """Set a table column to be the data source for this plot. + + Args: + table: A reference to the table. + colName: The name of the column to set as the data source. + type: The plot type. + """ + threadsafe_call(self._getHeldObject().setData, table._getHeldObject(),colName,type) + + def setMatrix(self, matrix): + """Set a matrix (N.B. not a MantidMatrix) to be the data source for this plot. + + Args: + matrix: A reference to the matrix. + """ + threadsafe_call(self._getHeldObject().setMatrix, matrix._getHeldObject()) + +#----------------------------------------------------------------------------- +class Spectrogram(QtProxyObject): + """Proxy for the _qti.Spectrogram object. + """ + def __init__(self, toproxy): + QtProxyObject.__init__(self,toproxy) + + def matrix(self): + """Get a handle to the data source.""" + return new_proxy(QtProxyObject, self._getHeldObject().matrix) + +#----------------------------------------------------------------------------- +class Folder(QtProxyObject): + """Proxy for the _qti.Folder object. + """ + def __init__(self, toproxy): + QtProxyObject.__init__(self,toproxy) + + def windows(self): + """Get a list of the windows in this folder""" + f = self._getHeldObject().windows() + ret = [] + for item in f: + ret.append(MDIWindow(item)) + return ret + + def folders(self): + """Get a list of the subfolders of this folder""" + f = self._getHeldObject().folders() + ret = [] + for item in f: + ret.append(Folder(item)) + return ret + + def folder(self, name, caseSensitive=True, partialMatch=False): + """Get a handle to a named subfolder. + + Args: + name: The name of the subfolder. + caseSensitive: Whether to search case-sensitively or not (default: yes). + partialMatch: Whether to return a partial match (default: no). + + Returns: + A handle to the requested folder, or None if no match found. + """ + return new_proxy(Folder, self._getHeldObject().folder, name,caseSensitive,partialMatch) + + def findWindow(self, name, searchOnName=True, searchOnLabel=True, caseSensitive=False, partialMatch=True): + """Get a handle to the first window matching the search criteria. + + Args: + name: The name of the window. + searchOnName: Whether to search the window names (takes precedence over searchOnLabel). + searchOnLabel: Whether to search the window labels. + caseSensitive: Whether to search case-sensitively or not (default: no). + partialMatch: Whether to return a partial match (default: yes). + + Returns: + A handle to the requested window, or None if no match found. + """ + return new_proxy(MDIWindow, self._getHeldObject().findWindow, name,searchOnName,searchOnLabel,caseSensitive,partialMatch) + + def window(self, name, cls='MdiSubWindow', recursive=False): + """Get a handle to a named window of a particular type. + + Args: + name: The name of the window. + cls: Search only for windows of type inheriting from this class (N.B. This is the C++ class name). + recursive: If True, do a depth-first recursive search (default: False). + + Returns: + A handle to the window, or None if no match found. + """ + return new_proxy(MDIWindow, self._getHeldObject().window, name,cls,recursive) + + def table(self, name, recursive=False): + """Get a handle to the table with the given name. + + Args: + name: The name of the table to search for. + recursive: If True, do a depth-first recursive search (default: False). + """ + return new_proxy(MDIWindow, self._getHeldObject().table, name,recursive) + + def matrix(self, name, recursive=False): + """Get a handle to the matrix with the given name. + + Args: + name: The name of the matrix to search for. + recursive: If True, do a depth-first recursive search (default: False). + """ + return new_proxy(MDIWindow, self._getHeldObject().matrix, name,recursive) + + def graph(self, name, recursive=False): + """Get a handle to the graph with the given name. + + Args: + name: The name of the graph to search for. + recursive: If True, do a depth-first recursive search (default: False). + """ + return new_proxy(Graph, self._getHeldObject().graph, name,recursive) + + def rootFolder(self): + """Get the folder at the root of the hierarchy""" + return new_proxy(Folder, self._getHeldObject().rootFolder) + +#----------------------------------------------------------------------------- +class MantidMatrix(MDIWindow): + """Proxy for the _qti.MantidMatrix object. + """ + def __init__(self, toproxy): + QtProxyObject.__init__(self,toproxy) + + def plotGraph3D(self, style=3): + """Create a 3D plot of the workspace data. + + Args: + style: The qwt3d plotstyle of the generated graph (default: filled mesh) + + Returns: + A handle to the newly created graph (a Graph3D object) + """ + return new_proxy(Graph3D, self._getHeldObject().plotGraph3D, style) + + def plotGraph2D(self, type=16): + """Create a spectrogram from the workspace data. + + Args: + type: The style of the plot (default: ColorMap) + + Returns: + A handle the newly created graph (a Graph object) + """ + return new_proxy(Graph, self._getHeldObject().plotGraph2D, type) + +#----------------------------------------------------------------------------- +class InstrumentWindow(MDIWindow): + """Proxy for the instrument window + """ + + def __init__(self, toproxy): + """Creates a proxy object around an instrument window + + Args: + toproxy: The raw C object to proxy + """ + QtProxyObject.__init__(self, toproxy) + + def getTab(self, name_or_tab): + """Retrieve a handle to the given tab + + Args: + name_or_index: A string containing the title or tab type + + Returns: + A handle to a tab widget + """ + handle = new_proxy(QtProxyObject, self._getHeldObject().getTab, name_or_tab) + if handle is None: + raise ValueError("Invalid tab title '%s'" % str(name_or_tab)) + return handle + + # ----- Deprecated functions ----- + def changeColormap(self, filename=None): + import warnings + warnings.warn("InstrumentWindow.changeColormap has been deprecated. Use the render tab method instead.") + callable = QtProxyObject.__getattr__(self, "changeColormap") + if filename is None: + callable() + else: + callable(filename) + + def setColorMapMinValue(self, value): + import warnings + warnings.warn("InstrumentWindow.setColorMapMinValue has been deprecated. Use the render tab setMinValue method instead.") + QtProxyObject.__getattr__(self, "setColorMapMinValue")(value) + + def setColorMapMaxValue(self, value): + import warnings + warnings.warn("InstrumentWindow.setColorMapMaxValue has been deprecated. Use the render tab setMaxValue method instead.") + QtProxyObject.__getattr__(self, "setColorMapMaxValue")(value) + + def setColorMapRange(self, minvalue, maxvalue): + import warnings + warnings.warn("InstrumentWindow.setColorMapRange has been deprecated. Use the render tab setRange method instead.") + QtProxyObject.__getattr__(self, "setColorMapRange")(minvalue,maxvalue) + + def setScaleType(self, scale_type): + import warnings + warnings.warn("InstrumentWindow.setScaleType has been deprecated. Use the render tab setScaleType method instead.") + QtProxyObject.__getattr__(self, "setScaleType")(scale_type) + + def setViewType(self, view_type): + import warnings + warnings.warn("InstrumentWindow.setViewType has been deprecated. Use the render tab setSurfaceType method instead.") + QtProxyObject.__getattr__(self, "setViewType")(view_type) + + def selectComponent(self, name): + import warnings + warnings.warn("InstrumentWindow.selectComponent has been deprecated. Use the tree tab selectComponentByName method instead.") + QtProxyObject.__getattr__(self, "selectComponent")(name) + +#----------------------------------------------------------------------------- +class SliceViewerWindowProxy(QtProxyObject): + """Proxy for a C++ SliceViewerWindow object. + + It will pass-through method calls that can be applied to the + SliceViewer widget contained within. + """ + def __init__(self, toproxy): + QtProxyObject.__init__(self, toproxy) + + def __getattr__(self, attr): + """ + Reroute a method call to the the stored object + """ + if self._getHeldObject() is None: + raise Exception("Error! The SliceViewerWindow has been deleted.") + + # Pass-through to the contained SliceViewer widget. + sv = self.getSlicer() + # But only those attributes that are methods on the SliceViewer + if attr in SliceViewerProxy.slicer_methods: + return getattr(sv, attr) + else: + # Otherwise, pass through to the stored object + return getattr(self._getHeldObject(), attr) + + def __str__(self): + """ + Return a string representation of the proxied object + """ + if self._getHeldObject() is None: + return "None" + else: + return 'SliceViewerWindow(workspace="%s")' % self._getHeldObject().getSlicer().getWorkspaceName() + + def __repr__(self): + """ + Return a string representation of the proxied object + """ + return `self._getHeldObject()` + + def __dir__(self): + """ + Returns the list of attributes for this object. + Might allow tab-completion to work under ipython + """ + return SliceViewerProxy.slicer_methods + ['showLine'] + + def getLiner(self): + """ + Returns the LineViewer widget that is part of this + SliceViewerWindow + """ + return LineViewerProxy(self._getHeldObject().getLiner()) + + def getSlicer(self): + """ + Returns the SliceViewer widget that is part of this + SliceViewerWindow + """ + return SliceViewerProxy(self._getHeldObject().getSlicer()) + + def showLine(self, start, end, width=None, planar_width=0.1, thicknesses=None, + num_bins=100): + """Opens the LineViewer and define a 1D line along which to integrate. + + The line is created in the same XY dimensions and at the same slice + point as is currently shown in the SliceViewer. + + Args: + start :: (X,Y) coordinates of the start point in the XY dimensions + of the current slice. + end :: (X,Y) coordinates of the end point in the XY dimensions + of the current slice. + width :: if specified, sets all the widths (planar and other + dimensions) to this integration width. + planar_width :: sets the XY-planar (perpendicular to the line) + integration width. Default 0.1. + thicknesses :: list with one thickness value for each dimension in the + workspace (including the XY dimensions, which are ignored). + e.g. [0,1,2,3] in a XYZT workspace. + num_bins :: number of bins by which to divide the line. + Default 100. + + Returns: + The LineViewer object of the SliceViewerWindow. There are methods + available to modify the line drawn. + """ + # First show the lineviewer + self.getSlicer().toggleLineMode(True) + liner = self.getLiner() + + # Start and end point + liner.setStartXY(start[0], start[1]) + liner.setEndXY(end[0], end[1]) + + # Set the width. + if not width is None: + liner.setThickness(width) + else: + liner.setPlanarWidth(planar_width) + if not thicknesses is None: + for d in xrange(len(thicknesses)): + liner.setThickness(d, thicknesses[i]) + # Bins + liner.setNumBins(num_bins) + liner.apply() + + # Return the proxy to the LineViewer widget + return liner + +#----------------------------------------------------------------------------- +def getWorkspaceNames(source): + """Takes a "source", which could be a WorkspaceGroup, or a list + of workspaces, or a list of names, and converts + it to a list of workspace names. + + Args: + source :: input list or workspace group + + Returns: + list of workspace names + """ + ws_names = [] + if isinstance(source, list) or isinstance(source,tuple): + for w in source: + names = getWorkspaceNames(w) + ws_names += names + elif hasattr(source, 'getName'): + if hasattr(source, '_getHeldObject'): + wspace = source._getHeldObject() + else: + wspace = source + if wspace == None: + return [] + if hasattr(wspace, 'getNames'): + grp_names = wspace.getNames() + for n in grp_names: + if n != wspace.getName(): + ws_names.append(n) + else: + ws_names.append(wspace.getName()) + elif isinstance(source,str): + w = None + try: + # for non-existent names this raises a KeyError + w = mantid.AnalysisDataService.Instance()[source] + except Exception, exc: + raise ValueError("Workspace '%s' not found!"%source) + + if w != None: + names = getWorkspaceNames(w) + for n in names: + ws_names.append(n) + else: + raise TypeError('Incorrect type passed as workspace argument "' + str(source) + '"') + return ws_names + +#----------------------------------------------------------------------------- +class ProxyCompositePeaksPresenter(QtProxyObject): + def __init__(self, toproxy): + QtProxyObject.__init__(self,toproxy) + + def getPeaksPresenter(self, source): + to_present = None + if isinstance(source, str): + to_present = source + elif isinstance(source, mantid.api.Workspace): + to_present = source.getName() + else: + raise ValueError("getPeaksPresenter expects a Workspace name or a Workspace object.") + if not mantid.api.mtd.doesExist(to_present): + raise ValueError("%s does not exist in the workspace list" % to_present) + + return new_proxy(QtProxyObject, self._getHeldObject().getPeaksPresenter, to_present) + +#----------------------------------------------------------------------------- +class SliceViewerProxy(QtProxyObject): + """Proxy for a C++ SliceViewer widget. + """ + # These are the exposed python method names + slicer_methods = ["setWorkspace", "getWorkspaceName", "showControls", "openFromXML", "getImage", "saveImage", "copyImageToClipboard", "setFastRender", "getFastRender", "toggleLineMode", "setXYDim", "setXYDim", "getDimX", "getDimY", "setSlicePoint", "setSlicePoint", "getSlicePoint", "getSlicePoint", "setXYLimits", "getXLimits", "getYLimits", "zoomBy", "setXYCenter", "resetZoom", "loadColorMap", "setColorScale", "setColorScaleMin", "setColorScaleMax", "setColorScaleLog", "getColorScaleMin", "getColorScaleMax", "getColorScaleLog", "setColorScaleAutoFull", "setColorScaleAutoSlice", "setColorMapBackground", "setTransparentZeros", "setNormalization", "getNormalization", "setRebinThickness", "setRebinNumBins", "setRebinMode", "setPeaksWorkspaces", "refreshRebin"] + + def __init__(self, toproxy): + QtProxyObject.__init__(self, toproxy) + + def __dir__(self): + """Returns the list of attributes for this object. """ + return self.slicer_methods() + + def setPeaksWorkspaces(self, source): + workspace_names = getWorkspaceNames(source) + if len(workspace_names) == 0: + raise ValueError("No workspace names given to setPeaksWorkspaces") + for name in workspace_names: + if not mantid.api.mtd.doesExist(name): + raise ValueError("%s does not exist in the workspace list" % name) + if not isinstance(mantid.api.mtd[name], mantid.api.IPeaksWorkspace): + raise ValueError("%s is not an IPeaksWorkspace" % name) + + return new_proxy(ProxyCompositePeaksPresenter, self._getHeldObject().setPeaksWorkspaces, workspace_names) + + +#----------------------------------------------------------------------------- +class LineViewerProxy(QtProxyObject): + """Proxy for a C++ LineViewer widget. + """ + def __init__(self, toproxy): + QtProxyObject.__init__(self, toproxy) + + def __dir__(self): + """Returns the list of attributes for this object. """ + return ["apply", "showPreview", "showFull", "setStartXY", "setEndXY", "setThickness", "setThickness", + "setThickness", "setPlanarWidth", "getPlanarWidth", "setNumBins", "setFixedBinWidthMode", "getFixedBinWidth", + "getFixedBinWidthMode", "getNumBins", "getBinWidth", "setPlotAxis", "getPlotAxis"] + + +#----------------------------------------------------------------------------- +class FitBrowserProxy(QtProxyObject): + """ + Proxy for the FitPropertyBrowser object. + """ + def __init__(self, toproxy): + QtProxyObject.__init__(self,toproxy) + + +#----------------------------------------------------------------------------- +class TiledWindowProxy(QtProxyObject): + """ + Proxy for the TiledWindow object. + """ + def __init__(self, toproxy): + QtProxyObject.__init__(self,toproxy) + + def addWidget(self, tile, row, col): + """ + Add a new sub-window at a given position in the layout. + The layout will re-shape itself if necessary to fit in the new tile. + + Args: + + tile :: An MdiSubWindow to add. + row :: A row index at which to place the new tile. + col :: A column index at which to place the new tile. + """ + threadsafe_call(self._getHeldObject().addWidget, tile._getHeldObject(), row, col) + + def insertWidget(self, tile, row, col): + """ + Insert a new sub-window at a given position in the layout. + The widgets to the right and below the inserted tile will be shifted + towards the bottom of the window. If necessary a new row will be appended. + The number of columns doesn't change. + + Args: + + tile :: An MdiSubWindow to insert. + row :: A row index at which to place the new tile. + col :: A column index at which to place the new tile. + """ + threadsafe_call(self._getHeldObject().insertWidget, tile._getHeldObject(), row, col) + + def getWidget(self, row, col): + """ + Get a sub-window at a location in this TiledWindow. + + Args: + row :: A row of a sub-window. + col :: A column of a sub-window. + """ + return MDIWindow( threadsafe_call(self._getHeldObject().getWidget, row, col) ) + + def clear(self): + """ + Clear the content this TiledWindow. + """ + threadsafe_call(self._getHeldObject().clear) -- GitLab