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

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 b6666d4 was 3b865c1, checked in by Mathieu Doucet <doucetm@…>, 15 years ago

prview: catch previously uncaught exception when exploring d_max values.

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