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

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

P(r) calculator returning reasonable answers. Plotting and unit tests left.

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