source: sasview/src/sas/sasgui/guiframe/local_perspectives/plotting/AnnulusSlicer.py @ 34f23c8

ticket-1249
Last change on this file since 34f23c8 was 34f23c8, checked in by Paul Kienzle <pkienzle@…>, 6 months ago

py3/wx4 compatibility changes for gui. Refs #1249

  • Property mode set to 100644
File size: 18.5 KB
Line 
1# TODO: the line slicer should listen to all 2DREFRESH events, get the data and slice it
2#      before pushing a new 1D data update.
3
4#
5# TODO: NEED MAJOR REFACTOR
6#
7
8import math
9# from copy import deepcopy
10
11import wx
12
13from sas.sasgui.guiframe.events import NewPlotEvent
14from sas.sasgui.guiframe.events import StatusEvent
15from sas.sasgui.guiframe.events import SlicerParameterEvent
16from sas.sasgui.guiframe.events import EVT_SLICER_PARS
17from sas.sasgui.guiframe.dataFitting import Data1D
18
19from .BaseInteractor import _BaseInteractor
20
21class AnnulusInteractor(_BaseInteractor):
22    """
23    Select an annulus through a 2D plot.
24    This interactor is used to average 2D data  with the region
25    defined by 2 radius.
26    this class is defined by 2 Ringinterators.
27    """
28    def __init__(self, base, axes, color='black', zorder=3):
29
30        _BaseInteractor.__init__(self, base, axes, color=color)
31        self.markers = []
32        self.axes = axes
33        self.base = base
34        self.qmax = min(math.fabs(self.base.data2D.xmax),
35                        math.fabs(self.base.data2D.xmin))  # must be positive
36        self.connect = self.base.connect
37
38        # # Number of points on the plot
39        self.nbins = 36
40        # Cursor position of Rings (Left(-1) or Right(1))
41        self.xmaxd = self.base.data2D.xmax
42        self.xmind = self.base.data2D.xmin
43
44        if (self.xmaxd + self.xmind) > 0:
45            self.sign = 1
46        else:
47            self.sign = -1
48        # Inner circle
49        self.inner_circle = RingInteractor(self, self.base.subplot,
50                                           zorder=zorder,
51                                           r=self.qmax / 2.0, sign=self.sign)
52        self.inner_circle.qmax = self.qmax
53        self.outer_circle = RingInteractor(self, self.base.subplot,
54                                           zorder=zorder + 1, r=self.qmax / 1.8,
55                                           sign=self.sign)
56        self.outer_circle.qmax = self.qmax * 1.2
57        self.update()
58        self._post_data()
59
60        # Bind to slice parameter events
61        self.base.Bind(EVT_SLICER_PARS, self._onEVT_SLICER_PARS)
62
63    def _onEVT_SLICER_PARS(self, event):
64        """
65        receive an event containing parameters values to reset the slicer
66
67        :param event: event of type SlicerParameterEvent with params as
68            attribute
69
70        """
71        wx.PostEvent(self.base,
72                     StatusEvent(status="AnnulusSlicer._onEVT_SLICER_PARS"))
73        event.Skip()
74        if event.type == self.__class__.__name__:
75            self.set_params(event.params)
76            self.base.update()
77
78    def set_layer(self, n):
79        """
80        Allow adding plot to the same panel
81
82        :param n: the number of layer
83
84        """
85        self.layernum = n
86        self.update()
87
88    def clear(self):
89        """
90        Clear the slicer and all connected events related to this slicer
91        """
92        self.clear_markers()
93        self.outer_circle.clear()
94        self.inner_circle.clear()
95        self.base.connect.clearall()
96        self.base.Unbind(EVT_SLICER_PARS)
97
98    def update(self):
99        """
100        Respond to changes in the model by recalculating the profiles and
101        resetting the widgets.
102        """
103        # Update locations
104        self.inner_circle.update()
105        self.outer_circle.update()
106
107    def save(self, ev):
108        """
109        Remember the roughness for this layer and the next so that we
110        can restore on Esc.
111        """
112        self.base.freeze_axes()
113        self.inner_circle.save(ev)
114        self.outer_circle.save(ev)
115
116    def _post_data(self, nbins=None):
117        """
118        Uses annulus parameters to plot averaged data into 1D data.
119
120        :param nbins: the number of points to plot
121
122        """
123        # Data to average
124        data = self.base.data2D
125        # If we have no data, just return
126        if data is None:
127            return
128
129        from sas.sascalc.dataloader.manipulations import Ring
130        rmin = min(math.fabs(self.inner_circle.get_radius()),
131                   math.fabs(self.outer_circle.get_radius()))
132        rmax = max(math.fabs(self.inner_circle.get_radius()),
133                   math.fabs(self.outer_circle.get_radius()))
134        # if the user does not specify the numbers of points to plot
135        # the default number will be nbins= 36
136        if nbins is None:
137            self.nbins = 36
138        else:
139            self.nbins = nbins
140        # # create the data1D Q average of data2D
141        sect = Ring(r_min=rmin, r_max=rmax, nbins=self.nbins)
142        sector = sect(self.base.data2D)
143
144        if hasattr(sector, "dxl"):
145            dxl = sector.dxl
146        else:
147            dxl = None
148        if hasattr(sector, "dxw"):
149            dxw = sector.dxw
150        else:
151            dxw = None
152        new_plot = Data1D(x=(sector.x - math.pi) * 180 / math.pi,
153                          y=sector.y, dy=sector.dy)
154        new_plot.dxl = dxl
155        new_plot.dxw = dxw
156        new_plot.name = "AnnulusPhi" + "(" + self.base.data2D.name + ")"
157
158        new_plot.source = self.base.data2D.source
159        # new_plot.info=self.base.data2D.info
160        new_plot.interactive = True
161        new_plot.detector = self.base.data2D.detector
162        # If the data file does not tell us what the axes are, just assume...
163        new_plot.xaxis(r"\rm{\phi}", 'degrees')
164        new_plot.yaxis(r"\rm{Intensity} ", "cm^{-1}")
165        if (hasattr(data, "scale") and data.scale == 'linear'
166                and self.base.data2D.name.count("Residuals") > 0):
167            new_plot.ytransform = 'y'
168            new_plot.yaxis(r"\rm{Residuals} ", "/")
169
170        new_plot.group_id = "AnnulusPhi" + self.base.data2D.name
171        new_plot.id = "AnnulusPhi" + self.base.data2D.name
172        new_plot.is_data = True
173        new_plot.xtransform = "x"
174        new_plot.ytransform = "y"
175        self.base.parent.update_theory(data_id=data.id, theory=new_plot)
176        wx.PostEvent(self.base.parent, NewPlotEvent(plot=new_plot, title="AnnulusPhi"))
177
178    def moveend(self, ev):
179        """
180        Called when any dragging motion ends.
181        Post an event (type =SlicerParameterEvent)
182        to plotter 2D with a copy  slicer parameters
183        Call  _post_data method
184        """
185        self.base.thaw_axes()
186        # Post parameters to plotter 2D
187        event = SlicerParameterEvent()
188        event.type = self.__class__.__name__
189        event.params = self.get_params()
190        wx.PostEvent(self.base, event)
191
192    def restore(self):
193        """
194        Restore the roughness for this layer.
195        """
196        self.inner_circle.restore()
197        self.outer_circle.restore()
198
199    def move(self, x, y, ev):
200        """
201        Process move to a new position, making sure that the move is allowed.
202        """
203        pass
204
205    def set_cursor(self, x, y):
206        pass
207
208    def get_params(self):
209        """
210        Store a copy of values of parameters of the slicer into a dictionary.
211
212        :return params: the dictionary created
213
214        """
215        params = {}
216        params["inner_radius"] = math.fabs(self.inner_circle._inner_mouse_x)
217        params["outer_radius"] = math.fabs(self.outer_circle._inner_mouse_x)
218        params["nbins"] = self.nbins
219        return params
220
221    def set_params(self, params):
222        """
223        Receive a dictionary and reset the slicer with values contained
224        in the values of the dictionary.
225
226        :param params: a dictionary containing name of slicer parameters and
227            values the user assigned to the slicer.
228
229        """
230        inner = math.fabs(params["inner_radius"])
231        outer = math.fabs(params["outer_radius"])
232        self.nbins = int(params["nbins"])
233        # # Update the picture
234        self.inner_circle.set_cursor(inner, self.inner_circle._inner_mouse_y)
235        self.outer_circle.set_cursor(outer, self.outer_circle._inner_mouse_y)
236        # # Post the data given the nbins entered by the user
237        self._post_data(self.nbins)
238
239    def freeze_axes(self):
240        """
241        """
242        self.base.freeze_axes()
243
244    def thaw_axes(self):
245        """
246        """
247        self.base.thaw_axes()
248
249    def draw(self):
250        """
251        """
252        self.base.draw()
253
254
255class RingInteractor(_BaseInteractor):
256    """
257     Draw a ring Given a radius
258    """
259    def __init__(self, base, axes, color='black', zorder=5, r=1.0, sign=1):
260        """
261        :param: the color of the line that defined the ring
262        :param r: the radius of the ring
263        :param sign: the direction of motion the the marker
264
265        """
266        _BaseInteractor.__init__(self, base, axes, color=color)
267        self.markers = []
268        self.axes = axes
269        # Current radius of the ring
270        self._inner_mouse_x = r
271        # Value of the center of the ring
272        self._inner_mouse_y = 0
273        # previous value of that radius
274        self._inner_save_x = r
275        # Save value of the center of the ring
276        self._inner_save_y = 0
277        # Class instantiating RingIterator class
278        self.base = base
279        # the direction of the motion of the marker
280        self.sign = sign
281        # # Create a marker
282        try:
283            # Inner circle marker
284            x_value = [self.sign * math.fabs(self._inner_mouse_x)]
285            self.inner_marker = self.axes.plot(x_value, [0], linestyle='',
286                                               marker='s', markersize=10,
287                                               color=self.color, alpha=0.6,
288                                               pickradius=5, label="pick",
289                                               zorder=zorder,
290                                               visible=True)[0]
291        except:
292            x_value = [self.sign * math.fabs(self._inner_mouse_x)]
293            self.inner_marker = self.axes.plot(x_value, [0], linestyle='',
294                                               marker='s', markersize=10,
295                                               color=self.color, alpha=0.6,
296                                               label="pick",
297                                               visible=True)[0]
298            message = "\nTHIS PROTOTYPE NEEDS THE LATEST"
299            message += " VERSION OF MATPLOTLIB\n"
300            message += "Get the SVN version that is at "
301            message += " least as recent as June 1, 2007"
302
303            owner = self.base.base.parent
304            wx.PostEvent(owner, StatusEvent(status="AnnulusSlicer: %s" % message))
305
306        # Draw a circle
307        [self.inner_circle] = self.axes.plot([], [], linestyle='-', marker='', color=self.color)
308        # the number of points that make the ring line
309        self.npts = 40
310
311        self.connect_markers([self.inner_marker])
312        self.update()
313
314    def set_layer(self, n):
315        """
316        Allow adding plot to the same panel
317
318        :param n: the number of layer
319
320        """
321        self.layernum = n
322        self.update()
323
324    def clear(self):
325        """
326        Clear the slicer and all connected events related to this slicer
327        """
328        self.clear_markers()
329        try:
330            self.inner_marker.remove()
331            self.inner_circle.remove()
332        except:
333            # Old version of matplotlib
334            for item in range(len(self.axes.lines)):
335                del self.axes.lines[0]
336
337    def get_radius(self):
338        """
339        :return self._inner_mouse_x: the current radius of the ring
340        """
341        return self._inner_mouse_x
342
343    def update(self):
344        """
345        Draw the new roughness on the graph.
346        """
347        # Plot inner circle
348        x = []
349        y = []
350        for i in range(self.npts):
351            phi = 2.0 * math.pi / (self.npts - 1) * i
352
353            xval = 1.0 * self._inner_mouse_x * math.cos(phi)
354            yval = 1.0 * self._inner_mouse_x * math.sin(phi)
355
356            x.append(xval)
357            y.append(yval)
358
359        self.inner_marker.set(xdata=[self.sign * math.fabs(self._inner_mouse_x)],
360                              ydata=[0])
361        self.inner_circle.set_data(x, y)
362
363    def save(self, ev):
364        """
365        Remember the roughness for this layer and the next so that we
366        can restore on Esc.
367        """
368        self._inner_save_x = self._inner_mouse_x
369        self._inner_save_y = self._inner_mouse_y
370        self.base.freeze_axes()
371
372    def moveend(self, ev):
373        """
374        Called after a dragging motion
375        """
376        self.base.moveend(ev)
377
378    def restore(self):
379        """
380        Restore the roughness for this layer.
381        """
382        self._inner_mouse_x = self._inner_save_x
383        self._inner_mouse_y = self._inner_save_y
384
385    def move(self, x, y, ev):
386        """
387        Process move to a new position, making sure that the move is allowed.
388        """
389        self._inner_mouse_x = x
390        self._inner_mouse_y = y
391        self.base.base.update()
392
393    def set_cursor(self, x, y):
394        """
395        draw the ring given x, y value
396        """
397        self.move(x, y, None)
398        self.update()
399
400
401    def get_params(self):
402        """
403        Store a copy of values of parameters of the slicer into a dictionary.
404
405        :return params: the dictionary created
406
407        """
408        params = {}
409        params["radius"] = math.fabs(self._inner_mouse_x)
410        return params
411
412    def set_params(self, params):
413        """
414        Receive a dictionary and reset the slicer with values contained
415        in the values of the dictionary.
416
417        :param params: a dictionary containing name of slicer parameters and
418            values the user assigned to the slicer.
419
420        """
421        x = params["radius"]
422        self.set_cursor(x, self._inner_mouse_y)
423
424class CircularMask(_BaseInteractor):
425    """
426     Draw a ring Given a radius
427    """
428    def __init__(self, base, axes, color='grey', zorder=3, side=None):
429        """
430        :param: the color of the line that defined the ring
431        :param r: the radius of the ring
432        :param sign: the direction of motion the the marker
433        """
434        _BaseInteractor.__init__(self, base, axes, color=color)
435        self.markers = []
436        self.axes = axes
437        self.base = base
438        self.is_inside = side
439        self.qmax = min(math.fabs(self.base.data.xmax),
440                        math.fabs(self.base.data.xmin))  # must be positive
441        self.connect = self.base.connect
442
443        # Cursor position of Rings (Left(-1) or Right(1))
444        self.xmaxd = self.base.data.xmax
445        self.xmind = self.base.data.xmin
446
447        if (self.xmaxd + self.xmind) > 0:
448            self.sign = 1
449        else:
450            self.sign = -1
451        # Inner circle
452        self.outer_circle = RingInteractor(self, self.base.subplot, 'blue',
453                                           zorder=zorder + 1, r=self.qmax / 1.8,
454                                           sign=self.sign)
455        self.outer_circle.qmax = self.qmax * 1.2
456        self.update()
457        self._post_data()
458
459        # Bind to slice parameter events
460        # self.base.Bind(EVT_SLICER_PARS, self._onEVT_SLICER_PARS)
461
462    def _onEVT_SLICER_PARS(self, event):
463        """
464        receive an event containing parameters values to reset the slicer
465
466        :param event: event of type SlicerParameterEvent with params as
467            attribute
468        """
469        wx.PostEvent(self.base,
470                     StatusEvent(status="AnnulusSlicer._onEVT_SLICER_PARS"))
471        event.Skip()
472        if event.type == self.__class__.__name__:
473            self.set_params(event.params)
474            self.base.update()
475
476    def set_layer(self, n):
477        """
478        Allow adding plot to the same panel
479
480        :param n: the number of layer
481
482        """
483        self.layernum = n
484        self.update()
485
486    def clear(self):
487        """
488        Clear the slicer and all connected events related to this slicer
489        """
490        self.clear_markers()
491        self.outer_circle.clear()
492        self.base.connect.clearall()
493        # self.base.Unbind(EVT_SLICER_PARS)
494
495    def update(self):
496        """
497        Respond to changes in the model by recalculating the profiles and
498        resetting the widgets.
499        """
500        # Update locations
501        self.outer_circle.update()
502        # if self.is_inside is not None:
503        out = self._post_data()
504        return out
505
506    def save(self, ev):
507        """
508        Remember the roughness for this layer and the next so that we
509        can restore on Esc.
510        """
511        self.base.freeze_axes()
512        self.outer_circle.save(ev)
513
514    def _post_data(self):
515        """
516        Uses annulus parameters to plot averaged data into 1D data.
517
518        :param nbins: the number of points to plot
519
520        """
521        # Data to average
522        data = self.base.data
523
524        # If we have no data, just return
525        if data is None:
526            return
527        mask = data.mask
528        from sas.sascalc.dataloader.manipulations import Ringcut
529
530        rmin = 0
531        rmax = math.fabs(self.outer_circle.get_radius())
532
533        # # create the data1D Q average of data2D
534        mask = Ringcut(r_min=rmin, r_max=rmax)
535
536        if self.is_inside:
537            out = (mask(data) == False)
538        else:
539            out = (mask(data))
540        # self.base.data.mask=out
541        return out
542
543
544    def moveend(self, ev):
545        """
546        Called when any dragging motion ends.
547        Post an event (type =SlicerParameterEvent)
548        to plotter 2D with a copy  slicer parameters
549        Call  _post_data method
550        """
551        self.base.thaw_axes()
552        # create a 1D data plot
553        self._post_data()
554
555    def restore(self):
556        """
557        Restore the roughness for this layer.
558        """
559        self.outer_circle.restore()
560
561    def move(self, x, y, ev):
562        """
563        Process move to a new position, making sure that the move is allowed.
564        """
565        pass
566
567    def set_cursor(self, x, y):
568        pass
569
570    def get_params(self):
571        """
572        Store a copy of values of parameters of the slicer into a dictionary.
573
574        :return params: the dictionary created
575
576        """
577        params = {}
578        params["outer_radius"] = math.fabs(self.outer_circle._inner_mouse_x)
579        return params
580
581    def set_params(self, params):
582        """
583        Receive a dictionary and reset the slicer with values contained
584        in the values of the dictionary.
585
586        :param params: a dictionary containing name of slicer parameters and
587            values the user assigned to the slicer.
588        """
589        outer = math.fabs(params["outer_radius"])
590        # # Update the picture
591        self.outer_circle.set_cursor(outer, self.outer_circle._inner_mouse_y)
592        # # Post the data given the nbins entered by the user
593        self._post_data()
594
595    def freeze_axes(self):
596        self.base.freeze_axes()
597
598    def thaw_axes(self):
599        self.base.thaw_axes()
600
601    def draw(self):
602        self.base.update()
603
Note: See TracBrowser for help on using the repository browser.