Ignore:
Timestamp:
Nov 12, 2024, 5:52:49 PM (12 days ago)
Author:
mmaurice
Message:

Generic PCM:

Python postprocessing: change the implementation of some routines,
add automatic traceur.def reader and possibility to export files
readable by the chemical pathway analyzer chempath.

MM

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/LMDZ.GENERIC/utilities/photochemistry/photochem_postproc.py

    r3431 r3511  
    1515warnings.filterwarnings("ignore", message="The following kwargs were not used by contour: 'lat'")
    1616
    17 M          = {'co2':44,  # Molar masses
    18               'o':16,    # TODO: automatic parser
    19               'o1d':16,
    20               'o2':32,
    21               'o3':48,
    22               'h':1,
    23               'h2':2,
    24               'oh':17,
    25               'h2o_vap':18,
    26               'ho2':33,
    27               'h2o2':34,
    28               'co':28,
    29               'cho':29,
    30               'ch2o':30}
    31 
    3217background = 'co2' # background gas of the atmosphere
    3318
     
    4631    Methods
    4732    -------
    48     get_profile(field,**kw)
    49         Get profile of a field (either local or averaged)
     33    get_subset(field,**kw)
     34        Get pa subset at fixed given coordinate of the data
    5035    plot_meridional_slice(field,logcb,labelcb,**kw)
    5136        Plot a meridional slice of a field
    5237    plot_time_evolution(field,logcb,labelcb,**kw)
    5338        Plot time evolution of a field (as a time vs altitude contour plot)
     39    plot_time_series(field,lat,lon,alt,logy)
     40        Plot time series of a field (at a specific location (lon, lat, alt), averged, or a combination thereof)
     41    plot_atlas(field,t,alt,logcb,labelcb)
     42        Plot atlas of a field
    5443    plot_profile(field,**kw)
    5544        Plot a profile of a field
     
    8069        self.data[field] = value
    8170
    82     def get_profile(self,field,**kw):
    83         """ Get profile of a field (either local or averaged)
    84 
    85         Parameters
    86         ----------
    87         field : str
    88             Field name
    89         t : float (optional)
    90             Time at which to select (if nothing specified use time-average)
    91         lat : float (optional)
    92             Latitude at which to select (if nothing specified use area-weighted meridional average)
    93         lon : float (optional)
    94             Longitude at which to select (if nothing specified use zonal average)
    95 
    96         """
    97        
    98         if self['latitude'].size == 1:
    99             # 1D
    100             return self[field][:,0,0]
    101         else:
    102             # 3D
    103             if 'lat' in kw:
    104                 if 'lon' in kw:
    105                     return self[field].sel(latitude=kw['lat'],method='nearest').sel(longitude=kw['lon'],method='nearest')
    106                 else:
    107                     return self[field].sel(latitude=kw['lat'],method='nearest').mean(dim='longitude')
    108             else:
    109                 # Latitude-averaged profile: need to weight by the grid cell surface area
    110                 if 'lon' in kw:
    111                     return (self['aire']*self[field]/self['aire'].mean(dim='latitude')).mean(dim='latitude').sel(longitude=kw['lon'],method='nearest')
    112                 else:
    113                     return (self['aire']*self[field]/self['aire'].mean(dim='latitude').mean(dim='longitude')).mean(dim='latitude').mean(dim='longitude')
     71    def __area_weight__(self,data_array):
     72        return self['aire']*data_array/self['aire'].mean('latitude')
    11473
    11574    def get_subset(self,field='all',**kw):
    11675        """ Get a subset at fixed given coordinate of the data
     76
     77        Can also average over a given dimension. In this case,
     78        the meridional average is area-weighted.
    11779
    11880        Parameters
     
    13395            Altitude of the slice. If nothing
    13496            specified, use time-average
    135 
    136         Raise
    137         -----
    138         Slice direction not provided
    139         """
    140         if len(kw) == 0:
    141             raise Exception('Slice direction not provided')
    142 
     97        """
    14398        if field == 'all':
    14499            data_subset = self.data
     
    147102           
    148103        if 't' in kw:
    149             data_subset = data_subset.sel(Time=kw['t'],method='nearest')
     104            if kw['t'] == 'avg':
     105                data_subset = data_subset.mean(dim='Time')
     106            else:
     107                data_subset = data_subset.sel(Time=kw['t'],method='nearest')
     108        if 'lat' in kw:
     109            if kw['lat'] == 'avg':
     110                data_subset = self.__area_weight__(data_subset).mean(dim='latitude')
     111            else:
     112                data_subset = data_subset.sel(latitude=kw['lat'],method='nearest')
    150113        if 'lon' in kw:
    151             data_subset = data_subset.sel(longitude=kw['lon'],method='nearest')
    152         if 'lat' in kw:
    153             data_subset = data_subset.sel(latitude=kw['lat'],method='nearest')
     114            if kw['lon'] == 'avg':
     115                data_subset = data_subset.mean(dim='longitude')
     116            else:
     117                data_subset = data_subset.sel(longitude=kw['lon'],method='nearest')
    154118        if 'alt' in kw:
    155             data_subset = data_subset.sel(altitude=kw['alt'],method='nearest')
     119            if kw['alt'] == 'avg':
     120                data_subset = data_subset.mean(dim='altitude')
     121            else:
     122                data_subset = data_subset.sel(altitude=kw['alt'],method='nearest')
    156123
    157124        return data_subset
     
    178145            raise Exception('Trying to plot a meridional slice of a 1D simulation')
    179146
    180         meridional_slice = self[field]
    181        
    182         if t == 'avg':
    183             meridional_slice = meridional_slice.mean(dim='Time')
     147        meridional_slice = self.get_subset(field,t=t,lon=lon)
     148           
     149        if logcb:
     150            plt.contourf(meridional_slice['latitude'],meridional_slice['altitude'],meridional_slice,locator=tk.LogLocator(),**plt_kw)
    184151        else:
    185             meridional_slice = meridional_slice.sel(Time=t,method='nearest')
    186         if lon == 'avg':
    187             meridional_slice = meridional_slice.mean(dim='longitude')
    188         else:
    189            meridional_slice = meridional_slice.sel(longitude=lon,method='nearest')
    190            
    191         if logcb:
    192             plt.contourf(self['latitude'],self['altitude'],meridional_slice,locator=tk.LogLocator(),**plt_kw)
    193         else:
    194             plt.contourf(self['latitude'],self['altitude'],meridional_slice,**plt_kw)
     152            plt.contourf(meridional_slice['latitude'],meridional_slice['altitude'],meridional_slice,**plt_kw)
    195153           
    196154        plt.colorbar(label=field if labelcb==None else labelcb)
     
    216174        """
    217175
    218         time_evolution = self[field]
    219        
    220         if lat == 'avg':
    221             time_evolution = (self['aire']*time_evolution/self['aire'].mean(dim='latitude')).mean(dim='latitude')
     176        time_evolution = self.get_subset(field,lon=lon,lat=lat)
     177           
     178        if logcb:
     179            plt.contourf(time_evolution['Time'],time_evolution['altitude'],time_evolution.T,locator=tk.LogLocator(),**plt_kw)
    222180        else:
    223             time_evolution = time_evolution.sel(latitude=lat,method='nearest')
    224         if lon == 'avg':
    225             time_evolution = time_evolution.mean(dim='longitude')
    226         else:
    227             time_evolution = time_evolution.sel(longitude=lon,method='nearest')
    228            
    229         if logcb:
    230             plt.contourf(self['Time'],self['altitude'],time_evolution.T,locator=tk.LogLocator(),**plt_kw)
    231         else:
    232             plt.contourf(self['Time'],self['altitude'],time_evolution.T,**plt_kw)
     181            plt.contourf(time_evolution['Time'],time_evolution['altitude'],time_evolution.T,**plt_kw)
    233182           
    234183        plt.colorbar(label=field if labelcb==None else labelcb)
    235184        plt.xlabel('time [day]')
    236185        plt.ylabel('altitude [km]')
     186
     187    def plot_time_series(self,field,lat='avg',lon='avg',alt='avg',logy=False,**plt_kw):
     188        """ Plot time series of a field (at a specific location (lon, lat, alt), averged, or a combination thereof)
     189
     190        Parameters
     191        ----------
     192        field : str
     193            Field name to plot
     194        lat : float (optional)
     195            Latitude at which to plot (if nothing specified use area-weighted meridional average)
     196        lon : float (optional)
     197            Longitude at which to plot (if nothing specified use zonal average)
     198        logy : bool (optional)
     199            Use logarithmic y-axis
     200        matplotlib plot keyword arguments
     201        """
     202
     203        time_series = self.get_subset(field,lon=lon,lat=lat,alt=alt)
     204
     205        if not 'label' in plt_kw:
     206            plt_kw['label'] = self[field].units
     207        if logy:
     208            plt.semilogy(time_series['Time'],time_series,**plt_kw)
     209        else:
     210            plt.plot(time_series['Time'],time_series,**plt_kw)
     211           
     212        plt.xlabel('time [day]')
     213        plt.ylabel(field+' ['+self[field].units+']')
    237214
    238215    def plot_atlas(self,field,t='avg',alt='avg',logcb=False,labelcb=None,**plt_kw):
     
    254231        """
    255232
    256         atlas = self[field]
    257        
    258         if t == 'avg':
    259             atlas = atlas.mean(dim='Time')
     233        if 'altitude' in self[field].dims:
     234            atlas = self.get_subset(field,t=t,alt=alt)
    260235        else:
    261             atlas = atlas.sel(Time=t,method='nearest')
    262            
    263         if  'altitude' in atlas.coords:
    264             if alt == 'avg':
    265                 atlas = atlas.mean(dim='altitude')
    266             else:
    267                 atlas = atlas.sel(altitude=alt,method='nearest')
     236            atlas = self.get_subset(field,t=t)
    268237           
    269238        if logcb:
    270             plt.contourf(self['longitude'],self['latitude'],atlas,locator=tk.LogLocator(),**plt_kw)
     239            plt.contourf(atlas['longitude'],atlas['latitude'],atlas,locator=tk.LogLocator(),**plt_kw)
    271240        else:
    272             plt.contourf(self['longitude'],self['latitude'],atlas,**plt_kw)
     241            plt.contourf(atlas['longitude'],atlas['latitude'],atlas,**plt_kw)
    273242           
    274243        plt.colorbar(label=field if labelcb==None else labelcb)
     
    294263        """
    295264
    296         profile = self[field]
     265        profile = self.get_subset(field,t=t,lon=lon,lat=lat)
    297266       
    298         if t == 'avg':
    299             profile = profile.mean(dim='Time')
     267        if logx:
     268            plt.semilogx(profile,profile['altitude'],**plt_kw)
    300269        else:
    301             profile = profile.sel(Time=t,method='nearest')
    302            
    303         if self['latitude'].size > 1:
    304            
    305             if lat == 'avg':
    306                 profile = (self['aire']*profile/self['aire'].mean(dim='latitude')).mean(dim='latitude')
    307             else:
    308                 profile = profile.sel(latitude=lat,method='nearest')
    309             if lon == 'avg':
    310                 profile = profile.mean(dim='longitude')
    311             else:
    312                 profile = profile.sel(longitude=lon,method='nearest')
    313            
    314         if logx:
    315             plt.semilogx(profile,self['altitude'],**plt_kw)
    316         else:
    317             plt.plot(profile,self['altitude'],**plt_kw)
     270            plt.plot(profile,profile['altitude'],**plt_kw)
    318271           
    319272        plt.xlabel(field+' ['+self[field].units+']')
    320273        plt.ylabel('altitude [km]')
     274
     275    def to_chempath(self,t,dt,filename_suffix='_chempath',lon='avg',lat='avg',alt='avg'):
     276        """ Create files redable by the chemical path analyzer chempath (DOI: 10.5194/gmd-2024-163)
     277
     278        Parameters
     279        ----------
     280        t : float
     281            Initial time
     282        dt : float
     283            Timestep
     284        filename_suffix : str (optional)
     285            Suffix to append to the files being created (species.txt,
     286            reactions.txt, model_time.dat, rates.dat, concentrations.dat)
     287        lat : float (optional)
     288            Latitude at which to plot (if nothing specified use area-weighted meridional average)
     289        lon : float (optional)
     290            Longitude at which to plot (if nothing specified use zonal average)
     291        alt : float (optional)
     292            Altitude of the slice. If nothing
     293        matplotlib's plot / semilogx keyword arguments
     294        """
     295
     296        # Save pecies list in chempath format
     297        with open(self.path+'/species'+filename_suffix+'.txt', 'w') as fp:
     298            for sp in self.species:
     299                fp.write("%s\n" % sp)
     300
     301        # Save reactions list in chempath format
     302        with open(self.path+'/reactions'+filename_suffix+'.txt', 'w') as fp:
     303            for r in self.reactions:
     304                current_formula = self.reactions[r].formula
     305                current_formula = current_formula.replace(' ','')
     306                current_formula = current_formula.replace('->','=')
     307                fp.write("%s\n" % current_formula)
     308
     309        # Save rates, concentrations and times in chempath format
     310        rates   = np.array([],dtype=np.float128)
     311        conc    = np.array([],dtype=np.float128)
     312        conc_dt = np.array([],dtype=np.float128)
     313        times   = np.array([t,t+dt],dtype=np.float128)
     314       
     315        out    = self.get_subset(t=t,lon=lon,lat=lat,alt=alt) 
     316        for r in self.reactions:
     317            rates = np.append(rates,out[f'rate ({r})'])
     318        for sp in self.species:
     319            conc = np.append(conc,out[f'{sp} vmr'])
     320       
     321        out    = self.get_subset(t=t+dt,lon=lon,lat=lat,alt=alt) 
     322        for sp in self.species:
     323            conc_dt = np.append(conc_dt,out[f'{sp} vmr'])
     324        conc = np.vstack((conc,conc_dt))
     325
     326        times.tofile(self.path+'/model_time'+filename_suffix+'.dat')
     327        rates.tofile(self.path+'/rates'+filename_suffix+'.dat')
     328        conc.tofile(self.path+'/concentrations'+filename_suffix+'.dat')
    321329           
    322330class reaction:
     
    603611    if reactions == 'read':
    604612        reactions      = read_reactfile(s.path)
     613        s.tracers   = read_traceurs(s.path)
    605614        s.species   = []
    606615        s.reactions = {} # reactions dict will be merged at the end
     
    609618    for r in reactions:
    610619        for sp in reactions[r].reactants:
     620            if not sp in s.tracers or not s.tracers[sp].is_chim:
     621                raise Warning(sp, 'is not recorded as a chimical tracer')
    611622            if not sp in s.species:
    612623                s.species.append(sp)
    613624        for sp in reactions[r].products:
     625            if not sp in s.tracers or not s.tracers[sp].is_chim:
     626                raise Warning(sp, 'is not recorded as a chimical tracer')
    614627            if not sp in s.species:
    615628                s.species.append(sp)
     
    622635    for sp in s.species:
    623636        # volume mixing ratios
    624         s[sp+' vmr'] = s[sp] * M['co2'] / M[sp]
     637        s[sp+' vmr'] = s[sp] * s.tracers['co2'].M / s.tracers[sp].M
    625638        s[sp+' vmr'] = s[sp+' vmr'].assign_attrs({'units':'m^3/m^3'})
    626639        # molecular densities
     
    678691    return s
    679692
     693class trac:
     694    """ Tracer class
     695   
     696    A class to store useful informations about simulations tracers
     697   
     698    Attributes
     699    ----------
     700    name : string
     701        Tracer's name
     702    M : float
     703        Tracer's molar mass [g/mol]
     704    is_chim : bool
     705        Is it a photochemical tracer?
     706    """
     707    def __init__(self,name,M,is_chim):
     708        self.name    = name
     709        self.M       = M
     710        self.is_chim = is_chim
     711
     712def read_traceurs(path):
     713    """ Read the traceurs of a simulation
     714
     715    Parameters
     716    ----------
     717    path : string
     718        path to simulation
     719
     720    Returns
     721    -------
     722    dict
     723        dictionnaries of all tracers
     724    """
     725    tracdict = {}
     726    with open(path+'/traceur.def') as tracfile:
     727       
     728        for iline,line in enumerate(tracfile):
     729           
     730            # Empty line
     731            if len(line.split()) == 0:
     732                continue
     733               
     734            # First line
     735            elif iline == 0:
     736                if not '#ModernTrac-v1' in line:
     737                    raise Exception('Can only read modern traceur.def')
     738                continue
     739               
     740            # Second line (number of tracers)
     741            elif iline == 1:
     742                ntrac = int(line)
     743                continue
     744           
     745            # Commented line
     746            elif line[0] == '!':
     747                continue
     748
     749            # Regular entry
     750            else:
     751                line    = line.split()
     752                name    = line[0]
     753                is_chim = False
     754               
     755                for param in line[1:]:
     756                    if param[:4] == 'mmol':
     757                        M = float(param[5:])
     758                    elif param[:7] == 'is_chim':
     759                        is_chim = bool(float(param[8:]))
     760                       
     761                tracdict[name] = trac(name,M,is_chim)
     762               
     763    if len(tracdict) != ntrac:
     764        raise Exception('Mismatch between announced an read number of tracers')
     765       
     766    return tracdict
Note: See TracChangeset for help on using the changeset viewer.