import sys
import os
import numpy
import logging
import time
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets
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.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)
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.reader = None
self.sld_data = None
self.parameters = []
self.data = None
self.datafile = None
self.file_name = ''
self.ext = None
self.default_shape = str(self.cbShape.currentText())
self.is_avg = False
self.data_to_plot = None
self.graph_num = 1 # index for name of graph
# combox box
self.cbOptionsCalc.setVisible(False)
# push buttons
self.cmdClose.clicked.connect(self.accept)
self.cmdHelp.clicked.connect(self.onHelp)
self.cmdLoad.clicked.connect(self.loadFile)
self.cmdCompute.clicked.connect(self.onCompute)
self.cmdReset.clicked.connect(self.onReset)
self.cmdSave.clicked.connect(self.onSaveFile)
self.cmdDraw.clicked.connect(lambda: self.plot3d(has_arrow=True))
self.cmdDrawpoints.clicked.connect(lambda: self.plot3d(has_arrow=False))
# 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))
# 0 < Qmax <= 1000
validat_regex_q = QtCore.QRegExp('^1000$|^[+]?(\d{1,3}([.]\d+)?)$')
self.txtQxMax.setValidator(QtGui.QRegExpValidator(validat_regex_q,
self.txtQxMax))
self.txtQxMax.textChanged.connect(self.check_value)
# 2 <= Qbin <= 1000
self.txtNoQBins.setValidator(QtGui.QRegExpValidator(validat_regex_q,
self.txtNoQBins))
self.txtNoQBins.textChanged.connect(self.check_value)
# 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 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 loadFile(self):
"""
Open menu to choose the datafile to load
Only extensions .SLD, .PDB, .OMF, .sld, .pdb, .omf
"""
try:
self.datafile = QtWidgets.QFileDialog.getOpenFileName(
self, "Choose a file", "","All supported files (*.OMF *.omf *.SLD *.sld *.pdb *.PDB);;"
"OMF files (*.OMF *.omf);;"
"SLD files (*.SLD *.sld);;"
"PDB files (*.pdb *.PDB);;"
"All files (*.*)")[0]
if self.datafile:
self.default_shape = str(self.cbShape.currentText())
self.file_name = os.path.basename(str(self.datafile))
self.ext = os.path.splitext(str(self.datafile))[1]
if self.ext in self.omf_reader.ext:
loader = self.omf_reader
elif self.ext in self.sld_reader.ext:
loader = self.sld_reader
elif self.ext in self.pdb_reader.ext:
loader = self.pdb_reader
else:
loader = None
# disable some entries depending on type of loaded file
# (according to documentation)
if self.ext.lower() in ['.sld', '.omf', '.pdb']:
self.txtUpFracIn.setEnabled(False)
self.txtUpFracOut.setEnabled(False)
self.txtUpTheta.setEnabled(False)
if self.reader is not None and self.reader.isrunning():
self.reader.stop()
self.cmdLoad.setEnabled(False)
self.cmdLoad.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=self.complete_loading_ex,
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):
"""
Send the finish message from calculate threads to main thread
"""
self.loadingFinishedSignal.emit(data)
[docs] def complete_loading(self, data=None):
""" Function used in GenRead"""
assert isinstance(data, list)
assert len(data)==1
data = data[0]
self.cbShape.setEnabled(False)
try:
is_pdbdata = False
self.txtData.setText(os.path.basename(str(self.datafile)))
self.is_avg = False
if self.ext in self.omf_reader.ext:
gen = sas_gen.OMF2SLD()
gen.set_data(data)
self.sld_data = gen.get_magsld()
self.check_units()
elif self.ext in self.sld_reader.ext:
self.sld_data = data
elif self.ext in self.pdb_reader.ext:
self.sld_data = data
is_pdbdata = True
# Display combobox of orientation only for pdb data
self.cbOptionsCalc.setVisible(is_pdbdata)
self.update_gui()
except IOError:
log_msg = "Loading Error: " \
"This file format is not supported for GenSAS."
logging.info(log_msg)
raise
except ValueError:
log_msg = "Could not find any data"
logging.info(log_msg)
raise
logging.info("Load Complete")
self.cmdLoad.setEnabled(True)
self.cmdLoad.setText('Load')
self.trigger_plot_3d.emit()
[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 unit
"""
# 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 check_value(self):
"""Check range of text edits for QMax and Number of Qbins """
text_edit = self.sender()
text_edit.setStyleSheet('background-color: rgb(255, 255, 255);')
if text_edit.text():
value = float(str(text_edit.text()))
if text_edit == self.txtQxMax:
if value <= 0 or value > 1000:
text_edit.setStyleSheet('background-color: rgb(255, 182, 193);')
else:
text_edit.setStyleSheet('background-color: rgb(255, 255, 255);')
elif text_edit == self.txtNoQBins:
if value < 2 or value > 1000:
self.txtNoQBins.setStyleSheet('background-color: rgb(255, 182, 193);')
else:
self.txtNoQBins.setStyleSheet('background-color: rgb(255, 255, 255);')
[docs] def update_gui(self):
""" Update the interface with values from loaded data """
self.model.set_is_avg(self.is_avg)
self.model.set_sld_data(self.sld_data)
self.model.params['total_volume'] = len(self.sld_data.sld_n)*self.sld_data.vol_pix[0]
# add condition for activation of save button
self.cmdSave.setEnabled(True)
# activation of 3D plots' buttons (with and without arrows)
self.cmdDraw.setEnabled(self.sld_data is not None)
self.cmdDrawpoints.setEnabled(self.sld_data is not None)
self.txtScale.setText(str(self.model.params['scale']))
self.txtBackground.setText(str(self.model.params['background']))
self.txtSolventSLD.setText(str(self.model.params['solvent_SLD']))
# Volume to write to interface: npts x volume of first pixel
self.txtTotalVolume.setText(str(len(self.sld_data.sld_n)*self.sld_data.vol_pix[0]))
self.txtUpFracIn.setText(str(self.model.params['Up_frac_in']))
self.txtUpFracOut.setText(str(self.model.params['Up_frac_out']))
self.txtUpTheta.setText(str(self.model.params['Up_theta']))
self.txtNoPixels.setText(str(len(self.sld_data.sld_n)))
self.txtNoPixels.setEnabled(False)
list_parameters = ['sld_mx', 'sld_my', 'sld_mz', 'sld_n', 'xnodes',
'ynodes', 'znodes', 'xstepsize', 'ystepsize',
'zstepsize']
list_gui_button = [self.txtMx, self.txtMy, self.txtMz, self.txtNucl,
self.txtXnodes, self.txtYnodes, self.txtZnodes,
self.txtXstepsize, self.txtYstepsize,
self.txtZstepsize]
# Fill right hand side of GUI
for indx, item in enumerate(list_parameters):
if getattr(self.sld_data, item) is None:
list_gui_button[indx].setText('NaN')
else:
value = getattr(self.sld_data, item)
if isinstance(value, numpy.ndarray):
item_for_gui = str(GuiUtils.formatNumber(numpy.average(value), True))
else:
item_for_gui = str(GuiUtils.formatNumber(value, True))
list_gui_button[indx].setText(item_for_gui)
# Enable / disable editing of right hand side of GUI
for indx, item in enumerate(list_parameters):
if indx < 4:
# this condition only applies to Mx,y,z and Nucl
value = getattr(self.sld_data, item)
enable = self.sld_data.pix_type == 'pixel' \
and numpy.min(value) == numpy.max(value)
else:
enable = not self.sld_data.is_data
list_gui_button[indx].setEnabled(enable)
[docs] def write_new_values_from_gui(self):
"""
update parameters using modified inputs from GUI
used before computing
"""
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.txtMx.isModified():
self.sld_data.sld_mx = float(self.txtMx.text())*\
numpy.ones(len(self.sld_data.sld_mx))
if self.txtMy.isModified():
self.sld_data.sld_my = float(self.txtMy.text())*\
numpy.ones(len(self.sld_data.sld_my))
if self.txtMz.isModified():
self.sld_data.sld_mz = float(self.txtMz.text())*\
numpy.ones(len(self.sld_data.sld_mz))
if self.txtNucl.isModified():
self.sld_data.sld_n = float(self.txtNucl.text())*\
numpy.ones(len(self.sld_data.sld_n))
if self.txtXnodes.isModified():
self.sld_data.xnodes = int(self.txtXnodes.text())
if self.txtYnodes.isModified():
self.sld_data.ynodes = int(self.txtYnodes.text())
if self.txtZnodes.isModified():
self.sld_data.znodes = int(self.txtZnodes.text())
if self.txtXstepsize.isModified():
self.sld_data.xstepsize = float(self.txtXstepsize.text())
if self.txtYstepsize.isModified():
self.sld_data.ystepsize = float(self.txtYstepsize.text())
if self.txtZstepsize.isModified():
self.sld_data.zstepsize = float(self.txtZstepsize.text())
if self.cbOptionsCalc.isVisible():
self.is_avg = (self.cbOptionsCalc.currentIndex() == 1)
[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 inputs of textEdit to default values """
try:
# reset values in textedits
self.txtUpFracIn.setText("1.0")
self.txtUpFracOut.setText("1.0")
self.txtUpTheta.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("50")
self.txtQxMax.setText("0.3")
self.txtNoPixels.setText("1000")
self.txtMx.setText("0")
self.txtMy.setText("0")
self.txtMz.setText("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")
# reset Load button and textedit
self.txtData.setText('Default SLD Profile')
self.cmdLoad.setEnabled(True)
self.cmdLoad.setText('Load')
# reset option for calculation
self.cbOptionsCalc.setCurrentIndex(0)
self.cbOptionsCalc.setVisible(False)
# reset shape button
self.cbShape.setCurrentIndex(0)
self.cbShape.setEnabled(True)
# reset compute button
self.cmdCompute.setText('Compute')
self.cmdCompute.setEnabled(True)
# TODO reload default data set
self._create_default_sld_data()
finally:
pass
def _create_default_2d_data(self):
"""
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
def _create_default_sld_data(self):
"""
Copied from previous version
Making default sld-data
"""
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)
def _create_default_1d_data(self):
"""
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 onCompute(self):
"""
Copied from previous version
Execute the computation of I(qx, qy)
"""
# Set default data when nothing loaded yet
if self.sld_data is None:
self._create_default_sld_data()
try:
self.model.set_sld_data(self.sld_data)
self.write_new_values_from_gui()
if self.is_avg or self.is_avg is None:
self._create_default_1d_data()
i_out = numpy.zeros(len(self.data.y))
inputs = [self.data.x, [], i_out]
else:
self._create_default_2d_data()
i_out = numpy.zeros(len(self.data.data))
inputs = [self.data.qx_data, self.data.qy_data, i_out]
logging.info("Computation is in progress...")
self.cmdCompute.setText('Wait...')
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:
log_msg = "{}. stop".format(sys.exc_info()[1])
logging.info(log_msg)
return
def _update(self, value):
"""
Copied from previous version
"""
pass
[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):
"""
Gen compute complete function
:Param input: input list [qx_data, qy_data, i_out]
"""
out = numpy.empty(0)
for ind in range(len(input[0])):
if self.is_avg:
if ind % 1 == 0 and update is not None:
# update()
percentage = int(100.0 * float(ind) / len(input[0]))
update(percentage)
time.sleep(0.001) # 0.1
inputi = [input[0][ind:ind + 1], [], input[2][ind:ind + 1]]
outi = self.model.run(inputi)
out = numpy.append(out, outi)
else:
if ind % 50 == 0 and update is not None:
percentage = int(100.0 * float(ind) / len(input[0]))
update(percentage)
time.sleep(0.001)
inputi = [input[0][ind:ind + 1], input[1][ind:ind + 1],
input[2][ind:ind + 1]]
outi = self.model.runXY(inputi)
out = numpy.append(out, outi)
self.data_to_plot = out
logging.info('Gen computation completed.')
self.cmdCompute.setText('Compute')
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'))
sas_gen.SLDReader().write(filename, self.sld_data)
except:
raise
[docs] def plot3d(self, has_arrow=False):
""" Generate 3D plot in real space with or without arrows """
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(self.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:
numpy.nan_to_num(self.data_to_plot)
data = Data2D(image=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
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):
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)