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

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since fbfc488 was 4992ff2, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Initial, in-progress version. Not really working atm. SASVIEW-787

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