"""Calculates CH4 response."""
import logging
from collections.abc import Callable
import numpy as np
from scipy.integrate import solve_ivp
from scipy.interpolate import interp1d
from .calc_co2 import N2O_0
from .construct_conc import interp_bg_conc
logger = logging.getLogger(__name__)
# CONSTANTS
TAU_GLOB = 8.0 # CH4 lifetime
TAU_PERT = 12.0 # Perturbation lifetime
CH4_0 = 731.41 # pre-industrial CH4 concentration [ppb] used as reference
CH4_VALIDITY_MIN = 340.0 # lower bound of Etminan et al. (2016) validity range [ppb]
CH4_VALIDITY_MAX = 3500.0 # upper bound of Etminan et al. (2016) validity range [ppb]
# Define the shared signature of ODE functions explicitly
OdeFunc = Callable[
[float, np.ndarray, Callable[[float], float], Callable[[float], float]],
np.ndarray,
]
[docs]
def calc_ch4_concentration(config: dict, tau_dict: dict) -> dict:
"""Calculates the methane (CH4) concentration over time.
This method uses methane background and methane lifetimes of idealized
emission boxes. Either the tagging or the perturbation approach is used.
Args:
config (dict): Configuration dictionary from config
tau_dict (dict): Dictionary of a np.ndarray, either inverse lifetimes (1 / tau)
or relative changes in lifetime (delta), key is "CH4"
NOTE: array must be of same size as time_range
Returns:
dict: A dictionary containing the calculated methane concentration
for each time step. The dictionary has a single key "CH4" with
corresponding values as a numpy array.
"""
approach = config["responses"]["CH4"]["tau"]["approach"]
time_config = config["time"]["range"]
time_range = np.arange(time_config[0], time_config[1], time_config[2], dtype=int)
ch4_bg_dict = interp_bg_conc(config, "CH4")
ch4_bg_arr = ch4_bg_dict["CH4"]
# Define function for evaluation at continous times,
# Use boundaries of array as fill_value, i.e. outside time_range
ch4_bg_func = interp1d(
x=time_range,
y=ch4_bg_arr,
bounds_error=False,
fill_value=(ch4_bg_arr[0], ch4_bg_arr[-1]),
)
# tau_arr comprising either inverse lifetimes (1 / tau) for tagging
# or relative changes in lifetime (delta) for perturbation method
tau_arr = tau_dict["CH4"]
# Define function for evaluation at continous times
tau_func = interp1d(
x=time_range,
y=tau_arr,
kind="zero",
bounds_error=False,
fill_value=(tau_arr[0], tau_arr[-1]),
)
# Explicit annotation of ode
ode: OdeFunc
if approach == "tagging":
# Ordinary Differential Equation
ode = ode_ch4_tagging
elif approach == "perturbation":
# Ordinary Differential Equation
ode = ode_ch4_perturbation
else:
raise ValueError("CH4.tau.approach in config file is invalid.")
# Initial condition
y0 = 0.0
solution = solve_ivp(
ode,
[(time_range[0]), time_range[-1]],
[y0],
method="Radau",
t_eval=time_range,
dense_output=False,
# radau has adaptive steps, so set max step to time step from config
max_step=float(time_config[2]),
args=(ch4_bg_func, tau_func),
)
conc_ch4_dict = {"CH4": solution.y[0]}
return conc_ch4_dict
[docs]
def ode_ch4_tagging(
t: float,
y: np.ndarray,
ch4_bg: Callable[[float], float],
tau_inverse: Callable[[float], float],
) -> np.ndarray:
"""Differential equation, contribution (tagging) approach.
This differential equation determines CH4 concentration, after equation 4.49
in Rieger, V.S., A new method to assess the climate effect of mitigation
strategies for road traffic, Delft University of Technology, PhD, 2018,
https://doi.org/10.4233/uuid:cc96a7c7-1ec7-449a-84b0-2f9a342a5be5
Args:
t (float): time
y (np.ndarray): CH4 concentration, tagged, required solution
ch4_bg (Callable[[float], float]): CH4 background concentration
tau_inverse (Callable[[float], float]): inverse CH4 lifetime, tagged
Returns:
np.ndarray: d/dt CH4 (CH4 concentration, tagged)
"""
return (-0.5) * (tau_inverse(t) * ch4_bg(t) + (1.0 / TAU_GLOB) * y)
[docs]
def ode_ch4_perturbation(
t: float,
y: np.ndarray,
ch4_bg: Callable[[float], float],
delta: Callable[[float], float],
) -> np.ndarray:
"""Differential equation for evaluating CH4 concentration changes.
Perturbation approach after equation (3) in Grewe & Stenke (2008),
https://doi.org/10.5194/acp-8-4621-2008
Args:
t (float): time
y (np.ndarray): CH4 concentration changes, required solution
ch4_bg (Callable[[float], float]): CH4 background concentration
delta (Callable[[float], float]): relative change in lifetime
Returns:
np.ndarray: d/dt CH4 (CH4 concentration change, perturbation)
"""
return (delta(t) / (1 + delta(t))) * (1 / TAU_PERT) * ch4_bg(t) - (
1 / (1 + delta(t))
) * (1 / TAU_PERT) * y
[docs]
def calc_ch4_rf(conc_dict: dict, config: dict) -> dict:
"""Calculates the Radiative Forcing values for emitted CH4 concentrations.
Args:
config (dict): Configuration dictionary from config
conc_dict (dict): Dictionary with array of concentrations
between the starting and ending years, keys is species
Raises:
ValueError: if CH4.rf.method not valid
Returns:
dict: Dictionary with :class:`numpy.ndarray` of CH4 Radiative Forcing values
between the starting and ending years, key is species CH4
"""
method = config["responses"]["CH4"]["rf"]["method"]
if method == "Etminan_2016":
conc_n2o_bg_dict = interp_bg_conc(config, "N2O")
rf_dict = calc_ch4_rf_etminan_2016(conc_dict, conc_n2o_bg_dict)
return rf_dict
# unknown or invalid CH4 RF method
raise ValueError("CH4.rf.method in config file is invalid.")
[docs]
def calc_ch4_rf_etminan_2016(conc_dict: dict, conc_n2o_bg_dict: dict) -> dict:
"""Calculates the CH4 Radiative Forcing values for emitted concentrations.
Reference: Etminan, Maryam, et al. Radiative forcing of carbon dioxide,
methane, and nitrous oxide: A significant revision of the methane
radiative forcing. Geophysical Research Letters 43.24 (2016): 12-614.
https://doi.org/10.1002/2016GL071930.
Args:
conc_dict (dict): Dictionary with array of concentrations
between the starting and ending years, keys is species
conc_ch4_bg_dict (dict): Dictionary of :class:`numpy.ndarray` of background CH4
concentrations between the starting and ending years, key is
species
conc_n2o_bg_dict (dict): Dictionary of :class:`numpy.ndarray` of background N2O
concentrations between the starting and ending years, key is
species
Returns:
dict: Dictionary with :class:`numpy.ndarray` of CH4 Radiative Forcing values
between the starting and ending years, key is species CH4
"""
# concentrations
d_ch4_conc = conc_dict["CH4"] # ΔCH4 concentration (compared to background)
ch4_conc = d_ch4_conc + CH4_0 # CH4 concentration on top of background
n2o_conc = conc_n2o_bg_dict["N2O"]
ch4_conc_mean = 0.5 * (ch4_conc + CH4_0)
n2o_conc_mean = 0.5 * (n2o_conc + N2O_0)
# check validity range: 340-3500 ppb for CH4 from Etminan et al. (2016)
if np.any((ch4_conc < CH4_VALIDITY_MIN) | (ch4_conc > CH4_VALIDITY_MAX)):
logger.warning(
"CH4 concentration is outside of the validity range 340 - 3500 ppb"
"given by Etminan et al. (2016)."
)
# coefficients
a3 = -1.3e-6 # W/m²/ppb
b3 = -8.2e-6 # W/m²/ppb
# calculate RF
x = a3 * ch4_conc_mean + b3 * n2o_conc_mean + 0.043
y = np.sqrt(ch4_conc) - np.sqrt(CH4_0)
rf_ch4_arr = x * y
return {"CH4": rf_ch4_arr}
[docs]
def calc_ch4_drf_dconc(conc_dict: dict, config: dict) -> dict:
"""Calculates the derivative of CH4 radiative forcing w.r.t. concentration.
This is used for the differential and marginal RF attribution methods.
The CH4 method is taken from the config file.
Args:
conc_dict (dict): Dictionary with array of concentrations (not including
background) between the starting and ending years, keys is species
config (dict): Configuration dictionary from config
Returns:
dict: Dictionary with :class:`numpy.ndarray` of CH4 radiative forcing derivative
values between the starting and ending years, key is species CH4
"""
method = config["responses"]["CH4"]["rf"]["method"]
if method == "Etminan_2016":
conc_n2o_bg_dict = interp_bg_conc(config, "N2O")
drf_dconc_dict = calc_ch4_drf_dconc_etminan_2016(conc_dict, conc_n2o_bg_dict)
return drf_dconc_dict
raise ValueError(
"CH4.rf.method does not have a valid concentration derivative method"
"and thus cannot be used with the selected attribution method."
)
[docs]
def calc_ch4_drf_dconc_etminan_2016(conc_dict: dict, conc_n2o_bg_dict: dict) -> dict:
"""Calculates the derivative of CH4 radiative forcing w.r.t. concentration.
Reference: Etminan, M., Myhre, G., Highwood, E. J., & Shine, K. P. (2016).
Radiative forcing of carbon dioxide, methane, and nitrous oxide: A
significant revision of the methane radiative forcing. Geophysical
Research Letters, 43(24), 12-614.
https://doi.org/10.1002/2016GL071930.
Args:
conc_dict (dict): Dictionary with array of concentrations (not including
background) between the starting and ending years, keys is species
conc_n2o_bg_dict (dict): Dictionary of :class:`numpy.ndarray` of background N2O
concentrations between the starting and ending years, key is species
Returns:
dict: Dictionary with :class:`numpy.ndarray` of dRF(CH4)/dconc values
between the starting and ending years, key is species CH4
"""
# concentrations
d_ch4_conc = conc_dict["CH4"] # ΔCH4 concentration (compared to background)
ch4_conc = d_ch4_conc + CH4_0 # CH4 concentration on top of background
n2o_conc = conc_n2o_bg_dict["N2O"]
ch4_conc_mean = 0.5 * (ch4_conc + CH4_0)
n2o_conc_mean = 0.5 * (n2o_conc + N2O_0)
# coefficients
a3 = -1.3e-6 # W/m²/ppb
b3 = -8.2e-6 # W/m²/ppb
# calculate derivative of RF w.r.t. concentration using product rule
x = a3 * ch4_conc_mean + b3 * n2o_conc_mean + 0.043
x_prime = a3 / 2.0
y = np.sqrt(ch4_conc) - np.sqrt(CH4_0)
y_prime = 1.0 / (2.0 * np.sqrt(ch4_conc))
drf_dconc_dict = x * y_prime + x_prime * y
return {"CH4": drf_dconc_dict}
[docs]
def calc_pmo_rf(out_dict: dict) -> dict:
"""Calculates PMO RF.
Args:
out_dict (dict): Dictionary with computed responses, keys are e.g.
'RF_CH4'
Returns:
dict: Dictionary of :class:`numpy.ndarray` of computed RF, key is PMO
Raises:
KeyError: If computed CH4 RF is not available.
"""
if "RF_CH4" in out_dict:
rf_ch4_arr = out_dict["RF_CH4"]
rf_pmo_arr = 0.29 * rf_ch4_arr
else:
msg = "PMO RF requires computed CH4 RF which is not available!"
raise KeyError(msg)
return {"PMO": rf_pmo_arr}