source: sasview/src/sas/qtgui/Utilities/GuiUtils.py @ 8eea1b1

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 8eea1b1 was 8eea1b1, checked in by Laura Forster <Awork@…>, 6 years ago

Merge branch 'ESS_GUI' of https://github.com/SasView/sasview into ESS_GUI

  • Property mode set to 100644
File size: 37.0 KB
Line 
1# -*- coding: utf-8 -*-
2"""
3Global defaults and various utility functions usable by the general GUI
4"""
5
6import os
7import re
8import sys
9import imp
10import warnings
11import webbrowser
12import urllib.parse
13
14import numpy as np
15
16warnings.simplefilter("ignore")
17import logging
18
19from PyQt5 import QtCore
20from PyQt5 import QtGui
21from PyQt5 import QtWidgets
22
23from periodictable import formula as Formula
24from sas.qtgui.Plotting import DataTransform
25from sas.qtgui.Plotting.ConvertUnits import convertUnit
26from sas.qtgui.Plotting.PlotterData import Data1D
27from sas.qtgui.Plotting.PlotterData import Data2D
28from sas.sascalc.dataloader.loader import Loader
29from sas.qtgui.Utilities import CustomDir
30
31if os.path.splitext(sys.argv[0])[1].lower() != ".py":
32        HELP_DIRECTORY_LOCATION = "doc"
33else:
34        HELP_DIRECTORY_LOCATION = "docs/sphinx-docs/build/html"
35IMAGES_DIRECTORY_LOCATION = HELP_DIRECTORY_LOCATION + "/_images"
36
37# This matches the ID of a plot created using FittingLogic._create1DPlot, e.g.
38# "5 [P(Q)] modelname"
39# or
40# "4 modelname".
41# Useful for determining whether the plot in question is for an intermediate result, such as P(Q) or S(Q) in the
42# case of a product model; the identifier for this is held in square brackets, as in the example above.
43theory_plot_ID_pattern = re.compile(r"^([0-9]+)\s+(\[(.*)\]\s+)?(.*)$")
44
45def get_app_dir():
46    """
47        The application directory is the one where the default custom_config.py
48        file resides.
49
50        :returns: app_path - the path to the applicatin directory
51    """
52    # First, try the directory of the executable we are running
53    app_path = sys.path[0]
54    if os.path.isfile(app_path):
55        app_path = os.path.dirname(app_path)
56    if os.path.isfile(os.path.join(app_path, "custom_config.py")):
57        app_path = os.path.abspath(app_path)
58        #logging.info("Using application path: %s", app_path)
59        return app_path
60
61    # Next, try the current working directory
62    if os.path.isfile(os.path.join(os.getcwd(), "custom_config.py")):
63        #logging.info("Using application path: %s", os.getcwd())
64        return os.path.abspath(os.getcwd())
65
66    # Finally, try the directory of the sasview module
67    # TODO: gui_manager will have to know about sasview until we
68    # clean all these module variables and put them into a config class
69    # that can be passed by sasview.py.
70    # logging.info(sys.executable)
71    # logging.info(str(sys.argv))
72    from sas import sasview as sasview
73    app_path = os.path.dirname(sasview.__file__)
74    # logging.info("Using application path: %s", app_path)
75    return app_path
76
77def get_user_directory():
78    """
79        Returns the user's home directory
80    """
81    userdir = os.path.join(os.path.expanduser("~"), ".sasview")
82    if not os.path.isdir(userdir):
83        os.makedirs(userdir)
84    return userdir
85
86def _find_local_config(confg_file, path):
87    """
88        Find configuration file for the current application
89    """
90    config_module = None
91    fObj = None
92    try:
93        fObj, path_config, descr = imp.find_module(confg_file, [path])
94        config_module = imp.load_module(confg_file, fObj, path_config, descr)
95    except ImportError:
96        pass
97        #logging.error("Error loading %s/%s: %s" % (path, confg_file, sys.exc_value))
98    except ValueError:
99        print("Value error")
100        pass
101    finally:
102        if fObj is not None:
103            fObj.close()
104    #logging.info("GuiManager loaded %s/%s" % (path, confg_file))
105    return config_module
106
107
108# Get APP folder
109PATH_APP = get_app_dir()
110DATAPATH = PATH_APP
111
112# Read in the local config, which can either be with the main
113# application or in the installation directory
114config = _find_local_config('local_config', PATH_APP)
115
116if config is None:
117    config = _find_local_config('local_config', os.getcwd())
118else:
119    pass
120
121c_conf_dir = CustomDir.setup_conf_dir(PATH_APP)
122
123custom_config = _find_local_config('custom_config', c_conf_dir)
124if custom_config is None:
125    custom_config = _find_local_config('custom_config', os.getcwd())
126    if custom_config is None:
127        msgConfig = "Custom_config file was not imported"
128
129#read some constants from config
130APPLICATION_STATE_EXTENSION = config.APPLICATION_STATE_EXTENSION
131APPLICATION_NAME = config.__appname__
132SPLASH_SCREEN_PATH = config.SPLASH_SCREEN_PATH
133WELCOME_PANEL_ON = config.WELCOME_PANEL_ON
134SPLASH_SCREEN_WIDTH = config.SPLASH_SCREEN_WIDTH
135SPLASH_SCREEN_HEIGHT = config.SPLASH_SCREEN_HEIGHT
136SS_MAX_DISPLAY_TIME = config.SS_MAX_DISPLAY_TIME
137if not WELCOME_PANEL_ON:
138    WELCOME_PANEL_SHOW = False
139else:
140    WELCOME_PANEL_SHOW = True
141try:
142    DATALOADER_SHOW = custom_config.DATALOADER_SHOW
143    TOOLBAR_SHOW = custom_config.TOOLBAR_SHOW
144    FIXED_PANEL = custom_config.FIXED_PANEL
145    if WELCOME_PANEL_ON:
146        WELCOME_PANEL_SHOW = custom_config.WELCOME_PANEL_SHOW
147    PLOPANEL_WIDTH = custom_config.PLOPANEL_WIDTH
148    DATAPANEL_WIDTH = custom_config.DATAPANEL_WIDTH
149    GUIFRAME_WIDTH = custom_config.GUIFRAME_WIDTH
150    GUIFRAME_HEIGHT = custom_config.GUIFRAME_HEIGHT
151    CONTROL_WIDTH = custom_config.CONTROL_WIDTH
152    CONTROL_HEIGHT = custom_config.CONTROL_HEIGHT
153    DEFAULT_PERSPECTIVE = custom_config.DEFAULT_PERSPECTIVE
154    CLEANUP_PLOT = custom_config.CLEANUP_PLOT
155    # custom open_path
156    open_folder = custom_config.DEFAULT_OPEN_FOLDER
157    if open_folder is not None and os.path.isdir(open_folder):
158        DEFAULT_OPEN_FOLDER = os.path.abspath(open_folder)
159    else:
160        DEFAULT_OPEN_FOLDER = PATH_APP
161except AttributeError:
162    DATALOADER_SHOW = True
163    TOOLBAR_SHOW = True
164    FIXED_PANEL = True
165    WELCOME_PANEL_SHOW = False
166    PLOPANEL_WIDTH = config.PLOPANEL_WIDTH
167    DATAPANEL_WIDTH = config.DATAPANEL_WIDTH
168    GUIFRAME_WIDTH = config.GUIFRAME_WIDTH
169    GUIFRAME_HEIGHT = config.GUIFRAME_HEIGHT
170    CONTROL_WIDTH = -1
171    CONTROL_HEIGHT = -1
172    DEFAULT_PERSPECTIVE = None
173    CLEANUP_PLOT = False
174    DEFAULT_OPEN_FOLDER = PATH_APP
175
176#DEFAULT_STYLE = config.DEFAULT_STYLE
177
178PLUGIN_STATE_EXTENSIONS = config.PLUGIN_STATE_EXTENSIONS
179OPEN_SAVE_MENU = config.OPEN_SAVE_PROJECT_MENU
180VIEW_MENU = config.VIEW_MENU
181EDIT_MENU = config.EDIT_MENU
182extension_list = []
183if APPLICATION_STATE_EXTENSION is not None:
184    extension_list.append(APPLICATION_STATE_EXTENSION)
185EXTENSIONS = PLUGIN_STATE_EXTENSIONS + extension_list
186try:
187    PLUGINS_WLIST = '|'.join(config.PLUGINS_WLIST)
188except AttributeError:
189    PLUGINS_WLIST = ''
190APPLICATION_WLIST = config.APPLICATION_WLIST
191IS_WIN = True
192IS_LINUX = False
193CLOSE_SHOW = True
194TIME_FACTOR = 2
195NOT_SO_GRAPH_LIST = ["BoxSum"]
196
197
198class Communicate(QtCore.QObject):
199    """
200    Utility class for tracking of the Qt signals
201    """
202    # File got successfully read
203    fileReadSignal = QtCore.pyqtSignal(list)
204
205    # Open File returns "list" of paths
206    fileDataReceivedSignal = QtCore.pyqtSignal(dict)
207
208    # Update Main window status bar with "str"
209    # Old "StatusEvent"
210    statusBarUpdateSignal = QtCore.pyqtSignal(str)
211
212    # Send data to the current perspective
213    updatePerspectiveWithDataSignal = QtCore.pyqtSignal(list)
214
215    # New data in current perspective
216    updateModelFromPerspectiveSignal = QtCore.pyqtSignal(QtGui.QStandardItem)
217
218    # New theory data in current perspective
219    updateTheoryFromPerspectiveSignal = QtCore.pyqtSignal(QtGui.QStandardItem)
220
221    # Request to delete plots (in the theory view) related to a given model ID
222    deleteIntermediateTheoryPlotsSignal = QtCore.pyqtSignal(str)
223
224    # New plot requested from the GUI manager
225    # Old "NewPlotEvent"
226    plotRequestedSignal = QtCore.pyqtSignal(list, int)
227
228    # Plot from file names
229    plotFromFilenameSignal = QtCore.pyqtSignal(str)
230
231    # Plot update requested from a perspective
232    plotUpdateSignal = QtCore.pyqtSignal(list)
233
234    # Progress bar update value
235    progressBarUpdateSignal = QtCore.pyqtSignal(int)
236
237    # Workspace charts added/removed
238    activeGraphsSignal = QtCore.pyqtSignal(list)
239
240    # Current workspace chart's name changed
241    activeGraphName = QtCore.pyqtSignal(tuple)
242
243    # Current perspective changed
244    perspectiveChangedSignal = QtCore.pyqtSignal(str)
245
246    # File/dataset got deleted
247    dataDeletedSignal = QtCore.pyqtSignal(list)
248
249    # Send data to Data Operation Utility panel
250    sendDataToPanelSignal = QtCore.pyqtSignal(dict)
251
252    # Send result of Data Operation Utility panel to Data Explorer
253    updateModelFromDataOperationPanelSignal = QtCore.pyqtSignal(QtGui.QStandardItem, dict)
254
255    # Notify about a new custom plugin being written/deleted/modified
256    customModelDirectoryChanged = QtCore.pyqtSignal()
257
258    # Notify the gui manager about new data to be added to the grid view
259    sendDataToGridSignal = QtCore.pyqtSignal(list)
260
261    # Action Save Analysis triggered
262    saveAnalysisSignal = QtCore.pyqtSignal()
263
264    # Mask Editor requested
265    maskEditorSignal = QtCore.pyqtSignal(Data2D)
266
267    #second Mask Editor for external
268    extMaskEditorSignal = QtCore.pyqtSignal()
269
270    # Fitting parameter copy to clipboard
271    copyFitParamsSignal = QtCore.pyqtSignal(str)
272
273    # Fitting parameter copy to clipboard for Excel
274    copyExcelFitParamsSignal = QtCore.pyqtSignal(str)
275
276    # Fitting parameter copy to clipboard for Latex
277    copyLatexFitParamsSignal = QtCore.pyqtSignal(str)
278
279    # Fitting parameter paste from clipboard
280    pasteFitParamsSignal = QtCore.pyqtSignal()
281
282    # Notify about new categories/models from category manager
283    updateModelCategoriesSignal = QtCore.pyqtSignal()
284
285    # Tell the data explorer to switch tabs
286    changeDataExplorerTabSignal = QtCore.pyqtSignal(int)
287
288def updateModelItemWithPlot(item, update_data, name=""):
289    """
290    Adds a checkboxed row named "name" to QStandardItem
291    Adds 'update_data' to that row.
292    """
293    assert isinstance(item, QtGui.QStandardItem)
294
295    # Check if data with the same ID is already present
296    for index in range(item.rowCount()):
297        plot_item = item.child(index)
298        if not plot_item.isCheckable():
299            continue
300        plot_data = plot_item.child(0).data()
301        if plot_data.id is not None and \
302                plot_data.name == update_data.name:
303                #(plot_data.name == update_data.name or plot_data.id == update_data.id):
304            # if plot_data.id is not None and plot_data.id == update_data.id:
305            # replace data section in item
306            plot_item.child(0).setData(update_data)
307            plot_item.setText(name)
308            # Plot title if any
309            if plot_item.child(1).hasChildren():
310                plot_item.child(1).child(0).setText("Title: %s"%name)
311            # Force redisplay
312            return
313
314    # Create the new item
315    checkbox_item = createModelItemWithPlot(update_data, name)
316
317    # Append the new row to the main item
318    item.appendRow(checkbox_item)
319
320def deleteRedundantPlots(item, new_plots):
321    """
322    Checks all plots that are children of the given item; if any have an ID or name not included in new_plots,
323    it is deleted. Useful for e.g. switching from P(Q)S(Q) to P(Q); this would remove the old S(Q) plot.
324
325    Ensure that new_plots contains ALL the relevant plots(!!!)
326    """
327    assert isinstance(item, QtGui.QStandardItem)
328
329    # lists of plots names/ids for all deletable plots on item
330    names = [p.name for p in new_plots if p.name is not None]
331    ids = [p.id for p in new_plots if p.id is not None]
332
333    items_to_delete = []
334
335    for index in range(item.rowCount()):
336        plot_item = item.child(index)
337        if not plot_item.isCheckable():
338            continue
339        plot_data = plot_item.child(0).data()
340        if (plot_data.id is not None) and \
341            (plot_data.id not in ids) and \
342            (plot_data.name not in names) and \
343            (plot_data.plot_role == Data1D.ROLE_DELETABLE):
344            items_to_delete.append(plot_item)
345
346    for plot_item in items_to_delete:
347        item.removeRow(plot_item.row())
348
349class HashableStandardItem(QtGui.QStandardItem):
350    """
351    Subclassed standard item with reimplemented __hash__
352    to allow for use as an index.
353    """
354    def __init__(self, parent=None):
355        super(HashableStandardItem, self).__init__()
356
357    def __hash__(self):
358        ''' just a random hash value '''
359        #return hash(self.__init__)
360        return 0
361
362    def clone(self):
363        ''' Assure __hash__ is cloned as well'''
364        clone = super(HashableStandardItem, self).clone()
365        clone.__hash__ = self.__hash__
366        return clone
367
368def getMonospaceFont():
369    """Convenience function; returns a monospace font to be used in any shells, code editors, etc."""
370
371    # Note: Consolas is only available on Windows; the style hint is used on other operating systems
372    font = QtGui.QFont("Consolas", 10)
373    font.setStyleHint(QtGui.QFont.Monospace, QtGui.QFont.PreferQuality)
374    return font
375
376def createModelItemWithPlot(update_data, name=""):
377    """
378    Creates a checkboxed QStandardItem named "name"
379    Adds 'update_data' to that row.
380    """
381    py_update_data = update_data
382
383    checkbox_item = HashableStandardItem()
384    checkbox_item.setCheckable(True)
385    checkbox_item.setCheckState(QtCore.Qt.Checked)
386    checkbox_item.setText(name)
387
388    # Add "Info" item
389    if isinstance(py_update_data, (Data1D, Data2D)):
390        # If Data1/2D added - extract Info from it
391        info_item = infoFromData(py_update_data)
392    else:
393        # otherwise just add a naked item
394        info_item = QtGui.QStandardItem("Info")
395
396    # Add the actual Data1D/Data2D object
397    object_item = QtGui.QStandardItem()
398    object_item.setData(update_data)
399
400    # Set the data object as the first child
401    checkbox_item.setChild(0, object_item)
402
403    # Set info_item as the second child
404    checkbox_item.setChild(1, info_item)
405
406    # And return the newly created item
407    return checkbox_item
408
409def updateModelItem(item, update_data, name=""):
410    """
411    Adds a simple named child to QStandardItem
412    """
413    assert isinstance(item, QtGui.QStandardItem)
414
415    # Add the actual Data1D/Data2D object
416    object_item = QtGui.QStandardItem()
417    object_item.setText(name)
418    object_item.setData(update_data)
419
420    # Append the new row to the main item
421    item.appendRow(object_item)
422
423def updateModelItemStatus(model_item, filename="", name="", status=2):
424    """
425    Update status of checkbox related to high- and low-Q extrapolation
426    choice in Invariant Panel
427    """
428    assert isinstance(model_item, QtGui.QStandardItemModel)
429
430    # Iterate over model looking for items with checkboxes
431    for index in range(model_item.rowCount()):
432        item = model_item.item(index)
433        if item.text() == filename and item.isCheckable() \
434                and item.checkState() == QtCore.Qt.Checked:
435            # Going 1 level deeper only
436            for index_2 in range(item.rowCount()):
437                item_2 = item.child(index_2)
438                if item_2 and item_2.isCheckable() and item_2.text() == name:
439                    item_2.setCheckState(status)
440
441    return
442
443def itemFromFilename(filename, model_item):
444    """
445    Returns the model item text=filename in the model
446    """
447    assert isinstance(model_item, QtGui.QStandardItemModel)
448    assert isinstance(filename, str)
449
450    # Iterate over model looking for named items
451    item = list([i for i in [model_item.item(index)
452                             for index in range(model_item.rowCount())]
453                 if str(i.text()) == filename])
454    return item[0] if len(item)>0 else None
455
456def plotsFromModel(model_name, model_item):
457    """
458    Returns the list of plots for the item with model name in the model
459    """
460    assert isinstance(model_item, QtGui.QStandardItem)
461    assert isinstance(model_name, str)
462
463    plot_data = []
464    # Iterate over model looking for named items
465    for index in range(model_item.rowCount()):
466        item = model_item.child(index)
467        if isinstance(item.data(), (Data1D, Data2D)):
468            plot_data.append(item.data())
469        if model_name in str(item.text()):
470            #plot_data.append(item.child(0).data())
471            # Going 1 level deeper only
472            for index_2 in range(item.rowCount()):
473                item_2 = item.child(index_2)
474                if item_2 and isinstance(item_2.data(), (Data1D, Data2D)):
475                    plot_data.append(item_2.data())
476
477    return plot_data
478
479def plotsFromFilename(filename, model_item):
480    """
481    Returns the list of plots for the item with text=filename in the model
482    """
483    assert isinstance(model_item, QtGui.QStandardItemModel)
484    assert isinstance(filename, str)
485
486    plot_data = {}
487    # Iterate over model looking for named items
488    for index in range(model_item.rowCount()):
489        item = model_item.item(index)
490        if filename in str(item.text()):
491            # TODO: assure item type is correct (either data1/2D or Plotter)
492            plot_data[item] = item.child(0).data()
493            # Going 1 level deeper only
494            for index_2 in range(item.rowCount()):
495                item_2 = item.child(index_2)
496                if item_2 and item_2.isCheckable():
497                    # TODO: assure item type is correct (either data1/2D or Plotter)
498                    plot_data[item_2] = item_2.child(0).data()
499
500    return plot_data
501
502def plotsFromCheckedItems(model_item):
503    """
504    Returns the list of plots for items in the model which are checked
505    """
506    assert isinstance(model_item, QtGui.QStandardItemModel)
507
508    plot_data = []
509    # Iterate over model looking for items with checkboxes
510    for index in range(model_item.rowCount()):
511        item = model_item.item(index)
512
513        # Going 1 level deeper only
514        for index_2 in range(item.rowCount()):
515            item_2 = item.child(index_2)
516            if item_2 and item_2.isCheckable() and item_2.checkState() == QtCore.Qt.Checked:
517                # TODO: assure item type is correct (either data1/2D or Plotter)
518                plot_data.append((item_2, item_2.child(0).data()))
519
520        if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
521            # TODO: assure item type is correct (either data1/2D or Plotter)
522            plot_data.append((item, item.child(0).data()))
523
524    return plot_data
525
526def infoFromData(data):
527    """
528    Given Data1D/Data2D object, extract relevant Info elements
529    and add them to a model item
530    """
531    assert isinstance(data, (Data1D, Data2D))
532
533    info_item = QtGui.QStandardItem("Info")
534
535    title_item = QtGui.QStandardItem("Title: " + data.title)
536    info_item.appendRow(title_item)
537    run_item = QtGui.QStandardItem("Run: " + str(data.run))
538    info_item.appendRow(run_item)
539    type_item = QtGui.QStandardItem("Type: " + str(data.__class__.__name__))
540    info_item.appendRow(type_item)
541
542    if data.path:
543        path_item = QtGui.QStandardItem("Path: " + data.path)
544        info_item.appendRow(path_item)
545
546    if data.instrument:
547        instr_item = QtGui.QStandardItem("Instrument: " + data.instrument)
548        info_item.appendRow(instr_item)
549
550    process_item = QtGui.QStandardItem("Process")
551    if isinstance(data.process, list) and data.process:
552        for process in data.process:
553            process_date = process.date
554            process_date_item = QtGui.QStandardItem("Date: " + process_date)
555            process_item.appendRow(process_date_item)
556
557            process_descr = process.description
558            process_descr_item = QtGui.QStandardItem("Description: " + process_descr)
559            process_item.appendRow(process_descr_item)
560
561            process_name = process.name
562            process_name_item = QtGui.QStandardItem("Name: " + process_name)
563            process_item.appendRow(process_name_item)
564
565    info_item.appendRow(process_item)
566
567    return info_item
568
569def dataFromItem(item):
570    """
571    Retrieve Data1D/2D component from QStandardItem.
572    The assumption - data stored in SasView standard, in child 0
573    """
574    try:
575        data = item.child(0).data()
576    except AttributeError:
577        data = None
578    return data
579
580def openLink(url):
581    """
582    Open a URL in an external browser.
583    Check the URL first, though.
584    """
585    parsed_url = urllib.parse.urlparse(url)
586    if parsed_url.scheme:
587        webbrowser.open(url)
588    else:
589        msg = "Attempt at opening an invalid URL"
590        raise AttributeError(msg)
591
592def showHelp(url):
593    """
594    Open a local url in the default browser
595    """
596    location = HELP_DIRECTORY_LOCATION + url
597    #WP: Added to handle OSX bundle docs
598    if os.path.isdir(location) == False:
599        sas_path = os.path.abspath(os.path.dirname(sys.argv[0]))
600        location = sas_path+"/"+location
601    try:
602        webbrowser.open('file://' + os.path.realpath(location))
603    except webbrowser.Error as ex:
604        logging.warning("Cannot display help. %s" % ex)
605
606def retrieveData1d(data):
607    """
608    Retrieve 1D data from file and construct its text
609    representation
610    """
611    if not isinstance(data, Data1D):
612        msg = "Incorrect type passed to retrieveData1d"
613        raise AttributeError(msg)
614    try:
615        xmin = min(data.x)
616        ymin = min(data.y)
617    except:
618        msg = "Unable to find min/max of \n data named %s" % \
619                    data.filename
620        #logging.error(msg)
621        raise ValueError(msg)
622
623    text = data.__str__()
624    text += 'Data Min Max:\n'
625    text += 'X_min = %s:  X_max = %s\n' % (xmin, max(data.x))
626    text += 'Y_min = %s:  Y_max = %s\n' % (ymin, max(data.y))
627    if data.dy is not None:
628        text += 'dY_min = %s:  dY_max = %s\n' % (min(data.dy), max(data.dy))
629    text += '\nData Points:\n'
630    x_st = "X"
631    for index in range(len(data.x)):
632        if data.dy is not None and len(data.dy) > index:
633            dy_val = data.dy[index]
634        else:
635            dy_val = 0.0
636        if data.dx is not None and len(data.dx) > index:
637            dx_val = data.dx[index]
638        else:
639            dx_val = 0.0
640        if data.dxl is not None and len(data.dxl) > index:
641            if index == 0:
642                x_st = "Xl"
643            dx_val = data.dxl[index]
644        elif data.dxw is not None and len(data.dxw) > index:
645            if index == 0:
646                x_st = "Xw"
647            dx_val = data.dxw[index]
648
649        if index == 0:
650            text += "<index> \t<X> \t<Y> \t<dY> \t<d%s>\n" % x_st
651        text += "%s \t%s \t%s \t%s \t%s\n" % (index,
652                                                data.x[index],
653                                                data.y[index],
654                                                dy_val,
655                                                dx_val)
656    return text
657
658def retrieveData2d(data):
659    """
660    Retrieve 2D data from file and construct its text
661    representation
662    """
663    if not isinstance(data, Data2D):
664        msg = "Incorrect type passed to retrieveData2d"
665        raise AttributeError(msg)
666
667    text = data.__str__()
668    text += 'Data Min Max:\n'
669    text += 'I_min = %s\n' % min(data.data)
670    text += 'I_max = %s\n\n' % max(data.data)
671    text += 'Data (First 2501) Points:\n'
672    text += 'Data columns include err(I).\n'
673    text += 'ASCII data starts here.\n'
674    text += "<index> \t<Qx> \t<Qy> \t<I> \t<dI> \t<dQparal> \t<dQperp>\n"
675    di_val = 0.0
676    dx_val = 0.0
677    dy_val = 0.0
678    len_data = len(data.qx_data)
679    for index in range(0, len_data):
680        x_val = data.qx_data[index]
681        y_val = data.qy_data[index]
682        i_val = data.data[index]
683        if data.err_data is not None:
684            di_val = data.err_data[index]
685        if data.dqx_data is not None:
686            dx_val = data.dqx_data[index]
687        if data.dqy_data is not None:
688            dy_val = data.dqy_data[index]
689
690        text += "%s \t%s \t%s \t%s \t%s \t%s \t%s\n" % (index,
691                                                        x_val,
692                                                        y_val,
693                                                        i_val,
694                                                        di_val,
695                                                        dx_val,
696                                                        dy_val)
697        # Takes too long time for typical data2d: Break here
698        if index >= 2500:
699            text += ".............\n"
700            break
701
702    return text
703
704def onTXTSave(data, path):
705    """
706    Save file as formatted txt
707    """
708    with open(path,'w') as out:
709        has_errors = True
710        if data.dy is None or not data.dy.any():
711            has_errors = False
712        # Sanity check
713        if has_errors:
714            try:
715                if len(data.y) != len(data.dy):
716                    has_errors = False
717            except:
718                has_errors = False
719        if has_errors:
720            if data.dx is not None and data.dx.any():
721                out.write("<X>   <Y>   <dY>   <dX>\n")
722            else:
723                out.write("<X>   <Y>   <dY>\n")
724        else:
725            out.write("<X>   <Y>\n")
726
727        for i in range(len(data.x)):
728            if has_errors:
729                if data.dx is not None and data.dx.any():
730                    if  data.dx[i] is not None:
731                        out.write("%g  %g  %g  %g\n" % (data.x[i],
732                                                        data.y[i],
733                                                        data.dy[i],
734                                                        data.dx[i]))
735                    else:
736                        out.write("%g  %g  %g\n" % (data.x[i],
737                                                    data.y[i],
738                                                    data.dy[i]))
739                else:
740                    out.write("%g  %g  %g\n" % (data.x[i],
741                                                data.y[i],
742                                                data.dy[i]))
743            else:
744                out.write("%g  %g\n" % (data.x[i],
745                                        data.y[i]))
746
747def saveData1D(data):
748    """
749    Save 1D data points
750    """
751    default_name = os.path.basename(data.filename)
752    default_name, extension = os.path.splitext(default_name)
753    if not extension:
754        extension = ".txt"
755    default_name += "_out" + extension
756
757    wildcard = "Text files (*.txt);;"\
758                "CanSAS 1D files(*.xml)"
759    kwargs = {
760        'caption'   : 'Save As',
761        'directory' : default_name,
762        'filter'    : wildcard,
763        'parent'    : None,
764    }
765    # Query user for filename.
766    filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
767    filename = filename_tuple[0]
768
769    # User cancelled.
770    if not filename:
771        return
772
773    #Instantiate a loader
774    loader = Loader()
775    if os.path.splitext(filename)[1].lower() == ".txt":
776        onTXTSave(data, filename)
777    if os.path.splitext(filename)[1].lower() == ".xml":
778        loader.save(filename, data, ".xml")
779
780def saveData2D(data):
781    """
782    Save data2d dialog
783    """
784    default_name = os.path.basename(data.filename)
785    default_name, _ = os.path.splitext(default_name)
786    ext_format = ".dat"
787    default_name += "_out" + ext_format
788
789    wildcard = "IGOR/DAT 2D file in Q_map (*.dat)"
790    kwargs = {
791        'caption'   : 'Save As',
792        'directory' : default_name,
793        'filter'    : wildcard,
794        'parent'    : None,
795    }
796    # Query user for filename.
797    filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
798    filename = filename_tuple[0]
799
800    # User cancelled.
801    if not filename:
802        return
803
804    #Instantiate a loader
805    loader = Loader()
806
807    if os.path.splitext(filename)[1].lower() == ext_format:
808        loader.save(filename, data, ext_format)
809
810class FormulaValidator(QtGui.QValidator):
811    def __init__(self, parent=None):
812        super(FormulaValidator, self).__init__(parent)
813 
814    def validate(self, input, pos):
815
816        self._setStyleSheet("")
817        return QtGui.QValidator.Acceptable, pos
818
819        #try:
820        #    Formula(str(input))
821        #    self._setStyleSheet("")
822        #    return QtGui.QValidator.Acceptable, pos
823
824        #except Exception as e:
825        #    self._setStyleSheet("background-color:pink;")
826        #    return QtGui.QValidator.Intermediate, pos
827
828    def _setStyleSheet(self, value):
829        try:
830            if self.parent():
831                self.parent().setStyleSheet(value)
832        except:
833            pass
834
835def xyTransform(data, xLabel="", yLabel=""):
836    """
837    Transforms x and y in View and set the scale
838    """
839    # Changing the scale might be incompatible with
840    # currently displayed data (for instance, going
841    # from ln to log when all plotted values have
842    # negative natural logs).
843    # Go linear and only change the scale at the end.
844    xscale = 'linear'
845    yscale = 'linear'
846    # Local data is either 1D or 2D
847    if data.id == 'fit':
848        return
849
850    # make sure we have some function to operate on
851    if xLabel is None:
852        xLabel = 'log10(x)'
853    if yLabel is None:
854        yLabel = 'log10(y)'
855
856    # control axis labels from the panel itself
857    yname, yunits = data.get_yaxis()
858    xname, xunits = data.get_xaxis()
859
860    # Goes through all possible scales
861    # self.x_label is already wrapped with Latex "$", so using the argument
862
863    # X
864    if xLabel == "x":
865        data.transformX(DataTransform.toX, DataTransform.errToX)
866        xLabel = "%s(%s)" % (xname, xunits)
867    if xLabel == "x^(2)":
868        data.transformX(DataTransform.toX2, DataTransform.errToX2)
869        xunits = convertUnit(2, xunits)
870        xLabel = "%s^{2}(%s)" % (xname, xunits)
871    if xLabel == "x^(4)":
872        data.transformX(DataTransform.toX4, DataTransform.errToX4)
873        xunits = convertUnit(4, xunits)
874        xLabel = "%s^{4}(%s)" % (xname, xunits)
875    if xLabel == "ln(x)":
876        data.transformX(DataTransform.toLogX, DataTransform.errToLogX)
877        xLabel = "\ln{(%s)}(%s)" % (xname, xunits)
878    if xLabel == "log10(x)":
879        data.transformX(DataTransform.toX_pos, DataTransform.errToX_pos)
880        xscale = 'log'
881        xLabel = "%s(%s)" % (xname, xunits)
882    if xLabel == "log10(x^(4))":
883        data.transformX(DataTransform.toX4, DataTransform.errToX4)
884        xunits = convertUnit(4, xunits)
885        xLabel = "%s^{4}(%s)" % (xname, xunits)
886        xscale = 'log'
887
888    # Y
889    if yLabel == "ln(y)":
890        data.transformY(DataTransform.toLogX, DataTransform.errToLogX)
891        yLabel = "\ln{(%s)}(%s)" % (yname, yunits)
892    if yLabel == "y":
893        data.transformY(DataTransform.toX, DataTransform.errToX)
894        yLabel = "%s(%s)" % (yname, yunits)
895    if yLabel == "log10(y)":
896        data.transformY(DataTransform.toX_pos, DataTransform.errToX_pos)
897        yscale = 'log'
898        yLabel = "%s(%s)" % (yname, yunits)
899    if yLabel == "y^(2)":
900        data.transformY(DataTransform.toX2, DataTransform.errToX2)
901        yunits = convertUnit(2, yunits)
902        yLabel = "%s^{2}(%s)" % (yname, yunits)
903    if yLabel == "1/y":
904        data.transformY(DataTransform.toOneOverX, DataTransform.errOneOverX)
905        yunits = convertUnit(-1, yunits)
906        yLabel = "1/%s(%s)" % (yname, yunits)
907    if yLabel == "y*x^(2)":
908        data.transformY(DataTransform.toYX2, DataTransform.errToYX2)
909        xunits = convertUnit(2, xunits)
910        yLabel = "%s \ \ %s^{2}(%s%s)" % (yname, xname, yunits, xunits)
911    if yLabel == "y*x^(4)":
912        data.transformY(DataTransform.toYX4, DataTransform.errToYX4)
913        xunits = convertUnit(4, xunits)
914        yLabel = "%s \ \ %s^{4}(%s%s)" % (yname, xname, yunits, xunits)
915    if yLabel == "1/sqrt(y)":
916        data.transformY(DataTransform.toOneOverSqrtX, DataTransform.errOneOverSqrtX)
917        yunits = convertUnit(-0.5, yunits)
918        yLabel = "1/\sqrt{%s}(%s)" % (yname, yunits)
919    if yLabel == "ln(y*x)":
920        data.transformY(DataTransform.toLogXY, DataTransform.errToLogXY)
921        yLabel = "\ln{(%s \ \ %s)}(%s%s)" % (yname, xname, yunits, xunits)
922    if yLabel == "ln(y*x^(2))":
923        data.transformY(DataTransform.toLogYX2, DataTransform.errToLogYX2)
924        xunits = convertUnit(2, xunits)
925        yLabel = "\ln (%s \ \ %s^{2})(%s%s)" % (yname, xname, yunits, xunits)
926    if yLabel == "ln(y*x^(4))":
927        data.transformY(DataTransform.toLogYX4, DataTransform.errToLogYX4)
928        xunits = convertUnit(4, xunits)
929        yLabel = "\ln (%s \ \ %s^{4})(%s%s)" % (yname, xname, yunits, xunits)
930    if yLabel == "log10(y*x^(4))":
931        data.transformY(DataTransform.toYX4, DataTransform.errToYX4)
932        xunits = convertUnit(4, xunits)
933        yscale = 'log'
934        yLabel = "%s \ \ %s^{4}(%s%s)" % (yname, xname, yunits, xunits)
935
936    # Perform the transformation of data in data1d->View
937    data.transformView()
938
939    return (xLabel, yLabel, xscale, yscale)
940
941def formatNumber(value, high=False):
942    """
943    Return a float in a standardized, human-readable formatted string.
944    This is used to output readable (e.g. x.xxxe-y) values to the panel.
945    """
946    try:
947        value = float(value)
948    except:
949        output = "NaN"
950        return output.lstrip().rstrip()
951
952    if high:
953        output = "%-7.5g" % value
954
955    else:
956        output = "%-5.3g" % value
957    return output.lstrip().rstrip()
958
959def replaceHTMLwithUTF8(html):
960    """
961    Replace some important HTML-encoded characters
962    with their UTF-8 equivalents
963    """
964    # Angstrom
965    html_out = html.replace("&#x212B;", "Å")
966    # infinity
967    html_out = html_out.replace("&#x221e;", "∞")
968    # +/-
969    html_out = html_out.replace("&#177;", "±")
970
971    return html_out
972
973def replaceHTMLwithASCII(html):
974    """
975    Replace some important HTML-encoded characters
976    with their ASCII equivalents
977    """
978    # Angstrom
979    html_out = html.replace("&#x212B;", "Ang")
980    # infinity
981    html_out = html_out.replace("&#x221e;", "inf")
982    # +/-
983    html_out = html_out.replace("&#177;", "+/-")
984
985    return html_out
986
987def convertUnitToUTF8(unit):
988    """
989    Convert ASCII unit display into UTF-8 symbol
990    """
991    if unit == "1/A":
992        return "Å<sup>-1</sup>"
993    elif unit == "1/cm":
994        return "cm<sup>-1</sup>"
995    elif unit == "Ang":
996        return "Å"
997    elif unit == "1e-6/Ang^2":
998        return "10<sup>-6</sup>/Å<sup>2</sup>"
999    elif unit == "inf":
1000        return "∞"
1001    elif unit == "-inf":
1002        return "-∞"
1003    else:
1004        return unit
1005
1006def convertUnitToHTML(unit):
1007    """
1008    Convert ASCII unit display into well rendering HTML
1009    """
1010    if unit == "1/A":
1011        return "&#x212B;<sup>-1</sup>"
1012    elif unit == "1/cm":
1013        return "cm<sup>-1</sup>"
1014    elif unit == "Ang":
1015        return "&#x212B;"
1016    elif unit == "1e-6/Ang^2":
1017        return "10<sup>-6</sup>/&#x212B;<sup>2</sup>"
1018    elif unit == "inf":
1019        return "&#x221e;"
1020    elif unit == "-inf":
1021        return "-&#x221e;"
1022    else:
1023        return unit
1024
1025def parseName(name, expression):
1026    """
1027    remove "_" in front of a name
1028    """
1029    if re.match(expression, name) is not None:
1030        word = re.split(expression, name, 1)
1031        for item in word:           
1032            if item.lstrip().rstrip() != '':
1033                return item
1034    else:
1035        return name
1036
1037def toDouble(value_string):
1038    """
1039    toFloat conversion which cares deeply about user's locale
1040    """
1041    # Holy shit this escalated quickly in Qt5.
1042    # No more float() cast on general locales.
1043    value = QtCore.QLocale().toFloat(value_string)
1044    if value[1]:
1045        return value[0]
1046
1047    # Try generic locale
1048    value = QtCore.QLocale(QtCore.QLocale('en_US')).toFloat(value_string)
1049    if value[1]:
1050        return value[0]
1051    else:
1052        raise TypeError
1053
1054def findNextFilename(filename, directory):
1055    """
1056    Finds the next available (non-existing) name for 'filename' in 'directory'.
1057    plugin.py -> plugin (n).py  - for first 'n' for which the file doesn't exist
1058    """
1059    basename, ext = os.path.splitext(filename)
1060    # limit the number of copies
1061    MAX_FILENAMES = 1000
1062    # Start with (1)
1063    number_ext = 1
1064    proposed_filename = ""
1065    found_filename = False
1066    # Find the next available filename or exit if too many copies
1067    while not found_filename or number_ext > MAX_FILENAMES:
1068        proposed_filename = basename + " ("+str(number_ext)+")" + ext
1069        if os.path.exists(os.path.join(directory, proposed_filename)):
1070            number_ext += 1
1071        else:
1072            found_filename = True
1073
1074    return proposed_filename
1075
1076
1077class DoubleValidator(QtGui.QDoubleValidator):
1078    """
1079    Allow only dots as decimal separator
1080    """
1081    def validate(self, input, pos):
1082        """
1083        Return invalid for commas
1084        """
1085        if input is not None and ',' in input:
1086            return (QtGui.QValidator.Invalid, input, pos)
1087        return super(DoubleValidator, self).validate(input, pos)
1088
1089    def fixup(self, input):
1090        """
1091        Correct (remove) potential preexisting content
1092        """
1093        super(DoubleValidator, self).fixup(input)
1094        input = input.replace(",", "")
1095
1096def checkModel(path):
1097    """
1098    Check that the model save in file 'path' can run.
1099    """
1100    # The following return needs to be removed once
1101    # the unittest related changes in Sasmodels are commited
1102    # return True
1103    # try running the model
1104    from sasmodels.sasview_model import load_custom_model
1105    Model = load_custom_model(path)
1106    model = Model()
1107    q =  np.array([0.01, 0.1])
1108    _ = model.evalDistribution(q)
1109    qx, qy =  np.array([0.01, 0.01]), np.array([0.1, 0.1])
1110    _ = model.evalDistribution([qx, qy])
1111
1112    # check the model's unit tests run
1113    from sasmodels.model_test import run_one
1114    # TestSuite module in Qt5 now deletes tests in the suite after running,
1115    # so suite[0] in run_one() in sasmodels/model_test.py will contain [None] and
1116    # test.info.tests will raise.
1117    # Not sure how to change the behaviour here, most likely sasmodels will have to
1118    # be modified
1119    result = run_one(path)
1120
1121    return result
1122
1123
1124def enum(*sequential, **named):
1125    """Create an enumeration object from a list of strings"""
1126    enums = dict(zip(sequential, range(len(sequential))), **named)
1127    return type('Enum', (), enums)
Note: See TracBrowser for help on using the repository browser.