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

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

Focus on plots when resizing in Corfunc

It needed to be easier to make the plots larger.

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