source: sasview/src/sas/qtgui/Utilities/GuiUtils.py @ 67346f9

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 67346f9 was 67346f9, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

Save load path and sas_opencl values to the custom config, if changed
during the session.

  • Property mode set to 100644
File size: 37.5 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    except ValueError:
98        print("Value error")
99        pass
100    finally:
101        if fObj is not None:
102            fObj.close()
103    return config_module
104
105
106# Get APP folder
107PATH_APP = get_app_dir()
108DATAPATH = PATH_APP
109
110# Read in the local config, which can either be with the main
111# application or in the installation directory
112config = _find_local_config('local_config', PATH_APP)
113
114if config is None:
115    config = _find_local_config('local_config', os.getcwd())
116else:
117    pass
118
119c_conf_dir = CustomDir.setup_conf_dir(PATH_APP)
120
121custom_config = _find_local_config('custom_config', c_conf_dir)
122if custom_config is None:
123    custom_config = _find_local_config('custom_config', os.getcwd())
124    if custom_config is None:
125        msgConfig = "Custom_config file was not imported"
126
127#read some constants from config
128APPLICATION_STATE_EXTENSION = config.APPLICATION_STATE_EXTENSION
129APPLICATION_NAME = config.__appname__
130SPLASH_SCREEN_PATH = config.SPLASH_SCREEN_PATH
131WELCOME_PANEL_ON = config.WELCOME_PANEL_ON
132SPLASH_SCREEN_WIDTH = config.SPLASH_SCREEN_WIDTH
133SPLASH_SCREEN_HEIGHT = config.SPLASH_SCREEN_HEIGHT
134SS_MAX_DISPLAY_TIME = config.SS_MAX_DISPLAY_TIME
135if not WELCOME_PANEL_ON:
136    WELCOME_PANEL_SHOW = False
137else:
138    WELCOME_PANEL_SHOW = True
139try:
140    DATALOADER_SHOW = custom_config.DATALOADER_SHOW
141    TOOLBAR_SHOW = custom_config.TOOLBAR_SHOW
142    FIXED_PANEL = custom_config.FIXED_PANEL
143    if WELCOME_PANEL_ON:
144        WELCOME_PANEL_SHOW = custom_config.WELCOME_PANEL_SHOW
145    PLOPANEL_WIDTH = custom_config.PLOPANEL_WIDTH
146    DATAPANEL_WIDTH = custom_config.DATAPANEL_WIDTH
147    GUIFRAME_WIDTH = custom_config.GUIFRAME_WIDTH
148    GUIFRAME_HEIGHT = custom_config.GUIFRAME_HEIGHT
149    CONTROL_WIDTH = custom_config.CONTROL_WIDTH
150    CONTROL_HEIGHT = custom_config.CONTROL_HEIGHT
151    DEFAULT_PERSPECTIVE = custom_config.DEFAULT_PERSPECTIVE
152    CLEANUP_PLOT = custom_config.CLEANUP_PLOT
153    SAS_OPENCL = custom_config.SAS_OPENCL
154    # custom open_path
155    open_folder = custom_config.DEFAULT_OPEN_FOLDER
156    if open_folder is not None and os.path.isdir(open_folder):
157        DEFAULT_OPEN_FOLDER = os.path.abspath(open_folder)
158    else:
159        DEFAULT_OPEN_FOLDER = PATH_APP
160except AttributeError:
161    DATALOADER_SHOW = True
162    TOOLBAR_SHOW = True
163    FIXED_PANEL = True
164    WELCOME_PANEL_SHOW = False
165    PLOPANEL_WIDTH = config.PLOPANEL_WIDTH
166    DATAPANEL_WIDTH = config.DATAPANEL_WIDTH
167    GUIFRAME_WIDTH = config.GUIFRAME_WIDTH
168    GUIFRAME_HEIGHT = config.GUIFRAME_HEIGHT
169    CONTROL_WIDTH = -1
170    CONTROL_HEIGHT = -1
171    DEFAULT_PERSPECTIVE = None
172    CLEANUP_PLOT = False
173    DEFAULT_OPEN_FOLDER = PATH_APP
174    SAS_OPENCL = config.SAS_OPENCL
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
288    # Plot fitting results (FittingWidget->GuiManager)
289    resultPlotUpdateSignal = QtCore.pyqtSignal(list)
290
291    # show the plot as a regular in-workspace object
292    forcePlotDisplaySignal = QtCore.pyqtSignal(list)
293
294def updateModelItemWithPlot(item, update_data, name=""):
295    """
296    Adds a checkboxed row named "name" to QStandardItem
297    Adds 'update_data' to that row.
298    """
299    assert isinstance(item, QtGui.QStandardItem)
300
301    # Check if data with the same ID is already present
302    for index in range(item.rowCount()):
303        plot_item = item.child(index)
304        if not plot_item.isCheckable():
305            continue
306        plot_data = plot_item.child(0).data()
307        if plot_data.id is not None and \
308                plot_data.name == update_data.name:
309                #(plot_data.name == update_data.name or plot_data.id == update_data.id):
310            # if plot_data.id is not None and plot_data.id == update_data.id:
311            # replace data section in item
312            plot_item.child(0).setData(update_data)
313            plot_item.setText(name)
314            # Plot title if any
315            if plot_item.child(1).hasChildren():
316                plot_item.child(1).child(0).setText("Title: %s"%name)
317            # Force redisplay
318            return
319
320    # Create the new item
321    checkbox_item = createModelItemWithPlot(update_data, name)
322
323    # Append the new row to the main item
324    item.appendRow(checkbox_item)
325
326def deleteRedundantPlots(item, new_plots):
327    """
328    Checks all plots that are children of the given item; if any have an ID or name not included in new_plots,
329    it is deleted. Useful for e.g. switching from P(Q)S(Q) to P(Q); this would remove the old S(Q) plot.
330
331    Ensure that new_plots contains ALL the relevant plots(!!!)
332    """
333    assert isinstance(item, QtGui.QStandardItem)
334
335    # lists of plots names/ids for all deletable plots on item
336    names = [p.name for p in new_plots if p.name is not None]
337    ids = [p.id for p in new_plots if p.id is not None]
338
339    items_to_delete = []
340
341    for index in range(item.rowCount()):
342        plot_item = item.child(index)
343        if not plot_item.isCheckable():
344            continue
345        plot_data = plot_item.child(0).data()
346        if (plot_data.id is not None) and \
347            (plot_data.id not in ids) and \
348            (plot_data.name not in names) and \
349            (plot_data.plot_role == Data1D.ROLE_DELETABLE):
350            items_to_delete.append(plot_item)
351
352    for plot_item in items_to_delete:
353        item.removeRow(plot_item.row())
354
355class HashableStandardItem(QtGui.QStandardItem):
356    """
357    Subclassed standard item with reimplemented __hash__
358    to allow for use as an index.
359    """
360    def __init__(self, parent=None):
361        super(HashableStandardItem, self).__init__()
362
363    def __hash__(self):
364        ''' just a random hash value '''
365        #return hash(self.__init__)
366        return 0
367
368    def clone(self):
369        ''' Assure __hash__ is cloned as well'''
370        clone = super(HashableStandardItem, self).clone()
371        clone.__hash__ = self.__hash__
372        return clone
373
374def getMonospaceFont():
375    """Convenience function; returns a monospace font to be used in any shells, code editors, etc."""
376
377    # Note: Consolas is only available on Windows; the style hint is used on other operating systems
378    font = QtGui.QFont("Consolas", 10)
379    font.setStyleHint(QtGui.QFont.Monospace, QtGui.QFont.PreferQuality)
380    return font
381
382def createModelItemWithPlot(update_data, name=""):
383    """
384    Creates a checkboxed QStandardItem named "name"
385    Adds 'update_data' to that row.
386    """
387    py_update_data = update_data
388
389    checkbox_item = HashableStandardItem()
390    checkbox_item.setCheckable(True)
391    checkbox_item.setCheckState(QtCore.Qt.Checked)
392    checkbox_item.setText(name)
393
394    # Add "Info" item
395    if isinstance(py_update_data, (Data1D, Data2D)):
396        # If Data1/2D added - extract Info from it
397        info_item = infoFromData(py_update_data)
398    else:
399        # otherwise just add a naked item
400        info_item = QtGui.QStandardItem("Info")
401
402    # Add the actual Data1D/Data2D object
403    object_item = QtGui.QStandardItem()
404    object_item.setData(update_data)
405
406    # Set the data object as the first child
407    checkbox_item.setChild(0, object_item)
408
409    # Set info_item as the second child
410    checkbox_item.setChild(1, info_item)
411
412    # And return the newly created item
413    return checkbox_item
414
415def updateModelItem(item, update_data, name=""):
416    """
417    Adds a simple named child to QStandardItem
418    """
419    assert isinstance(item, QtGui.QStandardItem)
420
421    # Add the actual Data1D/Data2D object
422    object_item = QtGui.QStandardItem()
423    object_item.setText(name)
424    object_item.setData(update_data)
425
426    # Append the new row to the main item
427    item.appendRow(object_item)
428
429def updateModelItemStatus(model_item, filename="", name="", status=2):
430    """
431    Update status of checkbox related to high- and low-Q extrapolation
432    choice in Invariant Panel
433    """
434    assert isinstance(model_item, QtGui.QStandardItemModel)
435
436    # Iterate over model looking for items with checkboxes
437    for index in range(model_item.rowCount()):
438        item = model_item.item(index)
439        if item.text() == filename and item.isCheckable() \
440                and item.checkState() == QtCore.Qt.Checked:
441            # Going 1 level deeper only
442            for index_2 in range(item.rowCount()):
443                item_2 = item.child(index_2)
444                if item_2 and item_2.isCheckable() and item_2.text() == name:
445                    item_2.setCheckState(status)
446
447    return
448
449def itemFromFilename(filename, model_item):
450    """
451    Returns the model item text=filename in the model
452    """
453    assert isinstance(model_item, QtGui.QStandardItemModel)
454    assert isinstance(filename, str)
455
456    # Iterate over model looking for named items
457    item = list([i for i in [model_item.item(index)
458                             for index in range(model_item.rowCount())]
459                 if str(i.text()) == filename])
460    return item[0] if len(item)>0 else None
461
462def plotsFromModel(model_name, model_item):
463    """
464    Returns the list of plots for the item with model name in the model
465    """
466    assert isinstance(model_item, QtGui.QStandardItem)
467    assert isinstance(model_name, str)
468
469    plot_data = []
470    # Iterate over model looking for named items
471    for index in range(model_item.rowCount()):
472        item = model_item.child(index)
473        if isinstance(item.data(), (Data1D, Data2D)):
474            plot_data.append(item.data())
475        if model_name in str(item.text()):
476            #plot_data.append(item.child(0).data())
477            # Going 1 level deeper only
478            for index_2 in range(item.rowCount()):
479                item_2 = item.child(index_2)
480                if item_2 and isinstance(item_2.data(), (Data1D, Data2D)):
481                    plot_data.append(item_2.data())
482
483    return plot_data
484
485def plotsFromFilename(filename, model_item):
486    """
487    Returns the list of plots for the item with text=filename in the model
488    """
489    assert isinstance(model_item, QtGui.QStandardItemModel)
490    assert isinstance(filename, str)
491
492    plot_data = {}
493    # Iterate over model looking for named items
494    for index in range(model_item.rowCount()):
495        item = model_item.item(index)
496        if filename in str(item.text()):
497            # TODO: assure item type is correct (either data1/2D or Plotter)
498            plot_data[item] = item.child(0).data()
499            # Going 1 level deeper only
500            for index_2 in range(item.rowCount()):
501                item_2 = item.child(index_2)
502                if item_2 and item_2.isCheckable():
503                    # TODO: assure item type is correct (either data1/2D or Plotter)
504                    plot_data[item_2] = item_2.child(0).data()
505
506    return plot_data
507
508def getChildrenFromItem(root):
509    """
510    Recursively go down the model item looking for all children
511    """
512    def recurse(parent):
513        for row in range(parent.rowCount()):
514            for column in range(parent.columnCount()):
515                child = parent.child(row, column)
516                yield child
517                if child.hasChildren():
518                    yield from recurse(child)
519    if root is not None:
520        yield from recurse(root)
521
522def plotsFromCheckedItems(model_item):
523    """
524    Returns the list of plots for items in the model which are checked
525    """
526    assert isinstance(model_item, QtGui.QStandardItemModel)
527
528    plot_data = []
529
530    # Iterate over model looking for items with checkboxes
531    for index in range(model_item.rowCount()):
532        item = model_item.item(index)
533        if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
534            data = item.child(0).data()
535            plot_data.append((item, data))
536
537        items = list(getChildrenFromItem(item))
538
539        for it in items:
540            if it.isCheckable() and it.checkState() == QtCore.Qt.Checked:
541                data = it.child(0).data()
542                plot_data.append((it, data))
543
544    return plot_data
545
546def infoFromData(data):
547    """
548    Given Data1D/Data2D object, extract relevant Info elements
549    and add them to a model item
550    """
551    assert isinstance(data, (Data1D, Data2D))
552
553    info_item = QtGui.QStandardItem("Info")
554
555    title_item = QtGui.QStandardItem("Title: " + data.title)
556    info_item.appendRow(title_item)
557    run_item = QtGui.QStandardItem("Run: " + str(data.run))
558    info_item.appendRow(run_item)
559    type_item = QtGui.QStandardItem("Type: " + str(data.__class__.__name__))
560    info_item.appendRow(type_item)
561
562    if data.path:
563        path_item = QtGui.QStandardItem("Path: " + data.path)
564        info_item.appendRow(path_item)
565
566    if data.instrument:
567        instr_item = QtGui.QStandardItem("Instrument: " + data.instrument)
568        info_item.appendRow(instr_item)
569
570    process_item = QtGui.QStandardItem("Process")
571    if isinstance(data.process, list) and data.process:
572        for process in data.process:
573            process_date = process.date
574            process_date_item = QtGui.QStandardItem("Date: " + process_date)
575            process_item.appendRow(process_date_item)
576
577            process_descr = process.description
578            process_descr_item = QtGui.QStandardItem("Description: " + process_descr)
579            process_item.appendRow(process_descr_item)
580
581            process_name = process.name
582            process_name_item = QtGui.QStandardItem("Name: " + process_name)
583            process_item.appendRow(process_name_item)
584
585    info_item.appendRow(process_item)
586
587    return info_item
588
589def dataFromItem(item):
590    """
591    Retrieve Data1D/2D component from QStandardItem.
592    The assumption - data stored in SasView standard, in child 0
593    """
594    try:
595        data = item.child(0).data()
596    except AttributeError:
597        data = None
598    return data
599
600def openLink(url):
601    """
602    Open a URL in an external browser.
603    Check the URL first, though.
604    """
605    parsed_url = urllib.parse.urlparse(url)
606    if parsed_url.scheme:
607        webbrowser.open(url)
608    else:
609        msg = "Attempt at opening an invalid URL"
610        raise AttributeError(msg)
611
612def showHelp(url):
613    """
614    Open a local url in the default browser
615    """
616    location = HELP_DIRECTORY_LOCATION + url
617    #WP: Added to handle OSX bundle docs
618    if os.path.isdir(location) == False:
619        sas_path = os.path.abspath(os.path.dirname(sys.argv[0]))
620        location = sas_path+"/"+location
621    try:
622        webbrowser.open('file://' + os.path.realpath(location))
623    except webbrowser.Error as ex:
624        logging.warning("Cannot display help. %s" % ex)
625
626def retrieveData1d(data):
627    """
628    Retrieve 1D data from file and construct its text
629    representation
630    """
631    if not isinstance(data, Data1D):
632        msg = "Incorrect type passed to retrieveData1d"
633        raise AttributeError(msg)
634    try:
635        xmin = min(data.x)
636        ymin = min(data.y)
637    except:
638        msg = "Unable to find min/max of \n data named %s" % \
639                    data.filename
640        #logging.error(msg)
641        raise ValueError(msg)
642
643    text = data.__str__()
644    text += 'Data Min Max:\n'
645    text += 'X_min = %s:  X_max = %s\n' % (xmin, max(data.x))
646    text += 'Y_min = %s:  Y_max = %s\n' % (ymin, max(data.y))
647    if data.dy is not None:
648        text += 'dY_min = %s:  dY_max = %s\n' % (min(data.dy), max(data.dy))
649    text += '\nData Points:\n'
650    x_st = "X"
651    for index in range(len(data.x)):
652        if data.dy is not None and len(data.dy) > index:
653            dy_val = data.dy[index]
654        else:
655            dy_val = 0.0
656        if data.dx is not None and len(data.dx) > index:
657            dx_val = data.dx[index]
658        else:
659            dx_val = 0.0
660        if data.dxl is not None and len(data.dxl) > index:
661            if index == 0:
662                x_st = "Xl"
663            dx_val = data.dxl[index]
664        elif data.dxw is not None and len(data.dxw) > index:
665            if index == 0:
666                x_st = "Xw"
667            dx_val = data.dxw[index]
668
669        if index == 0:
670            text += "<index> \t<X> \t<Y> \t<dY> \t<d%s>\n" % x_st
671        text += "%s \t%s \t%s \t%s \t%s\n" % (index,
672                                                data.x[index],
673                                                data.y[index],
674                                                dy_val,
675                                                dx_val)
676    return text
677
678def retrieveData2d(data):
679    """
680    Retrieve 2D data from file and construct its text
681    representation
682    """
683    if not isinstance(data, Data2D):
684        msg = "Incorrect type passed to retrieveData2d"
685        raise AttributeError(msg)
686
687    text = data.__str__()
688    text += 'Data Min Max:\n'
689    text += 'I_min = %s\n' % min(data.data)
690    text += 'I_max = %s\n\n' % max(data.data)
691    text += 'Data (First 2501) Points:\n'
692    text += 'Data columns include err(I).\n'
693    text += 'ASCII data starts here.\n'
694    text += "<index> \t<Qx> \t<Qy> \t<I> \t<dI> \t<dQparal> \t<dQperp>\n"
695    di_val = 0.0
696    dx_val = 0.0
697    dy_val = 0.0
698    len_data = len(data.qx_data)
699    for index in range(0, len_data):
700        x_val = data.qx_data[index]
701        y_val = data.qy_data[index]
702        i_val = data.data[index]
703        if data.err_data is not None:
704            di_val = data.err_data[index]
705        if data.dqx_data is not None:
706            dx_val = data.dqx_data[index]
707        if data.dqy_data is not None:
708            dy_val = data.dqy_data[index]
709
710        text += "%s \t%s \t%s \t%s \t%s \t%s \t%s\n" % (index,
711                                                        x_val,
712                                                        y_val,
713                                                        i_val,
714                                                        di_val,
715                                                        dx_val,
716                                                        dy_val)
717        # Takes too long time for typical data2d: Break here
718        if index >= 2500:
719            text += ".............\n"
720            break
721
722    return text
723
724def onTXTSave(data, path):
725    """
726    Save file as formatted txt
727    """
728    with open(path,'w') as out:
729        has_errors = True
730        if data.dy is None or not data.dy.any():
731            has_errors = False
732        # Sanity check
733        if has_errors:
734            try:
735                if len(data.y) != len(data.dy):
736                    has_errors = False
737            except:
738                has_errors = False
739        if has_errors:
740            if data.dx is not None and data.dx.any():
741                out.write("<X>   <Y>   <dY>   <dX>\n")
742            else:
743                out.write("<X>   <Y>   <dY>\n")
744        else:
745            out.write("<X>   <Y>\n")
746
747        for i in range(len(data.x)):
748            if has_errors:
749                if data.dx is not None and data.dx.any():
750                    if  data.dx[i] is not None:
751                        out.write("%g  %g  %g  %g\n" % (data.x[i],
752                                                        data.y[i],
753                                                        data.dy[i],
754                                                        data.dx[i]))
755                    else:
756                        out.write("%g  %g  %g\n" % (data.x[i],
757                                                    data.y[i],
758                                                    data.dy[i]))
759                else:
760                    out.write("%g  %g  %g\n" % (data.x[i],
761                                                data.y[i],
762                                                data.dy[i]))
763            else:
764                out.write("%g  %g\n" % (data.x[i],
765                                        data.y[i]))
766
767def saveData1D(data):
768    """
769    Save 1D data points
770    """
771    default_name = os.path.basename(data.filename)
772    default_name, extension = os.path.splitext(default_name)
773    if not extension:
774        extension = ".txt"
775    default_name += "_out" + extension
776
777    wildcard = "Text files (*.txt);;"\
778                "CanSAS 1D files(*.xml)"
779    kwargs = {
780        'caption'   : 'Save As',
781        'directory' : default_name,
782        'filter'    : wildcard,
783        'parent'    : None,
784    }
785    # Query user for filename.
786    filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
787    filename = filename_tuple[0]
788
789    # User cancelled.
790    if not filename:
791        return
792
793    #Instantiate a loader
794    loader = Loader()
795    if os.path.splitext(filename)[1].lower() == ".txt":
796        onTXTSave(data, filename)
797    if os.path.splitext(filename)[1].lower() == ".xml":
798        loader.save(filename, data, ".xml")
799
800def saveData2D(data):
801    """
802    Save data2d dialog
803    """
804    default_name = os.path.basename(data.filename)
805    default_name, _ = os.path.splitext(default_name)
806    ext_format = ".dat"
807    default_name += "_out" + ext_format
808
809    wildcard = "IGOR/DAT 2D file in Q_map (*.dat)"
810    kwargs = {
811        'caption'   : 'Save As',
812        'directory' : default_name,
813        'filter'    : wildcard,
814        'parent'    : None,
815    }
816    # Query user for filename.
817    filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
818    filename = filename_tuple[0]
819
820    # User cancelled.
821    if not filename:
822        return
823
824    #Instantiate a loader
825    loader = Loader()
826
827    if os.path.splitext(filename)[1].lower() == ext_format:
828        loader.save(filename, data, ext_format)
829
830class FormulaValidator(QtGui.QValidator):
831    def __init__(self, parent=None):
832        super(FormulaValidator, self).__init__(parent)
833 
834    def validate(self, input, pos):
835
836        self._setStyleSheet("")
837        return QtGui.QValidator.Acceptable, pos
838
839        #try:
840        #    Formula(str(input))
841        #    self._setStyleSheet("")
842        #    return QtGui.QValidator.Acceptable, pos
843
844        #except Exception as e:
845        #    self._setStyleSheet("background-color:pink;")
846        #    return QtGui.QValidator.Intermediate, pos
847
848    def _setStyleSheet(self, value):
849        try:
850            if self.parent():
851                self.parent().setStyleSheet(value)
852        except:
853            pass
854
855def xyTransform(data, xLabel="", yLabel=""):
856    """
857    Transforms x and y in View and set the scale
858    """
859    # Changing the scale might be incompatible with
860    # currently displayed data (for instance, going
861    # from ln to log when all plotted values have
862    # negative natural logs).
863    # Go linear and only change the scale at the end.
864    xscale = 'linear'
865    yscale = 'linear'
866    # Local data is either 1D or 2D
867    if data.id == 'fit':
868        return
869
870    # make sure we have some function to operate on
871    if xLabel is None:
872        xLabel = 'log10(x)'
873    if yLabel is None:
874        yLabel = 'log10(y)'
875
876    # control axis labels from the panel itself
877    yname, yunits = data.get_yaxis()
878    xname, xunits = data.get_xaxis()
879
880    # Goes through all possible scales
881    # self.x_label is already wrapped with Latex "$", so using the argument
882
883    # X
884    if xLabel == "x":
885        data.transformX(DataTransform.toX, DataTransform.errToX)
886        xLabel = "%s(%s)" % (xname, xunits)
887    if xLabel == "x^(2)":
888        data.transformX(DataTransform.toX2, DataTransform.errToX2)
889        xunits = convertUnit(2, xunits)
890        xLabel = "%s^{2}(%s)" % (xname, xunits)
891    if xLabel == "x^(4)":
892        data.transformX(DataTransform.toX4, DataTransform.errToX4)
893        xunits = convertUnit(4, xunits)
894        xLabel = "%s^{4}(%s)" % (xname, xunits)
895    if xLabel == "ln(x)":
896        data.transformX(DataTransform.toLogX, DataTransform.errToLogX)
897        xLabel = "\ln{(%s)}(%s)" % (xname, xunits)
898    if xLabel == "log10(x)":
899        data.transformX(DataTransform.toX_pos, DataTransform.errToX_pos)
900        xscale = 'log'
901        xLabel = "%s(%s)" % (xname, xunits)
902    if xLabel == "log10(x^(4))":
903        data.transformX(DataTransform.toX4, DataTransform.errToX4)
904        xunits = convertUnit(4, xunits)
905        xLabel = "%s^{4}(%s)" % (xname, xunits)
906        xscale = 'log'
907
908    # Y
909    if yLabel == "ln(y)":
910        data.transformY(DataTransform.toLogX, DataTransform.errToLogX)
911        yLabel = "\ln{(%s)}(%s)" % (yname, yunits)
912    if yLabel == "y":
913        data.transformY(DataTransform.toX, DataTransform.errToX)
914        yLabel = "%s(%s)" % (yname, yunits)
915    if yLabel == "log10(y)":
916        data.transformY(DataTransform.toX_pos, DataTransform.errToX_pos)
917        yscale = 'log'
918        yLabel = "%s(%s)" % (yname, yunits)
919    if yLabel == "y^(2)":
920        data.transformY(DataTransform.toX2, DataTransform.errToX2)
921        yunits = convertUnit(2, yunits)
922        yLabel = "%s^{2}(%s)" % (yname, yunits)
923    if yLabel == "1/y":
924        data.transformY(DataTransform.toOneOverX, DataTransform.errOneOverX)
925        yunits = convertUnit(-1, yunits)
926        yLabel = "1/%s(%s)" % (yname, yunits)
927    if yLabel == "y*x^(2)":
928        data.transformY(DataTransform.toYX2, DataTransform.errToYX2)
929        xunits = convertUnit(2, xunits)
930        yLabel = "%s \ \ %s^{2}(%s%s)" % (yname, xname, yunits, xunits)
931    if yLabel == "y*x^(4)":
932        data.transformY(DataTransform.toYX4, DataTransform.errToYX4)
933        xunits = convertUnit(4, xunits)
934        yLabel = "%s \ \ %s^{4}(%s%s)" % (yname, xname, yunits, xunits)
935    if yLabel == "1/sqrt(y)":
936        data.transformY(DataTransform.toOneOverSqrtX, DataTransform.errOneOverSqrtX)
937        yunits = convertUnit(-0.5, yunits)
938        yLabel = "1/\sqrt{%s}(%s)" % (yname, yunits)
939    if yLabel == "ln(y*x)":
940        data.transformY(DataTransform.toLogXY, DataTransform.errToLogXY)
941        yLabel = "\ln{(%s \ \ %s)}(%s%s)" % (yname, xname, yunits, xunits)
942    if yLabel == "ln(y*x^(2))":
943        data.transformY(DataTransform.toLogYX2, DataTransform.errToLogYX2)
944        xunits = convertUnit(2, xunits)
945        yLabel = "\ln (%s \ \ %s^{2})(%s%s)" % (yname, xname, yunits, xunits)
946    if yLabel == "ln(y*x^(4))":
947        data.transformY(DataTransform.toLogYX4, DataTransform.errToLogYX4)
948        xunits = convertUnit(4, xunits)
949        yLabel = "\ln (%s \ \ %s^{4})(%s%s)" % (yname, xname, yunits, xunits)
950    if yLabel == "log10(y*x^(4))":
951        data.transformY(DataTransform.toYX4, DataTransform.errToYX4)
952        xunits = convertUnit(4, xunits)
953        yscale = 'log'
954        yLabel = "%s \ \ %s^{4}(%s%s)" % (yname, xname, yunits, xunits)
955
956    # Perform the transformation of data in data1d->View
957    data.transformView()
958
959    return (xLabel, yLabel, xscale, yscale)
960
961def formatNumber(value, high=False):
962    """
963    Return a float in a standardized, human-readable formatted string.
964    This is used to output readable (e.g. x.xxxe-y) values to the panel.
965    """
966    try:
967        value = float(value)
968    except:
969        output = "NaN"
970        return output.lstrip().rstrip()
971
972    if high:
973        output = "%-7.5g" % value
974
975    else:
976        output = "%-5.3g" % value
977    return output.lstrip().rstrip()
978
979def replaceHTMLwithUTF8(html):
980    """
981    Replace some important HTML-encoded characters
982    with their UTF-8 equivalents
983    """
984    # Angstrom
985    html_out = html.replace("&#x212B;", "Å")
986    # infinity
987    html_out = html_out.replace("&#x221e;", "∞")
988    # +/-
989    html_out = html_out.replace("&#177;", "±")
990
991    return html_out
992
993def replaceHTMLwithASCII(html):
994    """
995    Replace some important HTML-encoded characters
996    with their ASCII equivalents
997    """
998    # Angstrom
999    html_out = html.replace("&#x212B;", "Ang")
1000    # infinity
1001    html_out = html_out.replace("&#x221e;", "inf")
1002    # +/-
1003    html_out = html_out.replace("&#177;", "+/-")
1004
1005    return html_out
1006
1007def convertUnitToUTF8(unit):
1008    """
1009    Convert ASCII unit display into UTF-8 symbol
1010    """
1011    if unit == "1/A":
1012        return "Å<sup>-1</sup>"
1013    elif unit == "1/cm":
1014        return "cm<sup>-1</sup>"
1015    elif unit == "Ang":
1016        return "Å"
1017    elif unit == "1e-6/Ang^2":
1018        return "10<sup>-6</sup>/Å<sup>2</sup>"
1019    elif unit == "inf":
1020        return "∞"
1021    elif unit == "-inf":
1022        return "-∞"
1023    else:
1024        return unit
1025
1026def convertUnitToHTML(unit):
1027    """
1028    Convert ASCII unit display into well rendering HTML
1029    """
1030    if unit == "1/A":
1031        return "&#x212B;<sup>-1</sup>"
1032    elif unit == "1/cm":
1033        return "cm<sup>-1</sup>"
1034    elif unit == "Ang":
1035        return "&#x212B;"
1036    elif unit == "1e-6/Ang^2":
1037        return "10<sup>-6</sup>/&#x212B;<sup>2</sup>"
1038    elif unit == "inf":
1039        return "&#x221e;"
1040    elif unit == "-inf":
1041        return "-&#x221e;"
1042    else:
1043        return unit
1044
1045def parseName(name, expression):
1046    """
1047    remove "_" in front of a name
1048    """
1049    if re.match(expression, name) is not None:
1050        word = re.split(expression, name, 1)
1051        for item in word:           
1052            if item.lstrip().rstrip() != '':
1053                return item
1054    else:
1055        return name
1056
1057def toDouble(value_string):
1058    """
1059    toFloat conversion which cares deeply about user's locale
1060    """
1061    # Holy shit this escalated quickly in Qt5.
1062    # No more float() cast on general locales.
1063    value = QtCore.QLocale().toFloat(value_string)
1064    if value[1]:
1065        return value[0]
1066
1067    # Try generic locale
1068    value = QtCore.QLocale(QtCore.QLocale('en_US')).toFloat(value_string)
1069    if value[1]:
1070        return value[0]
1071    else:
1072        raise TypeError
1073
1074def findNextFilename(filename, directory):
1075    """
1076    Finds the next available (non-existing) name for 'filename' in 'directory'.
1077    plugin.py -> plugin (n).py  - for first 'n' for which the file doesn't exist
1078    """
1079    basename, ext = os.path.splitext(filename)
1080    # limit the number of copies
1081    MAX_FILENAMES = 1000
1082    # Start with (1)
1083    number_ext = 1
1084    proposed_filename = ""
1085    found_filename = False
1086    # Find the next available filename or exit if too many copies
1087    while not found_filename or number_ext > MAX_FILENAMES:
1088        proposed_filename = basename + " ("+str(number_ext)+")" + ext
1089        if os.path.exists(os.path.join(directory, proposed_filename)):
1090            number_ext += 1
1091        else:
1092            found_filename = True
1093
1094    return proposed_filename
1095
1096
1097class DoubleValidator(QtGui.QDoubleValidator):
1098    """
1099    Allow only dots as decimal separator
1100    """
1101    def validate(self, input, pos):
1102        """
1103        Return invalid for commas
1104        """
1105        if input is not None and ',' in input:
1106            return (QtGui.QValidator.Invalid, input, pos)
1107        return super(DoubleValidator, self).validate(input, pos)
1108
1109    def fixup(self, input):
1110        """
1111        Correct (remove) potential preexisting content
1112        """
1113        super(DoubleValidator, self).fixup(input)
1114        input = input.replace(",", "")
1115
1116def checkModel(path):
1117    """
1118    Check that the model save in file 'path' can run.
1119    """
1120    # The following return needs to be removed once
1121    # the unittest related changes in Sasmodels are commited
1122    # return True
1123    # try running the model
1124    from sasmodels.sasview_model import load_custom_model
1125    Model = load_custom_model(path)
1126    model = Model()
1127    q =  np.array([0.01, 0.1])
1128    _ = model.evalDistribution(q)
1129    qx, qy =  np.array([0.01, 0.01]), np.array([0.1, 0.1])
1130    _ = model.evalDistribution([qx, qy])
1131
1132    # check the model's unit tests run
1133    from sasmodels.model_test import run_one
1134    # TestSuite module in Qt5 now deletes tests in the suite after running,
1135    # so suite[0] in run_one() in sasmodels/model_test.py will contain [None] and
1136    # test.info.tests will raise.
1137    # Not sure how to change the behaviour here, most likely sasmodels will have to
1138    # be modified
1139    result = run_one(path)
1140
1141    return result
1142
1143
1144def enum(*sequential, **named):
1145    """Create an enumeration object from a list of strings"""
1146    enums = dict(zip(sequential, range(len(sequential))), **named)
1147    return type('Enum', (), enums)
Note: See TracBrowser for help on using the repository browser.