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

ESS_GUIESS_GUI_bumps_abstractionESS_GUI_opencl
Last change on this file since e900a47 was e900a47, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 7 months ago

Fixed smearing control behaviour. SASVIEW-1212

  • Property mode set to 100644
File size: 15.6 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        self.cbSmearing.clear()
118        self.cbSmearing.addItem("None")
119        self.gAccuracy.setVisible(False)
120        self.data = data
121        if data is None:
122            self.setElementsVisibility(False)
123        model = self.kernel_model
124        self.updateKernelModel(model)
125
126    def updateKernelModel(self, kernel_model=None):
127        """
128        Update the model
129        """
130        self.kernel_model = kernel_model
131        self.cbSmearing.blockSignals(True)
132        self.cbSmearing.clear()
133        self.cbSmearing.addItem("None")
134        if self.data is None:
135            self.setElementsVisibility(False)
136            return
137        # Find out if data has dQ
138        (self.smear_type, self.dq_l, self.dq_r) = self.getSmearInfo()
139        index_to_show = 0
140        if self.smear_type is not None:
141            self.cbSmearing.addItem(SMEARING_QD)
142            index_to_show = 1
143
144        if self.kernel_model is None:
145            # No model definend yet - just use data file smearing, if any
146            self.cbSmearing.blockSignals(False)
147            self.cbSmearing.setCurrentIndex(index_to_show)
148            return
149
150        if isinstance(self.data, Data1D):
151            self.cbSmearing.addItems(SMEARING_1D)
152        else:
153            self.cbSmearing.addItems(SMEARING_2D)
154        self.cbSmearing.blockSignals(False)
155        self.cbSmearing.setCurrentIndex(index_to_show)
156
157    def smearer(self):
158        """ Returns the current smearer """
159        return self.current_smearer
160
161    def onIndexChange(self, index):
162        """
163        Callback for smearing combobox index change
164        """
165        text = self.cbSmearing.currentText()
166        if text == 'None':
167            self.setElementsVisibility(False)
168            self.current_smearer = None
169        elif text == "Use dQ Data":
170            self.setElementsVisibility(True)
171            self.setDQLabels()
172            if self.smear_type == "Pinhole":
173                self.onPinholeSmear()
174            else:
175                self.onSlitSmear()
176        elif text == "Custom Pinhole Smear":
177            self.setElementsVisibility(True)
178            self.setPinholeLabels()
179            self.onPinholeSmear()
180        elif text == "Custom Slit Smear":
181            self.setElementsVisibility(True)
182            self.setSlitLabels()
183            self.onSlitSmear()
184        self.smearingChangedSignal.emit()
185
186    def onModelChange(self):
187        """
188        Respond to model change by notifying any listeners
189        """
190        # Recalculate the smearing
191        index = self.cbSmearing.currentIndex()
192        ## update the backup values based on model choice
193        smearing, accuracy, d_down, d_up = self.state()
194        # don't save the state if dQ Data
195        if smearing == "Custom Pinhole Smear":
196            self.pinhole = d_down
197            self.accuracy = accuracy
198        elif smearing == 'Custom Slit Smear':
199            self.slit_height = d_up
200            self.slit_width = d_down
201
202        self.onIndexChange(index)
203
204    def setElementsVisibility(self, visible):
205        """
206        Labels and linedits visibility control
207        """
208        self.lblSmearDown.setVisible(visible)
209        self.lblSmearUp.setVisible(visible)
210        self.txtSmearDown.setVisible(visible)
211        self.txtSmearUp.setVisible(visible)
212        self.lblUnitUp.setVisible(visible)
213        self.lblUnitDown.setVisible(visible)
214        self.setAccuracyVisibility()
215
216    def setAccuracyVisibility(self):
217        """
218        Accuracy combobox visibility
219        """
220        if isinstance(self.data, Data2D) and self.cbSmearing.currentIndex() == 1:
221            self.gAccuracy.setVisible(True)
222        else:
223            self.gAccuracy.setVisible(False)
224
225    def setPinholeLabels(self):
226        """
227        Use pinhole labels
228        """
229        self.txtSmearDown.setVisible(False)
230        self.lblSmearDown.setText('')
231        self.lblUnitDown.setText('')
232        if isinstance(self.data, Data2D):
233            self.lblUnitUp.setText('<html><head/><body><p>Å<span style=" vertical-align:super;">-1</span></p></body></html>')
234            self.lblSmearUp.setText('<html><head/><body><p>&lt;dQ<span style=" vertical-align:sub;">low</span>&gt;</p></body></html>')
235        else:
236            self.lblSmearUp.setText('<html><head/><body><p>dQ/Q</p></body></html>')
237            self.lblUnitUp.setText('%')
238        self.txtSmearUp.setText(str(self.pinhole))
239
240        self.txtSmearDown.setEnabled(True)
241        self.txtSmearUp.setEnabled(True)
242
243    def setSlitLabels(self):
244        """
245        Use pinhole labels
246        """
247        self.lblSmearUp.setText('Slit height')
248        self.lblSmearDown.setText('Slit width')
249        self.lblUnitUp.setText('<html><head/><body><p>Å<span style=" vertical-align:super;">-1</span></p></body></html>')
250        self.lblUnitDown.setText('<html><head/><body><p>Å<span style=" vertical-align:super;">-1</span></p></body></html>')
251        self.txtSmearDown.setText(str(self.slit_height))
252        self.txtSmearUp.setText(str(self.slit_width))
253        self.txtSmearDown.setEnabled(True)
254        self.txtSmearUp.setEnabled(True)
255
256    def setDQLabels(self):
257        """
258        Use appropriate labels
259        """
260        if self.smear_type == "Pinhole":
261            text_down = '<html><head/><body><p>[dQ/Q]<span style=" vertical-align:sub;">max</span></p></body></html>'
262            text_up = '<html><head/><body><p>[dQ/Q]<span style=" vertical-align:sub;">min</span></p></body></html>'
263            text_unit = '%'
264        elif self.smear_type == "Slit":
265            text_down = '<html><head/><body><p>Slit width</p></body></html>'
266            text_up = '<html><head/><body><p>Slit height</p></body></html>'
267            text_unit = '<html><head/><body><p>Å<span style=" vertical-align:super;">-1</span></p></body></html>'
268        else:
269            text_unit = '%'
270            text_up = '<html><head/><body><p>&lsaquo;dQ/Q&rsaquo;<span style=" vertical-align:sub;">r</span></p></body></html>'
271            text_down = '<html><head/><body><p>&lsaquo;dQ/Q&rsaquo;<span style=" vertical-align:sub;">&phi;</span></p></body></html>'
272
273        self.lblSmearDown.setText(text_down)
274        self.lblSmearUp.setText(text_up)
275
276        self.lblUnitUp.setText(text_unit)
277        self.lblUnitDown.setText(text_unit)
278
279        self.txtSmearDown.setText(str(self.dq_r))
280        self.txtSmearUp.setText(str(self.dq_l))
281        self.txtSmearDown.setEnabled(False)
282        self.txtSmearUp.setEnabled(False)
283
284    def state(self):
285        """
286        Returns current state of controls
287        """
288        smearing = self.cbSmearing.currentText()
289        accuracy = ""
290        d_down = None
291        d_up = None
292        if smearing != "None":
293            accuracy = str(self.model.item(MODEL.index('ACCURACY')).text())
294            try:
295                d_down = float(self.txtSmearUp.text())
296            except ValueError:
297                d_down = None
298            try:
299                d_up = float(self.txtSmearDown.text())
300            except ValueError:
301                d_up = None
302
303        return (smearing, accuracy, d_down, d_up)
304
305    def setState(self, smearing, accuracy, d_down, d_up):
306        """
307        Sets new values for the controls
308        """
309        # Update the model -> controls update automatically
310        if accuracy is not None:
311            self.model.item(MODEL.index('ACCURACY')).setText(accuracy)
312        if d_down is not None:
313            self.model.item(MODEL.index('PINHOLE_MIN')).setText(d_down)
314        if d_up is not None:
315            self.model.item(MODEL.index('PINHOLE_MAX')).setText(d_up)
316
317    def onPinholeSmear(self):
318        """
319        Create a custom pinhole smear object that will change the way residuals
320        are compute when fitting
321        """
322        _, accuracy, d_percent, _ = self.state()
323        if d_percent is None or d_percent == 0.0:
324            self.current_smearer=None
325            return
326        percent = d_percent/100.0
327        # copy data
328        data = copy.deepcopy(self.data)
329        if isinstance(self.data, Data2D):
330            len_data = len(data.data)
331            data.dqx_data = np.zeros(len_data)
332            data.dqy_data = np.zeros(len_data)
333            data.dqx_data[data.dqx_data == 0] = percent * data.qx_data
334            data.dqy_data[data.dqy_data == 0] = percent * data.qy_data
335            q = np.sqrt(data.qx_data**2 + data.qy_data**2)
336            data.dx_data = data.dqy_data = percent*q
337        else:
338            len_data = len(data.x)
339            data.dx = np.zeros(len_data)
340            data.dx = percent * data.x
341            data.dxl = None
342            data.dxw = None
343
344        self.current_smearer = smear_selection(data, self.kernel_model)
345        # need to set accuracy for 2D
346        if isinstance(self.data, Data2D):
347            backend_accuracy = ACCURACY_DICT.get(accuracy)
348            if backend_accuracy:
349                self.current_smearer.set_accuracy(accuracy=backend_accuracy)
350
351    def onSlitSmear(self):
352        """
353        Create a custom slit smear object that will change the way residuals
354        are compute when fitting
355        """
356        _, accuracy, d_height, d_width = self.state()
357
358        # Check changes in slit width
359        if d_width is None:
360            d_width = 0.0
361        if d_height is None:
362            d_height = 0.0
363
364        self.slit_width = d_width
365        self.slit_height = d_height
366
367        if isinstance(self.data, Data2D):
368            self.current_smearer = smear_selection(self.data, self.kernel_model)
369            return
370        # make sure once more if it is smearer
371        data = copy.deepcopy(self.data)
372        data_len = len(data.x)
373        data.dx = None
374        data.dxl = None
375        data.dxw = None
376
377        try:
378            self.dxl = d_height
379            data.dxl = self.dxl * np.ones(data_len)
380        except:
381            self.dxl = None
382            data.dxl = np.zeros(data_len)
383        try:
384            self.dxw = d_width
385            data.dxw = self.dxw * np.ones(data_len)
386        except:
387            self.dxw = None
388            data.dxw = np.zeros(data_len)
389
390        self.current_smearer = smear_selection(data, self.kernel_model)
391
392    def getSmearInfo(self):
393        """
394        Get the smear info from data.
395
396        :return: self.smear_type, self.dq_l and self.dq_r,
397            respectively the type of the smear, the average <dq/q> radial(p)
398            and <dq/q> theta (s)s for 2D pinhole resolution in % (slit is not
399            currently supported in 2D), (dq/q)_min and (dq/q)_max for 1D pinhole
400            smeared data, again in %, and dxl and/or dxw for slit smeared data
401            given in 1/A and assumed constant.
402        """
403        # default
404        smear_type = None
405        dq_l = None
406        dq_r = None
407        data = self.data
408        if self.data is None:
409            return smear_type, dq_l, dq_r
410        # First check if data is 2D
411        # If so check that data set has smearing info and that none are zero.
412        # Otherwise no smearing can be applied using smear from data (a Gaussian
413        # width of zero will cause a divide by zero error)
414        elif isinstance(data, Data2D):
415            if data.dqx_data is None or data.dqy_data is None:
416                return smear_type, dq_l, dq_r
417            elif data.dqx_data.any() != 0 and data.dqy_data.any() != 0:
418                smear_type = "Pinhole2d"
419                dq_l = GuiUtils.formatNumber(np.average(data.dqx_data/np.abs(data.qx_data))*100., high=True)
420                dq_r = GuiUtils.formatNumber(np.average(data.dqy_data/np.abs(data.qy_data))*100., high=True)
421                return smear_type, dq_l, dq_r
422            else:
423                return smear_type, dq_l, dq_r
424        # check if it is pinhole smear and get min max if it is.
425        if data.dx is not None and np.all(data.dx):
426            smear_type = "Pinhole"
427            dq_l = GuiUtils.formatNumber(data.dx[0]/data.x[0] *100., high=True)
428            dq_r = GuiUtils.formatNumber(data.dx[-1]/data.x[-1] *100., high=True)
429
430        # check if it is slit smear and get min max if it is.
431        elif data.dxl is not None or data.dxw is not None:
432            smear_type = "Slit"
433            if data.dxl is not None and np.all(data.dxl, 0):
434                dq_l = GuiUtils.formatNumber(data.dxl[0])
435            if data.dxw is not None and np.all(data.dxw, 0):
436                dq_r = GuiUtils.formatNumber(data.dxw[0])
437
438        return smear_type, dq_l, dq_r
Note: See TracBrowser for help on using the repository browser.