source: sasview/guiframe/local_perspectives/plotting/Plotter2D.py @ e6fa43e

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalccostrafo411magnetic_scattrelease-4.1.1release-4.1.2release-4.2.2release_4.0.1ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since e6fa43e was 4ac8556, checked in by Gervaise Alina <gervyh@…>, 15 years ago

change on data_loader

  • Property mode set to 100644
File size: 20.3 KB
RevLine 
[1bf33c1]1"""
2This software was developed by the University of Tennessee as part of the
3Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
4project funded by the US National Science Foundation.
5
6See the license text in license.txt
7
8copyright 2008, University of Tennessee
9"""
10
11
12import wx
[ea290ee]13import sys, math
[0d9dae8]14import pylab
15
[1bf33c1]16import danse.common.plottools
17from danse.common.plottools.PlotPanel import PlotPanel
[4ac8556]18from danse.common.plottools.plottables import Graph
[0bd2cd8]19from sans.guicomm.events import EVT_NEW_PLOT
[0d9dae8]20from sans.guicomm.events import EVT_SLICER_PARS
21from sans.guicomm.events import StatusEvent ,NewPlotEvent,SlicerEvent
22from sans.guiframe.utils import PanelMenu
[1bf33c1]23from binder import BindArtist
24from Plotter1D import ModelPanel1D
[4ac8556]25 
26from sans.guiframe.dataFitting import Data1D
[0d9dae8]27(InternalEvent, EVT_INTERNAL)   = wx.lib.newevent.NewEvent()
28
[1bf33c1]29
[0d9dae8]30
31DEFAULT_QMAX = 0.05
[1bf33c1]32DEFAULT_QSTEP = 0.001
33DEFAULT_BEAM = 0.005
[ef0c170]34BIN_WIDTH = 1.0
[0d9dae8]35
36
37
38
[1bf33c1]39class ModelPanel2D( ModelPanel1D):
40    """
41        Plot panel for use with the GUI manager
42    """
43   
44    ## Internal name for the AUI manager
45    window_name = "plotpanel"
46    ## Title to appear on top of the window
47    window_caption = "Plot Panel"
48    ## Flag to tell the GUI manager that this panel is not
49    #  tied to any perspective
50    ALWAYS_ON = True
51    ## Group ID
52    group_id = None
53   
54   
55    def __init__(self, parent, id = -1,data2d=None, color = None,\
56        dpi = None, style = wx.NO_FULL_REPAINT_ON_RESIZE, **kwargs):
57        """
58            Initialize the panel
59        """
60        ModelPanel1D.__init__(self, parent, id = id, style = style, **kwargs)
61       
62        ## Reference to the parent window
63        self.parent = parent
[6c0568b]64        ## Dictionary containing Plottables
[1bf33c1]65        self.plots = {}
[6c0568b]66        ## Save reference of the current plotted
67        self.data2D = data2d
[1bf33c1]68        ## Unique ID (from gui_manager)
69        self.uid = None
70        ## Action IDs for internal call-backs
71        self.action_ids = {}
[6c0568b]72        ## Create Artist and bind it
[1bf33c1]73        self.connect = BindArtist(self.subplot.figure)
[6c0568b]74        ## Beam stop
[1bf33c1]75        self.beamstop_radius = DEFAULT_BEAM
[6c0568b]76        ## to set the order of lines drawn first.
[f15ed33]77        self.slicer_z = 5
[6c0568b]78        ## Reference to the current slicer
[1bf33c1]79        self.slicer = None
[6c0568b]80        ## event to send slicer info
[d468daa]81        self.Bind(EVT_INTERNAL, self._onEVT_INTERNAL)
[1bf33c1]82       
[6c0568b]83        self.axes_frozen = False
84        ## panel that contains result from slicer motion (ex: Boxsum info)
[54cc36a]85        self.panel_slicer=None
[8dfdd20]86       
[1bf33c1]87        ## Graph       
88        self.graph = Graph()
89        self.graph.xaxis("\\rm{Q}", 'A^{-1}')
90        self.graph.yaxis("\\rm{Intensity} ","cm^{-1}")
91        self.graph.render(self)
[8dfdd20]92        ## store default value of zmin and zmax
93        self.default_zmin_ctl = self.zmin_2D
94        self.default_zmax_ctl = self.zmax_2D
[0d9dae8]95       
[1bf33c1]96    def _onEVT_1DREPLOT(self, event):
97        """
98            Data is ready to be displayed
[4b91fd1]99           
100            #TODO: this name should be changed to something more appropriate
101            # Don't forget that changing this name will mean changing code
102            # in plotting.py
103             
[1bf33c1]104            @param event: data event
105        """
[6c0568b]106        ## Update self.data2d with the current plot
107        self.data2D = event.plot
[15550f4]108       
[1bf33c1]109        #TODO: Check for existence of plot attribute
[6c0568b]110       
[1bf33c1]111        # Check whether this is a replot. If we ask for a replot
112        # and the plottable no longer exists, ignore the event.
113        if hasattr(event, "update") and event.update==True \
114            and event.plot.name not in self.plots.keys():
115            return
[ab8f936]116       
[1bf33c1]117        if hasattr(event, "reset"):
118            self._reset()
119        is_new = True
120        if event.plot.name in self.plots.keys():
121            # Check whether the class of plottable changed
122            if not event.plot.__class__==self.plots[event.plot.name].__class__:
[ab8f936]123                #overwrite a plottable using the same name
[1bf33c1]124                self.graph.delete(self.plots[event.plot.name])
125            else:
[ab8f936]126                # plottable is already draw on the panel
[1bf33c1]127                is_new = False
[ab8f936]128           
129        if is_new:
130            # a new plottable overwrites a plotted one  using the same id
131            for plottable in self.plots.itervalues():
[e48a62e]132                if hasattr(event.plot,"id"):
133                    if event.plot.id==plottable.id :
134                        self.graph.delete(plottable)
[ab8f936]135           
136            self.plots[event.plot.name] = event.plot
137            self.graph.add(self.plots[event.plot.name])
138        else:
[4b91fd1]139            # Update the plottable with the new data
140           
141            #TODO: we should have a method to do this,
142            #      something along the lines of:
143            #      plottable1.update_data_from_plottable(plottable2)
144           
145            self.plots[event.plot.name].xmin = event.plot.xmin
146            self.plots[event.plot.name].xmax = event.plot.xmax
147            self.plots[event.plot.name].ymin = event.plot.ymin
148            self.plots[event.plot.name].ymax = event.plot.ymax
149            self.plots[event.plot.name].data = event.plot.data
150            self.plots[event.plot.name].err_data = event.plot.err_data
[ac9a5f6]151            # update qmax with the new xmax of data plotted
152            self.qmax= event.plot.xmax
[4b91fd1]153           
[ac9a5f6]154        self.slicer= None
[1bf33c1]155        # Check axis labels
156        #TODO: Should re-factor this
[6c0568b]157        ## render the graph with its new content
[7fff5cd]158               
159        #data2D: put 'Pixel (Number)' for axis title and unit in case of having no detector info and none in _units
160        if len(self.data2D.detector) < 1: 
161            if len(event.plot._xunit)< 1 and len(event.plot._yunit) < 1:
[b03deea]162                event.plot._xaxis = '\\rm{x}'
163                event.plot._yaxis = '\\rm{y}'
164                event.plot._xunit = 'pixel'
165                event.plot._yunit = 'pixel'
[7fff5cd]166
[1bf33c1]167        self.graph.xaxis(event.plot._xaxis, event.plot._xunit)
168        self.graph.yaxis(event.plot._yaxis, event.plot._yunit)
[0690e1d]169        self.graph.title(self.data2D.name)
[1bf33c1]170        self.graph.render(self)
171        self.subplot.figure.canvas.draw_idle()
[8dfdd20]172        ## store default value of zmin and zmax
173        self.default_zmin_ctl = self.zmin_2D
174        self.default_zmax_ctl = self.zmax_2D
[1bf33c1]175
176
177    def onContextMenu(self, event):
178        """
179            2D plot context menu
180            @param event: wx context event
181        """
[15550f4]182       
[1bf33c1]183        slicerpop = PanelMenu()
184        slicerpop.set_plots(self.plots)
185        slicerpop.set_graph(self.graph)
[9a585d0]186             
187        id = wx.NewId()
188        slicerpop.Append(id, '&Save image')
189        wx.EVT_MENU(self, id, self.onSaveImage)
190       
191        id = wx.NewId()
192        slicerpop.Append(id,'&Print image', 'Print image ')
193        wx.EVT_MENU(self, id, self.onPrint)
194       
[1ce365f8]195        id = wx.NewId()
[18eba35]196        slicerpop.Append(id,'&Print Preview', 'image preview for print')
[1ce365f8]197        wx.EVT_MENU(self, id, self.onPrinterPreview)
198       
[9a585d0]199        slicerpop.AppendSeparator()
[7fff5cd]200        if len(self.data2D.detector) == 1:       
[0e13148]201           
202            item_list = self.parent.get_context_menu(self.graph)
203            if (not item_list==None) and (not len(item_list)==0):
204                   
205                    for item in item_list:
206                        try:
207                            id = wx.NewId()
208                            slicerpop.Append(id, item[0], item[1])
209                            wx.EVT_MENU(self, id, item[2])
210                        except:
211                            wx.PostEvent(self.parent, StatusEvent(status=\
212                            "ModelPanel1D.onContextMenu: bad menu item  %s"%sys.exc_value))
213                            pass
214                    slicerpop.AppendSeparator()
215           
[15550f4]216            id = wx.NewId()
217            slicerpop.Append(id, '&Perform circular average')
218            wx.EVT_MENU(self, id, self.onCircular) 
219           
220            id = wx.NewId()
221            slicerpop.Append(id, '&Sector [Q view]')
222            wx.EVT_MENU(self, id, self.onSectorQ) 
223           
224            id = wx.NewId()
225            slicerpop.Append(id, '&Annulus [Phi view ]')
226            wx.EVT_MENU(self, id, self.onSectorPhi) 
227           
[92c2345]228            id = wx.NewId()
[15550f4]229            slicerpop.Append(id, '&Box Sum')
230            wx.EVT_MENU(self, id, self.onBoxSum) 
[6c0568b]231           
[15550f4]232            id = wx.NewId()
233            slicerpop.Append(id, '&Box averaging in Qx')
234            wx.EVT_MENU(self, id, self.onBoxavgX) 
235           
236            id = wx.NewId()
237            slicerpop.Append(id, '&Box averaging in Qy')
238            wx.EVT_MENU(self, id, self.onBoxavgY) 
239           
240            if self.slicer !=None :
[eba08f1a]241                id = wx.NewId()
[15550f4]242                slicerpop.Append(id, '&Clear slicer')
243                wx.EVT_MENU(self, id,  self.onClearSlicer) 
[6c0568b]244               
[15550f4]245                if self.slicer.__class__.__name__ !="BoxSum":
246                    id = wx.NewId()
247                    slicerpop.Append(id, '&Edit Slicer Parameters')
248                    wx.EVT_MENU(self, id, self._onEditSlicer) 
249                   
250            slicerpop.AppendSeparator() 
[0e13148]251           
252        id = wx.NewId()
253        slicerpop.Append(id, '&Detector Parameters')
254        wx.EVT_MENU(self, id, self._onEditDetector) 
255       
[9a585d0]256       
[1bf33c1]257        id = wx.NewId()
258        slicerpop.Append(id, '&Toggle Linear/Log scale')
259        wx.EVT_MENU(self, id, self._onToggleScale) 
[d468daa]260                 
[1bf33c1]261        pos = event.GetPosition()
262        pos = self.ScreenToClient(pos)
263        self.PopupMenu(slicerpop, pos)
[8bd764d]264   
[6c0568b]265   
[ea290ee]266    def _onEditDetector(self, event):
[6d920cd]267        """
[6c0568b]268            Allow to view and edits  detector parameters
269            @param event: wx.menu event
[6d920cd]270        """
[6c0568b]271       
[ea290ee]272        import detector_dialog
[8dfdd20]273       
274        dialog = detector_dialog.DetectorDialog(self, -1,base=self.parent,
275                       reset_zmin_ctl =self.default_zmin_ctl,
276                       reset_zmax_ctl = self.default_zmax_ctl,cmap=self.cmap)
[6c0568b]277        ## info of current detector and data2D
[ea290ee]278        xnpts = len(self.data2D.x_bins)
279        ynpts = len(self.data2D.y_bins)
280        xmax = max(self.data2D.xmin, self.data2D.xmax)
281        ymax = max(self.data2D.ymin, self.data2D.ymax)
282        qmax = math.sqrt(math.pow(xmax,2)+math.pow(ymax,2))
283        beam = self.data2D.xmin
[e50f15b]284     
[6c0568b]285        ## set dialog window content
[ea290ee]286        dialog.setContent(xnpts=xnpts,ynpts=ynpts,qmax=qmax,
287                           beam=self.data2D.xmin,
288                           zmin = self.zmin_2D,
289                          zmax = self.zmax_2D)
290        if dialog.ShowModal() == wx.ID_OK:
291            evt = dialog.getContent()
292            self.zmin_2D = evt.zmin
293            self.zmax_2D = evt.zmax
[8dfdd20]294            self.cmap= evt.cmap
[ea290ee]295       
296        dialog.Destroy()
[6c0568b]297        ## Redraw the current image
[ea290ee]298        self.image(data= self.data2D.data,
299                   xmin= self.data2D.xmin,
300                   xmax= self.data2D.xmax,
301                   ymin= self.data2D.ymin,
302                   ymax= self.data2D.ymax,
303                   zmin= self.zmin_2D,
304                   zmax= self.zmax_2D,
[8dfdd20]305                   cmap= self.cmap,
[e50f15b]306                   color=0,symbol=0,label=self.data2D.name)#'data2D')
[ea290ee]307        self.subplot.figure.canvas.draw_idle()
[1bf33c1]308       
[6c0568b]309       
310 
[1bf33c1]311    def freeze_axes(self):
312        self.axes_frozen = True
313       
314    def thaw_axes(self):
315        self.axes_frozen = False
316       
317    def onMouseMotion(self,event):
318        pass
319    def onWheel(self, event):
[6c0568b]320        pass 
321     
[1bf33c1]322    def update(self, draw=True):
323        """
324            Respond to changes in the model by recalculating the
325            profiles and resetting the widgets.
326        """
327        self.draw()
328       
329       
330    def _getEmptySlicerEvent(self):
[6c0568b]331        """
332            create an empty slicervent
333        """
[1bf33c1]334        return SlicerEvent(type=None,
335                           params=None,
336                           obj_class=None)
337    def _onEVT_INTERNAL(self, event):
338        """
[6c0568b]339            Draw the slicer
340            @param event: wx.lib.newevent (SlicerEvent) containing slicer
341            parameter
[1bf33c1]342        """
343        self._setSlicer(event.slicer)
344           
345    def _setSlicer(self, slicer):
[6c0568b]346        """
347            Clear the previous slicer and create a new one.Post an internal
348            event.
349            @param slicer: slicer class to create
350        """
[1bf33c1]351       
[6c0568b]352        ## Clear current slicer
[1bf33c1]353        if not self.slicer == None: 
354            self.slicer.clear()           
[6c0568b]355        ## Create a new slicer   
[1bf33c1]356        self.slicer_z += 1
357        self.slicer = slicer(self, self.subplot, zorder=self.slicer_z)
[240c805]358        self.subplot.set_ylim(self.data2D.ymin, self.data2D.ymax)
359        self.subplot.set_xlim(self.data2D.xmin, self.data2D.xmax)
[6c0568b]360        ## Draw slicer
[1bf33c1]361        self.update()
362        self.slicer.update()
[6c0568b]363        wx.PostEvent(self.parent, StatusEvent(status=\
364                        "Plotter2D._setSlicer  %s"%self.slicer.__class__.__name__))
[1bf33c1]365        # Post slicer event
366        event = self._getEmptySlicerEvent()
367        event.type = self.slicer.__class__.__name__
[54cc36a]368       
[1bf33c1]369        event.obj_class = self.slicer.__class__
370        event.params = self.slicer.get_params()
[d468daa]371        wx.PostEvent(self, event)
[1bf33c1]372
[6c0568b]373
[1bf33c1]374    def onCircular(self, event):
375        """
376            perform circular averaging on Data2D
[6c0568b]377            @param event: wx.menu event
[1bf33c1]378        """
379       
380        from DataLoader.manipulations import CircularAverage
[6c0568b]381        ## compute the maximum radius of data2D
[216efab]382        self.qmax= max(math.fabs(self.data2D.xmax),math.fabs(self.data2D.xmin))
383        self.ymax=max(math.fabs(self.data2D.ymax),math.fabs(self.data2D.ymin))
384        self.radius= math.sqrt( math.pow(self.qmax,2)+math.pow(self.ymax,2)) 
[6c0568b]385        ##Compute beam width
[8f584c9]386        bin_width = (self.qmax +self.qmax)/100
[6c0568b]387        ## Create data1D circular average of data2D
[c73d871]388        Circle = CircularAverage( r_min=0, r_max=self.radius, bin_width=bin_width)
[1bf33c1]389        circ = Circle(self.data2D)
[6c0568b]390       
[1bf33c1]391        from sans.guiframe.dataFitting import Data1D
392        if hasattr(circ,"dxl"):
393            dxl= circ.dxl
394        else:
395            dxl= None
396        if hasattr(circ,"dxw"):
397            dxw= circ.dxw
398        else:
399            dxw= None
[ef0c170]400       
[4ac8556]401        new_plot = Data1D(x=circ.x,y=circ.y,dy=circ.dy)
402        new_plot.dxl=dxl
403        new_plot.dxw=dxw
[1bf33c1]404        new_plot.name = "Circ avg "+ self.data2D.name
405        new_plot.source=self.data2D.source
[50cbace]406        #new_plot.info=self.data2D.info
[1bf33c1]407        new_plot.interactive = True
408        new_plot.detector =self.data2D.detector
[6c0568b]409        ## If the data file does not tell us what the axes are, just assume...
[8f584c9]410        new_plot.xaxis("\\rm{Q}","A^{-1}")
[1bf33c1]411        new_plot.yaxis("\\rm{Intensity} ","cm^{-1}")
412        new_plot.group_id = "Circ avg "+ self.data2D.name
[8b30e62]413        new_plot.id = "Circ avg "+ self.data2D.name
[70cf5d3]414        new_plot.is_data= True
[6c0568b]415       
[1bf33c1]416        wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title=new_plot.name))
[ef0c170]417       
[6c0568b]418       
[1bf33c1]419    def _onEditSlicer(self, event):
[6c0568b]420        """
421            Is available only when a slicer is drawn.Create a dialog
422            window where the user can enter value to reset slicer
423            parameters.
424            @param event: wx.menu event
425        """
[1bf33c1]426        if self.slicer !=None:
427            from SlicerParameters import SlicerParameterPanel
[4f8a00c]428            dialog = SlicerParameterPanel(self, -1, "Slicer Parameters")
[1bf33c1]429            dialog.set_slicer(self.slicer.__class__.__name__,
430                            self.slicer.get_params())
431            if dialog.ShowModal() == wx.ID_OK:
432                dialog.Destroy() 
433       
[6c0568b]434       
[1bf33c1]435    def onSectorQ(self, event):
436        """
[6c0568b]437            Perform sector averaging on Q and draw sector slicer
[1bf33c1]438        """
[ef0c170]439        from SectorSlicer import SectorInteractor
[1bf33c1]440        self.onClearSlicer(event)
[d468daa]441        wx.PostEvent(self, InternalEvent(slicer= SectorInteractor))
[1bf33c1]442       
443    def onSectorPhi(self, event):
444        """
[6c0568b]445            Perform sector averaging on Phi and draw annulus slicer
[1bf33c1]446        """
[ef0c170]447        from AnnulusSlicer import AnnulusInteractor
[1bf33c1]448        self.onClearSlicer(event)
[d468daa]449        wx.PostEvent(self, InternalEvent(slicer= AnnulusInteractor))
[1bf33c1]450       
[7ab9241]451    def onBoxSum(self,event):
452        from boxSum import BoxSum
453        self.onClearSlicer(event)
[6c0568b]454                   
[54cc36a]455        self.slicer_z += 1
456        self.slicer =  BoxSum(self, self.subplot, zorder=self.slicer_z)
[6c0568b]457       
[54cc36a]458        self.subplot.set_ylim(self.data2D.ymin, self.data2D.ymax)
459        self.subplot.set_xlim(self.data2D.xmin, self.data2D.xmax)
460       
461        self.update()
462        self.slicer.update()
[6c0568b]463        ## Value used to initially set the slicer panel
464        type = self.slicer.__class__.__name__
465        params = self.slicer.get_params()
466        ## Create a new panel to display results of summation of Data2D
[8a7a21b]467        from slicerpanel import SlicerPanel
[6c0568b]468        new_panel = SlicerPanel(parent= self.parent, id= -1,
469                                    base= self, type= type,
470                                    params= params, style= wx.RAISED_BORDER)
471       
472        new_panel.window_caption=self.slicer.__class__.__name__+" "+ str(self.data2D.name)
473        new_panel.window_name = self.slicer.__class__.__name__+" "+ str(self.data2D.name)
474        ## Store a reference of the new created panel
[54cc36a]475        self.panel_slicer= new_panel
[6c0568b]476        ## save the window_caption of the new panel in the current slicer
[0bd2cd8]477        self.slicer.set_panel_name( name= new_panel.window_caption)
[6c0568b]478        ## post slicer panel to guiframe to display it
[54cc36a]479        from sans.guicomm.events import SlicerPanelEvent
[1debb29]480        wx.PostEvent(self.parent, SlicerPanelEvent (panel= self.panel_slicer,
481                                                    main_panel =self))
[6c0568b]482
[8a7a21b]483       
484    def onBoxavgX(self,event):
[6c0568b]485        """
486            Perform 2D data averaging on Qx
487            Create a new slicer .
488            @param event: wx.menu event
489        """
[8a7a21b]490        from boxSlicer import BoxInteractorX
[38224f10]491        self.onClearSlicer(event)
[d468daa]492        wx.PostEvent(self, InternalEvent(slicer= BoxInteractorX))
493       
494       
[8a7a21b]495    def onBoxavgY(self,event):
[6c0568b]496        """
497            Perform 2D data averaging on Qy
498            Create a new slicer .
499            @param event: wx.menu event
500        """
[8a7a21b]501        from boxSlicer import BoxInteractorY
502        self.onClearSlicer(event)
[d468daa]503        wx.PostEvent(self, InternalEvent(slicer= BoxInteractorY))
[6c0568b]504       
[b319def8]505       
[1bf33c1]506    def onClearSlicer(self, event):
507        """
508            Clear the slicer on the plot
509        """
510        if not self.slicer==None:
511            self.slicer.clear()
512            self.subplot.figure.canvas.draw()
513            self.slicer = None
514       
515            # Post slicer None event
516            event = self._getEmptySlicerEvent()
[d468daa]517            wx.PostEvent(self, event)
[1bf33c1]518         
[6c0568b]519   
[9c0fe9a5]520    def old_onToggleScale(self, event):
[1bf33c1]521        """
522            toggle pixel scale and replot image
523        """
524        if self.scale == 'log':
525            self.scale = 'linear'
526        else:
527            self.scale = 'log'
[6c0568b]528        self.image(self.data2D.data,self.xmin_2D,self.xmax_2D,self.ymin_2D,
[1bf33c1]529                   self.ymax_2D,self.zmin_2D ,self.zmax_2D )
530        wx.PostEvent(self.parent, StatusEvent(status="Image is in %s scale"%self.scale))
531       
[0f6d05f8]532        """     
533            #TODO: this name should be changed to something more appropriate
534            # Don't forget that changing this name will mean changing code
535            # in plotting.py
536             
537            # Update the plottable with the new data
538           
539            #TODO: we should have a method to do this,
540            #      something along the lines of:
541            #      plottable1.update_data_from_plottable(plottable2)
542           
543            self.plots[event.plot.name].xmin = event.plot.xmin
544            self.plots[event.plot.name].xmax = event.plot.xmax
545            self.plots[event.plot.name].ymin = event.plot.ymin
546            self.plots[event.plot.name].ymax = event.plot.ymax
547            self.plots[event.plot.name].data = event.plot.data
548            self.plots[event.plot.name].err_data = event.plot.err_data
[15550f4]549        """
Note: See TracBrowser for help on using the repository browser.