source: sasview/src/sas/sasgui/perspectives/pr/explore_dialog.py @ 3a3f192

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