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

ESS_GUI_opencl
Last change on this file since e0d5b63 was 3beadede, checked in by awashington, 6 years ago

Add more titles and legends to corfunc plots

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