source: sasview/src/sas/sasgui/guiframe/local_perspectives/plotting/SectorSlicer.py @ a78446ce

magnetic_scattrelease-4.2.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249unittest-saveload
Last change on this file since a78446ce was 8de66b6, checked in by krzywon, 8 years ago

Integrate log binning into SectorSlicer?.

  • Property mode set to 100644
File size: 20.0 KB
Line 
1"""
2    Sector interactor
3"""
4import math
5import wx
6from BaseInteractor import _BaseInteractor
7from sas.sasgui.guiframe.events import NewPlotEvent
8from sas.sasgui.guiframe.events import StatusEvent
9from sas.sasgui.guiframe.events import SlicerParameterEvent
10from sas.sasgui.guiframe.events import EVT_SLICER_PARS
11from sas.sasgui.guiframe.dataFitting import Data1D
12
13
14class SectorInteractor(_BaseInteractor):
15    """
16    Draw a sector slicer.Allow to performQ averaging on data 2D
17    """
18    def __init__(self, base, axes, color='black', zorder=3):
19
20        _BaseInteractor.__init__(self, base, axes, color=color)
21        ## Class initialization
22        self.markers = []
23        self.axes = axes
24        ## connect the plot to event
25        self.connect = self.base.connect
26
27        ## compute qmax limit to reset the graph
28        x = math.pow(max(self.base.data2D.xmax,
29                         math.fabs(self.base.data2D.xmin)), 2)
30        y = math.pow(max(self.base.data2D.ymax,
31                         math.fabs(self.base.data2D.ymin)), 2)
32        self.qmax = math.sqrt(x + y)
33        ## Number of points on the plot
34        self.nbins = 20
35        ## Angle of the middle line
36        self.theta2 = math.pi / 3
37        ## Absolute value of the Angle between the middle line and any side line
38        self.phi = math.pi / 12
39        # Binning base for log/lin binning
40        self.bin_base = 0
41        ## Middle line
42        self.main_line = LineInteractor(self, self.base.subplot, color='blue',
43                                        zorder=zorder, r=self.qmax,
44                                        theta=self.theta2)
45        self.main_line.qmax = self.qmax
46        ## Right Side line
47        self.right_line = SideInteractor(self, self.base.subplot, color='black',
48                                         zorder=zorder, r=self.qmax,
49                                         phi=-1 * self.phi, theta2=self.theta2)
50        self.right_line.qmax = self.qmax
51        ## Left Side line
52        self.left_line = SideInteractor(self, self.base.subplot, color='black',
53                                        zorder=zorder, r=self.qmax,
54                                        phi=self.phi, theta2=self.theta2)
55        self.left_line.qmax = self.qmax
56        ## draw the sector
57        self.update()
58        self._post_data()
59        ## Bind to slice parameter events
60        self.base.Bind(EVT_SLICER_PARS, self._onEVT_SLICER_PARS)
61
62    def _onEVT_SLICER_PARS(self, event):
63        """
64        receive an event containing parameters values to reset the slicer
65
66        :param event: event of type SlicerParameterEvent with params as
67        attribute
68
69        """
70        wx.PostEvent(self.base.parent,
71                     StatusEvent(status="SectorSlicer._onEVT_SLICER_PARS"))
72        event.Skip()
73        if event.type == self.__class__.__name__:
74            self.set_params(event.params)
75            self.base.update()
76
77    def set_layer(self, n):
78        """
79         Allow adding plot to the same panel
80
81        :param n: the number of layer
82
83        """
84        self.layernum = n
85        self.update()
86
87    def clear(self):
88        """
89        Clear the slicer and all connected events related to this slicer
90        """
91        self.clear_markers()
92        self.main_line.clear()
93        self.left_line.clear()
94        self.right_line.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        ## Check if the middle line was dragged and
105        #update the picture accordingly
106        if self.main_line.has_move:
107            self.main_line.update()
108            self.right_line.update(delta=-self.left_line.phi / 2,
109                                   mline=self.main_line.theta)
110            self.left_line.update(delta=self.left_line.phi / 2,
111                                  mline=self.main_line.theta)
112        ## Check if the left side has moved and update the slicer accordingly
113        if self.left_line.has_move:
114            self.main_line.update()
115            self.left_line.update(phi=None, delta=None, mline=self.main_line,
116                                  side=True, left=True)
117            self.right_line.update(phi=self.left_line.phi, delta=None,
118                                   mline=self.main_line, side=True,
119                                   left=False, right=True)
120        ## Check if the right side line has moved and
121        #update the slicer accordingly
122        if self.right_line.has_move:
123            self.main_line.update()
124            self.right_line.update(phi=None, delta=None, mline=self.main_line,
125                                   side=True, left=False, right=True)
126            self.left_line.update(phi=self.right_line.phi, delta=None,
127                                  mline=self.main_line, side=True, left=False)
128
129    def save(self, ev):
130        """
131        Remember the roughness for this layer and the next so that we
132        can restore on Esc.
133        """
134        self.base.freeze_axes()
135        self.main_line.save(ev)
136        self.right_line.save(ev)
137        self.left_line.save(ev)
138
139    def _post_data(self, nbins=None):
140        """
141        compute sector averaging of data2D into data1D
142
143        :param nbins: the number of point to plot for the average 1D data
144        """
145        ## get the data2D to average
146        data = self.base.data2D
147        # If we have no data, just return
148        if data is None:
149            return
150        ## Averaging
151        from sas.sascalc.dataloader.manipulations import SectorQ
152        radius = self.qmax
153        phimin = -self.left_line.phi + self.main_line.theta
154        phimax = self.left_line.phi + self.main_line.theta
155        bin_base = self.bin_base
156        if nbins is None:
157            nbins = 20
158        sect = SectorQ(r_min=0.0, r_max=radius,
159                       phi_min=phimin + math.pi,
160                       phi_max=phimax + math.pi, nbins=nbins, base=bin_base)
161
162        sector = sect(self.base.data2D)
163        ##Create 1D data resulting from average
164
165        if hasattr(sector, "dxl"):
166            dxl = sector.dxl
167        else:
168            dxl = None
169        if hasattr(sector, "dxw"):
170            dxw = sector.dxw
171        else:
172            dxw = None
173        new_plot = Data1D(x=sector.x, y=sector.y, dy=sector.dy, dx=sector.dx)
174        new_plot.dxl = dxl
175        new_plot.dxw = dxw
176        new_plot.name = "SectorQ" + "(" + self.base.data2D.name + ")"
177        new_plot.source = self.base.data2D.source
178        #new_plot.info=self.base.data2D.info
179        new_plot.interactive = True
180        new_plot.detector = self.base.data2D.detector
181        ## If the data file does not tell us what the axes are, just assume...
182        new_plot.xaxis("\\rm{Q}", "A^{-1}")
183        new_plot.yaxis("\\rm{Intensity}", "cm^{-1}")
184        if hasattr(data, "scale") and data.scale == 'linear' and \
185                self.base.data2D.name.count("Residuals") > 0:
186            new_plot.ytransform = 'y'
187            new_plot.yaxis("\\rm{Residuals} ", "/")
188
189        new_plot.group_id = "2daverage" + self.base.data2D.name
190        new_plot.id = "SectorQ" + self.base.data2D.name
191        new_plot.is_data = True
192        self.base.parent.update_theory(data_id=data.id, theory=new_plot)
193        wx.PostEvent(self.base.parent,
194                     NewPlotEvent(plot=new_plot, title="SectorQ" + self.base.data2D.name))
195
196    def moveend(self, ev):
197        """
198        Called a dragging motion ends.Get slicer event
199        """
200        self.base.thaw_axes()
201        ## Post parameters
202        event = SlicerParameterEvent()
203        event.type = self.__class__.__name__
204        event.params = self.get_params()
205        ## Send slicer paramers to plotter2D
206        wx.PostEvent(self.base, event)
207
208    def restore(self):
209        """
210        Restore the roughness for this layer.
211        """
212        self.main_line.restore()
213        self.left_line.restore()
214        self.right_line.restore()
215
216    def move(self, x, y, ev):
217        """
218        Process move to a new position, making sure that the move is allowed.
219        """
220        pass
221
222    def set_cursor(self, x, y):
223        """
224        """
225        pass
226
227    def get_params(self):
228        """
229        Store a copy of values of parameters of the slicer into a dictionary.
230
231        :return params: the dictionary created
232
233        """
234        params = {}
235        ## Always make sure that the left and the right line are at phi
236        ## angle of the middle line
237        if math.fabs(self.left_line.phi) != math.fabs(self.right_line.phi):
238            msg = "Phi left and phi right are different"
239            msg += " %f, %f" % (self.left_line.phi, self.right_line.phi)
240            raise ValueError, msg
241        params["Phi [deg]"] = self.main_line.theta * 180 / math.pi
242        params["Delta_Phi [deg]"] = math.fabs(self.left_line.phi * 180 / math.pi)
243        params["nbins"] = self.nbins
244        params["binning base"] = self.bin_base
245        return params
246
247    def set_params(self, params):
248        """
249        Receive a dictionary and reset the slicer with values contained
250        in the values of the dictionary.
251
252        :param params: a dictionary containing name of slicer parameters and
253            values the user assigned to the slicer.
254        """
255        main = params["Phi [deg]"] * math.pi / 180
256        phi = math.fabs(params["Delta_Phi [deg]"] * math.pi / 180)
257        self.nbins = int(params["nbins"])
258        self.bin_base = params["binning base"]
259        self.main_line.theta = main
260        ## Reset the slicer parameters
261        self.main_line.update()
262        self.right_line.update(phi=phi, delta=None, mline=self.main_line,
263                               side=True, right=True)
264        self.left_line.update(phi=phi, delta=None,
265                              mline=self.main_line, side=True)
266        ## post the new corresponding data
267        self._post_data(nbins=self.nbins)
268
269    def freeze_axes(self):
270        """
271        """
272        self.base.freeze_axes()
273
274    def thaw_axes(self):
275        """
276        """
277        self.base.thaw_axes()
278
279    def draw(self):
280        """
281        """
282        self.base.draw()
283
284
285class SideInteractor(_BaseInteractor):
286    """
287    Draw an oblique line
288
289    :param phi: the phase between the middle line and one side line
290    :param theta2: the angle between the middle line and x- axis
291
292    """
293    def __init__(self, base, axes, color='black', zorder=5, r=1.0,
294                 phi=math.pi / 4, theta2=math.pi / 3):
295        """
296        """
297        _BaseInteractor.__init__(self, base, axes, color=color)
298        ## Initialize the class
299        self.markers = []
300        self.axes = axes
301        ## compute the value of the angle between the current line and
302        ## the x-axis
303        self.save_theta = theta2 + phi
304        self.theta = theta2 + phi
305        ## the value of the middle line angle with respect to the x-axis
306        self.theta2 = theta2
307        ## Radius to find polar coordinates this line's endpoints
308        self.radius = r
309        ## phi is the phase between the current line and the middle line
310        self.phi = phi
311        ## End points polar coordinates
312        x1 = self.radius * math.cos(self.theta)
313        y1 = self.radius * math.sin(self.theta)
314        x2 = -1 * self.radius * math.cos(self.theta)
315        y2 = -1 * self.radius * math.sin(self.theta)
316        ## defining a new marker
317        self.inner_marker = self.axes.plot([x1 / 2.5], [y1 / 2.5], linestyle='',
318                                           marker='s', markersize=10,
319                                           color=self.color, alpha=0.6,
320                                           pickradius=5, label="pick",
321                                           zorder=zorder, visible=True)[0]
322
323        ## Defining the current line
324        self.line = self.axes.plot([x1, x2], [y1, y2],
325                                   linestyle='-', marker='',
326                                   color=self.color, visible=True)[0]
327        ## Flag to differentiate the left line from the right line motion
328        self.left_moving = False
329        ## Flag to define a motion
330        self.has_move = False
331        ## connecting markers and draw the picture
332        self.connect_markers([self.inner_marker, self.line])
333
334    def set_layer(self, n):
335        """
336        Allow adding plot to the same panel
337
338        :param n: the number of layer
339
340        """
341        self.layernum = n
342        self.update()
343
344    def clear(self):
345        """
346        Clear the slicer and all connected events related to this slicer
347        """
348        self.clear_markers()
349        try:
350            self.line.remove()
351            self.inner_marker.remove()
352        except:
353            # Old version of matplotlib
354            for item in range(len(self.axes.lines)):
355                del self.axes.lines[0]
356
357    def update(self, phi=None, delta=None, mline=None,
358               side=False, left=False, right=False):
359        """
360        Draw oblique line
361
362        :param phi: the phase between the middle line and the current line
363        :param delta: phi/2 applied only when the mline was moved
364
365        """
366        #print "update left or right ", self.has_move
367        self.left_moving = left
368        theta3 = 0
369        if phi is not None:
370            self.phi = phi
371        if delta is None:
372            delta = 0
373        if  right:
374            self.phi = -1 * math.fabs(self.phi)
375            #delta=-delta
376        else:
377            self.phi = math.fabs(self.phi)
378        if side:
379            self.theta = mline.theta + self.phi
380
381        if mline is not None:
382            if delta != 0:
383                self.theta2 = mline + delta
384            else:
385                self.theta2 = mline.theta
386        if delta == 0:
387            theta3 = self.theta + delta
388        else:
389            theta3 = self.theta2 + delta
390        x1 = self.radius * math.cos(theta3)
391        y1 = self.radius * math.sin(theta3)
392        x2 = -1 * self.radius * math.cos(theta3)
393        y2 = -1 * self.radius * math.sin(theta3)
394        self.inner_marker.set(xdata=[x1 / 2.5], ydata=[y1 / 2.5])
395        self.line.set(xdata=[x1, x2], ydata=[y1, y2])
396
397    def save(self, ev):
398        """
399        Remember the roughness for this layer and the next so that we
400        can restore on Esc.
401        """
402        self.save_theta = self.theta
403        self.base.freeze_axes()
404
405    def moveend(self, ev):
406        """
407        """
408        self.has_move = False
409        self.base.moveend(ev)
410
411    def restore(self):
412        """
413        Restore the roughness for this layer.
414        """
415        self.theta = self.save_theta
416
417    def move(self, x, y, ev):
418        """
419        Process move to a new position, making sure that the move is allowed.
420        """
421        self.theta = math.atan2(y, x)
422        self.has_move = True
423        #ToDo: Simplify below
424        if not self.left_moving:
425            if  self.theta2 - self.theta <= 0 and self.theta2 > 0:
426                self.restore()
427                return
428            elif self.theta2 < 0 and self.theta < 0 and \
429                self.theta - self.theta2 >= 0:
430                self.restore()
431                return
432            elif  self.theta2 < 0 and self.theta > 0 and \
433                (self.theta2 + 2 * math.pi - self.theta) >= math.pi / 2:
434                self.restore()
435                return
436            elif  self.theta2 < 0 and self.theta < 0 and \
437                (self.theta2 - self.theta) >= math.pi / 2:
438                self.restore()
439                return
440            elif self.theta2 > 0 and (self.theta2 - self.theta >= math.pi / 2 or \
441                (self.theta2 - self.theta >= math.pi / 2)):
442                self.restore()
443                return
444        else:
445            if  self.theta < 0 and (self.theta + math.pi * 2 - self.theta2) <= 0:
446                self.restore()
447                return
448            elif self.theta2 < 0 and (self.theta - self.theta2) <= 0:
449                self.restore()
450                return
451            elif  self.theta > 0 and self.theta - self.theta2 <= 0:
452                self.restore()
453                return
454            elif self.theta - self.theta2 >= math.pi / 2 or  \
455                ((self.theta + math.pi * 2 - self.theta2) >= math.pi / 2 and \
456                 self.theta < 0 and self.theta2 > 0):
457                self.restore()
458                return
459
460        self.phi = math.fabs(self.theta2 - self.theta)
461        if self.phi > math.pi:
462            self.phi = 2 * math.pi - math.fabs(self.theta2 - self.theta)
463        self.base.base.update()
464
465    def set_cursor(self, x, y):
466        """
467        """
468        self.move(x, y, None)
469        self.update()
470
471    def get_params(self):
472        """
473        """
474        params = {}
475        params["radius"] = self.radius
476        params["theta"] = self.theta
477        return params
478
479    def set_params(self, params):
480        """
481        """
482        x = params["radius"]
483        self.set_cursor(x, None)
484
485
486class LineInteractor(_BaseInteractor):
487    """
488    Select an annulus through a 2D plot
489    """
490    def __init__(self, base, axes, color='black',
491                 zorder=5, r=1.0, theta=math.pi / 4):
492        """
493        """
494        _BaseInteractor.__init__(self, base, axes, color=color)
495
496        self.markers = []
497        self.axes = axes
498        self.save_theta = theta
499        self.theta = theta
500        self.radius = r
501        self.scale = 10.0
502        # Inner circle
503        x1 = self.radius * math.cos(self.theta)
504        y1 = self.radius * math.sin(self.theta)
505        x2 = -1 * self.radius * math.cos(self.theta)
506        y2 = -1 * self.radius * math.sin(self.theta)
507        # Inner circle marker
508        self.inner_marker = self.axes.plot([x1 / 2.5], [y1 / 2.5], linestyle='',
509                                           marker='s', markersize=10,
510                                           color=self.color, alpha=0.6,
511                                           pickradius=5, label="pick",
512                                           zorder=zorder,
513                                           visible=True)[0]
514        self.line = self.axes.plot([x1, x2], [y1, y2],
515                                   linestyle='-', marker='',
516                                   color=self.color, visible=True)[0]
517        self.npts = 20
518        self.has_move = False
519        self.connect_markers([self.inner_marker, self.line])
520        self.update()
521
522    def set_layer(self, n):
523        """
524        """
525        self.layernum = n
526        self.update()
527
528    def clear(self):
529        """
530        """
531        self.clear_markers()
532        try:
533            self.inner_marker.remove()
534            self.line.remove()
535        except:
536            # Old version of matplotlib
537            for item in range(len(self.axes.lines)):
538                del self.axes.lines[0]
539
540    def update(self, theta=None):
541        """
542        Draw the new roughness on the graph.
543        """
544
545        if theta is not None:
546            self.theta = theta
547        x1 = self.radius * math.cos(self.theta)
548        y1 = self.radius * math.sin(self.theta)
549        x2 = -1 * self.radius * math.cos(self.theta)
550        y2 = -1 * self.radius * math.sin(self.theta)
551
552        self.inner_marker.set(xdata=[x1 / 2.5], ydata=[y1 / 2.5])
553        self.line.set(xdata=[x1, x2], ydata=[y1, y2])
554
555    def save(self, ev):
556        """
557        Remember the roughness for this layer and the next so that we
558        can restore on Esc.
559        """
560        self.save_theta = self.theta
561        self.base.freeze_axes()
562
563    def moveend(self, ev):
564        """
565        """
566        self.has_move = False
567        self.base.moveend(ev)
568
569    def restore(self):
570        """
571        Restore the roughness for this layer.
572        """
573        self.theta = self.save_theta
574
575    def move(self, x, y, ev):
576        """
577        Process move to a new position, making sure that the move is allowed.
578        """
579        self.theta = math.atan2(y, x)
580        self.has_move = True
581        self.base.base.update()
582
583    def set_cursor(self, x, y):
584        """
585        """
586        self.move(x, y, None)
587        self.update()
588
589    def get_params(self):
590        """
591        """
592        params = {}
593        params["radius"] = self.radius
594        params["theta"] = self.theta
595        return params
596
597    def set_params(self, params):
598        """
599        """
600        x = params["radius"]
601        self.set_cursor(x, None)
Note: See TracBrowser for help on using the repository browser.