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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 63467b6 was 63467b6, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 6 years ago

Improved handling of 2d plot children. Refactored model tree search.

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