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

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

remove redundant intermediate plots when a new model is selected that does not plot them

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