source: sasview/src/sas/sasgui/perspectives/pr/explore_dialog.py @ 7677b4d

Last change on this file since 7677b4d was d85c194, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 9 years ago

Remaining modules refactored

  • Property mode set to 100644
File size: 14.9 KB
RevLine 
[a00ee4c]1
[7116b6e0]2################################################################################
3#This software was developed by the University of Tennessee as part of the
4#Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
[8b21fa7]5#project funded by the US National Science Foundation.
[7116b6e0]6#
7#See the license text in license.txt
8#
9#copyright 2009, University of Tennessee
10################################################################################
[a00ee4c]11
12"""
[7116b6e0]13Dialog panel to explore the P(r) inversion results for a range
14of D_max value. User picks a number of points and a range of
15distances, then can toggle between inversion outputs and see
16their distribution as a function of D_max.
17"""
[8b21fa7]18
[7116b6e0]19
[a00ee4c]20import wx
21import numpy
22import logging
[3b865c1]23import sys
[a00ee4c]24
[8b21fa7]25# Avoid Matplotlib complaining about the lack of legend on the plot
[a00ee4c]26import warnings
27warnings.simplefilter("ignore")
28
29# Import plotting classes
[d7bb526]30from sas.sasgui.plottools.PlotPanel import PlotPanel
31from sas.sasgui.plottools import Data1D as Model1D
[d85c194]32from sas.sasgui.guiframe.gui_style import GUIFRAME_ID
[d7bb526]33from sas.sasgui.plottools.plottables import Graph
[a00ee4c]34
[9ff861b]35from pr_widgets import PrTextCtrl
36
[a00ee4c]37# Default number of points on the output plot
38DEFAULT_NPTS = 10
39# Default output parameter to plot
40DEFAULT_OUTPUT = 'Chi2/dof'
41
42class OutputPlot(PlotPanel):
43    """
[7116b6e0]44    Plot panel used to show the selected results as a function
45    of D_max
[a00ee4c]46    """
[3d250da3]47    ## Title for plottools
48    window_caption = "D Explorer"
49
[8b21fa7]50    def __init__(self, d_min, d_max, parent, id= -1, color=None, \
51                 dpi=None, style=wx.NO_FULL_REPAINT_ON_RESIZE, **kwargs):
[a00ee4c]52        """
[7116b6e0]53        Initialization. The parameters added to PlotPanel are:
[8b21fa7]54
[7116b6e0]55        :param d_min: Minimum value of D_max to explore
56        :param d_max: Maximum value of D_max to explore
[8b21fa7]57
[a00ee4c]58        """
[8b21fa7]59        PlotPanel.__init__(self, parent, id=id, style=style, **kwargs)
[a00ee4c]60
61        self.parent = parent
62        self.min = d_min
63        self.max = d_max
64        self.npts = DEFAULT_NPTS
65
[8b21fa7]66        step = (self.max - self.min) / (self.npts - 1)
67        self.x = numpy.arange(self.min, self.max + step * 0.01, step)
[a00ee4c]68        dx = numpy.zeros(len(self.x))
69        y = numpy.ones(len(self.x))
70        dy = numpy.zeros(len(self.x))
[8b21fa7]71
[a00ee4c]72        # Plot area
[3e41f43]73        self.plot = Model1D(self.x, y=y, dy=dy)
[a00ee4c]74        self.plot.name = DEFAULT_OUTPUT
[c03fb16]75        self.plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM
[a00ee4c]76
77        # Graph       
78        self.graph = Graph()
79        self.graph.xaxis("\\rm{D_{max}}", 'A')
[8b21fa7]80        self.graph.yaxis("\\rm{%s}" % DEFAULT_OUTPUT, "")
[a00ee4c]81        self.graph.add(self.plot)
82        self.graph.render(self)
[8b21fa7]83
[67dd757]84        self.toolbar.DeleteToolByPos(0)
85        self.toolbar.DeleteToolByPos(8)
86        self.toolbar.Realize()
[a00ee4c]87
88    def onContextMenu(self, event):
89        """
[7116b6e0]90        Default context menu for the plot panel
[8b21fa7]91
[7116b6e0]92        :TODO: Would be nice to add printing and log/linear scales.
[8b21fa7]93            The current verison of plottools no longer plays well with
94            plots outside of guiframe. Guiframe team needs to fix this.
[a00ee4c]95        """
96        # Slicer plot popup menu
[8b21fa7]97        wx_id = wx.NewId()
[a00ee4c]98        slicerpop = wx.Menu()
[8b21fa7]99        slicerpop.Append(wx_id, '&Save image', 'Save image as PNG')
100        wx.EVT_MENU(self, wx_id, self.onSaveImage)
101
102        wx_id = wx.NewId()
[a00ee4c]103        slicerpop.AppendSeparator()
[8b21fa7]104        slicerpop.Append(wx_id, '&Reset Graph')
105        wx.EVT_MENU(self, wx_id, self.onResetGraph)
106
[a00ee4c]107        pos = event.GetPosition()
108        pos = self.ScreenToClient(pos)
109        self.PopupMenu(slicerpop, pos)
110
[8b21fa7]111class Results(object):
[a00ee4c]112    """
[7116b6e0]113    Class to hold the inversion output parameters
114    as a function of D_max
[a00ee4c]115    """
116    def __init__(self):
117        """
[7116b6e0]118        Initialization. Create empty arrays
119        and dictionary of labels.
[a00ee4c]120        """
121        # Array of output for each inversion
122        self.chi2 = []
123        self.osc = []
124        self.pos = []
125        self.pos_err = []
126        self.rg = []
127        self.iq0 = []
128        self.bck = []
129        self.d_max = []
[8b21fa7]130
[a00ee4c]131        # Dictionary of outputs
132        self.outputs = {}
[8b21fa7]133        self.outputs['Chi2/dof'] = ["\chi^2/dof", "a.u.", self.chi2]
134        self.outputs['Oscillation parameter'] = ["Osc", "a.u.", self.osc]
135        self.outputs['Positive fraction'] = ["P^+", "a.u.", self.pos]
136        self.outputs['1-sigma positive fraction'] = ["P^+_{1\ \sigma}",
137                                                     "a.u.", self.pos_err]
138        self.outputs['Rg'] = ["R_g", "A", self.rg]
139        self.outputs['I(q=0)'] = ["I(q=0)", "1/A", self.iq0]
140        self.outputs['Background'] = ["Bck", "1/A", self.bck]
141
[a00ee4c]142class ExploreDialog(wx.Dialog):
143    """
[7116b6e0]144    The explorer dialog box. This dialog is meant to be
145    invoked by the InversionControl class.
[a00ee4c]146    """
[8b21fa7]147
[a00ee4c]148    def __init__(self, pr_state, nfunc, *args, **kwds):
149        """
[7116b6e0]150        Initialization. The parameters added to Dialog are:
[8b21fa7]151
[b699768]152        :param pr_state: sas.sascalc.pr.invertor.Invertor object
[7116b6e0]153        :param nfunc: Number of terms in the expansion
[8b21fa7]154
[a00ee4c]155        """
[8b21fa7]156        kwds["style"] = wx.RESIZE_BORDER | wx.DEFAULT_DIALOG_STYLE
[a00ee4c]157        wx.Dialog.__init__(self, *args, **kwds)
[8b21fa7]158
[a00ee4c]159        # Initialize Results object
160        self.results = Results()
[8b21fa7]161
162        self.pr_state = pr_state
163        self._default_min = 0.9 * self.pr_state.d_max
164        self._default_max = 1.1 * self.pr_state.d_max
165        self.nfunc = nfunc
166
[a00ee4c]167        # Control for number of points
[3e41f43]168        self.npts_ctl = PrTextCtrl(self, -1, style=wx.TE_PROCESS_ENTER,
[8b21fa7]169                                   size=(60, 20))
[a00ee4c]170        # Control for the minimum value of D_max
[3e41f43]171        self.dmin_ctl = PrTextCtrl(self, -1, style=wx.TE_PROCESS_ENTER,
[8b21fa7]172                                   size=(60, 20))
[a00ee4c]173        # Control for the maximum value of D_max
[3e41f43]174        self.dmax_ctl = PrTextCtrl(self, -1, style=wx.TE_PROCESS_ENTER,
[8b21fa7]175                                   size=(60, 20))
[a00ee4c]176
177        # Output selection box for the y axis
178        self.output_box = None
179
180        # Create the plot object
[3e41f43]181        self.plotpanel = OutputPlot(self._default_min, self._default_max,
182                                    self, -1, style=wx.RAISED_BORDER)
[8b21fa7]183
[a00ee4c]184        # Create the layout of the dialog
185        self.__do_layout()
186        self.Fit()
[8b21fa7]187
[a00ee4c]188        # Calculate exploration results
189        self._recalc()
190        # Graph the default output curve
[164c7d0]191        self._plot_output()
[8b21fa7]192
193    class Event(object):
[a00ee4c]194        """
[7116b6e0]195        Class that holds the content of the form
[a00ee4c]196        """
197        ## Number of points to be plotted
[8b21fa7]198        npts = 0
[a00ee4c]199        ## Minimum value of D_max
[8b21fa7]200        dmin = 0
[a00ee4c]201        ## Maximum value of D_max
[8b21fa7]202        dmax = 0
203
[164c7d0]204    def _get_values(self, event=None):
[a00ee4c]205        """
[7116b6e0]206        Invoked when the user changes a value of the form.
207        Check that the values are of the right type.
[8b21fa7]208
209        :return: ExploreDialog.Event object if the content is good,
[7116b6e0]210            None otherwise
[a00ee4c]211        """
212        # Flag to make sure that all values are good
213        flag = True
[8b21fa7]214
[a00ee4c]215        # Empty ExploreDialog.Event content
216        content_event = self.Event()
[8b21fa7]217
[a00ee4c]218        # Read each text control and make sure the type is valid
219        # Let the user know if a type is invalid by changing the
220        # background color of the control.
221        try:
222            content_event.npts = int(self.npts_ctl.GetValue())
223            self.npts_ctl.SetBackgroundColour(wx.WHITE)
224            self.npts_ctl.Refresh()
225        except:
226            flag = False
227            self.npts_ctl.SetBackgroundColour("pink")
228            self.npts_ctl.Refresh()
[8b21fa7]229
[a00ee4c]230        try:
231            content_event.dmin = float(self.dmin_ctl.GetValue())
232            self.dmin_ctl.SetBackgroundColour(wx.WHITE)
233            self.dmin_ctl.Refresh()
234        except:
235            flag = False
236            self.dmin_ctl.SetBackgroundColour("pink")
237            self.dmin_ctl.Refresh()
[8b21fa7]238
[a00ee4c]239        try:
240            content_event.dmax = float(self.dmax_ctl.GetValue())
241            self.dmax_ctl.SetBackgroundColour(wx.WHITE)
242            self.dmax_ctl.Refresh()
243        except:
244            flag = False
245            self.dmax_ctl.SetBackgroundColour("pink")
246            self.dmax_ctl.Refresh()
[8b21fa7]247
[a00ee4c]248        # If the content of the form is valid, return the content,
249        # otherwise return None
250        if flag:
[164c7d0]251            if event is not None:
252                event.Skip(True)
[a00ee4c]253            return content_event
254        else:
255            return None
[8b21fa7]256
[a00ee4c]257    def _plot_output(self, event=None):
258        """
[8b21fa7]259        Invoked when a new output type is selected for plotting,
[7116b6e0]260        or when a new computation is finished.
[a00ee4c]261        """
262        # Get the output type selection
263        output_type = self.output_box.GetString(self.output_box.GetSelection())
[8b21fa7]264
[a00ee4c]265        # If the selected output type is part of the results ojbect,
[8b21fa7]266        # display the results.
[a00ee4c]267        # Note: by design, the output type should always be part of the
268        #       results object.
[8b21fa7]269        if self.results.outputs.has_key(output_type):
[a00ee4c]270            self.plotpanel.plot.x = self.results.d_max
271            self.plotpanel.plot.y = self.results.outputs[output_type][2]
272            self.plotpanel.plot.name = '_nolegend_'
[3e41f43]273            y_label = "\\rm{%s}" % self.results.outputs[output_type][0]
274            self.plotpanel.graph.yaxis(y_label,
275                                       self.results.outputs[output_type][1])
[8b21fa7]276
[a00ee4c]277            # Redraw
278            self.plotpanel.graph.render(self.plotpanel)
279            self.plotpanel.subplot.figure.canvas.draw_idle()
280        else:
[8b21fa7]281            msg = "ExploreDialog: the Results object's dictionary "
[3e41f43]282            msg += "does not contain "
283            msg += "the [%s] output type. This must be indicative of "
284            msg += "a change in the " % str(output_type)
[a00ee4c]285            msg += "ExploreDialog code."
286            logging.error(msg)
[8b21fa7]287
[a00ee4c]288    def __do_layout(self):
289        """
[7116b6e0]290        Do the layout of the dialog
[a00ee4c]291        """
292        # Dialog box properties
293        self.SetTitle("D_max Explorer")
294        self.SetSize((600, 595))
[8b21fa7]295
[a00ee4c]296        sizer_main = wx.BoxSizer(wx.VERTICAL)
297        sizer_button = wx.BoxSizer(wx.HORIZONTAL)
[3e41f43]298        sizer_params = wx.GridBagSizer(5, 5)
[a00ee4c]299
300        iy = 0
301        ix = 0
[8b21fa7]302        label_npts = wx.StaticText(self, -1, "Npts")
303        sizer_params.Add(label_npts, (iy, ix), (1, 1),
304                         wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
[a00ee4c]305        ix += 1
[8b21fa7]306        sizer_params.Add(self.npts_ctl, (iy, ix), (1, 1),
307                         wx.EXPAND | wx.ADJUST_MINSIZE, 0)
[a00ee4c]308        self.npts_ctl.SetValue("%g" % DEFAULT_NPTS)
[8b21fa7]309
[a00ee4c]310        ix += 1
[8b21fa7]311        label_dmin = wx.StaticText(self, -1, "Min Distance [A]")
[3e41f43]312        sizer_params.Add(label_dmin, (iy, ix), (1, 1),
[8b21fa7]313                         wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
[a00ee4c]314        ix += 1
[3e41f43]315        sizer_params.Add(self.dmin_ctl, (iy, ix), (1, 1),
[8b21fa7]316                         wx.EXPAND | wx.ADJUST_MINSIZE, 0)
[a00ee4c]317        self.dmin_ctl.SetValue(str(self._default_min))
[8b21fa7]318
[a00ee4c]319        ix += 1
320        label_dmax = wx.StaticText(self, -1, "Max Distance [A]")
[3e41f43]321        sizer_params.Add(label_dmax, (iy, ix), (1, 1),
[8b21fa7]322                         wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
[a00ee4c]323        ix += 1
[8b21fa7]324        sizer_params.Add(self.dmax_ctl, (iy, ix), (1, 1),
325                         wx.EXPAND | wx.ADJUST_MINSIZE, 0)
[a00ee4c]326        self.dmax_ctl.SetValue(str(self._default_max))
327
328        # Ouput selection box
329        selection_msg = wx.StaticText(self, -1, "Select a dependent variable:")
[df9492d]330        self.output_box = wx.ComboBox(self, -1, style=wx.CB_READONLY)
[a00ee4c]331        for item in self.results.outputs.keys():
332            self.output_box.Append(item, "")
333        self.output_box.SetStringSelection(DEFAULT_OUTPUT)
[8b21fa7]334
[3e41f43]335        output_sizer = wx.GridBagSizer(5, 5)
336        output_sizer.Add(selection_msg, (0, 0), (1, 1),
[8b21fa7]337                         wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 10)
[3e41f43]338        output_sizer.Add(self.output_box, (0, 1), (1, 2),
[8b21fa7]339                         wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 10)
340
[b3fd1c7]341        wx.EVT_COMBOBOX(self.output_box, -1, self._plot_output)
[a00ee4c]342        sizer_main.Add(output_sizer, 0, wx.EXPAND | wx.ALL, 10)
[8b21fa7]343
[a00ee4c]344        sizer_main.Add(self.plotpanel, 0, wx.EXPAND | wx.ALL, 10)
[3e41f43]345        sizer_main.SetItemMinSize(self.plotpanel, 400, 400)
[8b21fa7]346
347        sizer_main.Add(sizer_params, 0, wx.EXPAND | wx.ALL, 10)
[a00ee4c]348        static_line_3 = wx.StaticLine(self, -1)
349        sizer_main.Add(static_line_3, 0, wx.EXPAND, 0)
[8b21fa7]350
[a00ee4c]351        # Bottom area with the close button
[8b21fa7]352        sizer_button.Add((20, 20), 1, wx.EXPAND | wx.ADJUST_MINSIZE, 0)
[a00ee4c]353        button_OK = wx.Button(self, wx.ID_OK, "Close")
[8b21fa7]354        sizer_button.Add(button_OK, 0, wx.LEFT | wx.RIGHT | wx.ADJUST_MINSIZE, 10)
355
356        sizer_main.Add(sizer_button, 0, wx.EXPAND | wx.BOTTOM | wx.TOP, 10)
[a00ee4c]357        self.SetAutoLayout(True)
358        self.SetSizer(sizer_main)
359        self.Layout()
360        self.Centre()
[8b21fa7]361
[a00ee4c]362        # Bind the Enter key to recalculation
363        self.Bind(wx.EVT_TEXT_ENTER, self._recalc)
364
[e16e5c3]365    def set_plot_unfocus(self):
366        """
367        Not implemented
368        """
369        pass
[8b21fa7]370
[3d250da3]371    def send_focus_to_datapanel(self, name):
372        """
373            The GUI manager sometimes calls this method
374            TODO: refactor this
375        """
376        pass
377
[a00ee4c]378    def _recalc(self, event=None):
379        """
[7116b6e0]380        Invoked when the user changed a value on the form.
381        Process the form and compute the output to be plottted.
[a00ee4c]382        """
383        # Get the content of the form
384        content = self._get_values()
385        # If the content of the form is invalid, return and do nothing
386        if content is None:
387            return
[8b21fa7]388
[a00ee4c]389        # Results object to store the computation outputs.
390        results = Results()
[8b21fa7]391
[a00ee4c]392        # Loop over d_max values
[8b21fa7]393        for i in range(content.npts):
394            temp = (content.dmax - content.dmin) / (content.npts - 1.0)
[3e41f43]395            d = content.dmin + i * temp
[8b21fa7]396
[a00ee4c]397            self.pr_state.d_max = d
398            try:
[8b21fa7]399                out, cov = self.pr_state.invert(self.nfunc)
400
[3b865c1]401                # Store results
[a00ee4c]402                iq0 = self.pr_state.iq0(out)
403                rg = self.pr_state.rg(out)
404                pos = self.pr_state.get_positive(out)
405                pos_err = self.pr_state.get_pos_err(out, cov)
406                osc = self.pr_state.oscillations(out)
[8b21fa7]407
[a00ee4c]408                results.d_max.append(self.pr_state.d_max)
409                results.bck.append(self.pr_state.background)
410                results.chi2.append(self.pr_state.chi2)
411                results.iq0.append(iq0)
412                results.rg.append(rg)
413                results.pos.append(pos)
414                results.pos_err.append(pos_err)
[8b21fa7]415                results.osc.append(osc)
[a00ee4c]416            except:
417                # This inversion failed, skip this D_max value
[3e41f43]418                msg = "ExploreDialog: inversion failed "
419                msg += "for D_max=%s\n%s" % (str(d), sys.exc_value)
420                logging.error(msg)
[8b21fa7]421
422        self.results = results
423
[a00ee4c]424        # Plot the selected output
[8b21fa7]425        self._plot_output()
Note: See TracBrowser for help on using the repository browser.