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

Last change on this file since d32a594 was b9ab979, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 6 years ago

Automatically show sector/annulus/box plots SASVIEW-980

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