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

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

Return P(r) window to a single tab. Added a button to remove current data set from data list.

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