Skip to content

Commit 3e24752

Browse files
authored
Merge pull request #267 from zhujun98/add_curve_fitting_in_histogram_window
Add curve fitting in histogram window
2 parents ea6fefc + 2d5caea commit 3e24752

File tree

8 files changed

+131
-35
lines changed

8 files changed

+131
-35
lines changed

extra_foam/algorithms/curve_fitting.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,11 @@ def __init__(self):
8383
self.n = 4
8484

8585
def __call__(self, x, a, b, c, d):
86-
return a / ((x - c)**2 + b**2) + d
86+
return a / ((x - b)**2 + c**2) + d
8787

8888
@staticmethod
8989
def format(a, b, c, d):
90-
return f"y = a / (4 * (x - c)^2 + b ^ 2) + d, \n" \
90+
return f"y = a / (4 * (x - b)^2 + c ^ 2) + d, \n" \
9191
f"a = {a:.4E}, b = {b:.4E}, c = {c:.4E}, d = {d:.4E}"
9292

9393
class Erf(_BaseCurveFitting):

extra_foam/algorithms/tests/test_curve_fitting.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@ def testLorentzian(self):
7272
105.22065798, 78.23873672, 52.12398640, 40.89729233, 40.60966561,
7373
35.44761773, 40.06147386, 39.26495213, 36.69235285, 32.29345426])
7474

75-
popt = algo.fit(x, y, p0=[100, 1, 100, 1])
75+
popt = algo.fit(x, y, p0=[100, 100, 1, 1])
7676
assert abs(popt[0] - 100) < 10
77-
assert abs(popt[1] - 1) < 0.2
78-
assert abs(popt[2] - 100) < 1
77+
assert abs(popt[1] - 100) < 1
78+
assert abs(popt[2] - 1) < 0.2
7979
assert abs(popt[3] - 30) < 5
8080

8181
def testERF(self):

extra_foam/gui/ctrl_widgets/correlation_ctrl_widget.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
QLabel, QPushButton, QTableWidget
1818
)
1919

20-
from .curve_fitting_ctrl_widget import _BaseFittingCtrlWidget, FittingType
20+
from .curve_fitting_ctrl_widget import _BaseFittingCtrlWidget
2121
from .base_ctrl_widgets import _AbstractCtrlWidget
2222
from .smart_widgets import SmartLineEdit
2323
from ..gui_helpers import invert_dict
@@ -29,7 +29,7 @@
2929
_DEFAULT_RESOLUTION = 0.0
3030

3131

32-
class FittingCtrlWidget(_BaseFittingCtrlWidget):
32+
class _FittingCtrlWidget(_BaseFittingCtrlWidget):
3333

3434
def __init__(self, *args, **kwargs):
3535
super().__init__(*args, **kwargs)
@@ -121,7 +121,7 @@ def __init__(self, *args, **kwargs):
121121
}
122122
}
123123

124-
self._fitting = FittingCtrlWidget()
124+
self._fitting = _FittingCtrlWidget()
125125

126126
self.initParamTable()
127127
self.initUI()

extra_foam/gui/ctrl_widgets/curve_fitting_ctrl_widget.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212
import numpy as np
1313

1414
from PyQt5.QtWidgets import (
15-
QCheckBox, QComboBox, QGridLayout, QLabel, QPlainTextEdit, QPushButton,
16-
QWidget
15+
QCheckBox, QComboBox, QPlainTextEdit, QPushButton, QWidget
1716
)
1817

1918
from .smart_widgets import SmartLineEdit

extra_foam/gui/ctrl_widgets/histogram_ctrl_widget.py

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,56 @@
99
"""
1010
from collections import OrderedDict
1111

12-
from PyQt5.QtCore import Qt
12+
from PyQt5.QtCore import pyqtSignal, Qt
1313
from PyQt5.QtGui import QIntValidator
1414
from PyQt5.QtWidgets import (
1515
QCheckBox, QComboBox, QFrame, QGridLayout, QHBoxLayout, QLabel,
16-
QPushButton
16+
QPushButton, QWidget
1717
)
1818

1919
from .base_ctrl_widgets import _AbstractCtrlWidget
20+
from .curve_fitting_ctrl_widget import _BaseFittingCtrlWidget, FittingType
2021
from .smart_widgets import SmartBoundaryLineEdit, SmartLineEdit
2122
from ..gui_helpers import invert_dict
2223
from ...config import AnalysisType
2324
from ...database import Metadata as mt
2425

2526

27+
class _FittingCtrlWidget(_BaseFittingCtrlWidget):
28+
29+
def __init__(self, *args, **kwargs):
30+
super().__init__(*args, **kwargs)
31+
32+
self.initUI()
33+
self.initConnections()
34+
35+
def initUI(self):
36+
AR = Qt.AlignRight
37+
38+
layout = QGridLayout()
39+
layout.addWidget(QLabel("Param a0 = "), 1, 0, AR)
40+
layout.addWidget(self._params[0], 1, 1)
41+
layout.addWidget(QLabel("Param b0 = "), 1, 2, AR)
42+
layout.addWidget(self._params[1], 1, 3)
43+
layout.addWidget(QLabel("Param c0 = "), 1, 4, AR)
44+
layout.addWidget(self._params[2], 1, 5)
45+
layout.addWidget(QLabel("Param d0 = "), 2, 0, AR)
46+
layout.addWidget(self._params[3], 2, 1)
47+
layout.addWidget(QLabel("Param e0 = "), 2, 2, AR)
48+
layout.addWidget(self._params[4], 2, 3)
49+
layout.addWidget(QLabel("Param f0 = "), 2, 4, AR)
50+
layout.addWidget(self._params[5], 2, 5)
51+
layout.addWidget(self.fit_btn, 3, 0, 1, 2)
52+
layout.addWidget(self.clear_btn, 3, 2, 1, 2)
53+
layout.addWidget(QLabel("Fit type: "), 3, 4, AR)
54+
layout.addWidget(self.fit_type_cb, 3, 5)
55+
layout.addWidget(self._output, 4, 0, 1, 6)
56+
layout.setContentsMargins(0, 0, 0, 0)
57+
self.setLayout(layout)
58+
59+
self.setFixedWidth(self.minimumSizeHint().width())
60+
61+
2662
class HistogramCtrlWidget(_AbstractCtrlWidget):
2763
"""Widget for setting up histogram analysis parameters."""
2864

@@ -33,6 +69,9 @@ class HistogramCtrlWidget(_AbstractCtrlWidget):
3369
})
3470
_analysis_types_inv = invert_dict(_analysis_types)
3571

72+
fit_curve_sgn = pyqtSignal()
73+
clear_fitting_sgn = pyqtSignal()
74+
3675
def __init__(self, *args, **kwargs):
3776
super().__init__(*args, **kwargs)
3877

@@ -53,6 +92,8 @@ def __init__(self, *args, **kwargs):
5392

5493
self._reset_btn = QPushButton("Reset")
5594

95+
self._fitting = _FittingCtrlWidget()
96+
5697
self.initUI()
5798
self.initConnections()
5899

@@ -61,27 +102,23 @@ def __init__(self, *args, **kwargs):
61102
def initUI(self):
62103
"""Overload."""
63104
AR = Qt.AlignRight
64-
AT = Qt.AlignTop
65105

66-
lwidget = QFrame()
106+
ctrl_widget = QFrame()
67107
llayout = QGridLayout()
68108
llayout.addWidget(QLabel("Analysis type: "), 0, 0, AR)
69109
llayout.addWidget(self._analysis_type_cb, 0, 1)
70-
llayout.addWidget(self._pulse_resolved_cb, 1, 1)
71-
llayout.addWidget(self._reset_btn, 2, 0, 1, 2)
72-
lwidget.setLayout(llayout)
73-
74-
rwidget = QFrame()
75-
rlayout = QGridLayout()
76-
rlayout.addWidget(QLabel("Bin range: "), 1, 0, AR)
77-
rlayout.addWidget(self._bin_range_le, 1, 1)
78-
rlayout.addWidget(QLabel("# of bins: "), 2, 0, AR)
79-
rlayout.addWidget(self._n_bins_le, 2, 1)
80-
rwidget.setLayout(rlayout)
110+
llayout.addWidget(self._pulse_resolved_cb, 0, 3, AR)
111+
llayout.addWidget(self._reset_btn, 0, 5)
112+
llayout.addWidget(QLabel("Bin range: "), 1, 2, AR)
113+
llayout.addWidget(self._bin_range_le, 1, 3)
114+
llayout.addWidget(QLabel("# of bins: "), 1, 4, AR)
115+
llayout.addWidget(self._n_bins_le, 1, 5)
116+
llayout.setContentsMargins(0, 0, 0, 0)
117+
ctrl_widget.setLayout(llayout)
81118

82119
layout = QHBoxLayout()
83-
layout.addWidget(lwidget, alignment=AT)
84-
layout.addWidget(rwidget, alignment=AT)
120+
layout.addWidget(ctrl_widget, alignment=Qt.AlignTop)
121+
layout.addWidget(self._fitting)
85122
self.setLayout(layout)
86123

87124
def initConnections(self):
@@ -100,6 +137,9 @@ def initConnections(self):
100137

101138
self._reset_btn.clicked.connect(mediator.onHistReset)
102139

140+
self._fitting.fit_btn.clicked.connect(self.fit_curve_sgn)
141+
self._fitting.clear_btn.clicked.connect(self.clear_fitting_sgn)
142+
103143
def updateMetaData(self):
104144
"""Overload."""
105145
self._analysis_type_cb.currentTextChanged.emit(
@@ -127,3 +167,6 @@ def loadMetaData(self):
127167
def resetAnalysisType(self):
128168
self._analysis_type_cb.setCurrentText(
129169
self._analysis_types_inv[AnalysisType.UNDEFINED])
170+
171+
def fit_curve(self, x, y):
172+
return self._fitting.fit(x, y)

extra_foam/gui/windows/histogram_w.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from .base_window import _AbstractPlotWindow
1616
from ..ctrl_widgets import HistogramCtrlWidget
17+
from ..misc_widgets import FColor
1718
from ..plot_widgets import HistMixin, PlotWidgetF, TimedPlotWidgetF
1819
from ...config import config
1920

@@ -51,6 +52,7 @@ def __init__(self, *, parent=None):
5152
super().__init__(parent=parent)
5253

5354
self._plot = self.plotBar()
55+
self._fitted = self.plotCurve(pen=FColor.mkPen('r'))
5456

5557
self._title_template = Template(
5658
f"FOM Histogram (mean: $mean, median: $median, std: $std)")
@@ -68,6 +70,12 @@ def refresh(self):
6870
self._plot.setData(bin_centers, hist.hist)
6971
self.updateTitle(hist.mean, hist.median, hist.std)
7072

73+
def data(self):
74+
return self._plot.data()
75+
76+
def setFitted(self, x, y):
77+
self._fitted.setData(x, y)
78+
7179

7280
class HistogramWindow(_AbstractPlotWindow):
7381
"""HistogramWindow class.
@@ -115,7 +123,17 @@ def initUI(self):
115123

116124
def initConnections(self):
117125
"""Override."""
118-
pass
126+
self._ctrl_widget.fit_curve_sgn.connect(self._onCurveFit)
127+
self._ctrl_widget.clear_fitting_sgn.connect(self._onClearFitting)
128+
129+
def _onCurveFit(self):
130+
data = self._fom_hist.data()
131+
132+
x, y = self._ctrl_widget.fit_curve(*data)
133+
self._fom_hist.setFitted(x, y)
134+
135+
def _onClearFitting(self):
136+
self._fom_hist.setFitted([], [])
119137

120138
def closeEvent(self, QCloseEvent):
121139
self._ctrl_widget.resetAnalysisType()

extra_foam/gui/windows/tests/test_plot_widgets.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
import unittest
22
from unittest.mock import patch
33

4-
import numpy as np
5-
64
from extra_foam.logger import logger
75
from extra_foam.gui import mkQApp
8-
from extra_foam.gui.ctrl_widgets.correlation_ctrl_widget import (
9-
FittingType, FittingCtrlWidget
10-
)
116
from extra_foam.gui.plot_widgets.plot_items import ScatterPlotItem
127
from extra_foam.pipeline.data_model import ProcessedData
138
from extra_foam.pipeline.tests import _TestDataMixin

extra_foam/gui/windows/tests/test_plot_windows.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def testOpenCloseWindows(self):
101101
self.assertIsInstance(histogram_window, HistogramWindow)
102102
self._checkHistogramWindow(histogram_window)
103103
self._checkHistogramCtrlWidget(histogram_window)
104+
self._checkHistogramCurveFitting(histogram_window)
104105

105106
poi_window = self._check_open_window(self.poi_action)
106107
self.assertIsInstance(poi_window, PulseOfInterestWindow)
@@ -596,11 +597,11 @@ def _checkCorrelationCurveFitting(self, win):
596597
QTest.mouseClick(fitting.fit_btn, Qt.LeftButton)
597598
mocked_fit.assert_called_once_with(x1, y1, p0=[1.1, 2.2])
598599

599-
mocked_fit.side_effect=RuntimeError("runtime error")
600+
mocked_fit.side_effect = RuntimeError("runtime error")
600601
QTest.mouseClick(fitting.fit_btn, Qt.LeftButton)
601602
self.assertEqual("RuntimeError('runtime error')", fitting._output.toPlainText())
602603

603-
mocked_fit.side_effect=ValueError("value error")
604+
mocked_fit.side_effect = ValueError("value error")
604605
QTest.mouseClick(fitting.fit_btn, Qt.LeftButton)
605606
self.assertEqual("ValueError('value error')", fitting._output.toPlainText())
606607

@@ -808,6 +809,46 @@ def _checkHistogramCtrlWidget(self, win):
808809
self.assertEqual("55", widget._n_bins_le.text())
809810
self.assertEqual(True, widget._pulse_resolved_cb.isChecked())
810811

812+
def _checkHistogramCurveFitting(self, win):
813+
widget = win._ctrl_widget
814+
fitting = widget._fitting
815+
816+
with patch.object(win._fom_hist, "setFitted") as mocked_set_fitted:
817+
self.assertIsNone(fitting._algo)
818+
QTest.mouseClick(fitting.fit_btn, Qt.LeftButton)
819+
mocked_set_fitted.assert_called_with(None, None)
820+
mocked_set_fitted.reset_mock()
821+
822+
fitting.fit_type_cb.setCurrentText("Gaussian")
823+
with patch.object(fitting._algo, "fit") as mocked_fit:
824+
x1, y1 = np.random.rand(1), np.random.rand(1)
825+
win._fom_hist._plot.setData(x1, y1)
826+
QTest.mouseClick(fitting.fit_btn, Qt.LeftButton)
827+
mocked_fit.assert_not_called()
828+
mocked_set_fitted.assert_called_with(None, None)
829+
mocked_set_fitted.reset_mock()
830+
self.assertEqual("Not enough data", fitting._output.toPlainText())
831+
832+
x1, y1 = np.random.rand(10), np.random.rand(10)
833+
win._fom_hist._plot.setData(x1, y1)
834+
QTest.mouseClick(fitting.fit_btn, Qt.LeftButton)
835+
mocked_fit.assert_called_once_with(x1, y1, p0=[1.0, 1.0, 1.0, 1.0])
836+
mocked_fit.reset_mock()
837+
mocked_set_fitted.assert_called_once()
838+
mocked_set_fitted.reset_mock()
839+
fitting._params[2].setText("1.1")
840+
fitting._params[3].setText("2.2")
841+
QTest.mouseClick(fitting.fit_btn, Qt.LeftButton)
842+
mocked_fit.assert_called_once_with(x1, y1, p0=[1.0, 1.0, 1.1, 2.2])
843+
844+
mocked_fit.side_effect = RuntimeError("runtime error")
845+
QTest.mouseClick(fitting.fit_btn, Qt.LeftButton)
846+
self.assertEqual("RuntimeError('runtime error')", fitting._output.toPlainText())
847+
848+
mocked_fit.side_effect = ValueError("value error")
849+
QTest.mouseClick(fitting.fit_btn, Qt.LeftButton)
850+
self.assertEqual("ValueError('value error')", fitting._output.toPlainText())
851+
811852
def _checkHistogramCtrlWidgetTs(self, win):
812853
widget = win._ctrl_widget
813854

0 commit comments

Comments
 (0)