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

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 fa81e94 was fa81e94, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Initial commit of the P(r) inversion perspective.
Code merged from Jeff Krzywon's ESS_GUI_Pr branch.
Also, minor 2to3 mods to sascalc/sasgui to enble error free setup.

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