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@…>, 5 years ago

py3/wx4 compatibility changes for gui. Refs #1249

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