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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 5b144c6 was 5b144c6, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Clumsy fix to the single-data, multi-fitpage plotting issue SASVIEW-1018.
Fixed tests after replacing plot_dict indexing from .id to .name

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