Source code for pycif.plugins.models.iconart.io.inputs.inicond
import numpy as np
import xarray as xr
import os
import shutil
from logging import info
from ..utils import INICOND_FILENAME
from .tracers import change_tracers_xml_inicond
from ......utils.check.errclass import CifKeyError
from ......utils.hdf5 import _hdf5_lock
[docs]
def make_inicond(self, datastore, ddi, ddf, runsubdir, mode):
"""Prepare the ICON-ART initial-condition file for one sub-simulation period.
Fetches the meteorological initial-condition file (via
:func:`fetch_meteo_inicond_file`), inserts the CIF-modified tracer
concentrations from *datastore*, and writes ``meteo_inicond.nc`` into
*runsubdir*. Handles ensemble/perturbed species by mapping sample names
back to their reference species.
Args:
self: ICON-ART model plugin instance.
datastore (dict): tracer-ID-keyed CIF data-store entries.
ddi (datetime): period start date.
ddf (datetime): period end date (unused).
runsubdir (str): path to the period run directory.
mode (str): ``'fwd'``, ``'tl'``, or ``'adj'``.
"""
# Inputs and outputs
trids_in = list(datastore.keys())
trids_out = [("inicond", 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 output inicond data
if not hasattr(self, "dict_inicond_dataout"):
self.dict_inicond_dataout = {}
if ddi not in self.dict_inicond_dataout:
ds_ini_meteo = fetch_meteo_inicond_file(self, runsubdir)
else:
ds_ini_meteo = self.dict_inicond_dataout[ddi]
# 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 = ("inicond", spec_ref)
is_perturbed_comp = False
if trid_in not in datastore:
continue
tracer = datastore[trid_in]
tracer_data = tracer["data"][ddi]
# --------------------------------------------------------------------------
# -- Ensemble processing
# --------------------------------------------------------------------------
if is_ensemble and is_perturbed_comp:
info(f"The {spec_ref} inicond is perturbed.")
info(f"Calculating inicond scaling factors for {spec_ref}...")
# Check if only partial vertical data have been loaded
same_nlevs = len(np.unique(
[len(datastore[t]["data"][ddi]["spec"]["lev"]) for t in trids_in]
)) == 1
if same_nlevs:
# Fetch all the samples
list_da = [datastore[t]["data"][ddi]["spec"] for t in trids_in]
# Concatenate the data
da_ini_ens = xr.concat(list_da, dim='ens')
else:
# Fetch all the samples at the surface
list_da_surf = [datastore[t]["data"][ddi]["spec"].isel(lev=-1)
for t in trids_in]
# Calculate the scaling factors
inicond_lambdas = xr.concat(list_da_surf, dim="ens")
inicond_lambdas = inicond_lambdas / inicond_lambdas[0]
inicond_lambdas = inicond_lambdas.fillna(1)
inicond_lambdas = inicond_lambdas.squeeze()
# Concatenate the data
da_ini_ens = datastore[trids_in[0]]["data"][ddi]["spec"] * inicond_lambdas
da_ini_ens = da_ini_ens.transpose('ens', ...)
# If needed, convert from dry mmr (vmr) to moist mmr (vmr)
# In ICON-ART, inicond data must be in moist air mmr
if self.inicond_dry2moist:
info(f"Converting {spec_ref} ensemble from dry to moist air...")
qv = ds_ini_meteo['QV'].values[..., np.newaxis, :]
da_ini_ens = da_ini_ens.copy() * (1 - qv)
# Convert the dataarray to a dataset with multiple variables
ds_ini_ens = da_ini_ens.to_dataset(dim="ens")
ds_ini_ens = ds_ini_ens.isel(lat=0)
ds_ini_ens = ds_ini_ens.rename_dims({"lon": "ncells"})
# Change the attributes
for it, t in enumerate(trids_in):
ds_ini_ens[it].attrs['standard_name'] = f'{t[1]}'
ds_ini_ens[it].attrs['long_name'] = f'IC for {t[1]} generated by the CIF'
ds_ini_ens[it].attrs['units'] = 'mol per mol of moist air'
# Change the coords
ds_ini_ens['time'] = ds_ini_meteo['time']
ds_ini_ens['lev'] = ds_ini_meteo['lev']
ds_ini_ens['ncells'] = ds_ini_meteo['ncells']
# Change the name of variables
rename_map = {it: t[1] for it, t in enumerate(trids_in)}
ds_ini_ens = ds_ini_ens.rename_vars(rename_map)
# Add prior and posterior background tracer
ds_ini_ens[spec_ref + '_BG'] = ds_ini_ens[trids_in[0][1]].copy()
ds_ini_ens[spec_ref + '_BG_POST'] = ds_ini_ens[trids_in[2][1]].copy()
# Merge the ensemble inicond data with the meteo file
ds_ini_meteo = xr.merge([ds_ini_meteo, ds_ini_ens])
# --------------------------------------------------------------------------
# -- No ensemble processing
# --------------------------------------------------------------------------
else:
# Get the data
if "spec" not in tracer_data:
varname = datastore[trid_in]['varname']
ref_dir = datastore[trid_in]["dirorig"]
ref_file = datastore[trid_in]["fileorig"]
with _hdf5_lock:
ds_init_prior = xr.open_dataset(os.path.join(ref_dir, ref_file))
da_ini_prior = ds_init_prior[varname]
else:
da_ini_prior = tracer_data["spec"][:, :, 0, :]
da_ini_prior = da_ini_prior.rename({"lon": "ncells"})
# If needed, convert from dry vmr/mmr to moist vmr/mmr
# In ICON-ART, inicond data must be in moist air mmr
if self.inicond_dry2moist:
info(f"Converting {spec_ref} from dry to moist air...")
qv = ds_ini_meteo['QV']
da_ini_prior = da_ini_prior.copy() * (1 - qv)
# Change the attributes
da_ini_prior.attrs['standard_name'] = f'{spec_ref}'
da_ini_prior.attrs['long_name'] = f'IC for {spec_ref} generated by the CIF'
da_ini_prior.attrs['units'] = 'mol per mol of moist air'
# Change the coords
da_ini_prior['time'] = ds_ini_meteo['time']
da_ini_prior['lev'] = ds_ini_meteo['lev']
da_ini_prior['ncells'] = ds_ini_meteo['ncells']
# Add the new variable
ds_ini_meteo[spec_ref] = da_ini_prior
# Add background tracer
ds_ini_meteo[spec_ref + '_BG'] = da_ini_prior.copy()
if is_ensemble:
ds_ini_meteo[spec_ref + '_BG_POST'] = da_ini_prior.copy()
# --------------------------------------------------------------------------
# -- Save the data in the dictionary
# --------------------------------------------------------------------------
self.dict_inicond_dataout[ddi] = ds_ini_meteo
info(f"Added {spec_ref} to the inicond dictionary.")
# --------------------------------------------------------------------------
# -- Adapt tracers.xml
# --------------------------------------------------------------------------
info(f"Modifying tracers.xml for {spec_ref} inicond ({is_ensemble=})...")
for t in trids_out:
change_tracers_xml_inicond(self, t[1], is_ensemble=is_ensemble, is_perturbed_comp=is_perturbed_comp)
return
# --------------------------------------------------------------------
# -- OTHERS FUNCTIONS
# --------------------------------------------------------------------
[docs]
def dump_inicond_file(self, ddi, runsubdir):
"""Write the accumulated initial-condition dataset to ``meteo_inicond.nc``.
Retrieves the in-memory inicond dataset from ``self.dict_inicond_dataout[ddi]``,
updates its time dimension to *ddi*, and writes it to the run directory.
Args:
self: ICON-ART model plugin instance.
ddi (datetime): period start date (used as time coordinate).
runsubdir (str): path to the period run directory.
"""
# Get the saved inicond file
ds_ini = self.dict_inicond_dataout[ddi]
# Change the time dimension
ds_ini['time'] = np.array([ddi])
# Dump the updated inicond dataset
info(f"Dumping the updated inicond dataset...")
init_file = os.path.join(runsubdir, INICOND_FILENAME)
if os.path.exists(init_file):
os.remove(init_file)
with _hdf5_lock:
ds_ini.to_netcdf(init_file)
del self.dict_inicond_dataout[ddi]
return
[docs]
def fetch_meteo_inicond_file(self, runsubdir):
"""Symlink or copy the IFS meteorological initial-condition file into the run directory.
Checks that ``self.meteo_inicond_file`` exists, links it as
``meteo_inicond.nc`` in *runsubdir*, and opens it as an xarray Dataset
for in-place species modification.
Args:
self: ICON-ART model plugin instance.
runsubdir (str): path to the period run directory.
Returns:
xr.Dataset: the opened meteorological initial-condition dataset.
Raises:
FileNotFoundError: if ``self.meteo_inicond_file`` does not exist.
"""
# Check the meteo inicond file exists
ifs_init_file = os.path.join(self.meteo_inicond_file)
# Copy the file, as we will need to modify it maybe
init_file = os.path.join(runsubdir, INICOND_FILENAME)
shutil.copyfile(ifs_init_file, init_file)
info(f'Copied meteorological inicond file from {ifs_init_file} to {init_file}.')
# Add the surface pressure required by ART
with _hdf5_lock:
ds = xr.open_dataset(init_file)
if 'PS' not in ds:
if 'LNPS' not in ds:
raise CifKeyError(
f"'LNPS' must be found in the initial conditions file {init_file}"
)
ds['PS'] = np.exp(ds['LNPS'])
ds['PS'] = ds['PS'].squeeze(dim=['lev_2']) # Remove the lev 2 dimension
ds['PS'].attrs['standard_name'] = "Surface pressure" # Just change the attributes
ds['PS'].attrs['long_name'] = f'exp(LNPS), generated by cif '
ds['PS'].attrs['units'] = 'Pa'
# Copy topography_c from Extpar in GEOP_SFC
ds_extpar = xr.open_dataset(self.domain.extpar_file)
# ds = ds.drop_vars(['GEOSP'])
ds['GEOP_SFC'][:] = ds_extpar["topography_c"].values * 9.80665
ds['GEOSP'][:] = ds_extpar["topography_c"].values * 9.80665
# Add a Q variable
if 'Q' not in ds:
ds['Q'] = ds['QV']
return ds