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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since fa762f4 was ee22241, checked in by rozyczko <piotr.rozyczko@…>, 3 years ago

Refactored onHelp a bit to allow more encapsulation. SASVIEW-1112

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