source: sasview/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py @ 8289ae3

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 8289ae3 was 4992ff2, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

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

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