source: sasview/src/sas/qtgui/Perspectives/Corfunc/CorfuncPerspective.py @ d4881f6a

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since d4881f6a was d4881f6a, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Initial implementation of Adam Washington's Corfunc perspective.
Converted to py3/Qt5.

  • Property mode set to 100755
File size: 13.5 KB
Line 
1"""
2This module provides the intelligence behind the gui interface for Corfunc.
3"""
4# pylint: disable=E1101
5
6# global
7from PyQt5 import QtCore
8from PyQt5 import QtGui, QtWidgets
9
10# sas-global
11# pylint: disable=import-error, no-name-in-module
12import sas.qtgui.Utilities.GuiUtils as GuiUtils
13from sas.sascalc.corfunc.corfunc_calculator import CorfuncCalculator
14# pylint: enable=import-error, no-name-in-module
15
16# local
17from .UI.CorfuncPanel import Ui_CorfuncDialog
18from .CorfuncUtils import WIDGETS as W
19
20from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg \
21    as FigureCanvas
22from matplotlib.figure import Figure
23from numpy.linalg.linalg import LinAlgError
24
25
26class MyMplCanvas(FigureCanvas):
27    """Ultimately, this is a QWidget (as well as a FigureCanvasAgg, etc.)."""
28    def __init__(self, model, width=5, height=4, dpi=100):
29        self.model = model
30        self.fig = Figure(figsize=(width, height), dpi=dpi)
31        self.axes = self.fig.add_subplot(111)
32
33        FigureCanvas.__init__(self, self.fig)
34
35        self.data = None
36        self.extrap = None
37
38    def draw_q_space(self):
39        """Draw the Q space data in the plot window
40
41        This draws the q space data in self.data, as well
42        as the bounds set by self.qmin, self.qmax1, and self.qmax2.
43        It will also plot the extrpolation in self.extrap, if it exists."""
44
45        self.fig.clf()
46
47        self.axes = self.fig.add_subplot(111)
48        self.axes.set_xscale("log")
49        self.axes.set_yscale("log")
50
51        qmin = float(self.model.item(W.W_QMIN).text())
52        qmax1 = float(self.model.item(W.W_QMAX).text())
53        qmax2 = float(self.model.item(W.W_QCUTOFF).text())
54
55        if self.data:
56            self.axes.plot(self.data.x, self.data.y)
57            self.axes.axvline(qmin)
58            self.axes.axvline(qmax1)
59            self.axes.axvline(qmax2)
60            self.axes.set_xlim(min(self.data.x) / 2,
61                               max(self.data.x) * 1.5 - 0.5 * min(self.data.x))
62        if self.extrap:
63            self.axes.plot(self.extrap.x, self.extrap.y)
64
65        self.draw()
66
67    def draw_real_space(self):
68        """
69        This function draws the real space data onto the plot
70
71        The 1d correlation function in self.data, the 3d correlation function
72        in self.data3, and the interface distribution function in self.data_idf
73        are all draw in on the plot in linear cooredinates."""
74        self.fig.clf()
75
76        self.axes = self.fig.add_subplot(111)
77        self.axes.set_xscale("linear")
78        self.axes.set_yscale("linear")
79
80        if self.data:
81            data1, data3, data_idf = self.data
82            self.axes.plot(data1.x, data1.y, label="1D Correlation")
83            self.axes.plot(data3.x, data3.y, label="3D Correlation")
84            self.axes.plot(data_idf.x, data_idf.y,
85                           label="Interface Distribution Function")
86            self.axes.set_xlim(min(data1.x), max(data1.x) / 4)
87            self.axes.legend()
88
89        self.draw()
90
91
92class CorfuncWindow(QtWidgets.QDialog, Ui_CorfuncDialog):
93    """Displays the correlation function analysis of sas data."""
94    name = "Corfunc"  # For displaying in the combo box
95
96    trigger = QtCore.pyqtSignal(tuple)
97
98# pylint: disable=unused-argument
99    def __init__(self, parent=None):
100        super(CorfuncWindow, self).__init__()
101        self.setupUi(self)
102
103        self.setWindowTitle("Corfunc Perspective")
104
105        self.mapper = None
106        self.model = QtGui.QStandardItemModel(self)
107        self.communicate = GuiUtils.Communicate()
108        self._calculator = CorfuncCalculator()
109        self._allow_close = True
110
111        self._canvas = MyMplCanvas(self.model)
112        self._realplot = MyMplCanvas(self.model)
113        self.verticalLayout_7.insertWidget(0, self._canvas)
114        self.verticalLayout_7.insertWidget(1, self._realplot)
115
116        # Connect buttons to slots.
117        # Needs to be done early so default values propagate properly.
118        self.setup_slots()
119
120        # Set up the model.
121        self.setup_model()
122
123        # Set up the mapper
124        self.setup_mapper()
125
126    def setup_slots(self):
127        """Connect the buttons to their appropriate slots."""
128        self.extrapolateBtn.clicked.connect(self.extrapolate)
129        self.extrapolateBtn.setEnabled(False)
130        self.transformBtn.clicked.connect(self.transform)
131        self.transformBtn.setEnabled(False)
132
133        self.calculateBgBtn.clicked.connect(self.calculate_background)
134        self.calculateBgBtn.setEnabled(False)
135
136        self.model.itemChanged.connect(self.model_changed)
137
138        self.trigger.connect(self.finish_transform)
139
140    def setup_model(self):
141        """Populate the model with default data."""
142        self.model.setItem(W.W_QMIN,
143                           QtGui.QStandardItem("0.01"))
144        self.model.setItem(W.W_QMAX,
145                           QtGui.QStandardItem("0.20"))
146        self.model.setItem(W.W_QCUTOFF,
147                           QtGui.QStandardItem("0.22"))
148        self.model.setItem(W.W_BACKGROUND,
149                           QtGui.QStandardItem("0"))
150        self.model.setItem(W.W_TRANSFORM,
151                           QtGui.QStandardItem("Fourier"))
152        self.model.setItem(W.W_GUINIERA,
153                           QtGui.QStandardItem("0.0"))
154        self.model.setItem(W.W_GUINIERB,
155                           QtGui.QStandardItem("0.0"))
156        self.model.setItem(W.W_PORODK,
157                           QtGui.QStandardItem("0.0"))
158        self.model.setItem(W.W_PORODSIGMA,
159                           QtGui.QStandardItem("0.0"))
160        self.model.setItem(W.W_CORETHICK, QtGui.QStandardItem(str(0)))
161        self.model.setItem(W.W_INTTHICK, QtGui.QStandardItem(str(0)))
162        self.model.setItem(W.W_HARDBLOCK, QtGui.QStandardItem(str(0)))
163        self.model.setItem(W.W_CRYSTAL, QtGui.QStandardItem(str(0)))
164        self.model.setItem(W.W_POLY, QtGui.QStandardItem(str(0)))
165        self.model.setItem(W.W_PERIOD, QtGui.QStandardItem(str(0)))
166
167    def model_changed(self, _):
168        """Actions to perform when the data is updated"""
169        if not self.mapper:
170            return
171        self.mapper.toFirst()
172        self._canvas.draw_q_space()
173
174    def _update_calculator(self):
175        self._calculator.lowerq = float(self.model.item(W.W_QMIN).text())
176        qmax1 = float(self.model.item(W.W_QMAX).text())
177        qmax2 = float(self.model.item(W.W_QCUTOFF).text())
178        self._calculator.upperq = (qmax1, qmax2)
179        self._calculator.background = \
180            float(self.model.item(W.W_BACKGROUND).text())
181
182    def extrapolate(self):
183        """Extend the experiemntal data with guinier and porod curves."""
184        self._update_calculator()
185        try:
186            params, extrapolation, _ = self._calculator.compute_extrapolation()
187            self.model.setItem(W.W_GUINIERA, QtGui.QStandardItem(str(params['A'])))
188            self.model.setItem(W.W_GUINIERB, QtGui.QStandardItem(str(params['B'])))
189            self.model.setItem(W.W_PORODK, QtGui.QStandardItem(str(params['K'])))
190            self.model.setItem(W.W_PORODSIGMA,
191                               QtGui.QStandardItem(str(params['sigma'])))
192
193            self._canvas.extrap = extrapolation
194            self._canvas.draw_q_space()
195            self.transformBtn.setEnabled(True)
196        except (LinAlgError, ValueError):
197            message = "These is not enough data in the fitting range. "\
198                      "Try decreasing the upper Q, increasing the "\
199                      "cutoff Q, or increasing the lower Q."
200            QtWidgets.QMessageBox.warning(self, "Calculation Error",
201                                      message)
202            self._canvas.extrap = None
203            self._canvas.draw_q_space()
204
205
206    def transform(self):
207        """Calculate the real space version of the extrapolation."""
208        method = str(self.model.item(W.W_TRANSFORM).text()).lower()
209
210        extrap = self._canvas.extrap
211        background = float(self.model.item(W.W_BACKGROUND).text())
212
213        def updatefn(msg):
214            """Report progress of transformation."""
215            self.communicate.statusBarUpdateSignal.emit(msg)
216
217        def completefn(transforms):
218            """Extract the values from the transforms and plot"""
219            self.trigger.emit(transforms)
220
221        self._update_calculator()
222        self._calculator.compute_transform(extrap, method, background,
223                                           completefn, updatefn)
224
225
226    def finish_transform(self, transforms):
227        params = self._calculator.extract_parameters(transforms[0])
228        self.model.setItem(W.W_CORETHICK,
229                           QtGui.QStandardItem(str(params['d0'])))
230        self.model.setItem(W.W_INTTHICK,
231                           QtGui.QStandardItem(str(params['dtr'])))
232        self.model.setItem(W.W_HARDBLOCK,
233                           QtGui.QStandardItem(str(params['Lc'])))
234        self.model.setItem(W.W_CRYSTAL,
235                           QtGui.QStandardItem(str(params['fill'])))
236        self.model.setItem(W.W_POLY,
237                           QtGui.QStandardItem(str(params['A'])))
238        self.model.setItem(W.W_PERIOD,
239                           QtGui.QStandardItem(str(params['max'])))
240        self._realplot.data = transforms
241        self._realplot.draw_real_space()
242
243    def setup_mapper(self):
244        """Creating mapping between model and gui elements."""
245        self.mapper = QtWidgets.QDataWidgetMapper(self)
246        self.mapper.setOrientation(QtCore.Qt.Vertical)
247        self.mapper.setModel(self.model)
248
249        self.mapper.addMapping(self.qMin, W.W_QMIN)
250        self.mapper.addMapping(self.qMax1, W.W_QMAX)
251        self.mapper.addMapping(self.qMax2, W.W_QCUTOFF)
252        self.mapper.addMapping(self.bg, W.W_BACKGROUND)
253        self.mapper.addMapping(self.transformCombo, W.W_TRANSFORM)
254
255        self.mapper.addMapping(self.guinierA, W.W_GUINIERA)
256        self.mapper.addMapping(self.guinierB, W.W_GUINIERB)
257        self.mapper.addMapping(self.porodK, W.W_PORODK)
258        self.mapper.addMapping(self.porodSigma, W.W_PORODSIGMA)
259
260        self.mapper.addMapping(self.avgCoreThick, W.W_CORETHICK)
261        self.mapper.addMapping(self.avgIntThick, W.W_INTTHICK)
262        self.mapper.addMapping(self.avgHardBlock, W.W_HARDBLOCK)
263        self.mapper.addMapping(self.polydisp, W.W_POLY)
264        self.mapper.addMapping(self.longPeriod, W.W_PERIOD)
265        self.mapper.addMapping(self.localCrystal, W.W_CRYSTAL)
266
267        self.mapper.toFirst()
268
269    def calculate_background(self):
270        """Find a good estimate of the background value."""
271        self._update_calculator()
272        try:
273            background = self._calculator.compute_background()
274            temp = QtGui.QStandardItem(str(background))
275            self.model.setItem(W.W_BACKGROUND, temp)
276        except (LinAlgError, ValueError):
277            message = "These is not enough data in the fitting range. "\
278                      "Try decreasing the upper Q or increasing the cutoff Q"
279            QtWidgets.QMessageBox.warning(self, "Calculation Error",
280                                      message)
281
282
283    # pylint: disable=invalid-name
284    @staticmethod
285    def allowBatch():
286        """
287        We cannot perform corfunc analysis in batch at this time.
288        """
289        return False
290
291    def setData(self, data_item, is_batch=False):
292        """
293        Obtain a QStandardItem object and dissect it to get Data1D/2D
294        Pass it over to the calculator
295        """
296        if not isinstance(data_item, list):
297            msg = "Incorrect type passed to the Corfunc Perpsective"
298            raise AttributeError(msg)
299
300        if not isinstance(data_item[0], QtGui.QStandardItem):
301            msg = "Incorrect type passed to the Corfunc Perspective"
302            raise AttributeError(msg)
303
304        model_item = data_item[0]
305        data = GuiUtils.dataFromItem(model_item)
306        self._calculator.set_data(data)
307        self.calculateBgBtn.setEnabled(True)
308        self.extrapolateBtn.setEnabled(True)
309
310        self.model.setItem(W.W_GUINIERA, QtGui.QStandardItem(""))
311        self.model.setItem(W.W_GUINIERB, QtGui.QStandardItem(""))
312        self.model.setItem(W.W_PORODK, QtGui.QStandardItem(""))
313        self.model.setItem(W.W_PORODSIGMA, QtGui.QStandardItem(""))
314        self.model.setItem(W.W_CORETHICK, QtGui.QStandardItem(""))
315        self.model.setItem(W.W_INTTHICK, QtGui.QStandardItem(""))
316        self.model.setItem(W.W_HARDBLOCK, QtGui.QStandardItem(""))
317        self.model.setItem(W.W_CRYSTAL, QtGui.QStandardItem(""))
318        self.model.setItem(W.W_POLY, QtGui.QStandardItem(""))
319        self.model.setItem(W.W_PERIOD, QtGui.QStandardItem(""))
320
321        self._canvas.data = data
322        self._canvas.extrap = None
323        self._canvas.draw_q_space()
324        self.transformBtn.setEnabled(False)
325
326        self._realplot.data = None
327        self._realplot.draw_real_space()
328
329    def setClosable(self, value=True):
330        """
331        Allow outsiders close this widget
332        """
333        assert isinstance(value, bool)
334
335        self._allow_close = value
336
337    def closeEvent(self, event):
338        """
339        Overwrite QDialog close method to allow for custom widget close
340        """
341        if self._allow_close:
342            # reset the closability flag
343            self.setClosable(value=False)
344            # Tell the MdiArea to close the container
345            self.parentWidget().close()
346            event.accept()
347        else:
348            event.ignore()
349            # Maybe we should just minimize
350            self.setWindowState(QtCore.Qt.WindowMinimized)
351
352    def title(self):
353        """
354        Window title function used by certain error messages.
355        Check DataExplorer.py, line 355
356        """
357        return "Corfunc Perspective"
358    # pylint: enable=invalid-name
Note: See TracBrowser for help on using the repository browser.