source: sasview/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py @ 6b43c58

Last change on this file since 6b43c58 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: 16.8 KB
Line 
1import numpy
2from PyQt4 import QtGui
3from PyQt4 import QtCore
4
5import sas.qtgui.Utilities.GuiUtils as GuiUtils
6from BaseInteractor import BaseInteractor
7from sas.qtgui.Plotting.PlotterData import Data1D
8from sas.qtgui.Utilities.GuiUtils import formatNumber
9from sas.qtgui.Plotting.SlicerModel import SlicerModel
10
11class AnnulusInteractor(BaseInteractor, SlicerModel):
12    """
13    Select an annulus through a 2D plot.
14    This interactor is used to average 2D data  with the region
15    defined by 2 radius.
16    this class is defined by 2 Ringinterators.
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
23        self.markers = []
24        self.axes = axes
25        self.base = base
26        self._item = item
27        self.qmax = min(numpy.fabs(self.base.data.xmax),
28                        numpy.fabs(self.base.data.xmin))  # must be positive
29        self.connect = self.base.connect
30
31        # Number of points on the plot
32        self.nbins = 36
33        # Cursor position of Rings (Left(-1) or Right(1))
34        self.xmaxd = self.base.data.xmax
35        self.xmind = self.base.data.xmin
36
37        if (self.xmaxd + self.xmind) > 0:
38            self.sign = 1
39        else:
40            self.sign = -1
41        # Inner circle
42        self.inner_circle = RingInteractor(self, self.axes,
43                                           zorder=zorder,
44                                           r=self.qmax / 2.0, sign=self.sign)
45        self.inner_circle.qmax = self.qmax
46        self.outer_circle = RingInteractor(self, self.axes,
47                                           zorder=zorder + 1, r=self.qmax / 1.8,
48                                           sign=self.sign)
49        self.outer_circle.qmax = self.qmax * 1.2
50        self.update()
51        self._post_data()
52
53        self.setModelFromParams()
54
55    def set_layer(self, n):
56        """
57        Allow adding plot to the same panel
58        :param n: the number of layer
59        """
60        self.layernum = n
61        self.update()
62
63    def clear(self):
64        """
65        Clear the slicer and all connected events related to this slicer
66        """
67        self.clear_markers()
68        self.outer_circle.clear()
69        self.inner_circle.clear()
70        self.base.connect.clearall()
71
72    def update(self):
73        """
74        Respond to changes in the model by recalculating the profiles and
75        resetting the widgets.
76        """
77        # Update locations
78        self.inner_circle.update()
79        self.outer_circle.update()
80
81    def save(self, ev):
82        """
83        Remember the roughness for this layer and the next so that we
84        can restore on Esc.
85        """
86        self.inner_circle.save(ev)
87        self.outer_circle.save(ev)
88
89    def _post_data(self, nbins=None):
90        """
91        Uses annulus parameters to plot averaged data into 1D data.
92
93        :param nbins: the number of points to plot
94
95        """
96        # Data to average
97        data = self.base.data
98        if data is None:
99            return
100
101        from sas.sascalc.dataloader.manipulations import Ring
102        rmin = min(numpy.fabs(self.inner_circle.get_radius()),
103                   numpy.fabs(self.outer_circle.get_radius()))
104        rmax = max(numpy.fabs(self.inner_circle.get_radius()),
105                   numpy.fabs(self.outer_circle.get_radius()))
106        # If the user does not specify the numbers of points to plot
107        # the default number will be nbins= 36
108        if nbins is None:
109            self.nbins = 36
110        else:
111            self.nbins = nbins
112        # Create the data1D Q average of data2D
113        sect = Ring(r_min=rmin, r_max=rmax, nbins=self.nbins)
114        sector = sect(self.base.data)
115
116        if hasattr(sector, "dxl"):
117            dxl = sector.dxl
118        else:
119            dxl = None
120        if hasattr(sector, "dxw"):
121            dxw = sector.dxw
122        else:
123            dxw = None
124        new_plot = Data1D(x=(sector.x - numpy.pi) * 180 / numpy.pi,
125                          y=sector.y, dy=sector.dy)
126        new_plot.dxl = dxl
127        new_plot.dxw = dxw
128        new_plot.name = "AnnulusPhi" + "(" + self.base.data.name + ")"
129        new_plot.title = "AnnulusPhi" + "(" + self.base.data.name + ")"
130
131        new_plot.source = self.base.data.source
132        new_plot.interactive = True
133        new_plot.detector = self.base.data.detector
134        # If the data file does not tell us what the axes are, just assume...
135        new_plot.xaxis("\\rm{\phi}", 'degrees')
136        new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}")
137        if hasattr(data, "scale") and data.scale == 'linear' and \
138                self.base.data.name.count("Residuals") > 0:
139            new_plot.ytransform = 'y'
140            new_plot.yaxis("\\rm{Residuals} ", "/")
141
142        new_plot.group_id = "AnnulusPhi" + self.base.data.name
143        new_plot.id = "AnnulusPhi" + self.base.data.name
144        new_plot.is_data = True
145        new_plot.xtransform = "x"
146        new_plot.ytransform = "y"
147        variant_plot = QtCore.QVariant(new_plot)
148        GuiUtils.updateModelItemWithPlot(self._item, variant_plot, new_plot.id)
149        self.base.manager.communicator.plotUpdateSignal.emit([new_plot])
150
151        if self.update_model:
152            self.setModelFromParams()
153        self.draw()
154
155    def validate(self, param_name, param_value):
156        """
157        Test the proposed new value "value" for row "row" of parameters
158        """
159        MIN_DIFFERENCE = 0.01
160        isValid = True
161
162        if param_name == 'inner_radius':
163            # First, check the closeness
164            if numpy.fabs(param_value - self.getParams()['outer_radius']) < MIN_DIFFERENCE:
165                print("Inner and outer radii too close. Please adjust.")
166                isValid = False
167            elif param_value > self.qmax:
168                print("Inner radius exceeds maximum range. Please adjust.")
169                isValid = False
170        elif param_name == 'outer_radius':
171            # First, check the closeness
172            if numpy.fabs(param_value - self.getParams()['inner_radius']) < MIN_DIFFERENCE:
173                print("Inner and outer radii too close. Please adjust.")
174                isValid = False
175            elif param_value > self.qmax:
176                print("Outer radius exceeds maximum range. Please adjust.")
177                isValid = False
178        elif param_name == 'nbins':
179            # Can't be 0
180            if param_value < 1:
181                print("Number of bins cannot be less than or equal to 0. Please adjust.")
182                isValid = False
183
184        return isValid
185
186    def moveend(self, ev):
187        """
188        Called when any dragging motion ends.
189        Redraw the plot with new parameters.
190        """
191        self._post_data(self.nbins)
192
193    def restore(self):
194        """
195        Restore the roughness for this layer.
196        """
197        self.inner_circle.restore()
198        self.outer_circle.restore()
199
200    def move(self, x, y, ev):
201        """
202        Process move to a new position, making sure that the move is allowed.
203        """
204        pass
205
206    def set_cursor(self, x, y):
207        pass
208
209    def getParams(self):
210        """
211        Store a copy of values of parameters of the slicer into a dictionary.
212        :return params: the dictionary created
213        """
214        params = {}
215        params["inner_radius"] = numpy.fabs(self.inner_circle._inner_mouse_x)
216        params["outer_radius"] = numpy.fabs(self.outer_circle._inner_mouse_x)
217        params["nbins"] = self.nbins
218        return params
219
220    def setParams(self, params):
221        """
222        Receive a dictionary and reset the slicer with values contained
223        in the values of the dictionary.
224
225        :param params: a dictionary containing name of slicer parameters and
226            values the user assigned to the slicer.
227        """
228        inner = numpy.fabs(params["inner_radius"])
229        outer = numpy.fabs(params["outer_radius"])
230        self.nbins = int(params["nbins"])
231        # Update the picture
232        self.inner_circle.set_cursor(inner, self.inner_circle._inner_mouse_y)
233        self.outer_circle.set_cursor(outer, self.outer_circle._inner_mouse_y)
234        # Post the data given the nbins entered by the user
235        self._post_data(self.nbins)
236
237    def draw(self):
238        """
239        """
240        self.base.draw()
241
242
243class RingInteractor(BaseInteractor):
244    """
245     Draw a ring Given a radius
246    """
247    def __init__(self, base, axes, color='black', zorder=5, r=1.0, sign=1):
248        """
249        :param: the color of the line that defined the ring
250        :param r: the radius of the ring
251        :param sign: the direction of motion the the marker
252
253        """
254        BaseInteractor.__init__(self, base, axes, color=color)
255        self.markers = []
256        self.axes = axes
257        # Current radius of the ring
258        self._inner_mouse_x = r
259        # Value of the center of the ring
260        self._inner_mouse_y = 0
261        # previous value of that radius
262        self._inner_save_x = r
263        # Save value of the center of the ring
264        self._inner_save_y = 0
265        # Class instantiating RingIterator class
266        self.base = base
267        # the direction of the motion of the marker
268        self.sign = sign
269        # # Create a marker
270        # Inner circle marker
271        x_value = [self.sign * numpy.fabs(self._inner_mouse_x)]
272        self.inner_marker = self.axes.plot(x_value, [0], linestyle='',
273                                           marker='s', markersize=10,
274                                           color=self.color, alpha=0.6,
275                                           pickradius=5, label="pick",
276                                           zorder=zorder,
277                                           visible=True)[0]
278        # Draw a circle
279        [self.inner_circle] = self.axes.plot([], [], linestyle='-', marker='', color=self.color)
280        # The number of points that make the ring line
281        self.npts = 40
282
283        self.connect_markers([self.inner_marker])
284        self.update()
285
286    def set_layer(self, n):
287        """
288        Allow adding plot to the same panel
289
290        :param n: the number of layer
291
292        """
293        self.layernum = n
294        self.update()
295
296    def clear(self):
297        """
298        Clear the slicer and all connected events related to this slicer
299        """
300        self.clear_markers()
301        self.inner_marker.remove()
302        self.inner_circle.remove()
303
304    def get_radius(self):
305        """
306        :return self._inner_mouse_x: the current radius of the ring
307        """
308        return self._inner_mouse_x
309
310    def update(self):
311        """
312        Draw the new roughness on the graph.
313        """
314        # Plot inner circle
315        x = []
316        y = []
317        for i in range(self.npts):
318            phi = 2.0 * numpy.pi / (self.npts - 1) * i
319
320            xval = 1.0 * self._inner_mouse_x * numpy.cos(phi)
321            yval = 1.0 * self._inner_mouse_x * numpy.sin(phi)
322
323            x.append(xval)
324            y.append(yval)
325
326        self.inner_marker.set(xdata=[self.sign * numpy.fabs(self._inner_mouse_x)],
327                              ydata=[0])
328        self.inner_circle.set_data(x, y)
329
330    def save(self, ev):
331        """
332        Remember the roughness for this layer and the next so that we
333        can restore on Esc.
334        """
335        self._inner_save_x = self._inner_mouse_x
336        self._inner_save_y = self._inner_mouse_y
337
338    def moveend(self, ev):
339        """
340        Called after a dragging motion
341        """
342        self.base.moveend(ev)
343
344    def restore(self):
345        """
346        Restore the roughness for this layer.
347        """
348        self._inner_mouse_x = self._inner_save_x
349        self._inner_mouse_y = self._inner_save_y
350
351    def move(self, x, y, ev):
352        """
353        Process move to a new position, making sure that the move is allowed.
354        """
355        self._inner_mouse_x = x
356        self._inner_mouse_y = y
357        self.base.base.update()
358
359    def set_cursor(self, x, y):
360        """
361        draw the ring given x, y value
362        """
363        self.move(x, y, None)
364        self.update()
365
366    def getParams(self):
367        """
368        Store a copy of values of parameters of the slicer into a dictionary.
369        :return params: the dictionary created
370        """
371        params = {}
372        params["radius"] = numpy.fabs(self._inner_mouse_x)
373        return params
374
375    def setParams(self, params):
376        """
377        Receive a dictionary and reset the slicer with values contained
378        in the values of the dictionary.
379
380        :param params: a dictionary containing name of slicer parameters and
381            values the user assigned to the slicer.
382
383        """
384        x = params["radius"]
385        self.set_cursor(x, self._inner_mouse_y)
386
387class CircularMask(BaseInteractor):
388    """
389     Draw a ring Given a radius
390    """
391    def __init__(self, base, axes, color='grey', zorder=3, side=None):
392        """
393        :param: the color of the line that defined the ring
394        :param r: the radius of the ring
395        :param sign: the direction of motion the the marker
396        """
397        BaseInteractor.__init__(self, base, axes, color=color)
398        self.markers = []
399        self.axes = axes
400        self.base = base
401        self.is_inside = side
402        self.qmax = min(numpy.fabs(self.base.data.xmax),
403                        numpy.fabs(self.base.data.xmin))  # must be positive
404        self.connect = self.base.connect
405
406        # Cursor position of Rings (Left(-1) or Right(1))
407        self.xmaxd = self.base.data.xmax
408        self.xmind = self.base.data.xmin
409
410        if (self.xmaxd + self.xmind) > 0:
411            self.sign = 1
412        else:
413            self.sign = -1
414        # Inner circle
415        self.outer_circle = RingInteractor(self, self.axes, 'blue',
416                                           zorder=zorder + 1, r=self.qmax / 1.8,
417                                           sign=self.sign)
418        self.outer_circle.qmax = self.qmax * 1.2
419        self.update()
420        self._post_data()
421
422    def set_layer(self, n):
423        """
424        Allow adding plot to the same panel
425        :param n: the number of layer
426        """
427        self.layernum = n
428        self.update()
429
430    def clear(self):
431        """
432        Clear the slicer and all connected events related to this slicer
433        """
434        self.clear_markers()
435        self.outer_circle.clear()
436        self.base.connect.clearall()
437
438    def update(self):
439        """
440        Respond to changes in the model by recalculating the profiles and
441        resetting the widgets.
442        """
443        # Update locations
444        self.outer_circle.update()
445        self._post_data()
446        out = self._post_data()
447        return out
448
449    def save(self, ev):
450        """
451        Remember the roughness for this layer and the next so that we
452        can restore on Esc.
453        """
454        self.outer_circle.save(ev)
455
456    def _post_data(self):
457        """
458        Uses annulus parameters to plot averaged data into 1D data.
459
460        :param nbins: the number of points to plot
461
462        """
463        # Data to average
464        data = self.base.data
465
466        # If we have no data, just return
467        if data is None:
468            return
469        mask = data.mask
470        from sas.sascalc.dataloader.manipulations import Ringcut
471
472        rmin = 0
473        rmax = numpy.fabs(self.outer_circle.get_radius())
474
475        # Create the data1D Q average of data2D
476        mask = Ringcut(r_min=rmin, r_max=rmax)
477
478        if self.is_inside:
479            out = (mask(data) == False)
480        else:
481            out = (mask(data))
482        return out
483
484
485    def moveend(self, ev):
486        """
487        Called when any dragging motion ends.
488        Post an event (type =SlicerParameterEvent)
489        to plotter 2D with a copy  slicer parameters
490        Call  _post_data method
491        """
492        self.base.thaw_axes()
493        # create a 1D data plot
494        self._post_data()
495
496    def restore(self):
497        """
498        Restore the roughness for this layer.
499        """
500        self.outer_circle.restore()
501
502    def move(self, x, y, ev):
503        """
504        Process move to a new position, making sure that the move is allowed.
505        """
506        pass
507
508    def set_cursor(self, x, y):
509        pass
510
511    def getParams(self):
512        """
513        Store a copy of values of parameters of the slicer into a dictionary.
514
515        :return params: the dictionary created
516
517        """
518        params = {}
519        params["outer_radius"] = numpy.fabs(self.outer_circle._inner_mouse_x)
520        return params
521
522    def setParams(self, params):
523        """
524        Receive a dictionary and reset the slicer with values contained
525        in the values of the dictionary.
526
527        :param params: a dictionary containing name of slicer parameters and
528            values the user assigned to the slicer.
529        """
530        outer = numpy.fabs(params["outer_radius"])
531        # Update the picture
532        self.outer_circle.set_cursor(outer, self.outer_circle._inner_mouse_y)
533        # Post the data given the nbins entered by the user
534        self._post_data()
535
536    def draw(self):
537        self.base.update()
538
Note: See TracBrowser for help on using the repository browser.