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

Last change on this file since 99321b2 was 5251ec6, checked in by Paul Kienzle <pkienzle@…>, 6 years ago

improved support for py37 in sasgui

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