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

ESS_GUIESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_opencl
Last change on this file since d215517 was d215517, checked in by awashington, 5 years ago

Better plotting defaults for Corfunc in q-space

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