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

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since bb57068 was e90988c, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Show help pages in default browser. Fixed some help links and modified unit tests. SASVIEW-800

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