source: sasview/src/sas/qtgui/Plotter2D.py @ 5236449

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

Default datasets for fitting SASVIEW-498

  • Property mode set to 100644
File size: 17.7 KB
Line 
1import copy
2import numpy
3import pylab
4import functools
5
6from PyQt4 import QtGui
7from PyQt4 import QtCore
8
9DEFAULT_CMAP = pylab.cm.jet
10from mpl_toolkits.mplot3d import Axes3D
11
12import sas.qtgui.PlotUtilities as PlotUtilities
13import sas.qtgui.GuiUtils as GuiUtils
14from sas.qtgui.PlotterBase import PlotterBase
15from sas.qtgui.ColorMap import ColorMap
16from sas.sasgui.guiframe.dataFitting import Data1D
17from sas.sasgui.guiframe.dataFitting import Data2D
18from sas.sascalc.dataloader.manipulations import CircularAverage
19from sas.sasgui.guiframe.local_perspectives.plotting.binder import BindArtist
20from sas.qtgui.BoxSum import BoxSum
21from sas.qtgui.SlicerParameters import SlicerParameters
22from sas.sasgui.guiframe.local_perspectives.plotting.boxSlicer import BoxInteractorX
23from sas.sasgui.guiframe.local_perspectives.plotting.AnnulusSlicer import AnnulusInteractor
24from sas.sasgui.guiframe.local_perspectives.plotting.SectorSlicer import SectorInteractor
25from sas.sasgui.guiframe.local_perspectives.plotting.boxSum import BoxSumCalculator
26from sas.sasgui.guiframe.local_perspectives.plotting.boxSlicer import BoxInteractorY
27
28# Minimum value of Z for which we will present data.
29MIN_Z = -32
30
31class Plotter2DWidget(PlotterBase):
32    """
33    2D Plot widget for use with a QDialog
34    """
35    def __init__(self, parent=None, manager=None, quickplot=False, dimension=2):
36        self.dimension = dimension
37        super(Plotter2DWidget, self).__init__(parent, manager=manager, quickplot=quickplot)
38
39        self.cmap = DEFAULT_CMAP.name
40        # Default scale
41        self.scale = 'log_{10}'
42        # to set the order of lines drawn first.
43        self.slicer_z = 5
44        # Reference to the current slicer
45        self.slicer = None
46        self.slicer_widget = None
47        # Create Artist and bind it
48        self.connect = BindArtist(self.figure)
49        self.vmin = None
50        self.vmax = None
51
52    @property
53    def data(self):
54        return self._data
55
56    @data.setter
57    def data(self, data=None):
58        """ data setter """
59        self._data = data
60        self.qx_data = data.qx_data
61        self.qy_data = data.qy_data
62        self.xmin = data.xmin
63        self.xmax = data.xmax
64        self.ymin = data.ymin
65        self.ymax = data.ymax
66        self.zmin = data.zmin
67        self.zmax = data.zmax
68        self.label = data.name
69        self.xLabel = "%s(%s)"%(data._xaxis, data._xunit)
70        self.yLabel = "%s(%s)"%(data._yaxis, data._yunit)
71        self.title(title=data.title)
72
73    @property
74    def item(self):
75        ''' getter for this plot's QStandardItem '''
76        return self._item
77
78    @item.setter
79    def item(self, item=None):
80        ''' setter for this plot's QStandardItem '''
81        self._item = item
82
83    def plot(self, marker=None, data=None):
84        """
85        Plot 2D self._data
86        marker - unused
87        """
88        # Assing data
89        if isinstance(data, Data2D):
90            self.data = data
91
92        assert self._data
93
94        # Toggle the scale
95        zmin_2D_temp, zmax_2D_temp = self.calculateDepth()
96
97        # Prepare and show the plot
98        self.showPlot(data=self.data.data,
99                      qx_data=self.qx_data,
100                      qy_data=self.qy_data,
101                      xmin=self.xmin,
102                      xmax=self.xmax,
103                      ymin=self.ymin, ymax=self.ymax,
104                      cmap=self.cmap, zmin=zmin_2D_temp,
105                      zmax=zmax_2D_temp)
106
107    def calculateDepth(self):
108        """
109        Re-calculate the plot depth parameters depending on the scale
110        """
111        # Toggle the scale
112        zmin_temp = self.zmin
113        zmax_temp = self.zmax
114        # self.scale predefined in the baseclass
115        if self.scale == 'log_{10}':
116            if self.zmin is not None:
117                zmin_temp = numpy.power(10, self.zmin)
118            if self.zmax is not None:
119                zmax_temp = numpy.power(10, self.zmax)
120        else:
121            if self.zmin is not None:
122                # min log value: no log(negative)
123                zmin_temp = MIN_Z if self.zmin <= 0 else numpy.log10(self.zmin)
124            if self.zmax is not None:
125                zmax_temp = numpy.log10(self.zmax)
126
127        return (zmin_temp, zmax_temp)
128
129
130    def createContextMenu(self):
131        """
132        Define common context menu and associated actions for the MPL widget
133        """
134        self.defaultContextMenu()
135
136        self.contextMenu.addSeparator()
137        self.actionDataInfo = self.contextMenu.addAction("&DataInfo")
138        self.actionDataInfo.triggered.connect(
139             functools.partial(self.onDataInfo, self.data))
140
141        self.actionSavePointsAsFile = self.contextMenu.addAction("&Save Points as a File")
142        self.actionSavePointsAsFile.triggered.connect(
143             functools.partial(self.onSavePoints, self.data))
144        self.contextMenu.addSeparator()
145
146        self.actionCircularAverage = self.contextMenu.addAction("&Perform Circular Average")
147        self.actionCircularAverage.triggered.connect(self.onCircularAverage)
148
149        self.actionSectorView = self.contextMenu.addAction("&Sector [Q View]")
150        self.actionSectorView.triggered.connect(self.onSectorView)
151        self.actionAnnulusView = self.contextMenu.addAction("&Annulus [Phi View]")
152        self.actionAnnulusView.triggered.connect(self.onAnnulusView)
153        self.actionBoxSum = self.contextMenu.addAction("&Box Sum")
154        self.actionBoxSum.triggered.connect(self.onBoxSum)
155        self.actionBoxAveragingX = self.contextMenu.addAction("&Box Averaging in Qx")
156        self.actionBoxAveragingX.triggered.connect(self.onBoxAveragingX)
157        self.actionBoxAveragingY = self.contextMenu.addAction("&Box Averaging in Qy")
158        self.actionBoxAveragingY.triggered.connect(self.onBoxAveragingY)
159        # Additional items for slicer interaction
160        if self.slicer:
161            self.actionClearSlicer = self.contextMenu.addAction("&Clear Slicer")
162            self.actionClearSlicer.triggered.connect(self.onClearSlicer)
163            if self.slicer.__class__.__name__ != "BoxSumCalculator":
164                self.actionEditSlicer = self.contextMenu.addAction("&Edit Slicer Parameters")
165                self.actionEditSlicer.triggered.connect(self.onEditSlicer)
166        self.contextMenu.addSeparator()
167        self.actionColorMap = self.contextMenu.addAction("&2D Color Map")
168        self.actionColorMap.triggered.connect(self.onColorMap)
169        self.contextMenu.addSeparator()
170        self.actionChangeScale = self.contextMenu.addAction("Toggle Linear/Log Scale")
171        self.actionChangeScale.triggered.connect(self.onToggleScale)
172
173    def createContextMenuQuick(self):
174        """
175        Define context menu and associated actions for the quickplot MPL widget
176        """
177        self.defaultContextMenu()
178
179        if self.dimension == 2:
180            self.actionToggleGrid = self.contextMenu.addAction("Toggle Grid On/Off")
181            self.contextMenu.addSeparator()
182        self.actionChangeScale = self.contextMenu.addAction("Toggle Linear/Log Scale")
183
184        # Define the callbacks
185        self.actionChangeScale.triggered.connect(self.onToggleScale)
186        if self.dimension == 2:
187            self.actionToggleGrid.triggered.connect(self.onGridToggle)
188
189    def onToggleScale(self, event):
190        """
191        Toggle axis and replot image
192        """
193        # self.scale predefined in the baseclass
194        if self.scale == 'log_{10}':
195            self.scale = 'linear'
196        else:
197            self.scale = 'log_{10}'
198
199        self.plot()
200
201    def onClearSlicer(self):
202        """
203        Remove all sclicers from the chart
204        """
205        if self.slicer is None:
206            return
207
208        self.slicer.clear()
209        self.canvas.draw()
210        self.slicer = None
211
212    def onEditSlicer(self):
213        """
214        Present a small dialog for manipulating the current slicer
215        """
216        assert self.slicer
217        # Only show the dialog if not currently shown
218        if self.slicer_widget:
219            return
220        def slicer_closed():
221            # Need to disconnect the signal!!
222            self.slicer_widget.close_signal.disconnect()
223            # reset slicer_widget on "Edit Slicer Parameters" window close
224            self.slicer_widget = None
225
226        self.param_model = self.slicer.model()
227        # Pass the model to the Slicer Parameters widget
228        self.slicer_widget = SlicerParameters(model=self.param_model,
229                                              validate_method=self.slicer.validate)
230        self.slicer_widget.close_signal.connect(slicer_closed)
231        # Add the plot to the workspace
232        self.manager.parent.workspace().addWindow(self.slicer_widget)
233
234        self.slicer_widget.show()
235
236    def onCircularAverage(self):
237        """
238        Perform circular averaging on Data2D
239        """
240        # Find the best number of bins
241        npt = numpy.sqrt(len(self.data.data[numpy.isfinite(self.data.data)]))
242        npt = numpy.floor(npt)
243        # compute the maximum radius of data2D
244        self.qmax = max(numpy.fabs(self.data.xmax),
245                        numpy.fabs(self.data.xmin))
246        self.ymax = max(numpy.fabs(self.data.ymax),
247                        numpy.fabs(self.data.ymin))
248        self.radius = numpy.sqrt(numpy.power(self.qmax, 2) + numpy.power(self.ymax, 2))
249        #Compute beam width
250        bin_width = (self.qmax + self.qmax) / npt
251        # Create data1D circular average of data2D
252        circle = CircularAverage(r_min=0, r_max=self.radius, bin_width=bin_width)
253        circ = circle(self.data)
254        dxl = circ.dxl if hasattr(circ, "dxl") else None
255        dxw = circ.dxw if hasattr(circ, "dxw") else None
256
257        new_plot = Data1D(x=circ.x, y=circ.y, dy=circ.dy, dx=circ.dx)
258        new_plot.dxl = dxl
259        new_plot.dxw = dxw
260        new_plot.name = new_plot.title = "Circ avg " + self.data.name
261        new_plot.source = self.data.source
262        new_plot.interactive = True
263        new_plot.detector = self.data.detector
264
265        # Define axes if not done yet.
266        new_plot.xaxis("\\rm{Q}", "A^{-1}")
267        if hasattr(self.data, "scale") and \
268                    self.data.scale == 'linear':
269            new_plot.ytransform = 'y'
270            new_plot.yaxis("\\rm{Residuals} ", "normalized")
271        else:
272            new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}")
273
274        new_plot.group_id = "2daverage" + self.data.name
275        new_plot.id = "Circ avg " + self.data.name
276        new_plot.is_data = True
277        variant_plot = QtCore.QVariant(new_plot)
278        GuiUtils.updateModelItemWithPlot(self._item, variant_plot, new_plot.id)
279        # TODO: force immediate display (?)
280
281    def setSlicer(self, slicer):
282        """
283        Clear the previous slicer and create a new one.
284        slicer: slicer class to create
285        """
286        # Clear current slicer
287        if self.slicer is not None:
288            self.slicer.clear()
289        # Create a new slicer
290        self.slicer_z += 1
291        self.slicer = slicer(self, self.ax, item=self._item, zorder=self.slicer_z)
292        self.ax.set_ylim(self.data.ymin, self.data.ymax)
293        self.ax.set_xlim(self.data.xmin, self.data.xmax)
294        # Draw slicer
295        self.figure.canvas.draw()
296        self.slicer.update()
297
298        # Reset the model on the Edit slicer parameters widget
299        self.param_model = self.slicer.model()
300        if self.slicer_widget:
301            self.slicer_widget.setModel(self.param_model)
302
303
304    def onSectorView(self):
305        """
306        Perform sector averaging on Q and draw sector slicer
307        """
308        self.setSlicer(slicer=SectorInteractor)
309
310
311    def onAnnulusView(self):
312        """
313        Perform sector averaging on Phi and draw annulus slicer
314        """
315        self.setSlicer(slicer=AnnulusInteractor)
316
317    def onBoxSum(self):
318        """
319        Perform 2D Data averaging Qx and Qy.
320        Display box slicer details.
321        """
322        self.onClearSlicer()
323        self.slicer_z += 1
324        self.slicer = BoxSumCalculator(self, self.ax, zorder=self.slicer_z)
325
326        self.ax.set_ylim(self.data.ymin, self.data.ymax)
327        self.ax.set_xlim(self.data.xmin, self.data.xmax)
328        self.figure.canvas.draw()
329        self.slicer.update()
330
331        # Get the BoxSumCalculator model.
332        self.box_sum_model = self.slicer.model()
333        # Pass the BoxSumCalculator model to the BoxSum widget
334        self.boxwidget = BoxSum(self, model=self.box_sum_model)
335        # Add the plot to the workspace
336        self.manager.parent.workspace().addWindow(self.boxwidget)
337        self.boxwidget.show()
338
339    def onBoxAveragingX(self):
340        """
341        Perform 2D data averaging on Qx
342        Create a new slicer.
343        """
344        self.setSlicer(slicer=BoxInteractorX)
345
346    def onBoxAveragingY(self):
347        """
348        Perform 2D data averaging on Qy
349        Create a new slicer .
350        """
351        self.setSlicer(slicer=BoxInteractorY)
352
353    def onColorMap(self):
354        """
355        Display the color map dialog and modify the plot's map accordingly
356        """
357        color_map_dialog = ColorMap(self, cmap=self.cmap,
358                                    vmin=self.vmin,
359                                    vmax=self.vmax,
360                                    data=self.data)
361
362        color_map_dialog.apply_signal.connect(self.onApplyMap)
363
364        if color_map_dialog.exec_() == QtGui.QDialog.Accepted:
365            self.onApplyMap(color_map_dialog.norm(), color_map_dialog.cmap())
366
367    def onApplyMap(self, v_values, cmap):
368        """
369        Update the chart color map based on data passed from the widget
370        """
371        self.cmap = str(cmap)
372        self.vmin, self.vmax = v_values
373        # Redraw the chart with new cmap
374        self.plot()
375
376    def showPlot(self, data, qx_data, qy_data, xmin, xmax, ymin, ymax,
377                 zmin, zmax, label='data2D', cmap=DEFAULT_CMAP):
378        """
379        Render and show the current data
380        """
381        self.qx_data = qx_data
382        self.qy_data = qy_data
383        self.xmin = xmin
384        self.xmax = xmax
385        self.ymin = ymin
386        self.ymax = ymax
387        self.zmin = zmin
388        self.zmax = zmax
389        # If we don't have any data, skip.
390        if data is None:
391            return
392        if data.ndim == 1:
393            output = PlotUtilities.build_matrix(data, self.qx_data, self.qy_data)
394        else:
395            output = copy.deepcopy(data)
396
397        zmin_temp = self.zmin
398        # check scale
399        if self.scale == 'log_{10}':
400            try:
401                if  self.zmin <= 0  and len(output[output > 0]) > 0:
402                    zmin_temp = self.zmin
403                    output[output > 0] = numpy.log10(output[output > 0])
404                elif self.zmin <= 0:
405                    zmin_temp = self.zmin
406                    output[output > 0] = numpy.zeros(len(output))
407                    output[output <= 0] = MIN_Z
408                else:
409                    zmin_temp = self.zmin
410                    output[output > 0] = numpy.log10(output[output > 0])
411            except:
412                #Too many problems in 2D plot with scale
413                output[output > 0] = numpy.log10(output[output > 0])
414                pass
415
416        self.cmap = cmap
417        if self.dimension != 3:
418            #Re-adjust colorbar
419            self.figure.subplots_adjust(left=0.2, right=.8, bottom=.2)
420
421            zmax_temp = self.zmax
422            if self.vmin is not None:
423                zmin_temp = self.vmin
424                zmax_temp = self.vmax
425
426            im = self.ax.imshow(output, interpolation='nearest',
427                                origin='lower',
428                                vmin=zmin_temp, vmax=zmax_temp,
429                                cmap=self.cmap,
430                                extent=(self.xmin, self.xmax,
431                                        self.ymin, self.ymax))
432
433            cbax = self.figure.add_axes([0.84, 0.2, 0.02, 0.7])
434
435            # Current labels for axes
436            self.ax.set_ylabel(self.y_label)
437            self.ax.set_xlabel(self.x_label)
438
439            # Title only for regular charts
440            if not self.quickplot:
441                self.ax.set_title(label=self._title)
442
443            if cbax is None:
444                ax.set_frame_on(False)
445                cb = self.figure.colorbar(im, shrink=0.8, aspect=20)
446            else:
447                cb = self.figure.colorbar(im, cax=cbax)
448
449            cb.update_bruteforce(im)
450            cb.set_label('$' + self.scale + '$')
451
452            self.vmin = cb.vmin
453            self.vmax = cb.vmax
454
455        else:
456            # clear the previous 2D from memory
457            self.figure.clear()
458
459            self.figure.subplots_adjust(left=0.1, right=.8, bottom=.1)
460
461            data_x, data_y = numpy.meshgrid(self._data.x_bins[0:-1],
462                                            self._data.y_bins[0:-1])
463
464            ax = Axes3D(self.figure)
465
466            # Disable rotation for large sets.
467            # TODO: Define "large" for a dataset
468            SET_TOO_LARGE = 500
469            if len(data_x) > SET_TOO_LARGE:
470                ax.disable_mouse_rotation()
471
472            self.figure.canvas.resizing = False
473            im = ax.plot_surface(data_x, data_y, output, rstride=1,
474                                 cstride=1, cmap=cmap,
475                                 linewidth=0, antialiased=False)
476            self.ax.set_axis_off()
477
478        if self.dimension != 3:
479            self.figure.canvas.draw_idle()
480        else:
481            self.figure.canvas.draw()
482
483    def update(self):
484        self.figure.canvas.draw()
485
486    def draw(self):
487        self.figure.canvas.draw()
488
489
490class Plotter2D(QtGui.QDialog, Plotter2DWidget):
491    """
492    Plotter widget implementation
493    """
494    def __init__(self, parent=None, quickplot=False, dimension=2):
495        QtGui.QDialog.__init__(self)
496        Plotter2DWidget.__init__(self, manager=parent, quickplot=quickplot, dimension=dimension)
497        icon = QtGui.QIcon()
498        icon.addPixmap(QtGui.QPixmap(":/res/ball.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
499        self.setWindowIcon(icon)
Note: See TracBrowser for help on using the repository browser.