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

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 cb4d219 was cb4d219, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Minor layout changes to corfunc. Transformed plots now in Data Explorer. SASVIEW-608

  • Property mode set to 100644
File size: 14.6 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        # TODO: add interactivity to axvlines so qlimits are immediately updated!
46        self.fig.clf()
47
48        self.axes = self.fig.add_subplot(111)
49        self.axes.set_xscale("log")
50        self.axes.set_yscale("log")
51
52        qmin = float(self.model.item(W.W_QMIN).text())
53        qmax1 = float(self.model.item(W.W_QMAX).text())
54        qmax2 = float(self.model.item(W.W_QCUTOFF).text())
55
56        if self.data:
57            self.axes.plot(self.data.x, self.data.y)
58            self.axes.axvline(qmin)
59            self.axes.axvline(qmax1)
60            self.axes.axvline(qmax2)
61            self.axes.set_xlim(min(self.data.x) / 2,
62                               max(self.data.x) * 1.5 - 0.5 * min(self.data.x))
63        if self.extrap:
64            self.axes.plot(self.extrap.x, self.extrap.y)
65
66        self.draw()
67
68    def draw_real_space(self):
69        """
70        This function draws the real space data onto the plot
71
72        The 1d correlation function in self.data, the 3d correlation function
73        in self.data3, and the interface distribution function in self.data_idf
74        are all draw in on the plot in linear cooredinates."""
75        self.fig.clf()
76
77        self.axes = self.fig.add_subplot(111)
78        self.axes.set_xscale("linear")
79        self.axes.set_yscale("linear")
80
81        if self.data:
82            data1, data3, data_idf = self.data
83            self.axes.plot(data1.x, data1.y, label="1D Correlation")
84            self.axes.plot(data3.x, data3.y, label="3D Correlation")
85            self.axes.plot(data_idf.x, data_idf.y,
86                           label="Interface Distribution Function")
87            self.axes.set_xlim(min(data1.x), max(data1.x) / 4)
88            self.axes.legend()
89
90        self.draw()
91
92
93class CorfuncWindow(QtWidgets.QDialog, Ui_CorfuncDialog):
94    """Displays the correlation function analysis of sas data."""
95    name = "Corfunc"  # For displaying in the combo box
96
97    trigger = QtCore.pyqtSignal(tuple)
98
99# pylint: disable=unused-argument
100    def __init__(self, parent=None):
101        super(CorfuncWindow, self).__init__()
102        self.setupUi(self)
103
104        self.setWindowTitle("Corfunc Perspective")
105
106        self.parent = parent
107        self.mapper = None
108        self.model = QtGui.QStandardItemModel(self)
109        self.communicate = GuiUtils.Communicate()
110        self._calculator = CorfuncCalculator()
111        self._allow_close = True
112        self._model_item = None
113        self.txtLowerQMin.setText("0.0")
114        self.txtLowerQMin.setEnabled(False)
115
116        self._canvas = MyMplCanvas(self.model)
117        self.mainVerticalLayout.insertWidget(0, self._canvas)
118
119        # Connect buttons to slots.
120        # Needs to be done early so default values propagate properly.
121        self.setup_slots()
122
123        # Set up the model.
124        self.setup_model()
125
126        # Set up the mapper
127        self.setup_mapper()
128
129    def setup_slots(self):
130        """Connect the buttons to their appropriate slots."""
131        self.cmdExtrapolate.clicked.connect(self.extrapolate)
132        self.cmdExtrapolate.setEnabled(False)
133        self.cmdTransform.clicked.connect(self.transform)
134        self.cmdTransform.setEnabled(False)
135
136        self.cmdCalculateBg.clicked.connect(self.calculate_background)
137        self.cmdCalculateBg.setEnabled(False)
138
139        self.model.itemChanged.connect(self.model_changed)
140
141        self.trigger.connect(self.finish_transform)
142
143    def setup_model(self):
144        """Populate the model with default data."""
145        self.model.setItem(W.W_QMIN,
146                           QtGui.QStandardItem("0.01"))
147        self.model.setItem(W.W_QMAX,
148                           QtGui.QStandardItem("0.20"))
149        self.model.setItem(W.W_QCUTOFF,
150                           QtGui.QStandardItem("0.22"))
151        self.model.setItem(W.W_BACKGROUND,
152                           QtGui.QStandardItem("0"))
153        #self.model.setItem(W.W_TRANSFORM,
154        #                   QtGui.QStandardItem("Fourier"))
155        self.model.setItem(W.W_GUINIERA,
156                           QtGui.QStandardItem("0.0"))
157        self.model.setItem(W.W_GUINIERB,
158                           QtGui.QStandardItem("0.0"))
159        self.model.setItem(W.W_PORODK,
160                           QtGui.QStandardItem("0.0"))
161        self.model.setItem(W.W_PORODSIGMA,
162                           QtGui.QStandardItem("0.0"))
163        self.model.setItem(W.W_CORETHICK, QtGui.QStandardItem(str(0)))
164        self.model.setItem(W.W_INTTHICK, QtGui.QStandardItem(str(0)))
165        self.model.setItem(W.W_HARDBLOCK, QtGui.QStandardItem(str(0)))
166        self.model.setItem(W.W_CRYSTAL, QtGui.QStandardItem(str(0)))
167        self.model.setItem(W.W_POLY, QtGui.QStandardItem(str(0)))
168        self.model.setItem(W.W_PERIOD, QtGui.QStandardItem(str(0)))
169
170    def model_changed(self, _):
171        """Actions to perform when the data is updated"""
172        if not self.mapper:
173            return
174        self.mapper.toFirst()
175        self._canvas.draw_q_space()
176
177    def _update_calculator(self):
178        self._calculator.lowerq = float(self.model.item(W.W_QMIN).text())
179        qmax1 = float(self.model.item(W.W_QMAX).text())
180        qmax2 = float(self.model.item(W.W_QCUTOFF).text())
181        self._calculator.upperq = (qmax1, qmax2)
182        self._calculator.background = \
183            float(self.model.item(W.W_BACKGROUND).text())
184
185    def extrapolate(self):
186        """Extend the experiemntal data with guinier and porod curves."""
187        self._update_calculator()
188        try:
189            params, extrapolation, _ = self._calculator.compute_extrapolation()
190            self.model.setItem(W.W_GUINIERA, QtGui.QStandardItem("{:.3g}".format(params['A'])))
191            self.model.setItem(W.W_GUINIERB, QtGui.QStandardItem("{:.3g}".format(params['B'])))
192            self.model.setItem(W.W_PORODK, QtGui.QStandardItem("{:.3g}".format(params['K'])))
193            self.model.setItem(W.W_PORODSIGMA,
194                               QtGui.QStandardItem("{:.4g}".format(params['sigma'])))
195
196            self._canvas.extrap = extrapolation
197            self._canvas.draw_q_space()
198            self.cmdTransform.setEnabled(True)
199        except (LinAlgError, ValueError):
200            message = "These is not enough data in the fitting range. "\
201                      "Try decreasing the upper Q, increasing the "\
202                      "cutoff Q, or increasing the lower Q."
203            QtWidgets.QMessageBox.warning(self, "Calculation Error",
204                                      message)
205            self._canvas.extrap = None
206            self._canvas.draw_q_space()
207
208
209    def transform(self):
210        """Calculate the real space version of the extrapolation."""
211        #method = self.model.item(W.W_TRANSFORM).text().lower()
212
213        method = "fourier"
214
215        extrap = self._canvas.extrap
216        background = float(self.model.item(W.W_BACKGROUND).text())
217
218        def updatefn(msg):
219            """Report progress of transformation."""
220            self.communicate.statusBarUpdateSignal.emit(msg)
221
222        def completefn(transforms):
223            """Extract the values from the transforms and plot"""
224            self.trigger.emit(transforms)
225
226        self._update_calculator()
227        self._calculator.compute_transform(extrap, method, background,
228                                           completefn, updatefn)
229
230
231    def finish_transform(self, transforms):
232        params = self._calculator.extract_parameters(transforms[0])
233        self.model.setItem(W.W_CORETHICK, QtGui.QStandardItem("{:.3g}".format(params['d0'])))
234        self.model.setItem(W.W_INTTHICK, QtGui.QStandardItem("{:.3g}".format(params['dtr'])))
235        self.model.setItem(W.W_HARDBLOCK, QtGui.QStandardItem("{:.3g}".format(params['Lc'])))
236        self.model.setItem(W.W_CRYSTAL, QtGui.QStandardItem("{:.3g}".format(params['fill'])))
237        self.model.setItem(W.W_POLY, QtGui.QStandardItem("{:.3g}".format(params['A'])))
238        self.model.setItem(W.W_PERIOD, QtGui.QStandardItem("{:.3g}".format(params['max'])))
239        #self._realplot.data = transforms
240
241        self.update_real_space_plot(transforms)
242
243        #self._realplot.draw_real_space()
244
245    def update_real_space_plot(self, datas):
246        """take the datas tuple and create a plot in DE"""
247
248        assert isinstance(datas, tuple)
249        plot_id = id(self)
250        titles = ['1D Correlation', '3D Correlation', 'Interface Distribution Function']
251        for i, plot in enumerate(datas):
252            plot_to_add = self.parent.createGuiData(plot)
253            # set plot properties
254            title = plot_to_add.title
255            plot_to_add.scale = 'linear'
256            plot_to_add.symbol = 'Line'
257            plot_to_add._xaxis = "x"
258            plot_to_add._xunit = "A"
259            plot_to_add._yaxis = "\Gamma"
260            if i < len(titles):
261                title = titles[i]
262                plot_to_add.name = titles[i]
263            GuiUtils.updateModelItemWithPlot(self._model_item, plot_to_add, title)
264            #self.axes.set_xlim(min(data1.x), max(data1.x) / 4)
265        pass
266
267    def setup_mapper(self):
268        """Creating mapping between model and gui elements."""
269        self.mapper = QtWidgets.QDataWidgetMapper(self)
270        self.mapper.setOrientation(QtCore.Qt.Vertical)
271        self.mapper.setModel(self.model)
272
273        self.mapper.addMapping(self.txtLowerQMax, W.W_QMIN)
274        self.mapper.addMapping(self.txtUpperQMin, W.W_QMAX)
275        self.mapper.addMapping(self.txtUpperQMax, W.W_QCUTOFF)
276        self.mapper.addMapping(self.txtBackground, W.W_BACKGROUND)
277        #self.mapper.addMapping(self.transformCombo, W.W_TRANSFORM)
278
279        self.mapper.addMapping(self.txtGuinierA, W.W_GUINIERA)
280        self.mapper.addMapping(self.txtGuinierB, W.W_GUINIERB)
281        self.mapper.addMapping(self.txtPorodK, W.W_PORODK)
282        self.mapper.addMapping(self.txtPorodSigma, W.W_PORODSIGMA)
283
284        self.mapper.addMapping(self.txtAvgCoreThick, W.W_CORETHICK)
285        self.mapper.addMapping(self.txtAvgIntThick, W.W_INTTHICK)
286        self.mapper.addMapping(self.txtAvgHardBlock, W.W_HARDBLOCK)
287        self.mapper.addMapping(self.txtPolydisp, W.W_POLY)
288        self.mapper.addMapping(self.txtLongPeriod, W.W_PERIOD)
289        self.mapper.addMapping(self.txtLocalCrystal, W.W_CRYSTAL)
290
291        self.mapper.toFirst()
292
293    def calculate_background(self):
294        """Find a good estimate of the background value."""
295        self._update_calculator()
296        try:
297            background = self._calculator.compute_background()
298            temp = QtGui.QStandardItem("{:.4g}".format(background))
299            self.model.setItem(W.W_BACKGROUND, temp)
300        except (LinAlgError, ValueError):
301            message = "These is not enough data in the fitting range. "\
302                      "Try decreasing the upper Q or increasing the cutoff Q"
303            QtWidgets.QMessageBox.warning(self, "Calculation Error",
304                                      message)
305
306
307    # pylint: disable=invalid-name
308    @staticmethod
309    def allowBatch():
310        """
311        We cannot perform corfunc analysis in batch at this time.
312        """
313        return False
314
315    def setData(self, data_item, is_batch=False):
316        """
317        Obtain a QStandardItem object and dissect it to get Data1D/2D
318        Pass it over to the calculator
319        """
320        if not isinstance(data_item, list):
321            msg = "Incorrect type passed to the Corfunc Perpsective"
322            raise AttributeError(msg)
323
324        if not isinstance(data_item[0], QtGui.QStandardItem):
325            msg = "Incorrect type passed to the Corfunc Perspective"
326            raise AttributeError(msg)
327
328        model_item = data_item[0]
329        data = GuiUtils.dataFromItem(model_item)
330        self._model_item = model_item
331        self._calculator.set_data(data)
332        self.cmdCalculateBg.setEnabled(True)
333        self.cmdExtrapolate.setEnabled(True)
334
335        self.model.setItem(W.W_GUINIERA, QtGui.QStandardItem(""))
336        self.model.setItem(W.W_GUINIERB, QtGui.QStandardItem(""))
337        self.model.setItem(W.W_PORODK, QtGui.QStandardItem(""))
338        self.model.setItem(W.W_PORODSIGMA, QtGui.QStandardItem(""))
339        self.model.setItem(W.W_CORETHICK, QtGui.QStandardItem(""))
340        self.model.setItem(W.W_INTTHICK, QtGui.QStandardItem(""))
341        self.model.setItem(W.W_HARDBLOCK, QtGui.QStandardItem(""))
342        self.model.setItem(W.W_CRYSTAL, QtGui.QStandardItem(""))
343        self.model.setItem(W.W_POLY, QtGui.QStandardItem(""))
344        self.model.setItem(W.W_PERIOD, QtGui.QStandardItem(""))
345
346        self._canvas.data = data
347        self._canvas.extrap = None
348        self._canvas.draw_q_space()
349        self.cmdTransform.setEnabled(False)
350
351        #self._realplot.data = None
352        #self._realplot.draw_real_space()
353
354    def setClosable(self, value=True):
355        """
356        Allow outsiders close this widget
357        """
358        assert isinstance(value, bool)
359
360        self._allow_close = value
361
362    def closeEvent(self, event):
363        """
364        Overwrite QDialog close method to allow for custom widget close
365        """
366        if self._allow_close:
367            # reset the closability flag
368            self.setClosable(value=False)
369            # Tell the MdiArea to close the container
370            self.parentWidget().close()
371            event.accept()
372        else:
373            event.ignore()
374            # Maybe we should just minimize
375            self.setWindowState(QtCore.Qt.WindowMinimized)
376
377    def title(self):
378        """
379        Window title function used by certain error messages.
380        Check DataExplorer.py, line 355
381        """
382        return "Corfunc Perspective"
383    # pylint: enable=invalid-name
Note: See TracBrowser for help on using the repository browser.