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

ESS_GUIESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_opencl
Last change on this file since 28c2586 was 28c2586, checked in by awashington, 7 months ago

Better name for plotLayout in Corfunc perspective

columnLayout was not a descriptive name

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