"""
Adds a linear fit plot to the chart
"""
import re
import numpy
from numbers import Number
from typing import Optional
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets
from sas.qtgui.Utilities.GuiUtils import formatNumber, DoubleValidator
from sas.qtgui.Plotting import Fittings
from sas.qtgui.Plotting import DataTransform
from sas.qtgui.Plotting.LineModel import LineModel
from sas.qtgui.Plotting.QRangeSlider import QRangeSlider
import sas.qtgui.Utilities.GuiUtils as GuiUtils
# Local UI
from sas.qtgui.UI import main_resources_rc
from sas.qtgui.Plotting.UI.LinearFitUI import Ui_LinearFitUI
[docs]class LinearFit(QtWidgets.QDialog, Ui_LinearFitUI):
updatePlot = QtCore.pyqtSignal(tuple)
[docs] def __init__(self, parent=None,
data=None,
max_range=(0.0, 0.0),
fit_range=(0.0, 0.0),
xlabel="",
ylabel=""):
super(LinearFit, self).__init__(parent)
self.setupUi(self)
# disable the context help icon
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
assert(isinstance(max_range, tuple))
assert(isinstance(fit_range, tuple))
self.data = data
self.parent = parent
self.max_range = max_range
# Set fit minimum to 0.0 if below zero
if fit_range[0] < 0.0:
fit_range = (0.0, fit_range[1])
self.fit_range = fit_range
self.xLabel = xlabel
self.yLabel = ylabel
self.rg_on = False
self.rg_yx = False
self.bg_on = False
# Scale dependent content
self.guinier_box.setVisible(False)
if (self.yLabel == "ln(y)" or self.yLabel == "ln(y*x)") and \
(self.xLabel == "x^(2)"):
if self.yLabel == "ln(y*x)":
self.label_12.setText('<html><head/><body><p>Rod diameter [Å]</p></body></html>')
self.rg_yx = True
self.rg_on = True
self.guinier_box.setVisible(True)
if (self.xLabel == "x^(4)") and (self.yLabel == "y*x^(4)"):
self.bg_on = True
self.label_3.setText('Background')
self.x_is_log = self.xLabel == "log10(x)"
self.y_is_log = self.yLabel == "log10(y)"
self.txtFitRangeMin.setValidator(DoubleValidator())
self.txtFitRangeMax.setValidator(DoubleValidator())
# Default values in the line edits
self.txtA.setText("1")
self.txtB.setText("1")
self.txtAerr.setText("0")
self.txtBerr.setText("0")
self.txtChi2.setText("0")
# Initial ranges
self.txtRangeMin.setText(str(max_range[0]))
self.txtRangeMax.setText(str(max_range[1]))
# Assure nice display of ranges
fr_min = GuiUtils.formatNumber(fit_range[0])
fr_max = GuiUtils.formatNumber(fit_range[1])
self.txtFitRangeMin.setText(str(fr_min))
self.txtFitRangeMax.setText(str(fr_max))
# cast xLabel into html
label = re.sub(r'\^\((.)\)(.*)', r'<span style=" vertical-align:super;">\1</span>\2',
str(self.xLabel).rstrip())
self.lblRange.setText('Fit range of ' + label)
self.model = LineModel()
# Display the fittings values
self.default_A = self.model.getParam('A')
self.default_B = self.model.getParam('B')
self.cstA = Fittings.Parameter(self.model, 'A', self.default_A)
self.cstB = Fittings.Parameter(self.model, 'B', self.default_B)
self.transform = DataTransform
self.q_sliders = None
self.drawSliders()
self.setFixedSize(self.minimumSizeHint())
# connect Fit button
self.cmdFit.clicked.connect(self.fit)
[docs] def setRangeLabel(self, label=""):
"""
Overwrite default fit range label to correspond to actual unit
"""
assert(isinstance(label, str))
self.lblRange.setText(label)
[docs] def range(self):
return (float(self.txtFitRangeMin.text()) if float(self.txtFitRangeMin.text()) > 0 else 0.0,
float(self.txtFitRangeMax.text()))
[docs] def fit(self, event):
"""
Performs the fit. Receive an event when clicking on
the button Fit.Computes chisqr ,
A and B parameters of the best linear fit y=Ax +B
Push a plottable to the caller
"""
tempx = []
tempy = []
tempdy = []
# Checks to assure data correctness
if len(self.data.view.x) < 2:
return
if not self.checkFitValues(self.txtFitRangeMin):
return
self.xminFit, self.xmaxFit = self.range()
xmin = self.xminFit
xmax = self.xmaxFit
xminView = xmin
xmaxView = xmax
# Set the qmin and qmax in the panel that matches the
# transformed min and max
value_xmin = self.floatInvTransform(xmin)
value_xmax = self.floatInvTransform(xmax)
self.txtRangeMin.setText(formatNumber(value_xmin))
self.txtRangeMax.setText(formatNumber(value_xmax))
tempx, tempy, tempdy = self.origData()
# Find the fitting parameters
self.cstA = Fittings.Parameter(self.model, 'A', self.default_A)
self.cstB = Fittings.Parameter(self.model, 'B', self.default_B)
tempdy = numpy.asarray(tempdy)
tempdy[tempdy == 0] = 1
if self.x_is_log:
xmin = numpy.log10(xmin)
xmax = numpy.log10(xmax)
chisqr, out, cov = Fittings.sasfit(self.model,
[self.cstA, self.cstB],
tempx, tempy, tempdy,
xmin, xmax)
# Use chi2/dof
if len(tempx) > 0:
chisqr = chisqr / len(tempx)
# Check that cov and out are iterable before displaying them
errA = numpy.sqrt(cov[0][0]) if cov is not None else 0
errB = numpy.sqrt(cov[1][1]) if cov is not None else 0
cstA = out[0] if out is not None else 0.0
cstB = out[1] if out is not None else 0.0
# Reset model with the right values of A and B
self.model.setParam('A', float(cstA))
self.model.setParam('B', float(cstB))
tempx = []
tempy = []
y_model = 0.0
# load tempy with the minimum transformation
y_model = self.model.run(xmin)
tempx.append(xminView)
tempy.append(numpy.power(10.0, y_model) if self.y_is_log else y_model)
# load tempy with the maximum transformation
y_model = self.model.run(xmax)
tempx.append(xmaxView)
tempy.append(numpy.power(10.0, y_model) if self.y_is_log else y_model)
# Set the fit parameter display when FitDialog is opened again
self.Avalue = cstA
self.Bvalue = cstB
self.ErrAvalue = errA
self.ErrBvalue = errB
self.Chivalue = chisqr
# Update the widget
self.txtA.setText(formatNumber(self.Avalue))
self.txtAerr.setText(formatNumber(self.ErrAvalue))
self.txtB.setText(formatNumber(self.Bvalue))
self.txtBerr.setText(formatNumber(self.ErrBvalue))
self.txtChi2.setText(formatNumber(self.Chivalue))
# Possibly Guinier analysis
i0 = numpy.exp(cstB)
self.txtGuinier_1.setText(formatNumber(i0))
err = numpy.abs(numpy.exp(cstB) * errB)
self.txtGuinier1_Err.setText(formatNumber(err))
if self.rg_yx:
rg = numpy.sqrt(-2 * float(cstA))
diam = 4 * numpy.sqrt(-float(cstA))
value = formatNumber(diam)
if rg is not None and rg != 0:
err = formatNumber(8 * float(errA) / diam)
else:
err = ''
else:
rg = numpy.sqrt(-3 * float(cstA))
value = formatNumber(rg)
if rg is not None and rg != 0:
err = formatNumber(3 * float(errA) / (2 * rg))
else:
err = ''
self.txtGuinier_2.setText(value)
self.txtGuinier2_Err.setText(err)
value = formatNumber(rg * self.floatInvTransform(self.xminFit))
self.txtGuinier_4.setText(value)
value = formatNumber(rg * self.floatInvTransform(self.xmaxFit))
self.txtGuinier_3.setText(value)
tempx = numpy.array(tempx)
tempy = numpy.array(tempy)
self.clearSliders()
self.updatePlot.emit((tempx, tempy))
self.drawSliders()
[docs] def origData(self):
# Store the transformed values of view x, y and dy before the fit
xmin_check = numpy.log10(self.xminFit)
# Local shortcuts
x = self.data.view.x
y = self.data.view.y
dy = self.data.view.dy
if self.y_is_log:
if self.x_is_log:
tempy = [numpy.log10(y[i])
for i in range(len(x)) if x[i] >= xmin_check]
tempdy = [DataTransform.errToLogX(y[i], 0, dy[i], 0)
for i in range(len(x)) if x[i] >= xmin_check]
else:
tempy = list(map(numpy.log10, y))
tempdy = list(map(lambda t1,t2:DataTransform.errToLogX(t1,0,t2,0),y,dy))
else:
tempy = y
tempdy = dy
if self.x_is_log:
tempx = [numpy.log10(x) for x in self.data.view.x if x > xmin_check]
else:
tempx = x
return numpy.array(tempx), numpy.array(tempy), numpy.array(tempdy)
[docs] def checkFitValues(self, item):
"""
Check the validity of input values
"""
flag = True
value = item.text()
p_white = item.palette()
p_white.setColor(item.backgroundRole(), QtCore.Qt.white)
p_pink = item.palette()
p_pink.setColor(item.backgroundRole(), QtGui.QColor(255, 128, 128))
item.setAutoFillBackground(True)
# Check for possible values entered
if self.x_is_log:
if float(value) > 0:
item.setPalette(p_white)
else:
flag = False
item.setPalette(p_pink)
return flag
[docs] def drawSliders(self):
"""Show new Q-range sliders"""
self.data.show_q_range_sliders = True
self.q_sliders = QRangeSlider(self.parent, self.parent.ax, data=self.data)
self.q_sliders.line_min.input = self.txtFitRangeMin
self.q_sliders.line_max.input = self.txtFitRangeMax
# Ensure values are updated on redraw of plots
self.q_sliders.line_min.inputChanged()
self.q_sliders.line_max.inputChanged()
[docs] def clearSliders(self):
"""Clear existing sliders"""
if self.q_sliders:
self.q_sliders.clear()
self.data.show_q_range_sliders = False
self.q_sliders = None
[docs] def closeEvent(self, ev):
self.clearSliders()
self.parent.update()
[docs] def accept(self, ev):
self.close()
[docs] def reject(self, ev):
self.close()