Source code for pycif.plugins.models.iconart.io.inputs.lbc

import xarray as xr
import numpy as np
import pandas as pd
import os
import multiprocessing as mp
from itertools import repeat
from logging import info, warning
from .tracers import change_tracers_xml_lbc

from ..utils import LBC_DIRNAME
from ......utils import path
from ......utils.hdf5 import _hdf5_lock


[docs] def make_lbc(self, datastore, ddi, ddf, runsubdir, mode): """Fill the meteorological lbc files with lbc for tracers. """ # Inputs and outputs trids_in = list(datastore.keys()) trids_out = [("lbc", s) for s in self.chemistry.acspecies.attributes] # Flags to detect the ensemble is_ensemble = False is_perturbed_comp = False # Create the dictionary to store the input data if not hasattr(self, "dict_lbc_dataout"): self.dict_lbc_dataout = {} if ddi not in self.dict_lbc_dataout: self.dict_lbc_dataout[ddi] = {} # Create the OEM dir oem_dir = os.path.join(runsubdir, 'OEM') path.init_dir(oem_dir) # Loop over the species that must be transported for trid_out in trids_out: trid_in = trid_out spec_ref = spec = trid_out[1] # If ensemble, check what the datastore contains if "__sample#" in spec: is_ensemble = True is_perturbed_comp = True spec_ref = spec.split("__sample#")[0] sample_id = spec.split("__sample#")[1] if int(sample_id) > 0: continue if trid_in not in datastore: trid_in = ("lbc", spec_ref) is_perturbed_comp = False if trid_in not in datastore: continue tracer = datastore[trid_in] tracer_data = tracer["data"][ddi] # Emitted species corresponding to reference species emi_specs = self.chemistry.mapping_active2emi_ref[spec_ref] # -------------------------------------------------------------------------- # -- Get the prior data # -------------------------------------------------------------------------- # If lbc are not loaded, read the input files if "spec" not in tracer_data: varname = datastore[trid_in]['varname'] input_dates = [dd[0] for dd in datastore[trid_in]['input_dates'][ddi]] input_files = datastore[trid_in]['input_files'][ddi] func_arguments = zip(repeat(varname), input_files) with mp.Pool(min(len(input_dates), 36)) as pool: list_data2dump = pool.starmap(mp_read_data, func_arguments) # If lbc are loaded, read the datastore else: da_lbc_prior = tracer_data["spec"].copy() input_dates = pd.to_datetime(da_lbc_prior.time) # TODO: problem with last timestep == 0 sometimes... if da_lbc_prior[-1].mean() == 0: da_lbc_prior[-1] = da_lbc_prior[-2] list_data2dump = [da_lbc_prior.sel(time=dd, lat=0).values[np.newaxis] for dd in input_dates] list_data2dump_post = list_data2dump # -------------------------------------------------------------------------- # -- Ensemble processing # -------------------------------------------------------------------------- if is_ensemble: if is_perturbed_comp: info(f"The {spec_ref} lbc is perturbed.") info(f"Calculating lbc scaling factors for {spec_ref}...") list_da = [datastore[t]["data"][ddi]["spec"].isel(lev=-1) for t in trids_in] lbc_lambdas = xr.concat(list_da, dim="ens") lbc_lambdas = lbc_lambdas / lbc_lambdas[0] lbc_lambdas = lbc_lambdas.squeeze() lbc_lambdas = lbc_lambdas.isel(time=slice(0, -1)).mean(dim='time') lbc_lambdas = lbc_lambdas.fillna(1) warning("At present, note that the optimization of LBCs with ICON-ART " "can only be performed with hpixels=global or regions " "and vpixels=columns." ) else: info(f"The {spec_ref} lbc is NOT perturbed.") info(f"Creating fake emission scaling factors (1.0) for {spec_ref}...") nsamples = getattr(self.chemistry, "nsamples", 1) lbc_lambdas = xr.DataArray( np.ones((nsamples, 1)), dims=("ens", "reg") ) # Get the posterior background lbc_lambdas_post = lbc_lambdas[2] da_lbc_post = da_lbc_prior * lbc_lambdas_post.values list_data2dump_post = [da_lbc_post.sel(time=dd, lat=0).values[np.newaxis] for dd in input_dates] # Define the regions if 'tracer' in tracer and hasattr(tracer['tracer'], 'regions'): data_regions = getattr(tracer['tracer'], 'regions')[0] da_regions = xr.DataArray(data_regions, dims=('lon')) else: da_regions = xr.DataArray(np.zeros((self.domain.nlon)), dims=('lon')) # Calculate the scaling factors for each region regions_id = xr.DataArray(np.unique(da_regions), dims=('reg')) lbc_lambdas = lbc_lambdas.where(da_regions == regions_id).mean(dim='lon') # Store the scaling factors self.lbc_lambdas[ddi][spec_ref] = lbc_lambdas # Create and dump the regions file lbc_ens_regions_filepath = os.path.join(oem_dir, 'boundary_regions.nc') if not os.path.exists(lbc_ens_regions_filepath): info(f"Creating the ensemble regions file for the lbc...") da_lbc_reg = xr.DataArray((da_regions == regions_id).astype(int), dims=("cell", "reg")) ds_lbc_reg = da_lbc_reg.to_dataset(name='boundaryregion') ds_lbc_reg['global_cell_idx'] = xr.DataArray(range(1, self.domain.nlon + 1), dims=("cell")) lbc_ens_regions_filepath = os.path.join(oem_dir, 'boundary_regions.nc') with _hdf5_lock: ds_lbc_reg.to_netcdf(lbc_ens_regions_filepath) # -------------------------------------------------------------------------- # -- Merge the lbc data to meteo files # -------------------------------------------------------------------------- for date in input_dates: if date not in self.dict_lbc_dataout[ddi]: self.dict_lbc_dataout[ddi][date] = None func_arguments = zip( list(self.dict_lbc_dataout[ddi].values()), input_dates, list_data2dump, list_data2dump_post, repeat(self.meteo_lbc_dir), repeat(self.meteo_lbc_file), repeat(spec_ref), repeat(is_ensemble), repeat(is_perturbed_comp), repeat(self.lbc_dry2moist), repeat(self.domain.extpar_file), repeat(emi_specs) ) with mp.Pool(min(len(input_dates), 36)) as pool: list_ds_lbc = pool.starmap(mp_merge_data_with_meteo_lbc_file, func_arguments) for date, ds_lbc in zip(input_dates, list_ds_lbc): self.dict_lbc_dataout[ddi][date] = ds_lbc info(f"Added {spec_ref} to the lbc dictionary.") # -------------------------------------------------------------------------- # -- Adapt tracers.xml # -------------------------------------------------------------------------- info(f"Modifying tracers.xml for {spec_ref} lbc ({is_ensemble=})...") for t in trids_out: change_tracers_xml_lbc(self, t[1], is_ensemble=is_ensemble) return
# -------------------------------------------------------------------- # -- OTHERS FUNCTIONS FOR MULTIPROCESSING # --------------------------------------------------------------------
[docs] def dump_lbc_files(self, ddi, runsubdir): """Write accumulated lateral boundary condition datasets to disk. Iterates over all LBC dates stored in ``self.dict_lbc_dataout[ddi]`` and writes each as a separate ``ifs_YYYYMMDDHH_lbc.nc`` file in the ``LBC/`` sub-directory of *runsubdir*, using parallel processes. Args: self: ICON-ART model plugin instance. ddi (datetime): period start date. runsubdir (str): path to the period run directory. """ lbc_dir = os.path.join(runsubdir, '..', LBC_DIRNAME) path.init_dir(lbc_dir) list_dates = list(self.dict_lbc_dataout[ddi].keys()) list_ds_lbc = list(self.dict_lbc_dataout[ddi].values()) func_arguments = zip(list_dates, list_ds_lbc, repeat(lbc_dir)) with mp.Pool(min(len(list_ds_lbc), 36)) as pool: pool.starmap(mp_dump_lbc_files, func_arguments) del self.dict_lbc_dataout[ddi] return
[docs] def mp_read_data(varname, file): """Read a single variable from a NetCDF file (multiprocessing-safe helper). Args: varname (str): variable name to extract. file (str): path to the NetCDF file. Returns: np.ndarray: the variable's values array. """ with _hdf5_lock: return xr.open_dataset(file)[varname].values
[docs] def mp_merge_data_with_meteo_lbc_file(ds_lbc, date, data, data_post, meteo_lbc_dir, meteo_lbc_file, spec_ref, is_ensemble, is_perturbed_comp, lbc_dry2moist, extpar_file, emi_specs): """Merge CIF tracer LBC data with the IFS meteorological LBC file for one date. Reads the IFS LBC NetCDF for *date*, inserts CIF tracer fields (*data* / *data_post*), applies an optional dry-to-moist VMR conversion, and returns the merged dataset. Args: ds_lbc: in-progress LBC xr.Dataset accumulator. date (datetime): LBC date. data: CIF tracer concentration array. data_post: post-processed (interpolated) tracer array. meteo_lbc_dir (str): directory of IFS LBC files. meteo_lbc_file (str): IFS LBC filename pattern. spec_ref (str): reference species name. is_ensemble (bool): whether running in ensemble mode. is_perturbed_comp (bool): whether the component is a perturbed ensemble member. lbc_dry2moist (bool): convert dry VMR to moist VMR. extpar_file (str): external parameter file path. emi_specs (list): emitted species names. Returns: xr.Dataset: merged LBC dataset for *date*. """ # Fetch the meteo file if ds_lbc is None: meteo_file = os.path.join(meteo_lbc_dir, date.strftime(meteo_lbc_file)) with _hdf5_lock: ds_lbc = xr.open_dataset(meteo_file) # Create a DataArray with the correct data da = ds_lbc['T'].copy(data=data) da_post = ds_lbc['T'].copy(data=data_post) # Copy topography_c from Extpar in GEOP_SFC with _hdf5_lock: ds_extpar = xr.open_dataset(extpar_file) ds_lbc['GEOP_SFC'][:] = ds_extpar["topography_c"].values * 9.80665 ds_lbc['GEOSP'][:] = ds_extpar["topography_c"].values * 9.80665 # If needed, convert from dry vmr/mmr to moist vmr/mmr # In ICON-ART, lbc data must be in moist air mmr if lbc_dry2moist: info(f"LBC for {spec_ref} at {date} converted from dry to moist air.") qv = ds_lbc['QV'] da = da.copy() * (1 - qv.data) da_post = da_post.copy() * (1 - qv.data) # Change the attributes da.attrs['standard_name'] = spec_ref da.attrs['long_name'] = f'LBC for {spec_ref} generated by cif ' da.attrs['units'] = 'mol mol-1' da_post.attrs['standard_name'] = spec_ref da_post.attrs['long_name'] = f'LBC posterior for {spec_ref} generated by cif ' da_post.attrs['units'] = 'mol mol-1' # Change the time dimension if needed ds_lbc['time'] = np.array([date]) # Change the coords da['time'] = ds_lbc['time'] da['lev'] = ds_lbc['lev'] da['ncells'] = ds_lbc['ncells'] da_post['time'] = ds_lbc['time'] da_post['lev'] = ds_lbc['lev'] da_post['ncells'] = ds_lbc['ncells'] # Fill the prior and posterior background variable with the input data ds_lbc[spec_ref + '_BG'] = da if is_ensemble: ds_lbc[spec_ref + '_BG_POST'] = da_post # Fill the reference and emitted variables with the input data if not is_ensemble: ds_lbc[spec_ref] = da for emi_spec in emi_specs: ds_lbc[emi_spec] = da return ds_lbc
[docs] def mp_dump_lbc_files(date, ds_lbc, lbc_dir): """Write a single LBC dataset to disk (multiprocessing-safe helper). Writes *ds_lbc* as ``{lbc_dir}/ifs_YYYYMMDDHH_lbc.nc``, overwriting any existing file at the same path. Args: date (datetime): LBC date (used to format the filename). ds_lbc (xr.Dataset): LBC dataset to write. lbc_dir (str): destination directory. """ lbc_file = date.strftime( f"{lbc_dir}/ifs_%Y%m%d%H_lbc.nc" ) if os.path.exists(lbc_file): os.remove(lbc_file) with _hdf5_lock: ds_lbc.to_netcdf(lbc_file) info(f"Dumped lbc file for {date}.")