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

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 a54bbf2b was a54bbf2b, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Added plot roles to Data1D/Data2D structures to allow for smoother plot logic.

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