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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 20f4857 was 20f4857, checked in by Laura Forster <Awork@…>, 6 years ago

Few fixes for Copy Excel/Latex? menu option

Just connecting these up in the same way as main copy function so that only current FitPage? tab can be accessed

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