import numpy as np
import xarray as xr
import shutil
import pandas as pd
import os
import calendar
from ......utils.hdf5 import _hdf5_lock
BOLTZ = 1.38044e-16
DRY_MASS = 28.966
P_REF = 101325.
[docs]
def findspec(charspec, iallqmax, all_species):
"""Find the index of a species by name in the LMDZ-ACC species list.
Args:
charspec (str): species name to search for.
iallqmax (int): total number of species.
all_species (list[dict]): list of species dicts with a ``'name'`` key.
Returns:
int: 0-based index of *charspec* in *all_species*, or ``-1`` if not found.
"""
nospec = -1
for iq in range(iallqmax):
if charspec == all_species[iq]['name']:
nospec = iq
break
return nospec
[docs]
def comp_rates(nreac, temp, pmid, idtyperate, tabrate):
"""Compute gas-phase reaction rates for the LMDZ-ACC chemistry scheme.
Evaluates reaction rate expressions of multiple types (constant,
Arrhenius, pressure-dependent, …) using lookup table *tabrate* and
the atmospheric state (*temp*, *pmid*).
Args:
nreac (int): number of reactions.
temp (np.ndarray): temperature field (K), shape ``(...)``.
pmid (np.ndarray): mid-level pressure field (Pa), same shape.
idtyperate (np.ndarray): integer reaction-type codes, shape
``(nreac,)``.
tabrate (np.ndarray): rate constant table, shape
``(nparams, nreac)``.
Returns:
np.ndarray: reaction rates, shape ``temp.shape + (nreac,)``.
"""
rate = np.zeros(temp.shape + (nreac,))
# -- Computation of rates
for nr in range(nreac):
ity = idtyperate[nr]
# -- Constant rates
if ity == 1:
rate[..., nr] = tabrate[0, nr]
# -- Arrhenius simplified rates
elif ity == 2:
rate[..., nr] = tabrate[0, nr] * np.exp(-tabrate[1, nr] / temp)
# -- Arrhenius complete rates
elif ity == 3:
rate[..., nr] = tabrate[0, nr] * np.exp(-tabrate[1, nr] / temp) * (300. / temp) ** tabrate[2, nr]
# -- Pressure rates
elif ity == 4:
rate[..., nr] = tabrate[0, nr] * (tabrate[1, nr] * tabrate[2, nr] * pmid / P_REF)
# -- Photolysis rates
# elif ity == 5:
# for ij in range(ijratesmax):
# if jrates[ij]['idreac'] == nr:
# nrj = ij
# rate[..., nr] = refjrates[..., nrj]
return rate
[docs]
def make_chem_modout(self, runsubdir, ddi, inicond, inicond_ad):
"""Compute adjoint sensitivity of initial conditions through the chemistry scheme.
Reads the chemical scheme (reactions, stoichiometry, rate parameters)
from the LMDZ-ACC run directory, integrates chemical production/loss
over the period using :func:`comp_rates`, and propagates the adjoint
sensitivity *inicond_ad* backward through the chemistry operator to
produce sensitivity w.r.t. initial mass mixing ratios.
Args:
self: LMDZ-ACC model plugin instance (carries chemistry and domain).
runsubdir (str): path to the period run directory.
ddi (datetime): period start date.
inicond (np.ndarray): forward initial conditions (mass mixing ratio).
inicond_ad (np.ndarray): adjoint sensitivity at period end.
Returns:
np.ndarray: adjoint sensitivity w.r.t. initial conditions, same
shape as *inicond_ad*.
"""
# Domain infos
domain = self.domain
chemistry = self.chemistry
nlev = domain.nlev
nlat = domain.nlat
nlon = domain.nlon
nspec = len(chemistry.acspecies.attributes)
if not self.do_chemistry:
return np.zeros((nlon, nlat, nlev, nspec))
iprescrmax = 0
iprodmax = 0
idepmax = 0
ijratesmax = 0
ntabmax = 22
# -- Change ltabrate to properly retrieve the right number of coefficients used for the reaction
ltabrate = [1, 2, 3, 3, 1, 7, 4, 8, 8, 4, 2, 8, 1, 7, 1, 6, 3, 4, 2, 5, 4, 5, 5, 5, 1]
# -- Read the chemistry scheme
workdir = chemistry.workdir
dirchem_ref = f"{workdir}/chemical_scheme/{chemistry.schemeid}/"
# -- Reading active/output species names
file_chem = f"{dirchem_ref}/ACTIVE_SPECIES.{chemistry.schemeid}"
df_chem = pd.read_csv(file_chem, header=None, sep=" ")
species = [{'id': id, 'name': spec}
for id, spec in enumerate(df_chem.iloc[:, 0])]
adv_mass = df_chem.iloc[:, 2].values
# -- Reading all species names
file_chem = f"{dirchem_ref}/ALL_SPECIES.{chemistry.schemeid}"
df_chem = pd.read_csv(file_chem, header=None, sep=" ")
all_species = [{'id': id, 'name': spec, 'type': type, 'dep': False, 'iddep': 0, 'prod': False, 'idprod': 0}
for id, (spec, type) in enumerate(zip(df_chem.iloc[:, 0], df_chem.iloc[:, 1]))]
iallqmax = len(all_species)
# -- Reading prescribed species names
file_chem = f"{dirchem_ref}/PRESCRIBED_SPECIES.{chemistry.schemeid}"
if os.path.getsize(file_chem) > 0:
df_chem = pd.read_csv(file_chem, header=None, sep=" ")
prescr_species = [{'id': id, 'name': spec}
for id, spec in enumerate(df_chem.iloc[:, 0])]
iprescrmax = len(prescr_species)
# -- Reading prodloss species names
file_chem = f"{dirchem_ref}/PRODLOSS_SPECIES.{chemistry.schemeid}"
if os.path.getsize(file_chem) > 0:
df_chem = pd.read_csv(file_chem, header=None, sep=" ")
prodloss_species = [{'id': id, 'name': spec}
for id, spec in enumerate(df_chem.iloc[:, 0])]
iprodmax = len(prodloss_species)
# -- Reading deposition species names
file_chem = f"{dirchem_ref}/DEPO_SPEC.{chemistry.schemeid}"
if os.path.getsize(file_chem) > 0:
df_chem = pd.read_csv(file_chem, header=None, sep=" ")
dep_species = [{'id': id, 'name': spec}
for id, spec in enumerate(df_chem.iloc[:, 0])]
idepmax = len(dep_species)
# -- Make dep and prod associations
for iall in range(iallqmax):
for ipl in range(iprodmax):
if all_species[iall]['name'] == prodloss_species[ipl]['name']:
all_species[iall]['prod'] = True
all_species[iall]['idprod'] = ipl
for idp in range(idepmax):
if all_species[iall]['name'] == dep_species[idp]['name']:
all_species[iall]['dep'] = True
all_species[iall]['iddep'] = idp
# -- Reading reaction addressing arrays of chemistry
file_chem = f"{dirchem_ref}/CHEMISTRY.{chemistry.schemeid}"
df_chem = pd.read_csv(file_chem, header=None, sep=" ")
nreac = df_chem.shape[0]
nreactants = np.zeros(nreac, dtype=np.int8)
kreacl = np.zeros(iallqmax, dtype=np.int8)
idreacl = np.zeros((iallqmax, nreac), dtype=np.int8)
idspecl = np.zeros((nreac, 10), dtype=np.int8)
kreacp = np.zeros(iallqmax, dtype=np.int8)
idreacp = np.zeros((iallqmax, nreac), dtype=np.int8)
stoi = np.ones((iallqmax, nreac), dtype=np.int8)
tabrate = np.zeros((ntabmax, nreac))
idtyperate = np.zeros(nreac, dtype=np.int8)
for ireac in range(nreac):
nreactants[ireac] = df_chem.iloc[ireac, 0]
reactants = df_chem.iloc[ireac, 1:nreactants[ireac] + 1].values
nprods = int(df_chem.iloc[ireac, nreactants[ireac] + 1])
prods = df_chem.iloc[ireac, nreactants[ireac] + 2:nprods + nreactants[ireac] + 2].values
# -- Addressing the reactant list
for ire in range(nreactants[ireac]):
idloss = findspec(reactants[ire], iallqmax, all_species)
if idloss >= 0:
kreacl[idloss] = kreacl[idloss] + 1
idreacl[idloss, kreacl[idloss] - 1] = ireac
idspecl[ireac, ire] = idloss
# -- Addressing the product list
for ip in range(nprods):
idprod = findspec(prods[ip], iallqmax, all_species)
if idprod >= 0:
kreacp[idprod] = kreacp[idprod] + 1
idreacp[idprod, kreacp[idprod] - 1] = ireac
# -- Reading and addressing stoichiometric coefficients
file_chem = f"{dirchem_ref}/STOICHIOMETRY.{chemistry.schemeid}"
if os.path.getsize(file_chem) > 0:
df_chem = pd.read_csv(file_chem, header=None, sep=" ")
nlines = df_chem.shape[0]
for i in range(nlines):
reactants[0] = df_chem.iloc[i, 0]
coeff = df_chem.iloc[i, 1]
ireac = df_chem.iloc[i, 2]
idprod = findspec(reactants[0], iallqmax, all_species)
if idprod > 0:
stoi[idprod, ireac] = coeff
# -- Reading the reaction rates constants
file_chem = f"{dirchem_ref}/REACTION_RATES.{chemistry.schemeid}"
df_chem = pd.read_csv(file_chem, header=None, sep=" ")
for ireac in range(nreac):
tr = df_chem.iloc[ireac, 0]
tabrate[:ltabrate[tr], ireac] = df_chem.iloc[ireac, 1:ltabrate[tr] + 1].values
idtyperate[ireac] = tr
# -- Reading the photolysis reaction rates constants
file_chem = f"{dirchem_ref}/PHOTO_RATES.{chemistry.schemeid}"
if os.path.getsize(file_chem) > 0:
df_chem = pd.read_csv(file_chem, header=None, sep=" ")
jrates = [{'idj': id, 'idreac': ireac}
for id, ireac in enumerate(df_chem.iloc[:, 0])]
ijratesmax = len(jrates)
# -- Fetch kinetic variables
kinetic_file = f"{runsubdir}/kinetic.nc"
with _hdf5_lock:
with xr.open_dataset(kinetic_file) as ds:
temp_chem = ds.temp[0, ...].T.astype(np.float64)
pmid_chem = ds.pmid[0, ...].T.astype(np.float64)
# make lon cyclic
temp_chem = np.concatenate([temp_chem, temp_chem[-1:, ...]])
pmid_chem = np.concatenate([pmid_chem, pmid_chem[-1:, ...]])
convert = 10. * pmid_chem[..., np.newaxis] / \
(BOLTZ * temp_chem[..., np.newaxis])
# -- Fetch prescribed species
refprescr = []
for iq in range(iprescrmax):
prescr = prescr_species[iq]['name']
prescr_file = f"{runsubdir}/prescr_{prescr}.nc"
with _hdf5_lock:
ds = xr.open_dataset(prescr_file)
refprescr.append(ds[prescr].mean(axis=0).values)
refprescr = np.array(refprescr).T
# make lon cyclic
refprescr = np.concatenate([refprescr, refprescr[-1:, ...]])
# -- Converting variables from MMR/VMR to molecules/cm3
adv_mass = adv_mass[np.newaxis, np.newaxis, np.newaxis, :]
vmr = inicond * DRY_MASS / adv_mass
molec = vmr * convert
refprescr *= convert
# -- Setting new adjoint variables
delt = 3600 * 24 * calendar.monthrange(ddi.year, ddi.month)[1]
d_chem_ad = inicond_ad * delt * adv_mass / (convert * DRY_MASS)
molec_ad = np.zeros_like(d_chem_ad)
refprescr_ad = np.zeros(d_chem_ad.shape[:-1] + (iprescrmax,))
# -- Loss reactions
loss_ad = -d_chem_ad
rates = comp_rates(nreac, temp_chem, pmid_chem, idtyperate, tabrate)
for ns in range(nspec)[::-1]:
for kr in range(kreacl[ns])[::-1]:
ir = idreacl[ns, kr]
for ire in range(nreactants[ir])[::-1]:
nrat = rates[..., ir]
for irea in range(nreactants[ir])[::-1]:
if irea != ire:
if all_species[idspecl[ir, irea]]['type'] == 'ac':
nrat = nrat * molec[..., idspecl[ir, irea]]
elif all_species[idspecl[ir, irea]]['type'] == 'pr':
nrat = nrat * refprescr[..., idspecl[ir, irea] - nspec]
for irea in range(nreactants[ir])[::-1]:
if irea == ire:
if all_species[idspecl[ir, irea]]['type'] == 'ac':
molec_ad[..., idspecl[ir, irea]] += nrat * loss_ad[..., ns]
elif all_species[idspecl[ir, irea]]['type'] == 'pr':
refprescr_ad[..., idspecl[ir, irea] - nspec] += nrat * loss_ad[..., ns]
vmr_ad = molec_ad * convert
mmr_ad = vmr_ad * DRY_MASS / adv_mass
return mmr_ad
[docs]
def make_mod_out(self, runsubdir, ddi, ref_fwd_dir):
"""Create a zeroed LMDZ-ACC adjoint-output restart file for the current period.
Reads the reference forward restart file from *ref_fwd_dir*, copies it
to the adjoint run directory, and zeros out all active-species mass
mixing ratios so the LMDZ-ACC adjoint executable can accumulate
sensitivities into it.
Args:
self: LMDZ-ACC model plugin instance (carries domain and chemistry).
runsubdir (str): path to the period run directory.
ddi (datetime): period start date.
ref_fwd_dir (str): directory of the reference forward run.
"""
# Domain info
domain = self.domain
nlon = domain.nlon
nlat = domain.nlat
nlev = domain.nlev
if not hasattr(domain, "areas"):
domain.calc_areas()
areas = domain.areas
# Fetch original end from reference forward
end_file = f"{runsubdir}/restart.nc"
ref_end = ddi.strftime(f"{ref_fwd_dir}/chain/restart_%Y%m%d%H%M.nc")
shutil.copy(ref_end, end_file)
# Fetch initial conditions from reference forward
inicond_file = ddi.strftime(f"{ref_fwd_dir}/%Y-%m-%d_%H-%M/start.nc")
if os.path.isfile(inicond_file):
with _hdf5_lock:
ds = xr.open_dataset(inicond_file)
inicond = []
for spec in self.chemistry.acspecies.attributes:
restartID = getattr(self.chemistry.acspecies, spec).restart_id
var = f"q{restartID:02d}" if type(restartID) != str \
else restartID
inicond.append(ds[var].values[0])
inicond = np.array(inicond).T
else:
inicond_file = ddi.strftime(f"{ref_fwd_dir}/%Y-%m-%d_%H-%M/start.bin")
inicond = np.fromfile(
inicond_file, offset=4).reshape((nlon, nlat, nlev, nspec),
order="F")
# Read mass of air in the atmosphere and increase precision
mass_file = f"{runsubdir}/fluxstoke.nc"
with _hdf5_lock:
mass = xr.open_dataset(mass_file)["masse"][0].values[np.newaxis]
mass = mass.astype(np.float64)
# Put adjoint sensitivities to zero or propagate previous values
with _hdf5_lock:
ds = xr.open_dataset(end_file, mode="a")
ref_sensit = {}
ini_chem_sensit = {}
for spec in self.chemistry.acspecies.attributes:
restartID = getattr(self.chemistry.acspecies, spec).restart_id
var = f"q{restartID:02d}" if type(restartID) != str \
else restartID
# Read adjoint sensitivity at beginning of run
ref_sensit[spec] = 0
ini_chem_sensit[spec] = 0
if ddi != self.subsimu_dates[-2]:
sensit_start_file = f"{runsubdir}/start.nc"
with _hdf5_lock:
ds_ini = xr.open_dataset(sensit_start_file)
inicond_ad = ds_ini[var][:].values.T
ref_sensit[spec] = inicond_ad.sum() / mass.sum()
ini_chem_sensit[spec] = \
make_chem_modout(self, runsubdir, ddi, inicond, inicond_ad).T
with _hdf5_lock:
ds[var][:] = mass * ref_sensit[spec] + ini_chem_sensit[spec]
# Put initial condition sensitivities to mod_init
aini_file = f"{runsubdir}/mod_init_{spec}_out.bin"
ds[var][:].values[0].T.flatten(order="F").tofile(aini_file)
with _hdf5_lock:
ds.to_netcdf(end_file, mode="a")
# Fluxes
for spec in self.chemistry.emis_species.attributes:
aemis_file = f"{runsubdir}/mod_fluxes_{spec}_out.bin"
ndates = len(self.flx_input_dates[ddi])
emis_sensit = \
ref_sensit[spec] \
* pd.to_timedelta(self.flx_tresol).total_seconds() \
* areas.T[..., np.newaxis] * np.ones((nlon, nlat, ndates - 1))
emis_sensit.flatten(order="F").tofile(aemis_file)
# Prescribed concentrations
ndates = len(self.input_dates[ddi])
if hasattr(self.chemistry, "prescrconcs"):
for spec in self.chemistry.prescrconcs.attributes:
aprescr_file = f"{runsubdir}/mod_scale_{spec}_out.bin"
np.zeros((nlon, nlat, nlev, ndates - 1)).tofile(aprescr_file)
# Prodloss scaling
if hasattr(self.chemistry, "prodloss3d"):
for spec in self.chemistry.prescrconcs.attributes:
aprescr_file = f"{runsubdir}/mod_prodscale_{spec}_out.bin"
np.zeros((nlon, nlat, nlev, ndates - 1)).tofile(aprescr_file)