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

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

Minor corrections to Inversion after PK's CR

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