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

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

Automatically show sector/annulus/box plots SASVIEW-980

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