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

ESS_GUI_bumps_abstraction
Last change on this file since 3a3f192 was fa81e94, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Initial commit of the P(r) inversion perspective.
Code merged from Jeff Krzywon's ESS_GUI_Pr branch.
Also, minor 2to3 mods to sascalc/sasgui to enble error free setup.

  • Property mode set to 100644
File size: 19.5 KB
Line 
1"""
2    Sector interactor
3"""
4import numpy
5from PyQt4 import QtGui
6from PyQt4 import QtCore
7
8from BaseInteractor import _BaseInteractor
9from sas.sasgui.guiframe.dataFitting 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
123        :param nbins: the number of point to plot for the average 1D data
124        """
125        # Get the data2D to average
126        data = self.base.data
127        # If we have no data, just return
128        if data is None:
129            return
130        # Averaging
131        from sas.sascalc.dataloader.manipulations import SectorQ
132        radius = self.qmax
133        phimin = -self.left_line.phi + self.main_line.theta
134        phimax = self.left_line.phi + self.main_line.theta
135        if nbins is None:
136            nbins = 20
137        sect = SectorQ(r_min=0.0, r_max=radius,
138                       phi_min=phimin + numpy.pi,
139                       phi_max=phimax + numpy.pi, nbins=nbins)
140
141        sector = sect(self.base.data)
142        # Create 1D data resulting from average
143
144        if hasattr(sector, "dxl"):
145            dxl = sector.dxl
146        else:
147            dxl = None
148        if hasattr(sector, "dxw"):
149            dxw = sector.dxw
150        else:
151            dxw = None
152        new_plot = Data1D(x=sector.x, y=sector.y, dy=sector.dy, dx=sector.dx)
153        new_plot.dxl = dxl
154        new_plot.dxw = dxw
155        new_plot.name = "SectorQ" + "(" + self.base.data.name + ")"
156        new_plot.title = "SectorQ" + "(" + self.base.data.name + ")"
157        new_plot.source = self.base.data.source
158        new_plot.interactive = True
159        new_plot.detector = self.base.data.detector
160        # If the data file does not tell us what the axes are, just assume them.
161        new_plot.xaxis("\\rm{Q}", "A^{-1}")
162        new_plot.yaxis("\\rm{Intensity}", "cm^{-1}")
163        if hasattr(data, "scale") and data.scale == 'linear' and \
164                self.base.data.name.count("Residuals") > 0:
165            new_plot.ytransform = 'y'
166            new_plot.yaxis("\\rm{Residuals} ", "/")
167
168        new_plot.group_id = "2daverage" + self.base.data.name
169        new_plot.id = "SectorQ" + self.base.data.name
170        new_plot.is_data = True
171        GuiUtils.updateModelItemWithPlot(self._item, new_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        """
220        """
221        pass
222
223    def getParams(self):
224        """
225        Store a copy of values of parameters of the slicer into a dictionary.
226        :return params: the dictionary created
227        """
228        params = {}
229        # Always make sure that the left and the right line are at phi
230        # angle of the middle line
231        if numpy.fabs(self.left_line.phi) != numpy.fabs(self.right_line.phi):
232            msg = "Phi left and phi right are different"
233            msg += " %f, %f" % (self.left_line.phi, self.right_line.phi)
234            raise (ValueError, msg)
235        params["Phi [deg]"] = self.main_line.theta * 180 / numpy.pi
236        params["Delta_Phi [deg]"] = numpy.fabs(self.left_line.phi * 180 / numpy.pi)
237        params["nbins"] = self.nbins
238        return params
239
240    def setParams(self, params):
241        """
242        Receive a dictionary and reset the slicer with values contained
243        in the values of the dictionary.
244
245        :param params: a dictionary containing name of slicer parameters and
246            values the user assigned to the slicer.
247        """
248        main = params["Phi [deg]"] * numpy.pi / 180
249        phi = numpy.fabs(params["Delta_Phi [deg]"] * numpy.pi / 180)
250
251        # phi should not be too close.
252        if numpy.fabs(phi) < MIN_PHI:
253            phi = MIN_PHI
254            params["Delta_Phi [deg]"] = MIN_PHI
255
256        self.nbins = int(params["nbins"])
257        self.main_line.theta = main
258        # Reset the slicer parameters
259        self.main_line.update()
260        self.right_line.update(phi=phi, delta=None, mline=self.main_line,
261                               side=True, right=True)
262        self.left_line.update(phi=phi, delta=None,
263                              mline=self.main_line, side=True)
264        # Post the new corresponding data
265        self._post_data(nbins=self.nbins)
266
267    def draw(self):
268        """
269        Redraw canvas
270        """
271        self.base.draw()
272
273
274class SideInteractor(_BaseInteractor):
275    """
276    Draw an oblique line
277
278    :param phi: the phase between the middle line and one side line
279    :param theta2: the angle between the middle line and x- axis
280
281    """
282    def __init__(self, base, axes, color='black', zorder=5, r=1.0,
283                 phi=numpy.pi / 4, theta2=numpy.pi / 3):
284        """
285        """
286        _BaseInteractor.__init__(self, base, axes, color=color)
287        # Initialize the class
288        self.markers = []
289        self.axes = axes
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
355        if phi != None:
356            self.phi = phi
357        if delta is None:
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
367        if mline != None:
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        """
392        """
393        self.has_move = False
394        self.base.moveend(ev)
395
396    def restore(self):
397        """
398        Restore the roughness for this layer.
399        """
400        self.theta = self.save_theta
401
402    def move(self, x, y, ev):
403        """
404        Process move to a new position, making sure that the move is allowed.
405        """
406        self.theta = numpy.arctan2(y, x)
407        self.has_move = True
408        if not self.left_moving:
409            if  self.theta2 - self.theta <= 0 and self.theta2 > 0:
410                self.restore()
411                return
412            elif self.theta2 < 0 and self.theta < 0 and \
413                self.theta - self.theta2 >= 0:
414                self.restore()
415                return
416            elif  self.theta2 < 0 and self.theta > 0 and \
417                (self.theta2 + 2 * numpy.pi - self.theta) >= numpy.pi / 2:
418                self.restore()
419                return
420            elif  self.theta2 < 0 and self.theta < 0 and \
421                (self.theta2 - self.theta) >= numpy.pi / 2:
422                self.restore()
423                return
424            elif self.theta2 > 0 and (self.theta2 - self.theta >= numpy.pi / 2 or \
425                (self.theta2 - self.theta >= numpy.pi / 2)):
426                self.restore()
427                return
428        else:
429            if  self.theta < 0 and (self.theta + numpy.pi * 2 - self.theta2) <= 0:
430                self.restore()
431                return
432            elif self.theta2 < 0 and (self.theta - self.theta2) <= 0:
433                self.restore()
434                return
435            elif  self.theta > 0 and self.theta - self.theta2 <= 0:
436                self.restore()
437                return
438            elif self.theta - self.theta2 >= numpy.pi / 2 or  \
439                ((self.theta + numpy.pi * 2 - self.theta2) >= numpy.pi / 2 and \
440                 self.theta < 0 and self.theta2 > 0):
441                self.restore()
442                return
443        self.phi = numpy.fabs(self.theta2 - self.theta)
444        if self.phi > numpy.pi:
445            self.phi = 2 * numpy.pi - numpy.fabs(self.theta2 - self.theta)
446        self.base.base.update()
447
448    def set_cursor(self, x, y):
449        """
450        """
451        self.move(x, y, None)
452        self.update()
453
454    def getParams(self):
455        """
456        """
457        params = {}
458        params["radius"] = self.radius
459        params["theta"] = self.theta
460        return params
461
462    def setParams(self, params):
463        """
464        """
465        x = params["radius"]
466        self.set_cursor(x, None)
467
468
469class LineInteractor(_BaseInteractor):
470    """
471    Select an annulus through a 2D plot
472    """
473    def __init__(self, base, axes, color='black',
474                 zorder=5, r=1.0, theta=numpy.pi / 4):
475        """
476        """
477        _BaseInteractor.__init__(self, base, axes, color=color)
478
479        self.markers = []
480        self.axes = axes
481        self.save_theta = theta
482        self.theta = theta
483        self.radius = r
484        self.scale = 10.0
485        # Inner circle
486        x1 = self.radius * numpy.cos(self.theta)
487        y1 = self.radius * numpy.sin(self.theta)
488        x2 = -1 * self.radius * numpy.cos(self.theta)
489        y2 = -1 * self.radius * numpy.sin(self.theta)
490        # Inner circle marker
491        self.inner_marker = self.axes.plot([x1 / 2.5], [y1 / 2.5], linestyle='',
492                                           marker='s', markersize=10,
493                                           color=self.color, alpha=0.6,
494                                           pickradius=5, label="pick",
495                                           zorder=zorder,
496                                           visible=True)[0]
497        self.line = self.axes.plot([x1, x2], [y1, y2],
498                                   linestyle='-', marker='',
499                                   color=self.color, visible=True)[0]
500        self.npts = 20
501        self.has_move = False
502        self.connect_markers([self.inner_marker, self.line])
503        self.update()
504
505    def set_layer(self, n):
506        """
507        """
508        self.layernum = n
509        self.update()
510
511    def clear(self):
512        """
513        """
514        self.clear_markers()
515        try:
516            self.inner_marker.remove()
517            self.line.remove()
518        except:
519            # Old version of matplotlib
520            for item in range(len(self.axes.lines)):
521                del self.axes.lines[0]
522
523    def update(self, theta=None):
524        """
525        Draw the new roughness on the graph.
526        """
527
528        if theta != None:
529            self.theta = theta
530        x1 = self.radius * numpy.cos(self.theta)
531        y1 = self.radius * numpy.sin(self.theta)
532        x2 = -1 * self.radius * numpy.cos(self.theta)
533        y2 = -1 * self.radius * numpy.sin(self.theta)
534
535        self.inner_marker.set(xdata=[x1 / 2.5], ydata=[y1 / 2.5])
536        self.line.set(xdata=[x1, x2], ydata=[y1, y2])
537
538    def save(self, ev):
539        """
540        Remember the roughness for this layer and the next so that we
541        can restore on Esc.
542        """
543        self.save_theta = self.theta
544
545    def moveend(self, ev):
546        """
547        """
548        self.has_move = False
549        self.base.moveend(ev)
550
551    def restore(self):
552        """
553        Restore the roughness for this layer.
554        """
555        self.theta = self.save_theta
556
557    def move(self, x, y, ev):
558        """
559        Process move to a new position, making sure that the move is allowed.
560        """
561        self.theta = numpy.arctan2(y, x)
562        self.has_move = True
563        self.base.base.update()
564
565    def set_cursor(self, x, y):
566        """
567        """
568        self.move(x, y, None)
569        self.update()
570
571    def getParams(self):
572        """
573        """
574        params = {}
575        params["radius"] = self.radius
576        params["theta"] = self.theta
577        return params
578
579    def setParams(self, params):
580        """
581        """
582        x = params["radius"]
583        self.set_cursor(x, None)
Note: See TracBrowser for help on using the repository browser.