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@…>, 5 years ago

Fixed smearing control behaviour. SASVIEW-1212

  • Property mode set to 100644
File size: 15.6 KB
RevLine 
[e1e3e09]1"""
2Widget/logic for smearing data.
3"""
[9a7c81c]4import copy
5import numpy as np
[4992ff2]6from PyQt5 import QtCore
7from PyQt5 import QtGui
8from PyQt5 import QtWidgets
[e1e3e09]9
[9a7c81c]10from sas.sascalc.fit.qsmearing import smear_selection
[dc5ef15]11from sas.qtgui.Plotting.PlotterData import Data1D
12from sas.qtgui.Plotting.PlotterData import Data2D
[d6b8a1d]13import sas.qtgui.Utilities.GuiUtils as GuiUtils
[e1e3e09]14
15# Local UI
[1bc27f1]16from sas.qtgui.Perspectives.Fitting.UI.SmearingWidgetUI import Ui_SmearingWidgetUI
[e1e3e09]17
[4992ff2]18class DataWidgetMapper(QtWidgets.QDataWidgetMapper):
[180bd54]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
[4992ff2]29        if isinstance(widget, QtWidgets.QComboBox):
[180bd54]30            delegate = self.itemDelegate()
[e1e3e09]31            widget.currentIndexChanged.connect(lambda: delegate.commitData.emit(widget))
32
33SMEARING_1D = ["Custom Pinhole Smear", "Custom Slit Smear"]
34SMEARING_2D = ["Custom Pinhole Smear"]
[b14db78]35SMEARING_QD = "Use dQ Data"
[e1e3e09]36
37MODEL = [
38    'SMEARING',
39    'PINHOLE_MIN',
40    'PINHOLE_MAX',
41    'ACCURACY']
[9a7c81c]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
[e1e3e09]49
[4992ff2]50class SmearingWidget(QtWidgets.QWidget, Ui_SmearingWidgetUI):
[9a7c81c]51    smearingChangedSignal = QtCore.pyqtSignal()
[e1e3e09]52    def __init__(self, parent=None):
53        super(SmearingWidget, self).__init__()
54
55        self.setupUi(self)
[98b13f72]56
57        # Local model for holding data
[e1e3e09]58        self.model = None
[98b13f72]59        # Mapper for model update
[e1e3e09]60        self.mapper = None
[9a7c81c]61        # Data from the widget
62        self.data = None
63        self.current_smearer = None
[8b6e4be]64        self.kernel_model = None
[b14db78]65        # dQ data variables
[e900a47]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
[98b13f72]75        # Let only floats in the line edits
[d6b8a1d]76        self.txtSmearDown.setValidator(GuiUtils.DoubleValidator())
77        self.txtSmearUp.setValidator(GuiUtils.DoubleValidator())
[98b13f72]78
79        # Attach slots
80        self.cbSmearing.currentIndexChanged.connect(self.onIndexChange)
81        self.cbSmearing.setCurrentIndex(0)
[9a7c81c]82        self.txtSmearUp.setText(str(DEFAULT_PINHOLE_UP))
83        self.txtSmearDown.setText(str(DEFAULT_PINHOLE_DOWN))
[98b13f72]84
[e1e3e09]85        self.initModel()
86        self.initMapper()
87
88    def initModel(self):
89        """
90        Initialize the state
91        """
92        self.model = QtGui.QStandardItemModel()
[b3e8629]93        for model_item in range(len(MODEL)):
[e1e3e09]94            self.model.setItem(model_item, QtGui.QStandardItem())
[98b13f72]95        # Attach slot
[e1e3e09]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'))
[4992ff2]110
[9a7c81c]111        self.mapper.toFirst()
[e1e3e09]112
[9a7c81c]113    def updateData(self, data=None):
[e1e3e09]114        """
[9a7c81c]115        Update control elements based on data and model passed
[e1e3e09]116        """
117        self.cbSmearing.clear()
118        self.cbSmearing.addItem("None")
[9a7c81c]119        self.gAccuracy.setVisible(False)
120        self.data = data
[e1e3e09]121        if data is None:
122            self.setElementsVisibility(False)
[b415abc]123        model = self.kernel_model
124        self.updateKernelModel(model)
[e1e3e09]125
[9a7c81c]126    def updateKernelModel(self, kernel_model=None):
127        """
128        Update the model
129        """
130        self.kernel_model = kernel_model
[f20ea3f]131        self.cbSmearing.blockSignals(True)
[b14db78]132        self.cbSmearing.clear()
133        self.cbSmearing.addItem("None")
[8b6e4be]134        if self.data is None:
135            self.setElementsVisibility(False)
136            return
[b14db78]137        # Find out if data has dQ
138        (self.smear_type, self.dq_l, self.dq_r) = self.getSmearInfo()
[f20ea3f]139        index_to_show = 0
[b14db78]140        if self.smear_type is not None:
141            self.cbSmearing.addItem(SMEARING_QD)
[f20ea3f]142            index_to_show = 1
[b415abc]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
[b14db78]150        if isinstance(self.data, Data1D):
[8b6e4be]151            self.cbSmearing.addItems(SMEARING_1D)
152        else:
153            self.cbSmearing.addItems(SMEARING_2D)
[f20ea3f]154        self.cbSmearing.blockSignals(False)
155        self.cbSmearing.setCurrentIndex(index_to_show)
[9a7c81c]156
157    def smearer(self):
158        """ Returns the current smearer """
159        return self.current_smearer
160
[e1e3e09]161    def onIndexChange(self, index):
162        """
163        Callback for smearing combobox index change
164        """
[b14db78]165        text = self.cbSmearing.currentText()
166        if text == 'None':
[e1e3e09]167            self.setElementsVisibility(False)
[9a7c81c]168            self.current_smearer = None
[b14db78]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":
[e1e3e09]177            self.setElementsVisibility(True)
178            self.setPinholeLabels()
[9a7c81c]179            self.onPinholeSmear()
[b14db78]180        elif text == "Custom Slit Smear":
[e1e3e09]181            self.setElementsVisibility(True)
182            self.setSlitLabels()
[9a7c81c]183            self.onSlitSmear()
184        self.smearingChangedSignal.emit()
[e1e3e09]185
[9a7c81c]186    def onModelChange(self):
[e1e3e09]187        """
[9a7c81c]188        Respond to model change by notifying any listeners
[e1e3e09]189        """
[9a7c81c]190        # Recalculate the smearing
191        index = self.cbSmearing.currentIndex()
[e900a47]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
[9a7c81c]202        self.onIndexChange(index)
[e1e3e09]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)
[9a7c81c]212        self.lblUnitUp.setVisible(visible)
213        self.lblUnitDown.setVisible(visible)
[e1e3e09]214        self.setAccuracyVisibility()
215
216    def setAccuracyVisibility(self):
217        """
218        Accuracy combobox visibility
219        """
[9a7c81c]220        if isinstance(self.data, Data2D) and self.cbSmearing.currentIndex() == 1:
221            self.gAccuracy.setVisible(True)
[e1e3e09]222        else:
[9a7c81c]223            self.gAccuracy.setVisible(False)
[e1e3e09]224
225    def setPinholeLabels(self):
226        """
227        Use pinhole labels
228        """
[9a7c81c]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:
[b415abc]236            self.lblSmearUp.setText('<html><head/><body><p>dQ/Q</p></body></html>')
[9a7c81c]237            self.lblUnitUp.setText('%')
[e900a47]238        self.txtSmearUp.setText(str(self.pinhole))
239
[b14db78]240        self.txtSmearDown.setEnabled(True)
241        self.txtSmearUp.setEnabled(True)
[e1e3e09]242
243    def setSlitLabels(self):
244        """
245        Use pinhole labels
246        """
247        self.lblSmearUp.setText('Slit height')
248        self.lblSmearDown.setText('Slit width')
[9a7c81c]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>')
[e900a47]251        self.txtSmearDown.setText(str(self.slit_height))
252        self.txtSmearUp.setText(str(self.slit_width))
[b14db78]253        self.txtSmearDown.setEnabled(True)
254        self.txtSmearUp.setEnabled(True)
255
256    def setDQLabels(self):
257        """
[b415abc]258        Use appropriate labels
[b14db78]259        """
260        if self.smear_type == "Pinhole":
[b415abc]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>'
[b14db78]268        else:
[b415abc]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
[b14db78]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)
[e1e3e09]283
284    def state(self):
285        """
286        Returns current state of controls
287        """
[9a7c81c]288        smearing = self.cbSmearing.currentText()
[180bd54]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:
[e900a47]295                d_down = float(self.txtSmearUp.text())
[180bd54]296            except ValueError:
297                d_down = None
298            try:
[e900a47]299                d_up = float(self.txtSmearDown.text())
[180bd54]300            except ValueError:
301                d_up = None
[98b13f72]302
303        return (smearing, accuracy, d_down, d_up)
[672b8ab]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
[9a7c81c]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
[b2572f4]335            q = np.sqrt(data.qx_data**2 + data.qy_data**2)
336            data.dx_data = data.dqy_data = percent*q
[9a7c81c]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()
[04e1c80]357
[9a7c81c]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
[e900a47]364        self.slit_width = d_width
365        self.slit_height = d_height
366
[9a7c81c]367        if isinstance(self.data, Data2D):
[04e1c80]368            self.current_smearer = smear_selection(self.data, self.kernel_model)
[9a7c81c]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)
[b14db78]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,
[b415abc]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.
[b14db78]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
[b415abc]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)
[b14db78]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
[b415abc]417            elif data.dqx_data.any() != 0 and data.dqy_data.any() != 0:
[b14db78]418                smear_type = "Pinhole2d"
[a5f7bf4]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)
[b14db78]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.
[b415abc]425        if data.dx is not None and np.all(data.dx):
[b14db78]426            smear_type = "Pinhole"
[b415abc]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)
[b14db78]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):
[b415abc]434                dq_l = GuiUtils.formatNumber(data.dxl[0])
[b14db78]435            if data.dxw is not None and np.all(data.dxw, 0):
[b415abc]436                dq_r = GuiUtils.formatNumber(data.dxw[0])
[b14db78]437
438        return smear_type, dq_l, dq_r
Note: See TracBrowser for help on using the repository browser.