import sys
import os
from matplotlib.figure import Figure
import numpy
import logging
import time
import timeit
from scipy.spatial.transform import Rotation
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets
from matplotlib.backends.backend_qt5agg import (FigureCanvas, NavigationToolbar2QT as NavigationToolbar)
from mpl_toolkits.mplot3d.axes3d import Axes3D
from matplotlib import __version__ as mpl_version
from twisted.internet import threads
import sas.qtgui.Utilities.GuiUtils as GuiUtils
from sas.qtgui.Utilities.GenericReader import GenReader
from sas.sascalc.dataloader.data_info import Detector
from sas.sascalc.dataloader.data_info import Source
from sas.sascalc.calculator import sas_gen
from sas.qtgui.Plotting.PlotterBase import PlotterBase
from sas.qtgui.Plotting.Plotter2D import Plotter2D
from sas.qtgui.Plotting.Plotter import Plotter
from sas.qtgui.Plotting.Arrow3D import Arrow3D
from sas.qtgui.Plotting.PlotterData import Data1D
from sas.qtgui.Plotting.PlotterData import Data2D
# Local UI
from .UI.GenericScatteringCalculator import Ui_GenericScatteringCalculator
_Q1D_MIN = 0.001
[docs]class GenericScatteringCalculator(QtWidgets.QDialog, Ui_GenericScatteringCalculator):
trigger_plot_3d = QtCore.pyqtSignal()
calculationFinishedSignal = QtCore.pyqtSignal()
loadingFinishedSignal = QtCore.pyqtSignal(list, bool)
# class constants for textbox background colours
TEXTBOX_DEFAULT_STYLESTRING = 'background-color: rgb(255, 255, 255);'
TEXTBOX_WARNING_STYLESTRING = 'background-color: rgb(255, 226, 110);'
TEXTBOX_ERROR_STYLESTRING = 'background-color: rgb(255, 182, 193);'
[docs] def __init__(self, parent=None):
super(GenericScatteringCalculator, self).__init__()
self.setupUi(self)
# disable the context help icon
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
self.manager = parent
self.communicator = self.manager.communicator()
self.model = sas_gen.GenSAS()
self.omf_reader = sas_gen.OMFReader()
self.sld_reader = sas_gen.SLDReader()
self.pdb_reader = sas_gen.PDBReader()
self.vtk_reader = sas_gen.VTKReader()
self.reader = None
# sld data for nuclear and magnetic cases
self.nuc_sld_data = None
self.mag_sld_data = None
self.verified = False
# prevent layout shifting when widget hidden
# TODO: Is there a way to lcoate this policy in the ui file?
sizePolicy = self.lblVerifyError.sizePolicy()
sizePolicy.setRetainSizeWhenHidden(True)
self.lblVerifyError.setSizePolicy(sizePolicy)
self.lblVerifyError.setVisible(False)
self.parameters = []
self.data = None
self.datafile = None
self.ext = None
self.default_shape = str(self.cbShape.currentText())
self.is_avg = False
self.is_nuc = False
self.is_mag = False
self.data_to_plot = None
self.graph_num = 1 # index for name of graph
# finish UI setup - install qml window
self.setup_display()
# combox box
self.cbOptionsCalc.currentIndexChanged.connect(self.change_is_avg)
# prevent layout shifting when widget hidden
# TODO: Is there a way to lcoate this policy in the ui file?
sizePolicy = self.cbOptionsCalc.sizePolicy()
sizePolicy.setRetainSizeWhenHidden(True)
self.cbOptionsCalc.setSizePolicy(sizePolicy)
# code to highlight incompleted values in the GUI and prevent calculation
# list of lineEdits to be checked
self.lineEdits = [self.txtUpFracIn, self.txtUpFracOut, self.txtUpTheta, self.txtUpPhi, self.txtBackground,
self.txtScale, self.txtSolventSLD, self.txtTotalVolume, self.txtNoQBins, self.txtQxMax,
self.txtMx, self.txtMy, self.txtMz, self.txtNucl, self.txtXnodes, self.txtYnodes,
self.txtZnodes, self.txtXstepsize, self.txtYstepsize, self.txtZstepsize, self.txtEnvYaw,
self.txtEnvPitch, self.txtEnvRoll, self.txtSampleYaw, self.txtSamplePitch, self.txtSampleRoll]
self.invalidLineEdits = []
for lineEdit in self.lineEdits:
lineEdit.textChanged.connect(self.gui_text_changed_slot) # when text is changed
lineEdit.installEventFilter(self) # when textbox enabled/disabled
# push buttons
self.cmdClose.clicked.connect(self.accept)
self.cmdHelp.clicked.connect(self.onHelp)
self.cmdNucLoad.clicked.connect(self.loadFile)
self.cmdMagLoad.clicked.connect(self.loadFile)
self.cmdCompute.clicked.connect(self.onCompute)
self.cmdReset.clicked.connect(self.onReset)
self.cmdSave.clicked.connect(self.onSaveFile)
# checkboxes
self.checkboxNucData.stateChanged.connect(self.change_data_type)
self.checkboxMagData.stateChanged.connect(self.change_data_type)
self.cmdDraw.clicked.connect(lambda: self.plot3d(has_arrow=True))
self.cmdDrawpoints.clicked.connect(lambda: self.plot3d(has_arrow=False))
# update pixel no./total volume when changed in GUI
self.txtXnodes.textChanged.connect(self.update_geometry_effects)
self.txtYnodes.textChanged.connect(self.update_geometry_effects)
self.txtZnodes.textChanged.connect(self.update_geometry_effects)
self.txtXstepsize.textChanged.connect(self.update_geometry_effects)
self.txtYstepsize.textChanged.connect(self.update_geometry_effects)
self.txtZstepsize.textChanged.connect(self.update_geometry_effects)
#check for presence of magnetism
self.txtMx.textChanged.connect(self.check_for_magnetic_controls)
self.txtMy.textChanged.connect(self.check_for_magnetic_controls)
self.txtMz.textChanged.connect(self.check_for_magnetic_controls)
#update coord display
self.txtEnvYaw.textChanged.connect(self.update_coords)
self.txtEnvPitch.textChanged.connect(self.update_coords)
self.txtEnvRoll.textChanged.connect(self.update_coords)
self.txtSampleYaw.textChanged.connect(self.update_coords)
self.txtSamplePitch.textChanged.connect(self.update_coords)
self.txtSampleRoll.textChanged.connect(self.update_coords)
self.txtUpTheta.textChanged.connect(self.update_polarisation_coords)
self.txtUpPhi.textChanged.connect(self.update_polarisation_coords)
# setup initial configuration
self.checkboxNucData.setEnabled(False)
self.checkboxMagData.setEnabled(False)
self.change_data_type()
# verify that the new enabled files are compatible
self.verified = self.model.file_verification(self.nuc_sld_data, self.mag_sld_data)
self.toggle_error_functionality()
# validators
# scale, volume and background must be positive
validat_regex_pos = QtCore.QRegExp('^[+]?([.]\d+|\d+([.]\d+)?)$')
self.txtScale.setValidator(QtGui.QRegExpValidator(validat_regex_pos,
self.txtScale))
self.txtBackground.setValidator(QtGui.QRegExpValidator(
validat_regex_pos, self.txtBackground))
self.txtTotalVolume.setValidator(QtGui.QRegExpValidator(
validat_regex_pos, self.txtTotalVolume))
# fraction of spin up between 0 and 1
validat_regexbetween0_1 = QtCore.QRegExp('^(0(\.\d*)*|1(\.0+)?)$')
self.txtUpFracIn.setValidator(
QtGui.QRegExpValidator(validat_regexbetween0_1, self.txtUpFracIn))
self.txtUpFracOut.setValidator(
QtGui.QRegExpValidator(validat_regexbetween0_1, self.txtUpFracOut))
# angles, SLD must be float values
validat_regex_float = QtCore.QRegExp('^[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?$')
self.txtUpTheta.setValidator(
QtGui.QRegExpValidator(validat_regex_float, self.txtUpTheta))
self.txtUpPhi.setValidator(
QtGui.QRegExpValidator(validat_regex_float, self.txtUpPhi))
self.txtSolventSLD.setValidator(
QtGui.QRegExpValidator(validat_regex_float, self.txtSolventSLD))
self.txtNucl.setValidator(
QtGui.QRegExpValidator(validat_regex_float, self.txtNucl))
self.txtMx.setValidator(
QtGui.QRegExpValidator(validat_regex_float, self.txtMx))
self.txtMy.setValidator(
QtGui.QRegExpValidator(validat_regex_float, self.txtMy))
self.txtMz.setValidator(
QtGui.QRegExpValidator(validat_regex_float, self.txtMz))
self.txtXstepsize.setValidator(
QtGui.QRegExpValidator(validat_regex_float, self.txtXstepsize))
self.txtYstepsize.setValidator(
QtGui.QRegExpValidator(validat_regex_float, self.txtYstepsize))
self.txtZstepsize.setValidator(
QtGui.QRegExpValidator(validat_regex_float, self.txtZstepsize))
self.txtEnvYaw.setValidator(
QtGui.QRegExpValidator(validat_regex_float, self.txtEnvYaw))
self.txtEnvPitch.setValidator(
QtGui.QRegExpValidator(validat_regex_float, self.txtEnvPitch))
self.txtEnvRoll.setValidator(
QtGui.QRegExpValidator(validat_regex_float, self.txtEnvRoll))
self.txtSampleYaw.setValidator(
QtGui.QRegExpValidator(validat_regex_float, self.txtSampleYaw))
self.txtSamplePitch.setValidator(
QtGui.QRegExpValidator(validat_regex_float, self.txtSamplePitch))
self.txtSampleRoll.setValidator(
QtGui.QRegExpValidator(validat_regex_float, self.txtSampleRoll))
# 0 < Qmax <= 1000
validat_regex_q = QtCore.QRegExp('^1000$|^[+]?(\d{1,3}([.]\d+)?)$')
self.txtQxMax.setValidator(QtGui.QRegExpValidator(validat_regex_q,
self.txtQxMax))
# 2 <= Qbin and nodes integers < 1000
validat_regex_int = QtCore.QRegExp('^[2-9]|[1-9]\d{1,2}$')
self.txtNoQBins.setValidator(QtGui.QRegExpValidator(validat_regex_int,
self.txtNoQBins))
self.txtXnodes.setValidator(
QtGui.QRegExpValidator(validat_regex_int, self.txtXnodes))
self.txtYnodes.setValidator(
QtGui.QRegExpValidator(validat_regex_int, self.txtYnodes))
self.txtZnodes.setValidator(
QtGui.QRegExpValidator(validat_regex_int, self.txtZnodes))
# plots - 3D in real space
self.trigger_plot_3d.connect(lambda: self.plot3d(has_arrow=False))
# plots - 3D in real space
self.calculationFinishedSignal.connect(self.plot_1_2d)
# notify main thread about file load complete
self.loadingFinishedSignal.connect(self.complete_loading)
# TODO the option Ellipsoid has not been implemented
self.cbShape.currentIndexChanged.connect(self.selectedshapechange)
# New font to display angstrom symbol
new_font = 'font-family: -apple-system, "Helvetica Neue", "Ubuntu";'
self.lblUnitSolventSLD.setStyleSheet(new_font)
self.lblUnitVolume.setStyleSheet(new_font)
self.lbl5.setStyleSheet(new_font)
self.lblUnitMx.setStyleSheet(new_font)
self.lblUnitMy.setStyleSheet(new_font)
self.lblUnitMz.setStyleSheet(new_font)
self.lblUnitNucl.setStyleSheet(new_font)
self.lblUnitx.setStyleSheet(new_font)
self.lblUnity.setStyleSheet(new_font)
self.lblUnitz.setStyleSheet(new_font)
[docs] def setup_display(self):
"""
This function sets up the GUI display of the different coordinate systems.
Since these cannot be set in the .ui file they should be QWidgets added to the self.coord_display layout.
This is one of four functions affecting the coordinate system visualisation which should be updated if
a new 3D rendering library is used: `setup_display()`, `update_coords()`, `update_polarisation_coords()`, `set_polarisation_visible()`.
"""
self.view_azim = 45
self.view_elev = 45
self.mouse_down = False
sampleWindow = FigureCanvas(Figure())
axes_sample = Axes3D(sampleWindow.figure, azim=self.view_azim, elev=self.view_elev)
envWindow = FigureCanvas(Figure())
axes_env = Axes3D(envWindow.figure, azim=self.view_azim, elev=self.view_elev)
beamWindow = FigureCanvas(Figure())
axes_beam = Axes3D(beamWindow.figure, azim=self.view_azim, elev=self.view_elev)
self.coord_windows = [sampleWindow, envWindow, beamWindow]
self.coord_axes = [axes_sample, axes_env, axes_beam]
self.coord_arrows = []
titles = ["sample", "environment", "beamline"]
for i in range(len(self.coord_windows)):
self.coordDisplay.addWidget(self.coord_windows[i])
if int(mpl_version.split(".")[0]) >= 3: # how mpl plots 3D graphs changed in 3.3.0 to allow better aspect ratios
if int(mpl_version.split(".")[1]) >= 3:
self.coord_axes[i].set_box_aspect((1,1,1))
self.coord_windows[i].installEventFilter(self)
# stack in order zs, xs, ys to match the coord system used in sasview
self.coord_arrows.append(Arrow3D(self.coord_axes[i].figure, [[0, 0],[0, 0],[0, 1]], [[0, 1],[0, 0],[0, 0]], [[0, 0],[0, 1],[0, 0]], [[1, 0 ,0],[0, 1, 0],[0, 0, 1]], arrowstyle = "->", mutation_scale=10, lw=2))
self.coord_arrows[i].set_realtime(True)
self.coord_axes[i].add_artist(self.coord_arrows[i])
self.coord_axes[i].set_xlim3d(-1, 1)
self.coord_axes[i].set_ylim3d(-1, 1)
self.coord_axes[i].set_zlim3d(-1, 1)
self.coord_axes[i].set_axis_off()
self.coord_axes[i].set_title(titles[i])
self.coord_axes[i].disable_mouse_rotation()
self.polarisation_arrow = Arrow3D(self.coord_axes[1].figure, [[0, 0.8]], [[0, 0]], [[0, 0]], [[1, 0 ,0.7]], arrowstyle = "->", mutation_scale=10, lw=3)
self.polarisation_arrow.set_realtime(True)
self.coord_axes[1].add_artist(self.polarisation_arrow)
self.coord_axes[0].text2D(0.75, 0.01, 'x', verticalalignment='bottom', horizontalalignment='right', color='red', fontsize=15, transform=self.coord_axes[0].transAxes)
self.coord_axes[0].text2D(0.85, 0.01, 'y', verticalalignment='bottom', horizontalalignment='right', color='green', fontsize=15, transform=self.coord_axes[0].transAxes)
self.coord_axes[0].text2D(0.95, 0.01, 'z', verticalalignment='bottom', horizontalalignment='right', color='blue', fontsize=15, transform=self.coord_axes[0].transAxes)
self.p_text = self.coord_axes[1].text2D(0.65, 0.01, 'p', verticalalignment='bottom', horizontalalignment='right', color='#ff00bb', fontsize=15, transform=self.coord_axes[1].transAxes)
self.p_text.set_visible(False)
self.coord_axes[1].text2D(0.75, 0.01, 'u', verticalalignment='bottom', horizontalalignment='right', color='red', fontsize=15, transform=self.coord_axes[1].transAxes)
self.coord_axes[1].text2D(0.85, 0.01, 'v', verticalalignment='bottom', horizontalalignment='right', color='green', fontsize=15, transform=self.coord_axes[1].transAxes)
self.coord_axes[1].text2D(0.95, 0.01, 'w', verticalalignment='bottom', horizontalalignment='right', color='blue', fontsize=15, transform=self.coord_axes[1].transAxes)
self.coord_axes[2].text2D(0.75, 0.01, 'U', verticalalignment='bottom', horizontalalignment='right', color='red', fontsize=15, transform=self.coord_axes[2].transAxes)
self.coord_axes[2].text2D(0.85, 0.01, 'V', verticalalignment='bottom', horizontalalignment='right', color='green', fontsize=15, transform=self.coord_axes[2].transAxes)
self.coord_axes[2].text2D(0.95, 0.01, 'W', verticalalignment='bottom', horizontalalignment='right', color='blue', fontsize=15, transform=self.coord_axes[2].transAxes)
[docs] def update_coords(self):
"""
This function rotates the visualisation of the coordinate systems
This is one of four functions affecting the coordinate system visualisation which should be updated if
a new 3D rendering library is used: `setup_display()`, `update_coords()`, `update_polarisation_coords()`, `set_polarisation_visible()`.
"""
if self.txtEnvYaw.hasAcceptableInput() and self.txtEnvPitch.hasAcceptableInput() and self.txtEnvRoll.hasAcceptableInput() \
and self.txtSampleYaw.hasAcceptableInput() and self.txtSamplePitch.hasAcceptableInput() and self.txtSampleRoll.hasAcceptableInput():
UVW_to_uvw, UVW_to_xyz = self.create_rotation_matrices()
basis_vectors = numpy.array([[1,0,0],[0,1,0],[0,0,1]])
#TODO: when scipy version updated can just use Rotation.as_matrix() to get new basis vectors - function name currently varies between versions used.
uvw_matrix = UVW_to_uvw.apply(basis_vectors)
xs, ys, zs = numpy.transpose(numpy.stack((numpy.zeros_like(uvw_matrix), uvw_matrix)), axes=(2, 1, 0))
self.coord_arrows[1].update_data(zs, xs, ys) # stack in order zs, xs, ys to match the coord system used in sasview
xyz_matrix = UVW_to_xyz.apply(basis_vectors)
xs, ys, zs = numpy.transpose(numpy.stack((numpy.zeros_like(xyz_matrix), xyz_matrix)), axes=(2, 1, 0))
self.coord_arrows[0].update_data(zs, xs, ys) # stack in order zs, xs, ys to match the coord system used in sasview
self.update_polarisation_coords()
[docs] def update_polarisation_coords(self):
"""
This function rotates the visualisation of the polarisation vector
This is one of four functions affecting the coordinate system visualisation which should be updated if
a new 3D rendering library is used: `setup_display()`, `update_coords()`, `update_polarisation_coords()`, `set_polarisation_visible()`.
"""
if self.txtUpTheta.hasAcceptableInput() and self.txtUpPhi.hasAcceptableInput():
theta = numpy.radians(float(self.txtUpTheta.text()))
phi = numpy.radians(float(self.txtUpPhi.text()))
UVW_to_uvw, _ = self.create_rotation_matrices()
p_vec = (UVW_to_uvw * Rotation.from_euler("ZY", [phi, theta])).apply(numpy.array([0, 0, 0.8])) # vector relative to beamline coords
self.polarisation_arrow.update_data([[0, p_vec[2]]], [[0, p_vec[0]]], [[0, p_vec[1]]])
[docs] def set_polarisation_visible(self, visible):
"""
This function updates the visibility of the polarisation vector
This is one of four functions affecting the coordinate system visualisation which should be updated if
a new 3D rendering library is used: `setup_display()`, `update_coords()`, `update_polarisation_coords()`, `set_polarisation_visible()`.
"""
self.polarisation_arrow.set_visible(visible)
self.p_text.set_visible(visible)
self.polarisation_arrow.base.canvas.draw()
[docs] def reset_camera(self):
self.view_azim = 45
self.view_elev = 45
self.mouse_down = False
for axes in self.coord_axes:
axes.view_init(elev=self.view_elev, azim=self.view_azim)
axes.figure.canvas.draw()
[docs] def gui_text_changed_slot(self):
"""Catches the signal that a textbox has beeen altered"""
self.gui_text_changed(self.sender())
[docs] def eventFilter(self, target, event):
"""Catches the event that a textbox has been enabled/disabled"""
if target in self.lineEdits and event.type() == QtCore.QEvent.EnabledChange:
self.gui_text_changed(target)
elif target in self.coord_windows:
if event.type() == QtCore.QEvent.MouseButtonPress:
mEvent = QtGui.QMouseEvent(event)
self.mouse_x = mEvent.x()
self.mouse_y = mEvent.y()
self.mouse_down = True
elif event.type() == QtCore.QEvent.MouseButtonRelease:
self.mouse_down = False
elif event.type() == QtCore.QEvent.MouseMove and self.mouse_down:
mEvent = QtGui.QMouseEvent(event)
self.view_azim = (self.view_azim - mEvent.x() + self.mouse_x) % 360
self.view_elev = min(max(self.view_elev + mEvent.y() - self.mouse_y, -90), 90)
self.mouse_x = mEvent.x()
self.mouse_y = mEvent.y()
for axes in self.coord_axes:
axes.view_init(elev=self.view_elev, azim=self.view_azim)
axes.figure.canvas.draw()
return False
[docs] def gui_text_changed(self, sender):
"""check whether lineEdit values are valid
This function checks whether lineEdits are valid, and if not highlights them and
calls for functionality to be disabled.
It checks for both errors and warnings. Error states highlight red and disable
functionality. These are 'intermediate' states which do not match the regex.
Warning states are highlighted orange and warn the user the value may be problematic.
Warnings were previously checked for in the check_value() method.
For warnings this checks that QMax and the number of Qbins is suitable
given the user chosen values. Unlike the hard limits imposed by the
regex, this does not prevent the user using the given value, but warns
them that it may be unsuitable with a backcolour.
:param sender: The QLineEdit in question
:type sender: QWidget
"""
senderInvalid = sender in self.invalidLineEdits
# If the LineEdit is disabled (i.e. value set programmatically) we trust the value
if (not sender.isEnabled()):
if senderInvalid:
self.invalidLineEdits.remove(sender)
self.toggle_error_functionality()
sender.setStyleSheet("")
# If the LineEdit has been corrected from an invalid value restore functionality
elif sender.hasAcceptableInput() and senderInvalid:
self.invalidLineEdits.remove(sender)
self.toggle_error_functionality()
sender.setStyleSheet(self.TEXTBOX_DEFAULT_STYLESTRING)
# If the LineEdit has had an invalid value stored then remove functionality
elif (not sender.hasAcceptableInput()) and (not senderInvalid):
self.invalidLineEdits.append(sender)
self.toggle_error_functionality()
sender.setStyleSheet(self.TEXTBOX_ERROR_STYLESTRING)
# If the LineEdit is an acceptable value according to the regex apply warnings
# This functionality was previously found in check_value()
if not(sender in self.invalidLineEdits):
if sender == self.txtNoQBins :
xnodes = float(self.txtXnodes.text())
ynodes = float(self.txtYnodes.text())
znodes = float(self.txtZnodes.text())
value = float(str(self.txtNoQBins.text()))
max_step = 3*max(xnodes, ynodes, znodes)
# limits qmin > maxq / nodes
if value < 2 or value > max_step:
self.txtNoQBins.setStyleSheet(self.TEXTBOX_WARNING_STYLESTRING)
else:
self.txtNoQBins.setStyleSheet(self.TEXTBOX_DEFAULT_STYLESTRING)
elif sender == self.txtQxMax:
xstepsize = float(self.txtXstepsize.text())
ystepsize = float(self.txtYstepsize.text())
zstepsize = float(self.txtZstepsize.text())
value = float(str(self.txtQxMax.text()))
max_q = numpy.pi / (max(xstepsize, ystepsize, zstepsize))
if value <= 0 or value > max_q:
self.txtQxMax.setStyleSheet(self.TEXTBOX_WARNING_STYLESTRING)
else:
self.txtQxMax.setStyleSheet(self.TEXTBOX_DEFAULT_STYLESTRING)
[docs] def selectedshapechange(self):
"""
TODO Temporary solution to display information about option 'Ellipsoid'
"""
print("The option Ellipsoid has not been implemented yet.")
self.communicator.statusBarUpdateSignal.emit(
"The option Ellipsoid has not been implemented yet.")
[docs] def toggle_error_functionality(self):
"""Disables/Enables some functionality if the state of the GUI means calculation cannot proceed
This function is called during any process whenever there is a risk that the state
of the GUI will make the data invalid for plotting, drawing or saving. If that is the
case then this functionality is disabled. This function is currently called when two
files are being verified for compatibility, and when textboxes enter 'intermediate' states.
"""
verificationEnable = self.verified or not (self.is_mag and self.is_nuc)
lineEditsEnable = len(self.invalidLineEdits) == 0
# disable necessary buttons to prevent the attempted merging of incompatible files
self.cmdDraw.setEnabled(verificationEnable and lineEditsEnable)
self.cmdDrawpoints.setEnabled(verificationEnable and lineEditsEnable)
self.cmdSave.setEnabled(verificationEnable and lineEditsEnable)
self.cmdCompute.setEnabled(verificationEnable and lineEditsEnable)
[docs] def change_data_type(self):
"""Adjusts the GUI for the enabled nuclear/magnetic data files
When different combinations of nuclear and magnetic data files are loaded
various options must be enabled/disabled or hidden/made visible. This function
controls that behaviour and is called whenever the checkboxes for enabling files
are altered. If the data file for a given type of data is not loaded then the
average value textbox is enabled to allow the user to give a constant value for
all points. If no data files are loaded then the node and stepsize textboxes are
enabled to allow the user to specify a simple rectangular lattice.
"""
# update information on which files are enabled
self.is_nuc = self.checkboxNucData.isChecked()
self.is_mag = self.checkboxMagData.isChecked()
# enable the corresponding text displays to show this to the user clearly
self.txtNucData.setEnabled(self.is_nuc)
self.txtMagData.setEnabled(self.is_mag)
# only allow editing of mean values if no data file for that vlaue has been loaded
# user provided mean values are taken as a constant across all points
self.txtMx.setEnabled(not self.is_mag)
self.txtMy.setEnabled(not self.is_mag)
self.txtMz.setEnabled(not self.is_mag)
if not self.is_mag:
self.txtMx.setText("0.0")
self.txtMy.setText("0.0")
self.txtMz.setText("0.0")
self.txtNucl.setEnabled(not self.is_nuc)
if not self.is_nuc:
self.txtNucl.setText("0.0")
# The ability to change the number of nodes and stepsizes only if no laoded data file enabled
both_disabled = (not self.is_mag) and (not self.is_nuc)
if both_disabled:
self.txtMx.setText("0.0")
self.txtMy.setText("0.0")
self.txtMz.setText("0.0")
self.txtNucl.setText("6.97e-06")
self.txtXnodes.setText("10")
self.txtYnodes.setText("10")
self.txtZnodes.setText("10")
self.txtXstepsize.setText("6")
self.txtYstepsize.setText("6")
self.txtZstepsize.setText("6")
self.txtXnodes.setEnabled(both_disabled)
self.txtYnodes.setEnabled(both_disabled)
self.txtZnodes.setEnabled(both_disabled)
self.txtXstepsize.setEnabled(both_disabled)
self.txtYstepsize.setEnabled(both_disabled)
self.txtZstepsize.setEnabled(both_disabled)
# update the gui with new values - sets the average values from enabled files
self.update_gui()
self.check_for_magnetic_controls()
[docs] def update_cbOptionsCalc_visibility(self):
# Only allow 1D averaging if no magnetic data and not elements
allow = not self.is_mag
if self.is_nuc and allow:
allow = not self.nuc_sld_data.is_elements
self.cbOptionsCalc.setVisible(allow)
if (allow):
# A helper function to set up the averaging system
self.change_is_avg()
else:
# If magnetic data present then no averaging is allowed
self.is_avg = False
self.txtMx.setEnabled(not self.is_mag)
self.txtMy.setEnabled(not self.is_mag)
self.txtMz.setEnabled(not self.is_mag)
[docs] def change_is_avg(self):
"""Adjusts the GUI for whether 1D averaging is enabled
If the user has chosen to carry out Debye full averaging then the magnetic sld
values must be set to 0, and made uneditable - because the calculator in geni.py
is incapable of averaging systems with non-zero magnetic slds or polarisation.
This function is called whenever different files are enabled or the user edits the
averaging combobox.
"""
# update the averaging option fromthe button on the GUI
# required as the button may have been previously hidden with
# any value, and preserves this - we must update the variable to match the GUI
self.is_avg = (self.cbOptionsCalc.currentIndex() == 1)
# If averaging then set to 0 and diable the magnetic SLD textboxes
if self.is_avg:
self.txtMx.setEnabled(False)
self.txtMy.setEnabled(False)
self.txtMz.setEnabled(False)
self.txtMx.setText("0.0")
self.txtMy.setText("0.0")
self.txtMz.setText("0.0")
# If not averaging then re-enable the magnetic sld textboxes
else:
self.txtMx.setEnabled(True)
self.txtMy.setEnabled(True)
self.txtMz.setEnabled(True)
[docs] def check_for_magnetic_controls(self):
if self.txtMx.hasAcceptableInput() and self.txtMy.hasAcceptableInput() and self.txtMz.hasAcceptableInput():
if (not self.is_mag) and float(self.txtMx.text()) == 0 and float(self.txtMy.text()) == 0 and float(self.txtMy.text()) == 0:
self.txtUpFracIn.setEnabled(False)
self.txtUpFracOut.setEnabled(False)
self.txtUpTheta.setEnabled(False)
self.txtUpPhi.setEnabled(False)
self.set_polarisation_visible(False)
return
self.txtUpFracIn.setEnabled(True)
self.txtUpFracOut.setEnabled(True)
self.txtUpTheta.setEnabled(True)
self.txtUpPhi.setEnabled(True)
self.set_polarisation_visible(True)
[docs] def loadFile(self):
"""Opens a menu to choose the datafile to load
Opens a file dialog to allow the user to select a datafile to be loaded.
If a nuclear sld datafile is loaded then the allowed file types are: .SLD .sld .PDB .pdb
If a magnetic sld datafile is loaded then the allowed file types are: .SLD .sld .OMF .omf
This function then loads in the requested datafile, but does not enable it.
If no previous datafile of this type was loaded then the checkbox to enable
this file is enabled.
:param load_nuc: Specifies whether the loaded file is nuclear or magnetic data. Defaults to `True`.
`load_nuc=True` gives nuclear sld data.
`load_nuc=False` gives magnetic sld data.
:type load_nuc: bool
"""
try:
load_nuc = self.sender() == self.cmdNucLoad
# request a file from the user
if load_nuc:
self.datafile = QtWidgets.QFileDialog.getOpenFileName(
self, "Choose a file", "","All supported files (*.SLD *.sld *.pdb *.PDB, *.vtk, *.VTK);;"
"SLD files (*.SLD *.sld);;"
"PDB files (*.pdb *.PDB);;"
"VTK files (*.vtk *.VTK);;"
"All files (*.*)",
options=QtWidgets.QFileDialog.DontUseNativeDialog |
QtWidgets.QFileDialog.DontUseCustomDirectoryIcons,
)[0]
else:
self.datafile = QtWidgets.QFileDialog.getOpenFileName(
self, "Choose a file", "","All supported files (*.OMF *.omf *.SLD *.sld, *.vtk, *.VTK);;"
"OMF files (*.OMF *.omf);;"
"SLD files (*.SLD *.sld);;"
"VTK files (*.vtk *.VTK);;"
"All files (*.*)",
options=QtWidgets.QFileDialog.DontUseNativeDialog |
QtWidgets.QFileDialog.DontUseCustomDirectoryIcons
)[0]
# If a file has been sucessfully chosen
if self.datafile:
# set basic data about the file
self.default_shape = str(self.cbShape.currentText())
self.ext = os.path.splitext(str(self.datafile))[1]
# select the required loader for the data format
if self.ext in self.omf_reader.ext and (not load_nuc):
# only load omf files for magnetic data
loader = self.omf_reader
elif self.ext in self.sld_reader.ext:
loader = self.sld_reader
elif self.ext in self.vtk_reader.ext:
loader = self.vtk_reader
elif self.ext in self.pdb_reader.ext and load_nuc:
# only load pdb files for nuclear data
loader = self.pdb_reader
else:
logging.warning("The selected file does not have a suitable file extension")
return
if self.reader is not None and self.reader.isrunning():
self.reader.stop()
self.cmdNucLoad.setEnabled(False)
self.cmdNucLoad.setText('Loading...')
self.cmdMagLoad.setEnabled(False)
self.cmdMagLoad.setText('Loading...')
self.cmdCompute.setEnabled(False)
self.cmdCompute.setText('Loading...')
self.communicator.statusBarUpdateSignal.emit(
"Loading File {}".format(os.path.basename(
str(self.datafile))))
self.reader = GenReader(path=str(self.datafile), loader=loader,
completefn=lambda data=None: self.complete_loading_ex(data=data, load_nuc=load_nuc),
updatefn=self.load_update)
self.reader.queue()
except (RuntimeError, IOError):
log_msg = "Generic SAS Calculator: %s" % sys.exc_info()[1]
logging.info(log_msg)
raise
return
[docs] def load_update(self):
""" Legacy function used in GenRead """
if self.reader.isrunning():
status_type = "progress"
else:
status_type = "stop"
logging.info(status_type)
[docs] def complete_loading_ex(self, data=None, load_nuc=True):
"""Send the finish message from calculate threads to main thread
:param data: The data loaded from the requested file.
:type data: OMFData, MagSLD depending on filetype
:param load_nuc: Specifies whether the loaded file is nuclear or magnetic
data. Defaults to `True`.
`load_nuc=True` gives nuclear sld data.
`load_nuc=False` gives magnetic sld data.
:type load_nuc: bool
"""
self.loadingFinishedSignal.emit(data, load_nuc)
[docs] def complete_loading(self, data=None, load_nuc=True):
"""Function which handles the datafiles once they have been loaded in - used in GenRead
Once the data has been loaded in by the required reader it is necessary to do a small
amount of final processing to put them in the required form. This involves converting
all the data to instances of MagSLD and reporting any errors. Additionally verification
of the newly loaded file is carried out.
:param data: The data loaded from the requested file.
:type data: OMFData, MagSLD depending on filetype
:param load_nuc: Specifies whether the loaded file is nuclear or magnetic
data. Defaults to `True`.
`load_nuc=True` gives nuclear sld data.
`load_nuc=False` gives magnetic sld data.
:type load_nuc: bool
"""
assert isinstance(data, list)
assert len(data)==1
data = data[0]
self.cbShape.setEnabled(False)
self.cmdNucLoad.setEnabled(True)
self.cmdNucLoad.setText('Load')
self.cmdMagLoad.setEnabled(True)
self.cmdMagLoad.setText('Load')
self.cmdCompute.setEnabled(True)
self.cmdCompute.setText('Compute')
if data is None:
return
try:
is_pdbdata = False
if load_nuc:
self.txtNucData.setText(os.path.basename(str(self.datafile)))
else:
self.txtMagData.setText(os.path.basename(str(self.datafile)))
if self.ext in self.omf_reader.ext:
# only magnetic data can be read from omf files
self.mag_sld_data = data
self.check_units()
elif self.ext in self.sld_reader.ext or self.ext in self.vtk_reader.ext:
if load_nuc:
self.nuc_sld_data = data
else:
self.mag_sld_data = data
elif self.ext in self.pdb_reader.ext:
# only nuclear data can be read from pdb files
self.nuc_sld_data = data
is_pdbdata = True
except IOError:
log_msg = "Loading Error: " \
"This file format is not supported for GenSAS."
logging.warning(log_msg)
raise
except ValueError:
log_msg = "Could not find any data"
logging.info(log_msg)
raise
logging.info("Load Complete")
# Once data files are loaded allow them to be enabled and then enable them
if load_nuc:
self.checkboxNucData.setEnabled(True)
self.checkboxNucData.setChecked(True)
else:
self.checkboxMagData.setEnabled(True)
self.checkboxMagData.setChecked(True)
self.update_gui()
# reset verification now we have loaded new files
self.verify = False
self.verified = self.model.file_verification(self.nuc_sld_data, self.mag_sld_data)
self.toggle_error_functionality()
[docs] def check_units(self):
"""
Check if the units from the OMF file correspond to the default ones
displayed on the interface.
If not, modify the GUI with the correct units
"""
# TODO: adopt the convention of font and symbol for the updated values
if sas_gen.OMFData().valueunit != 'A^(-2)':
value_unit = sas_gen.OMFData().valueunit
self.lbl_unitMx.setText(value_unit)
self.lbl_unitMy.setText(value_unit)
self.lbl_unitMz.setText(value_unit)
self.lbl_unitNucl.setText(value_unit)
if sas_gen.OMFData().meshunit != 'A':
mesh_unit = sas_gen.OMFData().meshunit
self.lbl_unitx.setText(mesh_unit)
self.lbl_unity.setText(mesh_unit)
self.lbl_unitz.setText(mesh_unit)
self.lbl_unitVolume.setText(mesh_unit+"^3")
[docs] def update_gui(self):
"""Update the interface and model with values from loaded data
This function updates the model parameter 'total_volume' with values from the loaded data
and then updates all values in the gui with either model paramters or paramaters from the
loaded data.
"""
self.update_cbOptionsCalc_visibility()
if self.is_nuc:
if self.nuc_sld_data.is_elements:
self.model.params['total_volume'] = numpy.sum(self.nuc_sld_data.vol_pix)
else:
self.model.params['total_volume'] = len(self.nuc_sld_data.sld_n)*self.nuc_sld_data.vol_pix[0]
elif self.is_mag:
if self.mag_sld_data.is_elements:
self.model.params['total_volume'] = numpy.sum(self.mag_sld_data.vol_pix)
else:
self.model.params['total_volume'] = len(self.mag_sld_data.sld_n)*self.mag_sld_data.vol_pix[0]
else:
# use same calculation of total volume as when converting OMF to SLD
self.model.params['total_volume'] = (float(self.txtXstepsize.text()) * float(self.txtYstepsize.text())
* float(self.txtZstepsize.text()) * float(self.txtXnodes.text())
* float(self.txtYnodes.text()) * float(self.txtZnodes.text()))
# add condition for activation of save button
self.cmdSave.setEnabled(True)
# Volume to write to interface: npts x volume of first pixel
self.txtTotalVolume.setText(str(self.model.params['total_volume']))
# Chagne capitalisation for consistency with other values
if self.txtTotalVolume.text() == "nan":
self.txtTotalVolume.setText("NaN")
# update the number of pixels with values from the loaded data or GUI if no datafiles enabled
if self.is_nuc:
if self.nuc_sld_data.is_elements:
self.txtNoPixels.setText(str(len(self.nuc_sld_data.elements)))
else:
self.txtNoPixels.setText(str(len(self.nuc_sld_data.sld_n)))
elif self.is_mag:
if self.mag_sld_data.is_elements:
self.txtNoPixels.setText(str(len(self.mag_sld_data.elements)))
else:
self.txtNoPixels.setText(str(len(self.mag_sld_data.sld_mx)))
elif not(self.txtXnodes.hasAcceptableInput() and self.txtYnodes.hasAcceptableInput() and self.txtZnodes.hasAcceptableInput()):
self.txtNoPixels.setText("NaN")
else:
self.txtNoPixels.setText(str(int(float(self.txtXnodes.text())
* float(self.txtYnodes.text()) * float(self.txtZnodes.text()))))
self.txtNoPixels.setEnabled(False)
# Fill right hand side of GUI
if self.is_mag:
self.txtMx.setText(GuiUtils.formatValue(self.mag_sld_data.sld_mx))
self.txtMy.setText(GuiUtils.formatValue(self.mag_sld_data.sld_my))
self.txtMz.setText(GuiUtils.formatValue(self.mag_sld_data.sld_mz))
if self.is_nuc:
self.txtNucl.setText(GuiUtils.formatValue(self.nuc_sld_data.sld_n))
self.txtXnodes.setText(GuiUtils.formatValue(self.nuc_sld_data.xnodes))
self.txtYnodes.setText(GuiUtils.formatValue(self.nuc_sld_data.ynodes))
self.txtZnodes.setText(GuiUtils.formatValue(self.nuc_sld_data.znodes))
self.txtXstepsize.setText(GuiUtils.formatValue(self.nuc_sld_data.xstepsize))
self.txtYstepsize.setText(GuiUtils.formatValue(self.nuc_sld_data.ystepsize))
self.txtZstepsize.setText(GuiUtils.formatValue(self.nuc_sld_data.zstepsize))
if self.is_mag and ((not self.is_nuc) or self.txtXnodes.text() == "NaN"):
# If unable to get node data from nuclear system (not enabled or not present)
self.txtXnodes.setText(GuiUtils.formatValue(self.mag_sld_data.xnodes))
self.txtYnodes.setText(GuiUtils.formatValue(self.mag_sld_data.ynodes))
self.txtZnodes.setText(GuiUtils.formatValue(self.mag_sld_data.znodes))
self.txtXstepsize.setText(GuiUtils.formatValue(self.mag_sld_data.xstepsize))
self.txtYstepsize.setText(GuiUtils.formatValue(self.mag_sld_data.ystepsize))
self.txtZstepsize.setText(GuiUtils.formatValue(self.mag_sld_data.zstepsize))
# otherwise leave as set since editable by user
# If nodes or stepsize changed then this may effect what values are allowed
self.gui_text_changed(sender=self.txtNoQBins)
self.gui_text_changed(sender=self.txtQxMax)
[docs] def update_geometry_effects(self):
"""This function updates the number of pixels and total volume when the number of nodes/stepsize is changed
This function only has an effect if no files are enabled otherwise the number of pixels and total
volume may be set differently by the data from the file.
"""
if self.is_mag or self.is_nuc:
# don't change the number if this is being set from a file as then the number of pixels may differ
return
if not(self.txtXnodes.hasAcceptableInput() and self.txtYnodes.hasAcceptableInput() and self.txtZnodes.hasAcceptableInput()):
# do not try to update if textbox invalid - this cannot be used for computation anyway
self.txtNoPixels.setText("NaN")
self.txtTotalVolume.setText("NaN")
return
self.txtNoPixels.setText(str(int(float(self.txtXnodes.text())
* float(self.txtYnodes.text()) * float(self.txtZnodes.text()))))
if not(self.txtXstepsize.hasAcceptableInput() and self.txtYstepsize.hasAcceptableInput() and self.txtZstepsize.hasAcceptableInput()):
# do not try to update if textbox invalid - this cannot be used for computation anyway
self.txtTotalVolume.setText("NaN")
return
self.model.params['total_volume'] = (float(self.txtXstepsize.text()) * float(self.txtYstepsize.text())
* float(self.txtZstepsize.text()) * float(self.txtXnodes.text())
* float(self.txtYnodes.text()) * float(self.txtZnodes.text()))
self.txtTotalVolume.setText(str(self.model.params['total_volume']))
# If nodes or stepsize changed then this may effect what values are allowed
self.gui_text_changed(sender=self.txtNoQBins)
self.gui_text_changed(sender=self.txtQxMax)
[docs] def write_new_values_from_gui(self):
"""Update parameters in model using modified inputs from GUI
Before the model is used to calculate any scattering patterns it needs
to be updated with values from the gui. This does not affect any fixed values,
whose textboxes are disabled, and means that any user chosen changes are made.
It also ensure that at all times the values in the GUI reflect the data output.
"""
if self.txtScale.isModified():
self.model.params['scale'] = float(self.txtScale.text())
if self.txtBackground.isModified():
self.model.params['background'] = float(self.txtBackground.text())
if self.txtSolventSLD.isModified():
self.model.params['solvent_SLD'] = float(self.txtSolventSLD.text())
# Different condition for total volume to get correct volume after
# applying set_sld_data in compute
if self.txtTotalVolume.isModified() \
or self.model.params['total_volume'] != float(self.txtTotalVolume.text()):
self.model.params['total_volume'] = float(self.txtTotalVolume.text())
if self.txtUpFracIn.isModified():
self.model.params['Up_frac_in'] = float(self.txtUpFracIn.text())
if self.txtUpFracOut.isModified():
self.model.params['Up_frac_out'] = float(self.txtUpFracOut.text())
if self.txtUpTheta.isModified():
self.model.params['Up_theta'] = float(self.txtUpTheta.text())
if self.txtUpPhi.isModified():
self.model.params['Up_phi'] = float(self.txtUpPhi.text())
[docs] def onHelp(self):
"""
Bring up the Generic Scattering calculator Documentation whenever
the HELP button is clicked.
Calls Documentation Window with the path of the location within the
documentation tree (after /doc/ ....".
"""
location = "/user/qtgui/Calculators/sas_calculator_help.html"
self.manager.showHelp(location)
[docs] def onReset(self):
""" Reset the GUI to its default state
This resets all GUI parameters to their default values and also resets
all GUI states such as loaded files, stored data, verification and disabled/enabled
widgets.
"""
try:
# reset values in textedits
self.txtUpFracIn.setText("1.0")
self.txtUpFracOut.setText("1.0")
self.txtUpTheta.setText("0.0")
self.txtUpPhi.setText("0.0")
self.txtBackground.setText("0.0")
self.txtScale.setText("1.0")
self.txtSolventSLD.setText("0.0")
self.txtTotalVolume.setText("216000.0")
self.txtNoQBins.setText("30")
self.txtQxMax.setText("0.3")
self.txtNoPixels.setText("1000")
self.txtMx.setText("0.0")
self.txtMy.setText("0.0")
self.txtMz.setText("0.0")
self.txtNucl.setText("6.97e-06")
self.txtXnodes.setText("10")
self.txtYnodes.setText("10")
self.txtZnodes.setText("10")
self.txtXstepsize.setText("6")
self.txtYstepsize.setText("6")
self.txtZstepsize.setText("6")
self.txtEnvYaw.setText("0.0")
self.txtEnvPitch.setText("0.0")
self.txtEnvRoll.setText("0.0")
self.txtSampleYaw.setText("0.0")
self.txtSamplePitch.setText("0.0")
self.txtSampleRoll.setText("0.0")
self.reset_camera()
# re-enable any options disabled by failed verification
self.verified = False
self.toggle_error_functionality()
# reset option for calculation
self.cbOptionsCalc.setCurrentIndex(0)
# reset shape button
self.cbShape.setCurrentIndex(0)
self.cbShape.setEnabled(True)
# reset compute button
self.cmdCompute.setText('Compute')
self.cmdCompute.setEnabled(True)
# reset Load button and textedit
self.txtNucData.setText('No File Loaded')
self.cmdNucLoad.setEnabled(True)
self.cmdNucLoad.setText('Load')
self.txtMagData.setText('No File Loaded')
self.cmdMagLoad.setEnabled(True)
self.cmdMagLoad.setText('Load')
# disable all file checkboxes, as no files are now loaded
self.checkboxNucData.setEnabled(False)
self.checkboxMagData.setEnabled(False)
self.checkboxNucData.setChecked(False)
self.checkboxMagData.setChecked(False)
# reset all file data to its default empty state
self.is_nuc = False
self.is_mag = False
self.nuc_sld_data = None
self.mag_sld_data = None
# update the gui for the no files loaded case
self.change_data_type()
# verify that the new enabled files are compatible
self.verified = self.model.file_verification(self.nuc_sld_data, self.mag_sld_data)
self.toggle_error_functionality()
finally:
pass
[docs] def _create_default_2d_data(self):
"""Create the 2D data range for qx,qy
Copied from previous version
Create 2D data by default
.. warning:: This data is never plotted.
"""
self.qmax_x = float(self.txtQxMax.text())
self.npts_x = int(self.txtNoQBins.text())
self.data = Data2D()
self.data.is_data = False
# Default values
self.data.detector.append(Detector())
index = len(self.data.detector) - 1
self.data.detector[index].distance = 8000 # mm
self.data.source.wavelength = 6 # A
self.data.detector[index].pixel_size.x = 5 # mm
self.data.detector[index].pixel_size.y = 5 # mm
self.data.detector[index].beam_center.x = self.qmax_x
self.data.detector[index].beam_center.y = self.qmax_x
xmax = self.qmax_x
xmin = -xmax
ymax = self.qmax_x
ymin = -ymax
qstep = self.npts_x
x = numpy.linspace(start=xmin, stop=xmax, num=qstep, endpoint=True)
y = numpy.linspace(start=ymin, stop=ymax, num=qstep, endpoint=True)
# use data info instead
new_x = numpy.tile(x, (len(y), 1))
new_y = numpy.tile(y, (len(x), 1))
new_y = new_y.swapaxes(0, 1)
# all data require now in 1d array
qx_data = new_x.flatten()
qy_data = new_y.flatten()
q_data = numpy.sqrt(qx_data * qx_data + qy_data * qy_data)
# set all True (standing for unmasked) as default
mask = numpy.ones(len(qx_data), dtype=bool)
self.data.source = Source()
self.data.data = numpy.ones(len(mask))
self.data.err_data = numpy.ones(len(mask))
self.data.qx_data = qx_data
self.data.qy_data = qy_data
self.data.q_data = q_data
self.data.mask = mask
# store x and y bin centers in q space
self.data.x_bins = x
self.data.y_bins = y
# max and min taking account of the bin sizes
self.data.xmin = xmin
self.data.xmax = xmax
self.data.ymin = ymin
self.data.ymax = ymax
[docs] def _create_default_sld_data(self):
"""Creates default sld data for use if no file has been loaded
Copied from previous version
.. warning:: deprecated
"""
sld_n_default = 6.97e-06 # what is this number??
omfdata = sas_gen.OMFData()
omf2sld = sas_gen.OMF2SLD()
omf2sld.set_data(omfdata, self.default_shape)
self.sld_data = omf2sld.output
self.sld_data.is_data = False
self.sld_data.filename = "Default SLD Profile"
self.sld_data.set_sldn(sld_n_default)
[docs] def _create_default_1d_data(self):
"""Create the 1D data range for q
Copied from previous version
Create 1D data by default
.. warning:: This data is never plotted.
residuals.x = data_copy.x[index]
residuals.dy = numpy.ones(len(residuals.y))
residuals.dx = None
residuals.dxl = None
residuals.dxw = None
"""
self.qmax_x = float(self.txtQxMax.text())
self.npts_x = int(self.txtNoQBins.text())
# Default values
xmax = self.qmax_x
xmin = self.qmax_x * _Q1D_MIN
qstep = self.npts_x
x = numpy.linspace(start=xmin, stop=xmax, num=qstep, endpoint=True)
# store x and y bin centers in q space
y = numpy.ones(len(x))
dy = numpy.zeros(len(x))
dx = numpy.zeros(len(x))
self.data = Data1D(x=x, y=y)
self.data.dx = dx
self.data.dy = dy
[docs] def create_full_sld_data(self):
"""Create the sld data to be used in the final calculation
This function creates an instance of MagSLD which contains
the required data for sas_gen and 3D plotting. It is the suitable combination of
data from the magnetic data, nuclear data and set GUI parameters. Where nuclear
and magnetic files are enabled it sometimes has to make a choice regarding which
version of a parameter to keep. This is usually the nuclear data version, as in
the case of .pdb files being used this version will contain more complete data.
:return: The full sld data created from the various different sources
:rtype: MagSLD
"""
# CARRY OUT COMPATIBILITY CHECK - ELSE RETURN None
# Set default data when nothing loaded yet
omfdata = sas_gen.OMFData()
# load in user chosen position data
# If no file given this will be used to generate the position data
if (not self.is_mag) and (not self.is_nuc):
omfdata.xnodes = int(self.txtXnodes.text())
omfdata.ynodes = int(self.txtYnodes.text())
omfdata.znodes = int(self.txtZnodes.text())
omfdata.xstepsize = float(self.txtXstepsize.text())
omfdata.ystepsize = float(self.txtYstepsize.text())
omfdata.zstepsize = float(self.txtZstepsize.text())
# convert into sld format
omf2sld = sas_gen.OMF2SLD()
omf2sld.set_data(omfdata, self.default_shape)
sld_data = omf2sld.get_output()
# only to be done once - load in the position data of the atoms
# verification ensures that this is the same across nuclear and magnetic datafiles
if self.is_nuc:
sld_data.vol_pix = self.nuc_sld_data.vol_pix
sld_data.pos_x = self.nuc_sld_data.pos_x
sld_data.pos_y = self.nuc_sld_data.pos_y
sld_data.pos_z = self.nuc_sld_data.pos_z
sld_data.data_length = len(self.nuc_sld_data.pos_x)
elif self.is_mag:
sld_data.vol_pix = self.mag_sld_data.vol_pix
sld_data.pos_x = self.mag_sld_data.pos_x
sld_data.pos_y = self.mag_sld_data.pos_y
sld_data.pos_z = self.mag_sld_data.pos_z
sld_data.data_length = len(self.mag_sld_data.pos_x)
if self.is_nuc:
if self.nuc_sld_data.is_elements:
sld_data.set_elements(self.nuc_sld_data.elements, self.nuc_sld_data.are_elements_array)
elif self.is_mag:
if self.mag_sld_data.is_elements:
sld_data.set_elements(self.mag_sld_data.elements, self.mag_sld_data.are_elements_array)
# set the sld data from the required model file/GUI textbox
if (self.is_nuc):
sld_data.set_sldn(self.nuc_sld_data.sld_n)
else:
sld_data.set_sldn(float(self.txtNucl.text()), non_zero_mag_only=False)
if (self.is_mag):
sld_data.set_sldms(self.mag_sld_data.sld_mx, self.mag_sld_data.sld_my, self.mag_sld_data.sld_mz)
else:
sld_data.set_sldms(float(self.txtMx.text()),
float(self.txtMy.text()),
float(self.txtMz.text()))
# Provide data giving connections between atoms for 3D drawing
# This SHOULD only occur in nuclear data files as it is a feature of
# pdb files - however the option for it to be drawn from magnetic files
# if present is given in case the sld file format is expanded to include them
if self.is_nuc:
if self.nuc_sld_data.has_conect:
sld_data.has_conect=True
sld_data.line_x = self.nuc_sld_data.line_x
sld_data.line_y = self.nuc_sld_data.line_y
sld_data.line_z = self.nuc_sld_data.line_z
# If the nuclear data does not contain conect data try to find it in the magnetic data.
# TODO: combine both lists properly. Probably only necessary if a filetype for magnetic data
# is used which can contain such data.
elif self.is_mag:
if self.mag_sld_data.has_conect:
sld_data.has_conect=True
sld_data.line_x = self.mag_sld_data.line_x
sld_data.line_y = self.mag_sld_data.line_y
sld_data.line_z = self.mag_sld_data.line_z
# take pixel data from nuclear sld as preference because may contatin atom types from pdb files
if self.is_nuc:
sld_data.pix_type = self.nuc_sld_data.pix_type
sld_data.pix_symbol = self.nuc_sld_data.pix_symbol
elif self.is_mag:
sld_data.pix_type = self.mag_sld_data.pix_type
sld_data.pix_symbol = self.mag_sld_data.pix_symbol
return sld_data
[docs] def create_rotation_matrices(self):
"""Create the rotation matrices between different coordinate systems
UVW coords: beamline coords
uvw coords: environment coords
xyz coords: sample coords
The GUI contains values of yaw, pitch and roll from uvw to xyz coordinates and
from UVW to uvw. These are right handed coordinate systems with UVW being the beamline
coordinate system with:
U: horizonatal axis
V: vertical axis
W: axis back from detector to sample
The rotation given by the user transforms the BASIS VECTORS - the user gives
the trasformation from beamline coords to samplecoords for example - so from there perspective the
beamline is the fixed object and the environment and sample rotate. The rotation is first a yaw angle
about the V axis (UVW -> U'V'W') then a pitch angle about the U' axis (U'V'W' -> U''V''W'') and
finally a roll rotation abot the W'' axis (U''V''W'' -> uvw).
This function expects that the textbox values are correct.
:return: two rotations, the first from UVW to xyz coords, the second from UVW to uvw coords
:rtype: tuple of scipy.spatial.transform.Rotation
"""
# in reverse
# NOTE: If scipy version is updated above 1.4.0 in the future then this conversion can be replaced with the degrees=True argument
# UVW to uvw
env = Rotation.from_euler("YXZ", [numpy.radians(float(self.txtEnvYaw.text())),
numpy.radians(float(self.txtEnvPitch.text())),
numpy.radians(float(self.txtEnvRoll.text()))])
# uvw to xyz
sample = Rotation.from_euler("YXZ", [numpy.radians(float(self.txtSampleYaw.text())),
numpy.radians(float(self.txtSamplePitch.text())),
numpy.radians(float(self.txtSampleRoll.text()))])
return env, sample*env
[docs] def onCompute(self):
"""Execute the computation of I(qx, qy)
Copied from previous version
"""
try:
# create the combined sld data and update from gui
sld_data = self.create_full_sld_data()
# TODO: implement fourier transform for meshes with multiple element or face types
# The easy option is to simply convert all elements to tetrahedra - but this could rapidly
# increase the calculation time.
if sld_data.is_elements:
if not sld_data.are_elements_array:
logging.warning("SasView does not currently support computation of meshes with multiple element or face types")
return
self.model.set_sld_data(sld_data)
UVW_to_uvw, UVW_to_xyz = self.create_rotation_matrices()
# We do NOT need to invert these matrices - they are UVW to xyz for the basis vectors
# and therefore xyz to UVW for the components of the vectors - as we desire
self.model.set_rotations(UVW_to_uvw, UVW_to_xyz)
self.write_new_values_from_gui()
# create 2D or 1D data as appropriate
if self.is_avg or self.is_avg is None:
self._create_default_1d_data()
inputs = [self.data.x, []]
else:
self._create_default_2d_data()
inputs = [self.data.qx_data, self.data.qy_data]
logging.info("Computation is in progress...")
self.cmdCompute.setText('Cancel')
self.cmdCompute.setToolTip("<html><head/><body><p>Cancel the computation of the scattering calculation.</p></body></html>")
self.cmdCompute.clicked.disconnect()
self.cmdCompute.clicked.connect(self.onCancel)
self.cancelCalculation = False
#self.cmdCompute.setEnabled(False)
d = threads.deferToThread(self.complete, inputs, self._update)
# Add deferred callback for call return
# d.addCallback(self.plot_1_2d)
d.addCallback(self.calculateComplete)
d.addErrback(self.calculateFailed)
except Exception:
log_msg = "{}. stop".format(sys.exc_info()[1])
logging.info(log_msg)
return
[docs] def onCancel(self):
"""Notify the calculation thread that the user has cancelled the calculation.
"""
self.cancelCalculation = True
self.cmdCompute.setEnabled(False) # don't allow user to start a new calculation until this one finishes cancelling
[docs] def _update(self, time=None, percentage=None):
"""
Copied from previous version
"""
if percentage is not None:
msg = "%d%% complete..." % int(percentage)
else:
msg = "Computing..."
logging.info(msg)
[docs] def calculateFailed(self, reason):
"""
"""
print("Calculate Failed with:\n", reason)
pass
[docs] def calculateComplete(self, d):
"""
Notify the main thread
"""
self.calculationFinishedSignal.emit()
[docs] def complete(self, input, update=None):
"""Carry out the compuation of I(qx, qy) in a new thread
Gen compute complete function
This function separates the range of q or (qx,qy) into chunks and then
calculates each chunk with calls to the model.
:param input: input list [qx_data, qy_data, i_out]
:type input: list
"""
timer = timeit.default_timer
update_rate = 1.0 # seconds between updates
next_update = timer() + update_rate if update is not None else numpy.inf
nq = len(input[0])
chunk_size = 32 if self.is_avg else 256
out = []
for ind in range(0, nq, chunk_size):
t = timer()
if t > next_update:
update(time=t, percentage=100*ind/nq)
time.sleep(0.01)
next_update = t + update_rate
if self.is_avg:
inputi = [input[0][ind:ind + chunk_size], []]
outi = self.model.run(inputi)
else:
inputi = [input[0][ind:ind + chunk_size],
input[1][ind:ind + chunk_size]]
outi = self.model.runXY(inputi)
out.append(outi)
if self.cancelCalculation:
update(time=t, percentage=100*(ind + chunk_size)/nq) # ensure final progress shown
self.data_to_plot = numpy.full(nq, numpy.nan)
self.data_to_plot[:ind + chunk_size] = numpy.hstack(out)
logging.info('Gen computation cancelled.')
break
else:
out = numpy.hstack(out)
self.data_to_plot = out
logging.info('Gen computation completed.')
self.cmdCompute.setText('Compute')
self.cmdCompute.setToolTip("<html><head/><body><p>Compute the scattering pattern and display 1D or 2D plot depending on the settings.</p></body></html>")
self.cmdCompute.clicked.disconnect()
self.cmdCompute.clicked.connect(self.onCompute)
self.cmdCompute.setEnabled(True)
return
[docs] def onSaveFile(self):
"""Save data as .sld file"""
path = os.path.dirname(str(self.datafile))
default_name = os.path.join(path, 'sld_file')
kwargs = {
'parent': self,
'directory': default_name,
'filter': 'SLD file (*.sld)',
'options': QtWidgets.QFileDialog.DontUseNativeDialog}
# Query user for filename.
filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
filename = filename_tuple[0]
if filename:
try:
_, extension = os.path.splitext(filename)
if not extension:
filename = '.'.join((filename, 'sld'))
sld_data = self.create_full_sld_data()
sas_gen.SLDReader().write(filename, sld_data)
except Exception:
raise
[docs] def file_name(self):
"""Creates a suitable filename for display on graphs depending on which files are enabled
:return: the filename
:rtype: str
"""
if self.is_nuc:
if self.is_mag:
if self.nuc_sld_data.filename == self.mag_sld_data.filename:
return self.nuc_sld_data.filename
else:
return self.nuc_sld_data.filename + " & " + self.mag_sld_data.filename
else:
return self.nuc_sld_data.filename
else:
if self.is_mag:
return self.mag_sld_data.filename
else:
return "Rectangular grid from GUI"
[docs] def plot3d(self, has_arrow=False):
""" Generate 3D plot in real space with or without arrows
:param has_arrow: Whether to plot arrows for the magnetic field on the plot.
Defaults to `False`
:type has_arrow: bool
"""
sld_data = self.create_full_sld_data()
self.write_new_values_from_gui()
graph_title = " Graph {}: {} 3D SLD Profile".format(self.graph_num,
self.file_name())
if has_arrow:
graph_title += ' - Magnetic Vector as Arrow'
plot3D = Plotter3D(self, graph_title)
plot3D.plot(sld_data, has_arrow=has_arrow)
plot3D.show()
self.graph_num += 1
[docs] def plot_1_2d(self):
""" Generate 1D or 2D plot, called in Compute"""
if self.is_avg or self.is_avg is None:
data = Data1D(x=self.data.x, y=self.data_to_plot)
data.title = "GenSAS {} #{} 1D".format(self.file_name(),
int(self.graph_num))
data.xaxis('\\rm{Q_{x}}', '\AA^{-1}')
data.yaxis('\\rm{Intensity}', 'cm^{-1}')
self.graph_num += 1
else:
data = Data2D(image=numpy.nan_to_num(self.data_to_plot),
qx_data=self.data.qx_data,
qy_data=self.data.qy_data,
q_data=self.data.q_data,
xmin=self.data.xmin, xmax=self.data.ymax,
ymin=self.data.ymin, ymax=self.data.ymax,
err_image=self.data.err_data)
data.title = "GenSAS {} #{} 2D".format(self.file_name(),
int(self.graph_num))
zeros = numpy.ones(data.data.size, dtype=bool)
data.mask = zeros
data.xmin = self.data.xmin
data.xmax = self.data.xmax
data.ymin = self.data.ymin
data.ymax = self.data.ymax
self.graph_num += 1
# TODO
new_item = GuiUtils.createModelItemWithPlot(data, name=data.title)
self.communicator.updateModelFromPerspectiveSignal.emit(new_item)
self.communicator.forcePlotDisplaySignal.emit([new_item, data])
[docs]class Plotter3D(QtWidgets.QDialog, Plotter3DWidget):
[docs] def __init__(self, parent=None, graph_title=''):
self.graph_title = graph_title
QtWidgets.QDialog.__init__(self)
Plotter3DWidget.__init__(self, manager=parent)
self.setWindowTitle(self.graph_title)