source: sasview/src/sas/qtgui/Perspectives/Invariant/InvariantPerspective.py @ 5032ea68

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 5032ea68 was 5032ea68, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 8 years ago

threaded file load, data object related fixes, more unit tests.

  • Property mode set to 100755
File size: 21.5 KB
Line 
1# global
2import sys
3import os
4from PyQt4 import QtCore
5from PyQt4 import QtGui
6from PyQt4 import QtWebKit
7
8from twisted.internet import threads
9
10# sas-global
11from sas.sascalc.invariant import invariant
12from sas.sasgui.guiframe.dataFitting import Data1D
13from sas.qtgui.GuiUtils import Communicate
14
15# local
16from UI.TabbedInvariantUI import tabbedInvariantUI
17from InvariantDetails import DetailsDialog
18from Plotter import Plotter
19from InvariantUtils import WIDGETS
20
21# The minimum q-value to be used when extrapolating
22Q_MINIMUM = 1e-5
23# The maximum q-value to be used when extrapolating
24Q_MAXIMUM = 10
25# the ratio of maximum q value/(qmax of data) to plot the theory data
26Q_MAXIMUM_PLOT = 3
27
28
29class MyModel(object):
30    def __init__(self):
31        self._model = QtGui.QStandardItemModel(self)
32
33    def addItem(self, item):
34        item = QtGui.QStandardItem(str(item))
35        self._model.appendRow(item)
36
37# class InvariantWindow(InvariantUI):
38class InvariantWindow(tabbedInvariantUI):
39    # The controller which is responsible for managing signal slots connections
40    # for the gui and providing an interface to the data model.
41    def __init__(self, manager=None, parent=None):
42        super(InvariantWindow, self).__init__(parent)
43        self.setWindowTitle("Invariant Perspective")
44        # initial input params
45        self._background = 0.0
46        self._scale = 1.0
47        self._contrast = 1.0
48        self._porod = 1.0
49        self._npoints_low = 10
50        self._npoints_high = 10
51        self._power_low = 4
52
53        self._manager = manager
54        self._reactor = self._manager.reactor()
55
56        self._helpView = QtWebKit.QWebView()
57        self.detailsDialog = DetailsDialog(self)
58        self._plotter = Plotter(self)
59
60        self._low_extrapolate = False
61        self._low_guinier  = True
62        self._low_fit  = True
63        self._high_extrapolate = False
64        self._high_power_value  = False
65
66        self.communicate = Communicate()
67
68        # Mask file selector
69        ###################################################
70        self._path = "cyl_400_20.txt"
71        from sas.sascalc.dataloader.loader import  Loader
72        loader = Loader()
73        try:
74            self._data = loader.load(self._path)
75        except:
76            raise
77        ###################################################
78
79        self.lineEdit_8.setText(str(Q_MINIMUM))
80        self.lineEdit_9.setText(str(Q_MAXIMUM))
81
82        # Let's choose the Standard Item Model.
83        self.model = QtGui.QStandardItemModel(self)
84
85        # Connect buttons to slots.
86        # Needs to be done early so default values propagate properly.
87        self.setupSlots()
88
89        # Set up the model.
90        self.setupModel()
91
92        # Set up the mapper
93        self.setupMapper()
94
95    def communicator(self):
96        """
97        """
98        return self.communicate
99
100    def updateFromModel(self):
101        """
102        update the globals based on the data in the model
103        """
104        self._background = float(self.model.item(WIDGETS.W_BACKGROUND).text())
105        self._contrast   = float(self.model.item(WIDGETS.W_CONTRAST).text())
106        self._scale      = float(self.model.item(WIDGETS.W_SCALE).text())
107
108        # High extrapolate
109        self._low_extrapolate = ( str(self.model.item(WIDGETS.W_ENABLE_LOWQ).text()) == 'true')
110        self._low_points = float(self.model.item(WIDGETS.W_NPTS_LOWQ).text())
111        self._low_guinier  = ( str(self.model.item(WIDGETS.W_LOWQ_GUINIER).text()) == 'true')
112        self._low_fit  = ( str(self.model.item(WIDGETS.W_LOWQ_FIT).text()) == 'true')
113        self._low_power_value  = float(self.model.item(WIDGETS.W_LOWQ_POWER_VALUE).text())
114
115        # High extrapolating
116        self._high_extrapolate = ( str(self.model.item(WIDGETS.W_ENABLE_HIGHQ).text()) == 'true')
117        self._high_points  = float(self.model.item(WIDGETS.W_NPTS_HIGHQ).text())
118        self._high_fit  = ( str(self.model.item(WIDGETS.W_HIGHQ_FIT).text()) == 'true')
119        self._high_power_value  = float(self.model.item(WIDGETS.W_HIGHQ_POWER_VALUE).text())
120
121    def calculate(self):
122        """
123        Use twisted to thread the calculations away.
124        """
125        # Find out if extrapolation needs to be used.
126        extrapolation = None
127        if self._low_extrapolate  and not self._high_extrapolate:
128            extrapolation = "low"
129        elif not self._low_extrapolate  and self._high_extrapolate:
130            extrapolation = "high"
131        elif self._low_extrapolate and self._high_extrapolate:
132            extrapolation = "both"
133        try:
134            # modify the Calculate button to indicate background process
135            self.pushButton.setText("Calculating...")
136            self.pushButton.setEnabled(False)
137            self.style = self.pushButton.styleSheet()
138            self.pushButton.setStyleSheet("background-color: rgb(255, 255, 0); color: rgb(0, 0, 0)")
139            # Send the calculations to separate thread.
140            d = threads.deferToThread(self.calculateThread, extrapolation)
141            # Add deferred callback for call return
142            d.addCallback(self.plotResult)
143        except Exception as ex:
144            # Set the button back to available
145            self.pushButton.setEnabled(True)
146            self.pushButton.setText("Calculate")
147            self.pushButton.setStyleSheet(self.style)
148
149
150    def plotResult(self, model):
151        """
152        """
153        self._plotter.show()
154        self.model = model
155        self.mapper.toFirst()
156
157        # Set the button back to available
158        self.pushButton.setEnabled(True)
159        self.pushButton.setText("Calculate")
160        self.pushButton.setStyleSheet(self.style)
161
162        # Send the new data to DE for keeping in the model
163        self.communicate.updateModelFromPerspectiveSignal.emit(self._data)
164
165
166    def calculateThread(self, extrapolation):
167        """
168        Perform Invariant calculations.
169
170        TODO: Create a dictionary of results to be sent to DE on completion.
171        """
172        self.updateFromModel()
173
174        qstar_low = 0.0
175        qstar_low_err = 0.0
176        qstar_high = 0.0
177        qstar_high_err = 0.0
178        qstar_total = 0.0
179        qstar_total_low_err = 0.0
180
181        # Prepare the invariant object
182        inv = invariant.InvariantCalculator(data=self._data,
183                                            background = self._background,
184                                            scale = self._scale)
185
186        if self._low_extrapolate:
187            function_low = "power_law"
188            if self._low_guinier:
189                function_low = "guinier"
190            if self._low_fit:
191                self._low_power_value = None
192            inv.set_extrapolation(range="low",
193                                  npts=self._low_points,
194                                  function=function_low,
195                                  power=self._low_power_value)
196
197        if self._high_extrapolate:
198            function_low = "power_law"
199            inv.set_extrapolation(range="high",
200                                  npts=self._high_points,
201                                  function=function_low,
202                                  power=self._low_power_value)
203
204        #Compute invariant
205        # TODO: proper exception handling and logic -
206        # display info, update lineedits, don't run extrapolations etc.
207        calculation_failed = False
208        try:
209            qstar_total, qstar_total_error         = inv.get_qstar_with_error()
210        except Exception as ex:
211            calculation_failed = True
212            # Display relevant information
213            item = QtGui.QStandardItem("ERROR")
214            self.model.setItem(WIDGETS.W_INVARIANT, item)
215            item = QtGui.QStandardItem("ERROR")
216            self.model.setItem(WIDGETS.W_INVARIANT_ERR, item)
217        try:
218            volume_fraction, volume_fraction_error = \
219                inv.get_volume_fraction_with_error(self._contrast)
220        except Exception as ex:
221            calculation_failed = True
222            # Display relevant information
223            item = QtGui.QStandardItem("ERROR")
224            self.model.setItem(WIDGETS.W_VOLUME_FRACTION, item)
225            item = QtGui.QStandardItem("ERROR")
226            self.model.setItem(WIDGETS.W_VOLUME_FRACTION_ERR, item)
227        try:
228            surface, surface_error                 = \
229                inv.get_surface_with_error(self._contrast, self._porod)
230        except Exception as ex:
231            calculation_failed = True
232            # Display relevant information
233            item = QtGui.QStandardItem("ERROR")
234            self.model.setItem(WIDGETS.W_SPECIFIC_SURFACE, item)
235            item = QtGui.QStandardItem("ERROR")
236            self.model.setItem(WIDGETS.W_SPECIFIC_SURFACE_ERR, item)
237
238        if(calculation_failed):
239            # TODO: NOTIFY THE GUI MANAGER!!
240            self.mapper.toFirst()
241            return self.model
242
243        self._plotter.clean()
244        self._plotter.x_label("Q(A$^{-1}$)")
245        self._plotter.y_label("Intensity(cm$^{-1}$)")
246
247        if self._low_extrapolate:
248            # for presentation in InvariantDetails
249            qstar_low, qstar_low_err = inv.get_qstar_low()
250            extrapolated_data = inv.get_extra_data_low(self._low_points)
251            power_low = inv.get_extrapolation_power(range='low')
252
253            #inv.data = extrapolated_data
254            #qstar_total, qstar_total_error = inv.get_qstar_with_error()
255
256            # Plot the chart
257            self._plotter.data(extrapolated_data)
258            self._plotter.title("Low-Q extrapolation")
259            self._plotter.plot()
260
261
262        if self._high_extrapolate:
263            # for presentation in InvariantDetails
264            qmax_plot = Q_MAXIMUM_PLOT * max(self._data.x)
265            if qmax_plot > Q_MAXIMUM:
266                qmax_plot = Q_MAXIMUM
267            qstar_high, qstar_high_err = inv.get_qstar_high()
268            power_high = inv.get_extrapolation_power(range='high')
269            high_out_data = inv.get_extra_data_high(q_end=qmax_plot, npts=500)
270
271            # find how to add this plot to the existing plot for low_extrapolate
272            # Plot the chart
273            self._plotter.data(high_out_data)
274            self._plotter.title("High-Q extrapolation")
275            self._plotter.plot()
276
277
278        item = QtGui.QStandardItem(str(float('%.5g'% volume_fraction)))
279        self.model.setItem(WIDGETS.W_VOLUME_FRACTION, item)
280        item = QtGui.QStandardItem(str(float('%.5g'% volume_fraction_error)))
281        self.model.setItem(WIDGETS.W_VOLUME_FRACTION_ERR, item)
282        item = QtGui.QStandardItem(str(float('%.5g'% surface)))
283        self.model.setItem(WIDGETS.W_SPECIFIC_SURFACE, item)
284        item = QtGui.QStandardItem(str(float('%.5g'% surface_error)))
285        self.model.setItem(WIDGETS.W_SPECIFIC_SURFACE_ERR, item)
286        item = QtGui.QStandardItem(str(float('%.5g'% qstar_total)))
287        self.model.setItem(WIDGETS.W_INVARIANT, item)
288        item = QtGui.QStandardItem(str(float('%.5g'% qstar_total_error)))
289        self.model.setItem(WIDGETS.W_INVARIANT_ERR, item)
290
291        #item = QtGui.QStandardItem(str(float('%.5g'% qstar_total)))
292        #self.model.setItem(WIDGETS.D_TOTAL_QSTAR, item)
293        #item = QtGui.QStandardItem(str(float('%.5g'% qstar_total_err)))
294        #self.model.setItem(WIDGETS.D_TOTAL_QSTAR_ERR, item)
295        item = QtGui.QStandardItem(str(float('%.5g'% qstar_low)))
296        self.model.setItem(WIDGETS.D_LOW_QSTAR, item)
297        item = QtGui.QStandardItem(str(float('%.5g'% qstar_low_err)))
298        self.model.setItem(WIDGETS.D_LOW_QSTAR_ERR, item)
299        item = QtGui.QStandardItem(str(float('%.5g'% qstar_high)))
300        self.model.setItem(WIDGETS.D_HIGH_QSTAR, item)
301        item = QtGui.QStandardItem(str(float('%.5g'% qstar_high_err)))
302        self.model.setItem(WIDGETS.D_HIGH_QSTAR_ERR, item)
303
304        self.mapper.toFirst()
305
306        return self.model
307               
308    def title(self):
309        """
310        Perspective name
311        """
312        return "Invariant panel"
313
314    def status(self):
315        """
316        """
317        self.detailsDialog.setModel(self.model)
318        self.detailsDialog.showDialog()
319
320    def help(self):
321        """
322        """
323        _TreeLocation = "html/user/sasgui/perspectives/invariant/invariant_help.html"
324        self._helpView.load(QtCore.QUrl(_TreeLocation))
325        self._helpView.show()
326
327    def setupSlots(self):
328        self.pushButton.clicked.connect(self.calculate)
329        self.pushButton_2.clicked.connect(self.status)
330        self.pushButton_3.clicked.connect(self.help)
331
332        self.radioButton.toggled.connect(self.lowGuinierAndPowerToggle)
333        self.radioButton_8.toggled.connect(self.hiFitAndFixToggle)
334
335        self.radioButton_3.toggled.connect(self.lowFitAndFixToggle)
336
337        # Bug workaround for QDataWidgetMapper() not reacting to checkbox clicks.
338        # https://bugreports.qt.io/browse/QTBUG-1818
339        self.checkBox.clicked.connect(self.setFocus)
340        self.checkBox_2.clicked.connect(self.setFocus)
341
342        self.model.itemChanged.connect(self.modelChanged)
343
344    def modelChanged(self, item):
345        """
346        """
347        if item.row() == WIDGETS.W_ENABLE_LOWQ:
348            toggle = (str(item.text()) == 'true')
349            self._low_extrapolate = toggle
350            self.lowQToggle(toggle)
351        elif item.row() == WIDGETS.W_ENABLE_HIGHQ:
352            toggle = (str(item.text()) == 'true')
353            self._high_extrapolate = toggle
354            self.highQToggle(toggle)
355       
356    def lowGuinierAndPowerToggle(self, toggle):
357        """
358        """
359        self._low_guinier = toggle
360        toggle = not toggle
361        self.lineEdit_11.setEnabled(toggle)
362        self.radioButton_3.setEnabled(toggle)
363        self.radioButton_4.setEnabled(toggle)
364        self.lineEdit_11.setEnabled(toggle and (not self._low_fit))
365
366    def lowFitAndFixToggle(self, toggle):
367        """
368        """
369        self._low_fit = toggle
370        toggle = not toggle
371        self.lineEdit_11.setEnabled(toggle)
372
373    def hiFitAndFixToggle(self, toggle):
374        """
375        """
376        self.lineEdit_13.setEnabled(toggle)
377
378    def highQToggle(self, clicked):
379        """
380        Disable/enable High Q extrapolation
381        """
382        self.radioButton_7.setEnabled(clicked)
383        self.radioButton_8.setEnabled(clicked)
384        self.lineEdit_12.setEnabled(clicked)
385        self.lineEdit_13.setEnabled(clicked)
386
387    def lowQToggle(self, clicked):
388        """
389        Disable/enable Low Q extrapolation
390        """
391        self.radioButton.setEnabled(clicked)
392        self.radioButton_2.setEnabled(clicked)
393        self.lineEdit_11.setEnabled(clicked and not self._low_fit)
394
395        # Enable subelements
396        self.radioButton_3.setEnabled(clicked and not self._low_guinier)
397        self.radioButton_4.setEnabled(clicked and not self._low_guinier)
398        self.lineEdit_10.setEnabled(clicked and not self._low_guinier)
399
400    def setupModel(self):
401
402        # filename
403        item = QtGui.QStandardItem(self._path)
404        self.model.setItem(WIDGETS.W_FILENAME, item)
405
406        # add Q parameters to the model
407        qmin = min(self._data.x)
408        item = QtGui.QStandardItem(str(qmin))
409        self.model.setItem(WIDGETS.W_QMIN, item)
410        item = QtGui.QStandardItem(str(max(self._data.x)))
411        self.model.setItem(WIDGETS.W_QMAX, item)
412
413        # add custom input params
414        item = QtGui.QStandardItem(str(self._background))
415        self.model.setItem(WIDGETS.W_BACKGROUND, item)
416        item = QtGui.QStandardItem(str(self._contrast))
417        self.model.setItem(WIDGETS.W_CONTRAST, item)
418        item = QtGui.QStandardItem(str(self._scale))
419        self.model.setItem(WIDGETS.W_SCALE, item)
420       
421        # Dialog elements
422        itemf = QtGui.QStandardItem("false")
423        self.model.setItem(WIDGETS.W_ENABLE_HIGHQ, itemf)
424        itemf = QtGui.QStandardItem("false")
425        self.model.setItem(WIDGETS.W_ENABLE_LOWQ, itemf)
426
427        item = QtGui.QStandardItem(str(self._npoints_low))
428        self.model.setItem(WIDGETS.W_NPTS_LOWQ, item)
429        item = QtGui.QStandardItem(str(self._npoints_high))
430        self.model.setItem(WIDGETS.W_NPTS_HIGHQ, item)
431
432        itemt = QtGui.QStandardItem("true")
433        self.model.setItem(WIDGETS.W_LOWQ_GUINIER, itemt)
434
435        itemt = QtGui.QStandardItem("true")
436        self.model.setItem(WIDGETS.W_LOWQ_FIT, itemt)
437        item = QtGui.QStandardItem(str(self._power_low))
438        self.model.setItem(WIDGETS.W_LOWQ_POWER_VALUE, item)
439
440        itemt = QtGui.QStandardItem("true")
441        self.model.setItem(WIDGETS.W_HIGHQ_FIT, itemt)
442        item = QtGui.QStandardItem(str(self._power_low))
443        self.model.setItem(WIDGETS.W_HIGHQ_POWER_VALUE, item)
444
445
446    def setupMapper(self):
447        # Set up the mapper.
448        self.mapper = QtGui.QDataWidgetMapper(self)
449        self.mapper.setOrientation(QtCore.Qt.Vertical)
450        self.mapper.setModel(self.model)
451
452        # Set up the view on the model for testing
453        # self.tableView.setModel(self.model)
454
455        # Filename
456        self.mapper.addMapping(self.lineEdit, WIDGETS.W_FILENAME)
457        # Qmin/Qmax
458        self.mapper.addMapping(self.lineEdit_2, WIDGETS.W_QMIN)
459        self.mapper.addMapping(self.lineEdit_3, WIDGETS.W_QMAX)
460
461        # Background
462        self.mapper.addMapping(self.lineEdit_4, WIDGETS.W_BACKGROUND)
463        # Scale
464        self.mapper.addMapping(self.lineEdit_5, WIDGETS.W_SCALE)
465        # Contrast
466        self.mapper.addMapping(self.lineEdit_6, WIDGETS.W_CONTRAST)
467
468        # Lowq/Highq items
469        self.mapper.addMapping(self.checkBox, WIDGETS.W_ENABLE_LOWQ)
470        self.mapper.addMapping(self.checkBox_2, WIDGETS.W_ENABLE_HIGHQ)
471
472        self.mapper.addMapping(self.lineEdit_10, WIDGETS.W_NPTS_LOWQ)
473        self.mapper.addMapping(self.radioButton, WIDGETS.W_LOWQ_GUINIER)
474
475        self.mapper.addMapping(self.radioButton_3, WIDGETS.W_LOWQ_FIT)
476        self.mapper.addMapping(self.lineEdit_11, WIDGETS.W_LOWQ_POWER_VALUE)
477
478        self.mapper.addMapping(self.radioButton_7, WIDGETS.W_HIGHQ_FIT)
479        self.mapper.addMapping(self.lineEdit_13, WIDGETS.W_HIGHQ_POWER_VALUE)
480   
481        # Output
482        self.mapper.addMapping(self.lineEdit_14, WIDGETS.W_VOLUME_FRACTION)
483        self.mapper.addMapping(self.lineEdit_15, WIDGETS.W_VOLUME_FRACTION_ERR)
484        self.mapper.addMapping(self.lineEdit_16, WIDGETS.W_SPECIFIC_SURFACE)
485        self.mapper.addMapping(self.lineEdit_17, WIDGETS.W_SPECIFIC_SURFACE_ERR)
486        self.mapper.addMapping(self.lineEdit_19, WIDGETS.W_INVARIANT)
487        self.mapper.addMapping(self.lineEdit_18, WIDGETS.W_INVARIANT_ERR)
488
489        self.mapper.toFirst()
490
491    def setData(self, data_list=None):
492        """
493        receive a list of data and compute invariant
494        """
495        msg = ""
496        data = None
497        if data_list is None:
498            data_list = []
499        if len(data_list) >= 1:
500            if len(data_list) == 1:
501                data = data_list[0]
502            else:
503                data_1d_list = []
504                data_2d_list = []
505                error_msg = ""
506                # separate data into data1d and data2d list
507                for data in data_list:
508                    if data is not None:
509                        if issubclass(data.__class__, Data1D):
510                            data_1d_list.append(data)
511                        else:
512                            error_msg += " %s  type %s \n" % (str(data.name),
513                                                              str(data.__class__.__name__))
514                            data_2d_list.append(data)
515                if len(data_2d_list) > 0:
516                    msg = "Invariant does not support the following data types:\n"
517                    msg += error_msg
518                if len(data_1d_list) == 0:
519                    # remake this as a qt event
520                    #wx.PostEvent(self.parent, StatusEvent(status=msg, info='error'))
521                    return
522                msg += "Invariant panel does not allow multiple data!\n"
523                msg += "Please select one.\n"
524                #if len(data_list) > 1:
525                    #from invariant_widgets import DataDialog
526                    #dlg = DataDialog(data_list=data_1d_list, text=msg)
527                    #if dlg.ShowModal() == wx.ID_OK:
528                    #    data = dlg.get_data()
529                    #else:
530                    #    data = None
531                    #dlg.Destroy()
532
533            if data is None:
534                msg += "invariant receives no data. \n"
535                #wx.PostEvent(self.parent, StatusEvent(status=msg, info='error'))
536                return
537            if not issubclass(data.__class__, Data1D):
538                msg += "invariant cannot be computed for data of "
539                msg += "type %s\n" % (data.__class__.__name__)
540                #wx.PostEvent(self.parent, StatusEvent(status=msg, info='error'))
541                return
542            else:
543                #wx.PostEvent(self.parent, NewPlotEvent(plot=data, title=data.title))
544                try:
545                    self._data = data
546                    self._path = "unique path"
547                    self.calculate()
548                except:
549                    msg = "Invariant Set_data: " + str(sys.exc_value)
550                    #wx.PostEvent(self.parent, StatusEvent(status=msg, info="error"))
551        else:
552            msg = "invariant cannot be computed for data of "
553            msg += "type %s" % (data.__class__.__name__)
554            #wx.PostEvent(self.parent, StatusEvent(status=msg, info='error'))
555
556    def allowBatch(self):
557        """
558        Tell the caller that we don't accept multiple data instances
559        """
560        return False
561
562if __name__ == "__main__":
563    app = QtGui.QApplication([])
564    import qt4reactor
565    qt4reactor.install()
566    # DO NOT move the following import to the top!
567    # (unless you know what you're doing)
568    from twisted.internet import reactor
569    dlg = InvariantWindow(reactor)
570    dlg.show()
571    reactor.run()
Note: See TracBrowser for help on using the repository browser.