source: sasview/src/sas/qtgui/Utilities/GuiUtils.py @ 685602a

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 685602a was 339e22b, checked in by Laura Forster <Awork@…>, 6 years ago

Mask Edit menu item added to Fitting

Mask edit was previously only available via right clicking data before fit, now available on fitting drop down menu option.

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