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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 9ce69ec was 9ce69ec, checked in by Piotr Rozyczko <rozyczko@…>, 8 months ago

Replaced 'smart' plot generation with explicit plot requests on "Show Plot". SASVIEW-1018

  • Property mode set to 100644
File size: 36.4 KB
Line 
1# -*- coding: utf-8 -*-
2"""
3Global defaults and various utility functions usable by the general GUI
4"""
5
6import os
7import re
8import sys
9import imp
10import warnings
11import webbrowser
12import urllib.parse
13
14import numpy as np
15
16warnings.simplefilter("ignore")
17import logging
18
19from PyQt5 import QtCore
20from PyQt5 import QtGui
21from PyQt5 import QtWidgets
22
23from periodictable import formula as Formula
24from sas.qtgui.Plotting import DataTransform
25from sas.qtgui.Plotting.ConvertUnits import convertUnit
26from sas.qtgui.Plotting.PlotterData import Data1D
27from sas.qtgui.Plotting.PlotterData import Data2D
28from sas.sascalc.dataloader.loader import Loader
29from sas.qtgui.Utilities import CustomDir
30
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 retrieveData1d(data):
588    """
589    Retrieve 1D data from file and construct its text
590    representation
591    """
592    if not isinstance(data, Data1D):
593        msg = "Incorrect type passed to retrieveData1d"
594        raise AttributeError(msg)
595    try:
596        xmin = min(data.x)
597        ymin = min(data.y)
598    except:
599        msg = "Unable to find min/max of \n data named %s" % \
600                    data.filename
601        #logging.error(msg)
602        raise ValueError(msg)
603
604    text = data.__str__()
605    text += 'Data Min Max:\n'
606    text += 'X_min = %s:  X_max = %s\n' % (xmin, max(data.x))
607    text += 'Y_min = %s:  Y_max = %s\n' % (ymin, max(data.y))
608    if data.dy is not None:
609        text += 'dY_min = %s:  dY_max = %s\n' % (min(data.dy), max(data.dy))
610    text += '\nData Points:\n'
611    x_st = "X"
612    for index in range(len(data.x)):
613        if data.dy is not None and len(data.dy) > index:
614            dy_val = data.dy[index]
615        else:
616            dy_val = 0.0
617        if data.dx is not None and len(data.dx) > index:
618            dx_val = data.dx[index]
619        else:
620            dx_val = 0.0
621        if data.dxl is not None and len(data.dxl) > index:
622            if index == 0:
623                x_st = "Xl"
624            dx_val = data.dxl[index]
625        elif data.dxw is not None and len(data.dxw) > index:
626            if index == 0:
627                x_st = "Xw"
628            dx_val = data.dxw[index]
629
630        if index == 0:
631            text += "<index> \t<X> \t<Y> \t<dY> \t<d%s>\n" % x_st
632        text += "%s \t%s \t%s \t%s \t%s\n" % (index,
633                                                data.x[index],
634                                                data.y[index],
635                                                dy_val,
636                                                dx_val)
637    return text
638
639def retrieveData2d(data):
640    """
641    Retrieve 2D data from file and construct its text
642    representation
643    """
644    if not isinstance(data, Data2D):
645        msg = "Incorrect type passed to retrieveData2d"
646        raise AttributeError(msg)
647
648    text = data.__str__()
649    text += 'Data Min Max:\n'
650    text += 'I_min = %s\n' % min(data.data)
651    text += 'I_max = %s\n\n' % max(data.data)
652    text += 'Data (First 2501) Points:\n'
653    text += 'Data columns include err(I).\n'
654    text += 'ASCII data starts here.\n'
655    text += "<index> \t<Qx> \t<Qy> \t<I> \t<dI> \t<dQparal> \t<dQperp>\n"
656    di_val = 0.0
657    dx_val = 0.0
658    dy_val = 0.0
659    len_data = len(data.qx_data)
660    for index in range(0, len_data):
661        x_val = data.qx_data[index]
662        y_val = data.qy_data[index]
663        i_val = data.data[index]
664        if data.err_data is not None:
665            di_val = data.err_data[index]
666        if data.dqx_data is not None:
667            dx_val = data.dqx_data[index]
668        if data.dqy_data is not None:
669            dy_val = data.dqy_data[index]
670
671        text += "%s \t%s \t%s \t%s \t%s \t%s \t%s\n" % (index,
672                                                        x_val,
673                                                        y_val,
674                                                        i_val,
675                                                        di_val,
676                                                        dx_val,
677                                                        dy_val)
678        # Takes too long time for typical data2d: Break here
679        if index >= 2500:
680            text += ".............\n"
681            break
682
683    return text
684
685def onTXTSave(data, path):
686    """
687    Save file as formatted txt
688    """
689    with open(path,'w') as out:
690        has_errors = True
691        if data.dy is None or not data.dy.any():
692            has_errors = False
693        # Sanity check
694        if has_errors:
695            try:
696                if len(data.y) != len(data.dy):
697                    has_errors = False
698            except:
699                has_errors = False
700        if has_errors:
701            if data.dx is not None and data.dx.any():
702                out.write("<X>   <Y>   <dY>   <dX>\n")
703            else:
704                out.write("<X>   <Y>   <dY>\n")
705        else:
706            out.write("<X>   <Y>\n")
707
708        for i in range(len(data.x)):
709            if has_errors:
710                if data.dx is not None and data.dx.any():
711                    if  data.dx[i] is not None:
712                        out.write("%g  %g  %g  %g\n" % (data.x[i],
713                                                        data.y[i],
714                                                        data.dy[i],
715                                                        data.dx[i]))
716                    else:
717                        out.write("%g  %g  %g\n" % (data.x[i],
718                                                    data.y[i],
719                                                    data.dy[i]))
720                else:
721                    out.write("%g  %g  %g\n" % (data.x[i],
722                                                data.y[i],
723                                                data.dy[i]))
724            else:
725                out.write("%g  %g\n" % (data.x[i],
726                                        data.y[i]))
727
728def saveData1D(data):
729    """
730    Save 1D data points
731    """
732    default_name = os.path.basename(data.filename)
733    default_name, extension = os.path.splitext(default_name)
734    if not extension:
735        extension = ".txt"
736    default_name += "_out" + extension
737
738    wildcard = "Text files (*.txt);;"\
739                "CanSAS 1D files(*.xml)"
740    kwargs = {
741        'caption'   : 'Save As',
742        'directory' : default_name,
743        'filter'    : wildcard,
744        'parent'    : None,
745    }
746    # Query user for filename.
747    filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
748    filename = filename_tuple[0]
749
750    # User cancelled.
751    if not filename:
752        return
753
754    #Instantiate a loader
755    loader = Loader()
756    if os.path.splitext(filename)[1].lower() == ".txt":
757        onTXTSave(data, filename)
758    if os.path.splitext(filename)[1].lower() == ".xml":
759        loader.save(filename, data, ".xml")
760
761def saveData2D(data):
762    """
763    Save data2d dialog
764    """
765    default_name = os.path.basename(data.filename)
766    default_name, _ = os.path.splitext(default_name)
767    ext_format = ".dat"
768    default_name += "_out" + ext_format
769
770    wildcard = "IGOR/DAT 2D file in Q_map (*.dat)"
771    kwargs = {
772        'caption'   : 'Save As',
773        'directory' : default_name,
774        'filter'    : wildcard,
775        'parent'    : None,
776    }
777    # Query user for filename.
778    filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
779    filename = filename_tuple[0]
780
781    # User cancelled.
782    if not filename:
783        return
784
785    #Instantiate a loader
786    loader = Loader()
787
788    if os.path.splitext(filename)[1].lower() == ext_format:
789        loader.save(filename, data, ext_format)
790
791class FormulaValidator(QtGui.QValidator):
792    def __init__(self, parent=None):
793        super(FormulaValidator, self).__init__(parent)
794 
795    def validate(self, input, pos):
796
797        self._setStyleSheet("")
798        return QtGui.QValidator.Acceptable, pos
799
800        #try:
801        #    Formula(str(input))
802        #    self._setStyleSheet("")
803        #    return QtGui.QValidator.Acceptable, pos
804
805        #except Exception as e:
806        #    self._setStyleSheet("background-color:pink;")
807        #    return QtGui.QValidator.Intermediate, pos
808
809    def _setStyleSheet(self, value):
810        try:
811            if self.parent():
812                self.parent().setStyleSheet(value)
813        except:
814            pass
815
816def xyTransform(data, xLabel="", yLabel=""):
817    """
818    Transforms x and y in View and set the scale
819    """
820    # Changing the scale might be incompatible with
821    # currently displayed data (for instance, going
822    # from ln to log when all plotted values have
823    # negative natural logs).
824    # Go linear and only change the scale at the end.
825    xscale = 'linear'
826    yscale = 'linear'
827    # Local data is either 1D or 2D
828    if data.id == 'fit':
829        return
830
831    # make sure we have some function to operate on
832    if xLabel is None:
833        xLabel = 'log10(x)'
834    if yLabel is None:
835        yLabel = 'log10(y)'
836
837    # control axis labels from the panel itself
838    yname, yunits = data.get_yaxis()
839    xname, xunits = data.get_xaxis()
840
841    # Goes through all possible scales
842    # self.x_label is already wrapped with Latex "$", so using the argument
843
844    # X
845    if xLabel == "x":
846        data.transformX(DataTransform.toX, DataTransform.errToX)
847        xLabel = "%s(%s)" % (xname, xunits)
848    if xLabel == "x^(2)":
849        data.transformX(DataTransform.toX2, DataTransform.errToX2)
850        xunits = convertUnit(2, xunits)
851        xLabel = "%s^{2}(%s)" % (xname, xunits)
852    if xLabel == "x^(4)":
853        data.transformX(DataTransform.toX4, DataTransform.errToX4)
854        xunits = convertUnit(4, xunits)
855        xLabel = "%s^{4}(%s)" % (xname, xunits)
856    if xLabel == "ln(x)":
857        data.transformX(DataTransform.toLogX, DataTransform.errToLogX)
858        xLabel = "\ln{(%s)}(%s)" % (xname, xunits)
859    if xLabel == "log10(x)":
860        data.transformX(DataTransform.toX_pos, DataTransform.errToX_pos)
861        xscale = 'log'
862        xLabel = "%s(%s)" % (xname, xunits)
863    if xLabel == "log10(x^(4))":
864        data.transformX(DataTransform.toX4, DataTransform.errToX4)
865        xunits = convertUnit(4, xunits)
866        xLabel = "%s^{4}(%s)" % (xname, xunits)
867        xscale = 'log'
868
869    # Y
870    if yLabel == "ln(y)":
871        data.transformY(DataTransform.toLogX, DataTransform.errToLogX)
872        yLabel = "\ln{(%s)}(%s)" % (yname, yunits)
873    if yLabel == "y":
874        data.transformY(DataTransform.toX, DataTransform.errToX)
875        yLabel = "%s(%s)" % (yname, yunits)
876    if yLabel == "log10(y)":
877        data.transformY(DataTransform.toX_pos, DataTransform.errToX_pos)
878        yscale = 'log'
879        yLabel = "%s(%s)" % (yname, yunits)
880    if yLabel == "y^(2)":
881        data.transformY(DataTransform.toX2, DataTransform.errToX2)
882        yunits = convertUnit(2, yunits)
883        yLabel = "%s^{2}(%s)" % (yname, yunits)
884    if yLabel == "1/y":
885        data.transformY(DataTransform.toOneOverX, DataTransform.errOneOverX)
886        yunits = convertUnit(-1, yunits)
887        yLabel = "1/%s(%s)" % (yname, yunits)
888    if yLabel == "y*x^(2)":
889        data.transformY(DataTransform.toYX2, DataTransform.errToYX2)
890        xunits = convertUnit(2, xunits)
891        yLabel = "%s \ \ %s^{2}(%s%s)" % (yname, xname, yunits, xunits)
892    if yLabel == "y*x^(4)":
893        data.transformY(DataTransform.toYX4, DataTransform.errToYX4)
894        xunits = convertUnit(4, xunits)
895        yLabel = "%s \ \ %s^{4}(%s%s)" % (yname, xname, yunits, xunits)
896    if yLabel == "1/sqrt(y)":
897        data.transformY(DataTransform.toOneOverSqrtX, DataTransform.errOneOverSqrtX)
898        yunits = convertUnit(-0.5, yunits)
899        yLabel = "1/\sqrt{%s}(%s)" % (yname, yunits)
900    if yLabel == "ln(y*x)":
901        data.transformY(DataTransform.toLogXY, DataTransform.errToLogXY)
902        yLabel = "\ln{(%s \ \ %s)}(%s%s)" % (yname, xname, yunits, xunits)
903    if yLabel == "ln(y*x^(2))":
904        data.transformY(DataTransform.toLogYX2, DataTransform.errToLogYX2)
905        xunits = convertUnit(2, xunits)
906        yLabel = "\ln (%s \ \ %s^{2})(%s%s)" % (yname, xname, yunits, xunits)
907    if yLabel == "ln(y*x^(4))":
908        data.transformY(DataTransform.toLogYX4, DataTransform.errToLogYX4)
909        xunits = convertUnit(4, xunits)
910        yLabel = "\ln (%s \ \ %s^{4})(%s%s)" % (yname, xname, yunits, xunits)
911    if yLabel == "log10(y*x^(4))":
912        data.transformY(DataTransform.toYX4, DataTransform.errToYX4)
913        xunits = convertUnit(4, xunits)
914        yscale = 'log'
915        yLabel = "%s \ \ %s^{4}(%s%s)" % (yname, xname, yunits, xunits)
916
917    # Perform the transformation of data in data1d->View
918    data.transformView()
919
920    return (xLabel, yLabel, xscale, yscale)
921
922def formatNumber(value, high=False):
923    """
924    Return a float in a standardized, human-readable formatted string.
925    This is used to output readable (e.g. x.xxxe-y) values to the panel.
926    """
927    try:
928        value = float(value)
929    except:
930        output = "NaN"
931        return output.lstrip().rstrip()
932
933    if high:
934        output = "%-7.5g" % value
935
936    else:
937        output = "%-5.3g" % value
938    return output.lstrip().rstrip()
939
940def replaceHTMLwithUTF8(html):
941    """
942    Replace some important HTML-encoded characters
943    with their UTF-8 equivalents
944    """
945    # Angstrom
946    html_out = html.replace("&#x212B;", "Å")
947    # infinity
948    html_out = html_out.replace("&#x221e;", "∞")
949    # +/-
950    html_out = html_out.replace("&#177;", "±")
951
952    return html_out
953
954def replaceHTMLwithASCII(html):
955    """
956    Replace some important HTML-encoded characters
957    with their ASCII equivalents
958    """
959    # Angstrom
960    html_out = html.replace("&#x212B;", "Ang")
961    # infinity
962    html_out = html_out.replace("&#x221e;", "inf")
963    # +/-
964    html_out = html_out.replace("&#177;", "+/-")
965
966    return html_out
967
968def convertUnitToUTF8(unit):
969    """
970    Convert ASCII unit display into UTF-8 symbol
971    """
972    if unit == "1/A":
973        return "Å<sup>-1</sup>"
974    elif unit == "1/cm":
975        return "cm<sup>-1</sup>"
976    elif unit == "Ang":
977        return "Å"
978    elif unit == "1e-6/Ang^2":
979        return "10<sup>-6</sup>/Å<sup>2</sup>"
980    elif unit == "inf":
981        return "∞"
982    elif unit == "-inf":
983        return "-∞"
984    else:
985        return unit
986
987def convertUnitToHTML(unit):
988    """
989    Convert ASCII unit display into well rendering HTML
990    """
991    if unit == "1/A":
992        return "&#x212B;<sup>-1</sup>"
993    elif unit == "1/cm":
994        return "cm<sup>-1</sup>"
995    elif unit == "Ang":
996        return "&#x212B;"
997    elif unit == "1e-6/Ang^2":
998        return "10<sup>-6</sup>/&#x212B;<sup>2</sup>"
999    elif unit == "inf":
1000        return "&#x221e;"
1001    elif unit == "-inf":
1002        return "-&#x221e;"
1003    else:
1004        return unit
1005
1006def parseName(name, expression):
1007    """
1008    remove "_" in front of a name
1009    """
1010    if re.match(expression, name) is not None:
1011        word = re.split(expression, name, 1)
1012        for item in word:           
1013            if item.lstrip().rstrip() != '':
1014                return item
1015    else:
1016        return name
1017
1018def toDouble(value_string):
1019    """
1020    toFloat conversion which cares deeply about user's locale
1021    """
1022    # Holy shit this escalated quickly in Qt5.
1023    # No more float() cast on general locales.
1024    value = QtCore.QLocale().toFloat(value_string)
1025    if value[1]:
1026        return value[0]
1027
1028    # Try generic locale
1029    value = QtCore.QLocale(QtCore.QLocale('en_US')).toFloat(value_string)
1030    if value[1]:
1031        return value[0]
1032    else:
1033        raise TypeError
1034
1035def findNextFilename(filename, directory):
1036    """
1037    Finds the next available (non-existing) name for 'filename' in 'directory'.
1038    plugin.py -> plugin (n).py  - for first 'n' for which the file doesn't exist
1039    """
1040    basename, ext = os.path.splitext(filename)
1041    # limit the number of copies
1042    MAX_FILENAMES = 1000
1043    # Start with (1)
1044    number_ext = 1
1045    proposed_filename = ""
1046    found_filename = False
1047    # Find the next available filename or exit if too many copies
1048    while not found_filename or number_ext > MAX_FILENAMES:
1049        proposed_filename = basename + " ("+str(number_ext)+")" + ext
1050        if os.path.exists(os.path.join(directory, proposed_filename)):
1051            number_ext += 1
1052        else:
1053            found_filename = True
1054
1055    return proposed_filename
1056
1057
1058class DoubleValidator(QtGui.QDoubleValidator):
1059    """
1060    Allow only dots as decimal separator
1061    """
1062    def validate(self, input, pos):
1063        """
1064        Return invalid for commas
1065        """
1066        if input is not None and ',' in input:
1067            return (QtGui.QValidator.Invalid, input, pos)
1068        return super(DoubleValidator, self).validate(input, pos)
1069
1070    def fixup(self, input):
1071        """
1072        Correct (remove) potential preexisting content
1073        """
1074        super(DoubleValidator, self).fixup(input)
1075        input = input.replace(",", "")
1076
1077def checkModel(path):
1078    """
1079    Check that the model save in file 'path' can run.
1080    """
1081    # The following return needs to be removed once
1082    # the unittest related changes in Sasmodels are commited
1083    # return True
1084    # try running the model
1085    from sasmodels.sasview_model import load_custom_model
1086    Model = load_custom_model(path)
1087    model = Model()
1088    q =  np.array([0.01, 0.1])
1089    _ = model.evalDistribution(q)
1090    qx, qy =  np.array([0.01, 0.01]), np.array([0.1, 0.1])
1091    _ = model.evalDistribution([qx, qy])
1092
1093    # check the model's unit tests run
1094    from sasmodels.model_test import run_one
1095    # TestSuite module in Qt5 now deletes tests in the suite after running,
1096    # so suite[0] in run_one() in sasmodels/model_test.py will contain [None] and
1097    # test.info.tests will raise.
1098    # Not sure how to change the behaviour here, most likely sasmodels will have to
1099    # be modified
1100    result = run_one(path)
1101
1102    return result
1103
1104
1105def enum(*sequential, **named):
1106    """Create an enumeration object from a list of strings"""
1107    enums = dict(zip(sequential, range(len(sequential))), **named)
1108    return type('Enum', (), enums)
Note: See TracBrowser for help on using the repository browser.