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

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

Move corfunc plots into a separate column

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