source: sasview/src/sas/qtgui/Perspectives/Inversion/InversionPerspective.py @ c00a28ff

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

Allow P(r) to calculate current data set or all. Update documentation and error handling.

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