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

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

UTlogo and magnetic orientation images in release. SASVIEW-1109
SASVIEW-1157

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