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

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

Initial implementation of Adam Washington's Corfunc perspective.
Converted to py3/Qt5.

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