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

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

Save status of data explorer

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