source: sasview/src/sas/qtgui/Utilities/GuiUtils.py @ 5d28d6b

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 5d28d6b was 5d28d6b, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Switch to the theory tab when a new theory is being generated. SASVIEW-1045

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