source: sasview/src/sas/qtgui/Perspectives/PrInversion/PrInversionPerspective.py @ dab3351

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 dab3351 was dab3351, checked in by krzywon, 6 years ago

P(r) calculation threads running in Qt GUI.

  • Property mode set to 100644
File size: 18.8 KB
Line 
1import sys
2import logging
3import numpy as np
4
5from PyQt4 import QtGui, QtCore, QtWebKit
6
7# sas-global
8import sas.qtgui.Utilities.GuiUtils as GuiUtils
9
10# pr inversion gui elements
11from PrInversionUtils import WIDGETS
12import UI.TabbedPrInversionUI
13from UI.TabbedPrInversionUI import Ui_PrInversion
14
15# pr inversion calculation elements
16from sas.sascalc.pr.invertor import Invertor
17
18
19class PrInversionWindow(QtGui.QTabWidget, Ui_PrInversion):
20    """
21    The main window for the P(r) Inversion perspective.
22    """
23
24    name = "PrInversion"
25
26    def __init__(self, parent=None, data=None):
27        super(PrInversionWindow, self).__init__()
28        self.setupUi(self)
29
30        self.setWindowTitle("P(r) Inversion Perspective")
31
32        self._manager = parent
33        self._model_item = QtGui.QStandardItem()
34        self._helpView = QtWebKit.QWebView()
35
36        self.communicate = GuiUtils.Communicate()
37
38        # The window should not close
39        self._allow_close = False
40
41        # is there data
42        self._has_data = False
43
44        # Current data object in view
45        self._data_index = 0
46        # list mapping data to p(r) calculation
47        self._data_list = []
48        if not isinstance(data, list):
49            data = [data]
50        for datum in data:
51            self._data_list.append({datum : None})
52
53        # p(r) calculator
54        self._calculator = Invertor()
55        self._last_calculator = None
56        self.calc_thread = None
57        self.estimation_thread = None
58
59        self.model = QtGui.QStandardItemModel(self)
60        self.mapper = QtGui.QDataWidgetMapper(self)
61        # Link user interactions with methods
62        self.setupLinks()
63        # Set values
64        self.setupModel()
65        # Set up the Widget Map
66        self.setupMapper()
67
68    ######################################################################
69    # Base Perspective Class Definitions
70
71    def allowBatch(self):
72        return False
73
74    def setClosable(self, value=True):
75        """
76        Allow outsiders close this widget
77        """
78        assert isinstance(value, bool)
79        self._allow_close = value
80
81    def closeEvent(self, event):
82        """
83        Overwrite QDialog close method to allow for custom widget close
84        """
85        if self._allow_close:
86            # reset the closability flag
87            self.setClosable(value=False)
88            event.accept()
89        else:
90            event.ignore()
91            # Maybe we should just minimize
92            self.setWindowState(QtCore.Qt.WindowMinimized)
93
94    ######################################################################
95    # Initialization routines
96
97    def setupLinks(self):
98        """Connect the use controls to their appropriate methods"""
99        self.enableButtons()
100        self.checkBgdClicked(False)
101        # TODO: enable the drop down box once batch is working
102        self.dataList.setEnabled(False)
103        # TODO: enable displayChange once batch is working
104        # self.dataList.currentIndexChanged.connect(self.displayChange)
105        self.calculateButton.clicked.connect(self._calculation)
106        self.helpButton.clicked.connect(self.help)
107        self.estimateBgd.toggled.connect(self.toggleBgd)
108        self.manualBgd.toggled.connect(self.toggleBgd)
109        self.regConstantSuggestionButton.clicked.connect(self.acceptAlpha)
110        self.noOfTermsSuggestionButton.clicked.connect(self.acceptNoTerms)
111        self.explorerButton.clicked.connect(self.openExplorerWindow)
112        self.model.itemChanged.connect(self.model_changed)
113
114    def setupMapper(self):
115        # Set up the mapper.
116        self.mapper.setOrientation(QtCore.Qt.Vertical)
117        self.mapper.setModel(self.model)
118
119        # Filename
120        self.mapper.addMapping(self.dataList, WIDGETS.W_FILENAME)
121        # Background
122        self.mapper.addMapping(self.backgroundInput, WIDGETS.W_BACKGROUND_INPUT)
123        self.mapper.addMapping(self.estimateBgd, WIDGETS.W_ESTIMATE)
124        self.mapper.addMapping(self.manualBgd, WIDGETS.W_MANUAL_INPUT)
125
126        # Qmin/Qmax
127        self.mapper.addMapping(self.minQInput, WIDGETS.W_QMIN)
128        self.mapper.addMapping(self.maxQInput, WIDGETS.W_QMAX)
129
130        # Slit Parameter items
131        self.mapper.addMapping(self.slitWidthInput, WIDGETS.W_SLIT_WIDTH)
132        self.mapper.addMapping(self.slitHeightInput, WIDGETS.W_SLIT_HEIGHT)
133
134        # Parameter Items
135        self.mapper.addMapping(self.regularizationConstantInput,
136                               WIDGETS.W_REGULARIZATION)
137        self.mapper.addMapping(self.regConstantSuggestionButton,
138                               WIDGETS.W_REGULARIZATION_SUGGEST)
139        self.mapper.addMapping(self.explorerButton, WIDGETS.W_EXPLORE)
140        self.mapper.addMapping(self.maxDistanceInput, WIDGETS.W_MAX_DIST)
141        self.mapper.addMapping(self.noOfTermsInput, WIDGETS.W_NO_TERMS)
142        self.mapper.addMapping(self.noOfTermsSuggestionButton,
143                               WIDGETS.W_NO_TERMS_SUGGEST)
144
145        # Output
146        self.mapper.addMapping(self.rgValue, WIDGETS.W_RG)
147        self.mapper.addMapping(self.iQ0Value, WIDGETS.W_I_ZERO)
148        self.mapper.addMapping(self.backgroundValue, WIDGETS.W_BACKGROUND_OUTPUT)
149        self.mapper.addMapping(self.computationTimeValue, WIDGETS.W_COMP_TIME)
150        self.mapper.addMapping(self.chiDofValue, WIDGETS.W_CHI_SQUARED)
151        self.mapper.addMapping(self.oscillationValue, WIDGETS.W_OSCILLATION)
152        self.mapper.addMapping(self.posFractionValue, WIDGETS.W_POS_FRACTION)
153        self.mapper.addMapping(self.sigmaPosFractionValue,
154                               WIDGETS.W_SIGMA_POS_FRACTION)
155
156        # Main Buttons
157        self.mapper.addMapping(self.calculateButton, WIDGETS.W_CALCULATE)
158        self.mapper.addMapping(self.helpButton, WIDGETS.W_HELP)
159
160        self.mapper.toFirst()
161
162    def setupModel(self):
163        """
164        Update boxes with initial values
165        """
166        item = QtGui.QStandardItem("")
167        self.model.setItem(WIDGETS.W_FILENAME, item)
168        item = QtGui.QStandardItem('0.0')
169        self.model.setItem(WIDGETS.W_BACKGROUND_INPUT, item)
170        item = QtGui.QStandardItem("")
171        self.model.setItem(WIDGETS.W_QMIN, item)
172        item = QtGui.QStandardItem("")
173        self.model.setItem(WIDGETS.W_QMAX, item)
174        item = QtGui.QStandardItem("")
175        self.model.setItem(WIDGETS.W_SLIT_WIDTH, item)
176        item = QtGui.QStandardItem("")
177        self.model.setItem(WIDGETS.W_SLIT_HEIGHT, item)
178        item = QtGui.QStandardItem("10")
179        self.model.setItem(WIDGETS.W_NO_TERMS, item)
180        item = QtGui.QStandardItem("0.0001")
181        self.model.setItem(WIDGETS.W_REGULARIZATION, item)
182        item = QtGui.QStandardItem("140.0")
183        self.model.setItem(WIDGETS.W_MAX_DIST, item)
184        item = QtGui.QStandardItem("")
185        self.model.setItem(WIDGETS.W_RG, item)
186        item = QtGui.QStandardItem("")
187        self.model.setItem(WIDGETS.W_I_ZERO, item)
188        item = QtGui.QStandardItem("")
189        self.model.setItem(WIDGETS.W_BACKGROUND_OUTPUT, item)
190        item = QtGui.QStandardItem("")
191        self.model.setItem(WIDGETS.W_COMP_TIME, item)
192        item = QtGui.QStandardItem("")
193        self.model.setItem(WIDGETS.W_CHI_SQUARED, item)
194        item = QtGui.QStandardItem("")
195        self.model.setItem(WIDGETS.W_OSCILLATION, item)
196        item = QtGui.QStandardItem("")
197        self.model.setItem(WIDGETS.W_POS_FRACTION, item)
198        item = QtGui.QStandardItem("")
199        self.model.setItem(WIDGETS.W_SIGMA_POS_FRACTION, item)
200
201    ######################################################################
202    # Methods for updating GUI
203
204    def enableButtons(self):
205        """
206        Enable buttons when data is present, else disable them
207        """
208        if self._has_data:
209            self.explorerButton.setEnabled(True)
210            self.calculateButton.setEnabled(True)
211        else:
212            self.calculateButton.setEnabled(False)
213            self.explorerButton.setEnabled(False)
214
215    def populateDataComboBox(self, filename):
216        """
217        Append a new file name to the data combobox
218        :param data: Data1D object
219        """
220        qt_item = QtCore.QString.fromUtf8(filename)
221        self.dataList.addItem(qt_item)
222
223    def acceptNoTerms(self):
224        """Send estimated no of terms to input"""
225        self.model.setItem(WIDGETS.W_NO_TERMS, QtGui.QStandardItem(
226            self.noOfTermsSuggestionButton.text()))
227
228    def acceptAlpha(self):
229        """Send estimated alpha to input"""
230        self.model.setItem(WIDGETS.W_REGULARIZATION, QtGui.QStandardItem(
231            self.regConstantSuggestionButton.text()))
232
233    ######################################################################
234    # GUI Interaction Events
235
236    def _calculation(self):
237        """
238        Calculate the P(r) for every data set in the data list
239        """
240        # TODO: Set all invertor values before calculation
241        self._calculator.__setattr__("qmin", UI.TabbedPrInversionUI._fromUtf8(
242            self.minQInput.text()))
243        self.startThread()
244
245    def model_changed(self):
246        """Update the values when user makes changes"""
247        if not self.mapper:
248            return
249        self.mapper.toFirst()
250        # TODO: Update plots, etc.
251
252    def help(self):
253        """
254        Open the P(r) Inversion help browser
255        """
256        tree_location = (GuiUtils.HELP_DIRECTORY_LOCATION +
257                         "user/sasgui/perspectives/pr/pr_help.html")
258
259        # Actual file anchor will depend on the combo box index
260        # Note that we can be clusmy here, since bad current_fitter_id
261        # will just make the page displayed from the top
262        self._helpView.load(QtCore.QUrl(tree_location))
263        self._helpView.show()
264
265    def checkBgdClicked(self, boolean=None):
266        if boolean or self.manualBgd.isChecked():
267            self.manualBgd.setChecked(True)
268            self.toggleBgd(self.manualBgd)
269        else:
270            self.estimateBgd.setChecked(True)
271            self.toggleBgd(self.estimateBgd)
272
273    def toggleBgd(self, item=None):
274        """
275        Toggle the background between manual and estimated
276        :param item: gui item that was triggered
277        """
278        if not item:
279            self.checkBgdClicked()
280        elif isinstance(item, QtGui.QRadioButton):
281            if item is self.estimateBgd:
282                self.backgroundInput.setEnabled(False)
283            else:
284                self.backgroundInput.setEnabled(True)
285
286    def openExplorerWindow(self):
287        """
288        Open the Explorer window to see correlations between params and results
289        """
290        # TODO: This depends on SVCC-45
291        pass
292
293    ######################################################################
294    # Response Actions
295
296    def setData(self, data_item=None, is_batch=False):
297        """
298        Assign new data set or sets to the P(r) perspective
299        Obtain a QStandardItem object and dissect it to get Data1D/2D
300        Pass it over to the calculator
301        """
302        assert data_item is not None
303
304        if not isinstance(data_item, list):
305            msg = "Incorrect type passed to the P(r) Perspective"
306            raise AttributeError, msg
307
308        if not isinstance(data_item[0], QtGui.QStandardItem):
309            msg = "Incorrect type passed to the P(r) Perspective"
310            raise AttributeError, msg
311
312        for data in data_item:
313            data_object = GuiUtils.dataFromItem(data)
314            self._data_list.append({data_object: None})
315            self._has_data = True
316            self.enableButtons()
317            self.populateDataComboBox(data_object.filename)
318            self.model.setItem(WIDGETS.W_QMIN,
319                               QtGui.QStandardItem(str(data_object.x.min())))
320            self.model.setItem(WIDGETS.W_QMAX,
321                               QtGui.QStandardItem(str(data_object.x.max())))
322
323            # Estimate initial values from data
324            self.performEstimate()
325
326            # TODO: Plot data on load
327
328            # TODO: Only load in the 1st data until batch mode is working
329            # TODO: Thus, the break
330            break
331
332    ######################################################################
333    # Thread Creators
334
335    def startThread(self):
336        """
337            Start a calculation thread
338        """
339        from PrThread import CalcPr
340
341        # If a thread is already started, stop it
342        if self.calc_thread is not None and self.calc_thread.isrunning():
343            self.calc_thread.stop()
344        pr = self._calculator.clone()
345        nfunc = int(UI.TabbedPrInversionUI._fromUtf8(
346            self.noOfTermsInput.text()))
347        self.calc_thread = CalcPr(pr, nfunc,
348                                  error_func=self._threadError,
349                                  completefn=self._completed, updatefn=None)
350        self.calc_thread.queue()
351        self.calc_thread.ready(2.5)
352
353    def performEstimateNT(self):
354        """
355            Perform parameter estimation
356        """
357        from PrThread import EstimateNT
358
359        # If a thread is already started, stop it
360        if (self.estimation_thread is not None and
361                self.estimation_thread.isrunning()):
362            self.estimation_thread.stop()
363        pr = self._calculator.clone()
364        # Skip the slit settings for the estimation
365        # It slows down the application and it doesn't change the estimates
366        pr.slit_height = 0.0
367        pr.slit_width = 0.0
368        nfunc = int(UI.TabbedPrInversionUI._fromUtf8(
369            self.noOfTermsInput.text()))
370        self.estimation_thread = EstimateNT(pr, nfunc,
371                                            error_func=self._threadError,
372                                            completefn=self._estimateNTCompleted,
373                                            updatefn=None)
374        self.estimation_thread.queue()
375        self.estimation_thread.ready(2.5)
376
377    def performEstimate(self):
378        """
379            Perform parameter estimation
380        """
381        from PrThread import EstimatePr
382
383        # If a thread is already started, stop it
384        if (self.estimation_thread is not None and
385                self.estimation_thread.isrunning()):
386            self.estimation_thread.stop()
387        pr = self._calculator.clone()
388        nfunc = int(UI.TabbedPrInversionUI._fromUtf8(
389            self.noOfTermsInput.text()))
390        self.estimation_thread = EstimatePr(pr, nfunc,
391                                            error_func=self._threadError,
392                                            completefn=self._estimateCompleted,
393                                            updatefn=None)
394        self.estimation_thread.queue()
395        self.estimation_thread.ready(2.5)
396
397    ######################################################################
398    # Thread Complete
399
400    def _estimateCompleted(self, alpha, message, elapsed):
401        """
402        Parameter estimation completed,
403        display the results to the user
404
405        :param alpha: estimated best alpha
406        :param elapsed: computation time
407        """
408        # Save useful info
409        self.model.setItem(WIDGETS.W_COMP_TIME,
410                           QtGui.QStandardItem(str(elapsed)))
411        self.regConstantSuggestionButton.setText(QtCore.QString(str(alpha)))
412        self.regConstantSuggestionButton.setEnabled(True)
413        if message is not None:
414            logging.info(message)
415        self.performEstimateNT()
416
417    def _estimateNTCompleted(self, nterms, alpha, message, elapsed):
418        """
419        Parameter estimation completed,
420        display the results to the user
421
422        :param alpha: estimated best alpha
423        :param nterms: estimated number of terms
424        :param elapsed: computation time
425
426        """
427        # Save useful info
428        self.noOfTermsSuggestionButton.setText(QtCore.QString(str(nterms)))
429        self.noOfTermsSuggestionButton.setEnabled(True)
430        self.regConstantSuggestionButton.setText(QtCore.QString(str(alpha)))
431        self.regConstantSuggestionButton.setEnabled(True)
432        self.model.setItem(WIDGETS.W_COMP_TIME,
433                           QtGui.QStandardItem(str(elapsed)))
434        if message is not None:
435            logging.info(message)
436            pass
437
438    def _completed(self, out, cov, pr, elapsed):
439        """
440        Method called with the results when the inversion is done
441
442        :param out: output coefficient for the base functions
443        :param cov: covariance matrix
444        :param pr: Invertor instance
445        :param elapsed: time spent computing
446
447        """
448        # Save useful info
449        # Keep a copy of the last result
450        self._last_calculator = pr.clone()
451
452        # TODO: Append to data dictionary
453        # self._data_list.append({s})
454
455        # Save Pr invertor
456        self._calculator = pr
457        cov = np.ascontiguousarray(cov)
458
459        # Show result on control panel
460
461        self.model.setItem(WIDGETS.W_RG, QtGui.QStandardItem(str(pr.rg(out))))
462        self.model.setItem(WIDGETS.W_I_ZERO,
463                           QtGui.QStandardItem(str(pr.iq0(out))))
464        self.model.setItem(WIDGETS.W_BACKGROUND_INPUT,
465                           QtGui.QStandardItem("{:.2g}".format(pr.background)))
466        self.model.setItem(WIDGETS.W_BACKGROUND_OUTPUT,
467                           QtGui.QStandardItem(str(pr.background)))
468        self.model.setItem(WIDGETS.W_CHI_SQUARED,
469                           QtGui.QStandardItem(str(pr.chi2)))
470        self.model.setItem(WIDGETS.W_COMP_TIME,
471                           QtGui.QStandardItem(str(elapsed)))
472        self.model.setItem(WIDGETS.W_OSCILLATION,
473                           QtGui.QStandardItem(str(pr.oscillations(out))))
474        self.model.setItem(WIDGETS.W_POS_FRACTION,
475                           QtGui.QStandardItem(str(pr.get_positive(out))))
476        self.model.setItem(WIDGETS.W_SIGMA_POS_FRACTION,
477                           QtGui.QStandardItem(str(pr.get_pos_err(out, cov))))
478
479        # TODO: Show plots
480        # Show I(q) fit
481        # self.show_iq(out, self._calculator)
482        # Show P(r) fit
483        # self.show_pr(out, self._calculator, cov)
484
485    def _threadError(self, error):
486        """
487            Call-back method for calculation errors
488        """
489        logging.warning(error)
490
491    def show_data(self, path=None, data=None, reset=False):
492        """
493        Show data read from a file
494
495        :param path: file path
496        :param reset: if True all other plottables will be cleared
497
498        """
499        if data is not None:
500            try:
501                pr = self._create_file_pr(data)
502            except:
503                status = "Problem reading data: %s" % sys.exc_value
504                raise RuntimeError, status
505
506            # If the file contains nothing, just return
507            if pr is None:
508                raise RuntimeError, "Loaded data is invalid"
509
510            self._calculator = pr
511
512        # Make a plot of I(q) data
513        if self._calculator.err is None:
514            logging.log(self._calculator.err)
515        else:
516            # TODO: Do something
517            pass
518        # Get Q range
519        #self.control_panel.q_min = min(self._calculator.x)
520        #self.control_panel.q_max = max(self._calculator.x)
Note: See TracBrowser for help on using the repository browser.