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

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

Plots generated and linked to data set.

  • Property mode set to 100644
File size: 19.8 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 = [data]
69        for datum in data:
70            self._data_list.append({datum: self._calculator.clone()})
71
72        # plots
73        self.pr_plot = None
74        self.data_plot = None
75
76        self.model = QtGui.QStandardItemModel(self)
77        self.mapper = QtGui.QDataWidgetMapper(self)
78        # Link user interactions with methods
79        self.setupLinks()
80        # Set values
81        self.setupModel()
82        # Set up the Widget Map
83        self.setupMapper()
84
85    ######################################################################
86    # Base Perspective Class Definitions
87
88    def communicator(self):
89        return self.communicate
90
91    def allowBatch(self):
92        return False
93
94    def setClosable(self, value=True):
95        """
96        Allow outsiders close this widget
97        """
98        assert isinstance(value, bool)
99        self._allow_close = value
100
101    def closeEvent(self, event):
102        """
103        Overwrite QDialog close method to allow for custom widget close
104        """
105        if self._allow_close:
106            # reset the closability flag
107            self.setClosable(value=False)
108            event.accept()
109        else:
110            event.ignore()
111            # Maybe we should just minimize
112            self.setWindowState(QtCore.Qt.WindowMinimized)
113
114    ######################################################################
115    # Initialization routines
116
117    def setupLinks(self):
118        """Connect the use controls to their appropriate methods"""
119        self.enableButtons()
120        # TODO: enable the drop down box once batch is working
121        self.dataList.setEnabled(False)
122        # TODO: enable displayChange once batch is working
123        # self.dataList.currentIndexChanged.connect(self.displayChange)
124        self.calculateButton.clicked.connect(self._calculation)
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        self.estimateBgd.setChecked(True)
154
155    def setupMapper(self):
156        # Set up the mapper.
157        self.mapper.setOrientation(QtCore.Qt.Vertical)
158        self.mapper.setModel(self.model)
159
160        # Filename
161        self.mapper.addMapping(self.dataList, WIDGETS.W_FILENAME)
162        # Background
163        self.mapper.addMapping(self.backgroundInput, WIDGETS.W_BACKGROUND_INPUT)
164        self.mapper.addMapping(self.estimateBgd, WIDGETS.W_ESTIMATE)
165        self.mapper.addMapping(self.manualBgd, WIDGETS.W_MANUAL_INPUT)
166
167        # Qmin/Qmax
168        self.mapper.addMapping(self.minQInput, WIDGETS.W_QMIN)
169        self.mapper.addMapping(self.maxQInput, WIDGETS.W_QMAX)
170
171        # Slit Parameter items
172        self.mapper.addMapping(self.slitWidthInput, WIDGETS.W_SLIT_WIDTH)
173        self.mapper.addMapping(self.slitHeightInput, WIDGETS.W_SLIT_HEIGHT)
174
175        # Parameter Items
176        self.mapper.addMapping(self.regularizationConstantInput,
177                               WIDGETS.W_REGULARIZATION)
178        self.mapper.addMapping(self.regConstantSuggestionButton,
179                               WIDGETS.W_REGULARIZATION_SUGGEST)
180        self.mapper.addMapping(self.explorerButton, WIDGETS.W_EXPLORE)
181        self.mapper.addMapping(self.maxDistanceInput, WIDGETS.W_MAX_DIST)
182        self.mapper.addMapping(self.noOfTermsInput, WIDGETS.W_NO_TERMS)
183        self.mapper.addMapping(self.noOfTermsSuggestionButton,
184                               WIDGETS.W_NO_TERMS_SUGGEST)
185
186        # Output
187        self.mapper.addMapping(self.rgValue, WIDGETS.W_RG)
188        self.mapper.addMapping(self.iQ0Value, WIDGETS.W_I_ZERO)
189        self.mapper.addMapping(self.backgroundValue, WIDGETS.W_BACKGROUND_OUTPUT)
190        self.mapper.addMapping(self.computationTimeValue, WIDGETS.W_COMP_TIME)
191        self.mapper.addMapping(self.chiDofValue, WIDGETS.W_CHI_SQUARED)
192        self.mapper.addMapping(self.oscillationValue, WIDGETS.W_OSCILLATION)
193        self.mapper.addMapping(self.posFractionValue, WIDGETS.W_POS_FRACTION)
194        self.mapper.addMapping(self.sigmaPosFractionValue,
195                               WIDGETS.W_SIGMA_POS_FRACTION)
196
197        # Main Buttons
198        self.mapper.addMapping(self.calculateButton, WIDGETS.W_CALCULATE)
199        self.mapper.addMapping(self.helpButton, WIDGETS.W_HELP)
200
201        self.mapper.toFirst()
202
203    def setupModel(self):
204        """
205        Update boxes with initial values
206        """
207        item = QtGui.QStandardItem("")
208        self.model.setItem(WIDGETS.W_FILENAME, item)
209        item = QtGui.QStandardItem('0.0')
210        self.model.setItem(WIDGETS.W_BACKGROUND_INPUT, item)
211        item = QtGui.QStandardItem("")
212        self.model.setItem(WIDGETS.W_QMIN, item)
213        item = QtGui.QStandardItem("")
214        self.model.setItem(WIDGETS.W_QMAX, item)
215        item = QtGui.QStandardItem("")
216        self.model.setItem(WIDGETS.W_SLIT_WIDTH, item)
217        item = QtGui.QStandardItem("")
218        self.model.setItem(WIDGETS.W_SLIT_HEIGHT, item)
219        item = QtGui.QStandardItem("10")
220        self.model.setItem(WIDGETS.W_NO_TERMS, item)
221        item = QtGui.QStandardItem("0.0001")
222        self.model.setItem(WIDGETS.W_REGULARIZATION, item)
223        item = QtGui.QStandardItem("140.0")
224        self.model.setItem(WIDGETS.W_MAX_DIST, item)
225        item = QtGui.QStandardItem("")
226        self.model.setItem(WIDGETS.W_RG, item)
227        item = QtGui.QStandardItem("")
228        self.model.setItem(WIDGETS.W_I_ZERO, item)
229        item = QtGui.QStandardItem("")
230        self.model.setItem(WIDGETS.W_BACKGROUND_OUTPUT, item)
231        item = QtGui.QStandardItem("")
232        self.model.setItem(WIDGETS.W_COMP_TIME, item)
233        item = QtGui.QStandardItem("")
234        self.model.setItem(WIDGETS.W_CHI_SQUARED, item)
235        item = QtGui.QStandardItem("")
236        self.model.setItem(WIDGETS.W_OSCILLATION, item)
237        item = QtGui.QStandardItem("")
238        self.model.setItem(WIDGETS.W_POS_FRACTION, item)
239        item = QtGui.QStandardItem("")
240        self.model.setItem(WIDGETS.W_SIGMA_POS_FRACTION, item)
241
242    ######################################################################
243    # Methods for updating GUI
244
245    def enableButtons(self):
246        """
247        Enable buttons when data is present, else disable them
248        """
249        if self.logic.data_is_loaded:
250            self.explorerButton.setEnabled(True)
251            self.calculateButton.setEnabled(True)
252        else:
253            self.calculateButton.setEnabled(False)
254            self.explorerButton.setEnabled(False)
255
256    def populateDataComboBox(self, filename):
257        """
258        Append a new file name to the data combobox
259        :param data: Data1D object
260        """
261        qt_item = QtCore.QString.fromUtf8(filename)
262        self.dataList.addItem(qt_item)
263
264    def acceptNoTerms(self):
265        """Send estimated no of terms to input"""
266        self.model.setItem(WIDGETS.W_NO_TERMS, QtGui.QStandardItem(
267            self.noOfTermsSuggestionButton.text()))
268
269    def acceptAlpha(self):
270        """Send estimated alpha to input"""
271        self.model.setItem(WIDGETS.W_REGULARIZATION, QtGui.QStandardItem(
272            self.regConstantSuggestionButton.text()))
273
274    ######################################################################
275    # GUI Interaction Events
276
277    def update_calculator(self):
278        """Update all p(r) params"""
279        self._calculator.set_x(self._data_set.x)
280        self._calculator.set_y(self._data_set.y)
281        self._calculator.set_err(self._data_set.dy)
282
283    def _calculation(self):
284        """
285        Calculate the P(r) for every data set in the data list
286        """
287        # Set data before running the calculations
288        self.update_calculator()
289        # Run
290        self.startThread()
291
292    def model_changed(self):
293        """Update the values when user makes changes"""
294        if not self.mapper:
295            return
296        if self.pr_plot is not None:
297            title = self.pr_plot.name
298            GuiUtils.updateModelItemWithPlot(
299                self._data, QtCore.QVariant(self.pr_plot), title)
300        if self.data_plot is not None:
301            title = self.data_plot.name
302            GuiUtils.updateModelItemWithPlot(
303                self._data, QtCore.QVariant(self.data_plot), title)
304        self.mapper.toFirst()
305
306    def help(self):
307        """
308        Open the P(r) Inversion help browser
309        """
310        tree_location = (GuiUtils.HELP_DIRECTORY_LOCATION +
311                         "user/sasgui/perspectives/pr/pr_help.html")
312
313        # Actual file anchor will depend on the combo box index
314        # Note that we can be clusmy here, since bad current_fitter_id
315        # will just make the page displayed from the top
316        self._helpView.load(QtCore.QUrl(tree_location))
317        self._helpView.show()
318
319    def toggleBgd(self):
320        """
321        Toggle the background between manual and estimated
322        :param item: gui item that was triggered
323        """
324        sender = self.sender()
325        if sender is self.estimateBgd:
326            self.backgroundInput.setEnabled(False)
327        else:
328            self.backgroundInput.setEnabled(True)
329
330    def openExplorerWindow(self):
331        """
332        Open the Explorer window to see correlations between params and results
333        """
334        # TODO: This depends on SVCC-45
335        pass
336
337    ######################################################################
338    # Response Actions
339
340    def setData(self, data_item=None, is_batch=False):
341        """
342        Assign new data set or sets to the P(r) perspective
343        Obtain a QStandardItem object and dissect it to get Data1D/2D
344        Pass it over to the calculator
345        """
346        assert data_item is not None
347
348        if not isinstance(data_item, list):
349            msg = "Incorrect type passed to the P(r) Perspective"
350            raise AttributeError, msg
351
352        if not isinstance(data_item[0], QtGui.QStandardItem):
353            msg = "Incorrect type passed to the P(r) Perspective"
354            raise AttributeError, msg
355
356        for data in data_item:
357            self._data = data
358            self._data_set = GuiUtils.dataFromItem(data)
359            self.populateDataComboBox(self._data_set.filename)
360
361            # Estimate initial values from data
362            self.performEstimate()
363            self.logic = InversionLogic(self._data_set)
364
365            qmin, qmax = self.logic.computeDataRange()
366
367            self.model.setItem(WIDGETS.W_QMIN, QtGui.QStandardItem(
368                "{:.4g}".format(qmin)))
369            self.model.setItem(WIDGETS.W_QMAX, QtGui.QStandardItem(
370                "{:.4g}".format(qmax)))
371
372            self.enableButtons()
373
374            # TODO: Only load 1st data until batch mode working. Thus, break
375            break
376
377    ######################################################################
378    # Thread Creators
379
380    def startThread(self):
381        """
382            Start a calculation thread
383        """
384        from Thread import CalcPr
385
386        # If a thread is already started, stop it
387        if self.calc_thread is not None and self.calc_thread.isrunning():
388            self.calc_thread.stop()
389        pr = self._calculator.clone()
390        nfunc = int(UI.TabbedInversionUI._fromUtf8(
391            self.noOfTermsInput.text()))
392        self.calc_thread = CalcPr(pr, nfunc,
393                                  error_func=self._threadError,
394                                  completefn=self._completed, updatefn=None)
395        self.calc_thread.queue()
396        self.calc_thread.ready(2.5)
397
398    def performEstimateNT(self):
399        """
400            Perform parameter estimation
401        """
402        from Thread import EstimateNT
403
404        # If a thread is already started, stop it
405        if (self.estimation_thread is not None and
406                self.estimation_thread.isrunning()):
407            self.estimation_thread.stop()
408        pr = self._calculator.clone()
409        # Skip the slit settings for the estimation
410        # It slows down the application and it doesn't change the estimates
411        pr.slit_height = 0.0
412        pr.slit_width = 0.0
413        nfunc = int(UI.TabbedInversionUI._fromUtf8(
414            self.noOfTermsInput.text()))
415        self.estimation_thread = EstimateNT(pr, nfunc,
416                                            error_func=self._threadError,
417                                            completefn=self._estimateNTCompleted,
418                                            updatefn=None)
419        self.estimation_thread.queue()
420        self.estimation_thread.ready(2.5)
421
422    def performEstimate(self):
423        """
424            Perform parameter estimation
425        """
426        from Thread import EstimatePr
427
428        self._calculation()
429
430        # If a thread is already started, stop it
431        if (self.estimation_thread is not None and
432                self.estimation_thread.isrunning()):
433            self.estimation_thread.stop()
434        pr = self._calculator.clone()
435        nfunc = int(UI.TabbedInversionUI._fromUtf8(
436            self.noOfTermsInput.text()))
437        self.estimation_thread = EstimatePr(pr, nfunc,
438                                            error_func=self._threadError,
439                                            completefn=self._estimateCompleted,
440                                            updatefn=None)
441        self.estimation_thread.queue()
442        self.estimation_thread.ready(2.5)
443
444    ######################################################################
445    # Thread Complete
446
447    def _estimateCompleted(self, alpha, message, elapsed):
448        """
449        Parameter estimation completed,
450        display the results to the user
451
452        :param alpha: estimated best alpha
453        :param elapsed: computation time
454        """
455        # Save useful info
456        self.model.setItem(WIDGETS.W_COMP_TIME,
457                           QtGui.QStandardItem(str(elapsed)))
458        self.regConstantSuggestionButton.setText(QtCore.QString(str(alpha)))
459        self.regConstantSuggestionButton.setEnabled(True)
460        if message:
461            logging.info(message)
462        self.performEstimateNT()
463
464    def _estimateNTCompleted(self, nterms, alpha, message, elapsed):
465        """
466        Parameter estimation completed,
467        display the results to the user
468
469        :param alpha: estimated best alpha
470        :param nterms: estimated number of terms
471        :param elapsed: computation time
472
473        """
474        # Save useful info
475        self.noOfTermsSuggestionButton.setText(QtCore.QString(
476            "{:n}".format(nterms)))
477        self.noOfTermsSuggestionButton.setEnabled(True)
478        self.regConstantSuggestionButton.setText(QtCore.QString(
479            "{:.3g}".format(alpha)))
480        self.regConstantSuggestionButton.setEnabled(True)
481        self.model.setItem(WIDGETS.W_COMP_TIME,
482                           QtGui.QStandardItem(str(elapsed)))
483        self.PrTabWidget.setCurrentIndex(0)
484        if message:
485            logging.info(message)
486
487    def _completed(self, out, cov, pr, elapsed):
488        """
489        Method called with the results when the inversion is done
490
491        :param out: output coefficient for the base functions
492        :param cov: covariance matrix
493        :param pr: Invertor instance
494        :param elapsed: time spent computing
495
496        """
497        # Save useful info
498        cov = np.ascontiguousarray(cov)
499        pr.cov = cov
500        pr.out = out
501        pr.elapsed = elapsed
502
503        # Show result on control panel
504
505        # TODO: Connect self._calculator to GUI
506        self.model.setItem(WIDGETS.W_RG, QtGui.QStandardItem(str(pr.rg(out))))
507        self.model.setItem(WIDGETS.W_I_ZERO,
508                           QtGui.QStandardItem(str(pr.iq0(out))))
509        self.model.setItem(WIDGETS.W_BACKGROUND_INPUT,
510                           QtGui.QStandardItem("{:.3f}".format(pr.background)))
511        self.model.setItem(WIDGETS.W_BACKGROUND_OUTPUT,
512                           QtGui.QStandardItem(str(pr.background)))
513        self.model.setItem(WIDGETS.W_CHI_SQUARED,
514                           QtGui.QStandardItem(str(pr.chi2[0])))
515        self.model.setItem(WIDGETS.W_COMP_TIME,
516                           QtGui.QStandardItem(str(elapsed)))
517        self.model.setItem(WIDGETS.W_OSCILLATION,
518                           QtGui.QStandardItem(str(pr.oscillations(out))))
519        self.model.setItem(WIDGETS.W_POS_FRACTION,
520                           QtGui.QStandardItem(str(pr.get_positive(out))))
521        self.model.setItem(WIDGETS.W_SIGMA_POS_FRACTION,
522                           QtGui.QStandardItem(str(pr.get_pos_err(out, cov))))
523
524        # Display results tab
525        self.PrTabWidget.setCurrentIndex(1)
526        # Save Pr invertor
527        self._calculator = pr
528        # Append data to data list
529        self._data_list.append({self._data: pr})
530
531        if self.pr_plot is None:
532            self.pr_plot = self.logic.newPRPlot(out, self._calculator, cov)
533        if self.data_plot is None:
534            self.data_plot = self.logic.new1DPlot(out, self._calculator)
535
536    def _threadError(self, error):
537        """
538            Call-back method for calculation errors
539        """
540        logging.warning(error)
Note: See TracBrowser for help on using the repository browser.