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

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

Add axes labels to Corfunc

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