source: sasview/prview/perspectives/pr/explore_dialog.py @ d9dc518

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalccostrafo411magnetic_scattrelease-4.1.1release-4.1.2release-4.2.2release_4.0.1ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since d9dc518 was 9ff861b, checked in by Mathieu Doucet <doucetm@…>, 15 years ago

prview: improved text control look&feel, added scrolled panel, got rid of old code.

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