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

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 fd7ef36 was fd7ef36, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

delete intermediate theory-only plots after model evaluation, before adding current ones

this applies only to beta approximation, whereby plots such as beta(Q)
and S_eff(Q) may be removed between calculations. however, since it does
not affect behaviour otherwise, I am pushing to ESS_GUI to ensure no
later conflicts occur

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