source: sasview/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py @ 4a9786d8

Last change on this file since 4a9786d8 was dc5ef15, checked in by Piotr Rozyczko <rozyczko@…>, 8 years ago

Removed qtgui dependency on sasgui and wx SASVIEW-590

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