source: sasview/src/sas/qtgui/Perspectives/Fitting/SmearingWidget.py

ESS_GUI
Last change on this file was 73fb503, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

Allow for smearing selection on theory plots.

  • Property mode set to 100644
File size: 15.8 KB
Line 
1"""
2Widget/logic for smearing data.
3"""
4import copy
5import numpy as np
6from PyQt5 import QtCore
7from PyQt5 import QtGui
8from PyQt5 import QtWidgets
9
10from sas.sascalc.fit.qsmearing import smear_selection
11from sas.qtgui.Plotting.PlotterData import Data1D
12from sas.qtgui.Plotting.PlotterData import Data2D
13import sas.qtgui.Utilities.GuiUtils as GuiUtils
14
15# Local UI
16from sas.qtgui.Perspectives.Fitting.UI.SmearingWidgetUI import Ui_SmearingWidgetUI
17
18class DataWidgetMapper(QtWidgets.QDataWidgetMapper):
19    """
20    Custom version of the standard QDataWidgetMapper allowing for proper
21    response to index change in comboboxes
22    """
23    def addMapping(self, widget, section, propertyName=None):
24        if propertyName is None:
25            super(DataWidgetMapper, self).addMapping(widget, section)
26        else:
27            super(DataWidgetMapper, self).addMapping(widget, section, propertyName)
28
29        if isinstance(widget, QtWidgets.QComboBox):
30            delegate = self.itemDelegate()
31            widget.currentIndexChanged.connect(lambda: delegate.commitData.emit(widget))
32
33SMEARING_1D = ["Custom Pinhole Smear", "Custom Slit Smear"]
34SMEARING_2D = ["Custom Pinhole Smear"]
35SMEARING_QD = "Use dQ Data"
36
37MODEL = [
38    'SMEARING',
39    'PINHOLE_MIN',
40    'PINHOLE_MAX',
41    'ACCURACY']
42ACCURACY_DICT={'Low': 'low',
43               'Medium': 'med',
44               'High': 'high',
45               'Extra high': 'xhigh'}
46
47DEFAULT_PINHOLE_UP=0.0
48DEFAULT_PINHOLE_DOWN=0.0
49
50class SmearingWidget(QtWidgets.QWidget, Ui_SmearingWidgetUI):
51    smearingChangedSignal = QtCore.pyqtSignal()
52    def __init__(self, parent=None):
53        super(SmearingWidget, self).__init__()
54
55        self.setupUi(self)
56
57        # Local model for holding data
58        self.model = None
59        # Mapper for model update
60        self.mapper = None
61        # Data from the widget
62        self.data = None
63        self.current_smearer = None
64        self.kernel_model = None
65        # dQ data variables
66        self.smear_type = None
67        self.dq_l = None
68        self.dq_r = None
69
70        # current pinhole/slot values
71        self.pinhole = 0.0
72        self.slit_height = 0.0
73        self.slit_width = 0.0
74
75        # Let only floats in the line edits
76        self.txtSmearDown.setValidator(GuiUtils.DoubleValidator())
77        self.txtSmearUp.setValidator(GuiUtils.DoubleValidator())
78
79        # Attach slots
80        self.cbSmearing.currentIndexChanged.connect(self.onIndexChange)
81        self.cbSmearing.setCurrentIndex(0)
82        self.txtSmearUp.setText(str(DEFAULT_PINHOLE_UP))
83        self.txtSmearDown.setText(str(DEFAULT_PINHOLE_DOWN))
84
85        self.initModel()
86        self.initMapper()
87
88    def initModel(self):
89        """
90        Initialize the state
91        """
92        self.model = QtGui.QStandardItemModel()
93        for model_item in range(len(MODEL)):
94            self.model.setItem(model_item, QtGui.QStandardItem())
95        # Attach slot
96        self.model.dataChanged.connect(self.onModelChange)
97
98    def initMapper(self):
99        """
100        Initialize model item <-> UI element mapping
101        """
102        self.mapper = DataWidgetMapper(self)
103
104        self.mapper.setModel(self.model)
105        self.mapper.setOrientation(QtCore.Qt.Vertical)
106
107        self.mapper.addMapping(self.txtSmearUp,   MODEL.index('PINHOLE_MIN'))
108        self.mapper.addMapping(self.txtSmearDown, MODEL.index('PINHOLE_MAX'))
109        self.mapper.addMapping(self.cbAccuracy,   MODEL.index('ACCURACY'))
110
111        self.mapper.toFirst()
112
113    def updateData(self, data=None):
114        """
115        Update control elements based on data and model passed
116        """
117        # retain the index
118        cur_index = self.cbSmearing.currentIndex()
119        self.cbSmearing.clear()
120        self.cbSmearing.addItem("None")
121        self.gAccuracy.setVisible(False)
122        self.data = data
123        if data is None:
124            self.setElementsVisibility(False)
125        model = self.kernel_model
126        self.updateKernelModel(model)
127        # already has the required setup so modify the index
128        self.cbSmearing.setCurrentIndex(cur_index)
129
130    def updateKernelModel(self, kernel_model=None):
131        """
132        Update the model
133        """
134        self.kernel_model = kernel_model
135        self.cbSmearing.blockSignals(True)
136        self.cbSmearing.clear()
137        self.cbSmearing.addItem("None")
138        if self.data is None:
139            self.setElementsVisibility(False)
140            return
141        # Find out if data has dQ
142        (self.smear_type, self.dq_l, self.dq_r) = self.getSmearInfo()
143        index_to_show = 0
144        if self.smear_type is not None:
145            self.cbSmearing.addItem(SMEARING_QD)
146            index_to_show = 1
147
148        if self.kernel_model is None:
149            # No model definend yet - just use data file smearing, if any
150            self.cbSmearing.blockSignals(False)
151            self.cbSmearing.setCurrentIndex(index_to_show)
152            return
153
154        if isinstance(self.data, Data1D):
155            self.cbSmearing.addItems(SMEARING_1D)
156        else:
157            self.cbSmearing.addItems(SMEARING_2D)
158        self.cbSmearing.blockSignals(False)
159        self.cbSmearing.setCurrentIndex(index_to_show)
160
161    def smearer(self):
162        """ Returns the current smearer """
163        return self.current_smearer
164
165    def onIndexChange(self, index):
166        """
167        Callback for smearing combobox index change
168        """
169        text = self.cbSmearing.currentText()
170        if text == 'None':
171            self.setElementsVisibility(False)
172            self.current_smearer = None
173        elif text == "Use dQ Data":
174            self.setElementsVisibility(True)
175            self.setDQLabels()
176            if self.smear_type == "Pinhole":
177                self.onPinholeSmear()
178            else:
179                self.onSlitSmear()
180        elif text == "Custom Pinhole Smear":
181            self.setElementsVisibility(True)
182            self.setPinholeLabels()
183            self.onPinholeSmear()
184        elif text == "Custom Slit Smear":
185            self.setElementsVisibility(True)
186            self.setSlitLabels()
187            self.onSlitSmear()
188        self.smearingChangedSignal.emit()
189
190    def onModelChange(self):
191        """
192        Respond to model change by notifying any listeners
193        """
194        # Recalculate the smearing
195        index = self.cbSmearing.currentIndex()
196        ## update the backup values based on model choice
197        smearing, accuracy, d_down, d_up = self.state()
198        # don't save the state if dQ Data
199        if smearing == "Custom Pinhole Smear":
200            self.pinhole = d_down
201            self.accuracy = accuracy
202        elif smearing == 'Custom Slit Smear':
203            self.slit_height = d_up
204            self.slit_width = d_down
205
206        self.onIndexChange(index)
207
208    def setElementsVisibility(self, visible):
209        """
210        Labels and linedits visibility control
211        """
212        self.lblSmearDown.setVisible(visible)
213        self.lblSmearUp.setVisible(visible)
214        self.txtSmearDown.setVisible(visible)
215        self.txtSmearUp.setVisible(visible)
216        self.lblUnitUp.setVisible(visible)
217        self.lblUnitDown.setVisible(visible)
218        self.setAccuracyVisibility()
219
220    def setAccuracyVisibility(self):
221        """
222        Accuracy combobox visibility
223        """
224        if isinstance(self.data, Data2D) and self.cbSmearing.currentIndex() == 1:
225            self.gAccuracy.setVisible(True)
226        else:
227            self.gAccuracy.setVisible(False)
228
229    def setPinholeLabels(self):
230        """
231        Use pinhole labels
232        """
233        self.txtSmearDown.setVisible(False)
234        self.lblSmearDown.setText('')
235        self.lblUnitDown.setText('')
236        if isinstance(self.data, Data2D):
237            self.lblUnitUp.setText('<html><head/><body><p>Å<span style=" vertical-align:super;">-1</span></p></body></html>')
238            self.lblSmearUp.setText('<html><head/><body><p>&lt;dQ<span style=" vertical-align:sub;">low</span>&gt;</p></body></html>')
239        else:
240            self.lblSmearUp.setText('<html><head/><body><p>dQ/Q</p></body></html>')
241            self.lblUnitUp.setText('%')
242        self.txtSmearUp.setText(str(self.pinhole))
243
244        self.txtSmearDown.setEnabled(True)
245        self.txtSmearUp.setEnabled(True)
246
247    def setSlitLabels(self):
248        """
249        Use pinhole labels
250        """
251        self.lblSmearUp.setText('Slit height')
252        self.lblSmearDown.setText('Slit width')
253        self.lblUnitUp.setText('<html><head/><body><p>Å<span style=" vertical-align:super;">-1</span></p></body></html>')
254        self.lblUnitDown.setText('<html><head/><body><p>Å<span style=" vertical-align:super;">-1</span></p></body></html>')
255        self.txtSmearDown.setText(str(self.slit_height))
256        self.txtSmearUp.setText(str(self.slit_width))
257        self.txtSmearDown.setEnabled(True)
258        self.txtSmearUp.setEnabled(True)
259
260    def setDQLabels(self):
261        """
262        Use appropriate labels
263        """
264        if self.smear_type == "Pinhole":
265            text_down = '<html><head/><body><p>[dQ/Q]<span style=" vertical-align:sub;">max</span></p></body></html>'
266            text_up = '<html><head/><body><p>[dQ/Q]<span style=" vertical-align:sub;">min</span></p></body></html>'
267            text_unit = '%'
268        elif self.smear_type == "Slit":
269            text_down = '<html><head/><body><p>Slit width</p></body></html>'
270            text_up = '<html><head/><body><p>Slit height</p></body></html>'
271            text_unit = '<html><head/><body><p>Å<span style=" vertical-align:super;">-1</span></p></body></html>'
272        else:
273            text_unit = '%'
274            text_up = '<html><head/><body><p>&lsaquo;dQ/Q&rsaquo;<span style=" vertical-align:sub;">r</span></p></body></html>'
275            text_down = '<html><head/><body><p>&lsaquo;dQ/Q&rsaquo;<span style=" vertical-align:sub;">&phi;</span></p></body></html>'
276
277        self.lblSmearDown.setText(text_down)
278        self.lblSmearUp.setText(text_up)
279
280        self.lblUnitUp.setText(text_unit)
281        self.lblUnitDown.setText(text_unit)
282
283        self.txtSmearDown.setText(str(self.dq_r))
284        self.txtSmearUp.setText(str(self.dq_l))
285        self.txtSmearDown.setEnabled(False)
286        self.txtSmearUp.setEnabled(False)
287
288    def state(self):
289        """
290        Returns current state of controls
291        """
292        smearing = self.cbSmearing.currentText()
293        accuracy = ""
294        d_down = None
295        d_up = None
296        if smearing != "None":
297            accuracy = str(self.model.item(MODEL.index('ACCURACY')).text())
298            try:
299                d_down = float(self.txtSmearUp.text())
300            except ValueError:
301                d_down = None
302            try:
303                d_up = float(self.txtSmearDown.text())
304            except ValueError:
305                d_up = None
306
307        return (smearing, accuracy, d_down, d_up)
308
309    def setState(self, smearing, accuracy, d_down, d_up):
310        """
311        Sets new values for the controls
312        """
313        # Update the model -> controls update automatically
314        if accuracy is not None:
315            self.model.item(MODEL.index('ACCURACY')).setText(accuracy)
316        if d_down is not None:
317            self.model.item(MODEL.index('PINHOLE_MIN')).setText(d_down)
318        if d_up is not None:
319            self.model.item(MODEL.index('PINHOLE_MAX')).setText(d_up)
320
321    def onPinholeSmear(self):
322        """
323        Create a custom pinhole smear object that will change the way residuals
324        are compute when fitting
325        """
326        _, accuracy, d_percent, _ = self.state()
327        if d_percent is None or d_percent == 0.0:
328            self.current_smearer=None
329            return
330        percent = d_percent/100.0
331        # copy data
332        data = copy.deepcopy(self.data)
333        if isinstance(self.data, Data2D):
334            len_data = len(data.data)
335            data.dqx_data = np.zeros(len_data)
336            data.dqy_data = np.zeros(len_data)
337            data.dqx_data[data.dqx_data == 0] = percent * data.qx_data
338            data.dqy_data[data.dqy_data == 0] = percent * data.qy_data
339            q = np.sqrt(data.qx_data**2 + data.qy_data**2)
340            data.dx_data = data.dqy_data = percent*q
341        else:
342            len_data = len(data.x)
343            data.dx = np.zeros(len_data)
344            data.dx = percent * data.x
345            data.dxl = None
346            data.dxw = None
347
348        self.current_smearer = smear_selection(data, self.kernel_model)
349        # need to set accuracy for 2D
350        if isinstance(self.data, Data2D):
351            backend_accuracy = ACCURACY_DICT.get(accuracy)
352            if backend_accuracy:
353                self.current_smearer.set_accuracy(accuracy=backend_accuracy)
354
355    def onSlitSmear(self):
356        """
357        Create a custom slit smear object that will change the way residuals
358        are compute when fitting
359        """
360        _, accuracy, d_height, d_width = self.state()
361
362        # Check changes in slit width
363        if d_width is None:
364            d_width = 0.0
365        if d_height is None:
366            d_height = 0.0
367
368        self.slit_width = d_width
369        self.slit_height = d_height
370
371        if isinstance(self.data, Data2D):
372            self.current_smearer = smear_selection(self.data, self.kernel_model)
373            return
374        # make sure once more if it is smearer
375        data = copy.deepcopy(self.data)
376        data_len = len(data.x)
377        data.dx = None
378        data.dxl = None
379        data.dxw = None
380
381        try:
382            self.dxl = d_height
383            data.dxl = self.dxl * np.ones(data_len)
384        except:
385            self.dxl = None
386            data.dxl = np.zeros(data_len)
387        try:
388            self.dxw = d_width
389            data.dxw = self.dxw * np.ones(data_len)
390        except:
391            self.dxw = None
392            data.dxw = np.zeros(data_len)
393
394        self.current_smearer = smear_selection(data, self.kernel_model)
395
396    def getSmearInfo(self):
397        """
398        Get the smear info from data.
399
400        :return: self.smear_type, self.dq_l and self.dq_r,
401            respectively the type of the smear, the average <dq/q> radial(p)
402            and <dq/q> theta (s)s for 2D pinhole resolution in % (slit is not
403            currently supported in 2D), (dq/q)_min and (dq/q)_max for 1D pinhole
404            smeared data, again in %, and dxl and/or dxw for slit smeared data
405            given in 1/A and assumed constant.
406        """
407        # default
408        smear_type = None
409        dq_l = None
410        dq_r = None
411        data = self.data
412        if self.data is None:
413            return smear_type, dq_l, dq_r
414        # First check if data is 2D
415        # If so check that data set has smearing info and that none are zero.
416        # Otherwise no smearing can be applied using smear from data (a Gaussian
417        # width of zero will cause a divide by zero error)
418        elif isinstance(data, Data2D):
419            if data.dqx_data is None or data.dqy_data is None:
420                return smear_type, dq_l, dq_r
421            elif data.dqx_data.any() != 0 and data.dqy_data.any() != 0:
422                smear_type = "Pinhole2d"
423                dq_l = GuiUtils.formatNumber(np.average(data.dqx_data/np.abs(data.qx_data))*100., high=True)
424                dq_r = GuiUtils.formatNumber(np.average(data.dqy_data/np.abs(data.qy_data))*100., high=True)
425                return smear_type, dq_l, dq_r
426            else:
427                return smear_type, dq_l, dq_r
428        # check if it is pinhole smear and get min max if it is.
429        if data.dx is not None and np.all(data.dx):
430            smear_type = "Pinhole"
431            dq_l = GuiUtils.formatNumber(data.dx[0]/data.x[0] *100., high=True)
432            dq_r = GuiUtils.formatNumber(data.dx[-1]/data.x[-1] *100., high=True)
433
434        # check if it is slit smear and get min max if it is.
435        elif data.dxl is not None or data.dxw is not None:
436            smear_type = "Slit"
437            if data.dxl is not None and np.all(data.dxl, 0):
438                dq_l = GuiUtils.formatNumber(data.dxl[0])
439            if data.dxw is not None and np.all(data.dxw, 0):
440                dq_r = GuiUtils.formatNumber(data.dxw[0])
441
442        return smear_type, dq_l, dq_r
Note: See TracBrowser for help on using the repository browser.