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

ESS_GUIESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_opencl
Last change on this file since f7e6b30 was f7e6b30, checked in by awashington, 17 months ago

Remote artificial limits on corfunc range.

With the new navigation toolbars, they aren't needed.

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