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

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

Fit result viewer SASVIEW-274, SASVIEW-275

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