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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 2eeda93 was 2eeda93, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

Working version of Save/Load? Analysis. SASVIEW-983.
Changed the default behaviour of Category/Model? combos:
Selecting a category does not pre-select the first model now.

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