Source code for message_ix_models.model.transport.emission

"""Transport emissions data."""

import logging
from typing import TYPE_CHECKING, Dict

import pandas as pd
from genno import Quantity
from genno.operator import convert_units, load_file, mul
from iam_units import registry
from message_ix import make_df

from message_ix_models import Context
from message_ix_models.util import package_data_path

from .util import path_fallback

if TYPE_CHECKING:
    from genno.types import AnyQuantity

log = logging.getLogger(__name__)


[docs]def get_emissions_data(context: Context) -> Dict[str, pd.DataFrame]: """Load emissions data from a file.""" fn = f"{context.transport.data_source.emissions}-emission_factor.csv" qty = load_file(path_fallback(context, "emi", fn)) return dict(emission_factor=qty.to_dataframe())
[docs]def get_intensity(context: Context) -> "AnyQuantity": """Load emissions intensity data from a file.""" # FIXME use through the build computer return load_file(package_data_path("transport", "fuel-emi-intensity.csv"))
[docs]def strip_emissions_data(scenario, context): """Remove base model's parametrization of freight transport emissions. They are re-added by :func:`get_freight_data`. """ log.warning("Not implemented")
# TODO read from configuration # https://www.eia.gov/environment/emissions/co2_vol_mass.php # https://www.epa.gov/sites/default/files/2015-07/documents/emission-factors_2014.pdf EI_TEMP = { # This was used temporarily for developing reporting. For a correct value, the # emissions intensity of electricity in each region should be reported and # multiplied to by the amount of electricity used by transport technologies. # ("CO2", "electr"): "10 kg / MBTU", ("CO2", "ethanol"): "47.84 kg / MBTU", # 5.75 kg / gallon ("CO2", "gas"): "52.91 kg / MBTU", ("CO2", "hydrogen"): "10 kg / MBTU", # FIXME ditto electr, above ("CO2", "lightoil"): "70.66 kg / MBTU", ("CO2", "methanol"): "34.11 kg / MBTU", # 4.10 kg / gallon }
[docs]def ef_for_input( context: Context, input_data: pd.DataFrame, species: str = "CO2", units_out: str = "kt / (Gv km)", ) -> Dict[str, pd.DataFrame]: """Calculate emissions factors given data for the ``input`` parameter. Parameters ---------- input_data : Data for the ``input`` parameter. species : str Species of emissions. units_out : str Preferred output units. Should be units of emissions mass (for respective species) divided by units of activity (for respective technology). Returns ------- pandas.DataFrame Data for the ``emission_factor`` parameter. """ def _ef_and_unit(s: pd.Series) -> pd.Series: """Look up emission factor multiplier and units given `s`. Returns `s` extended with columns "_ef" and "_unit_out". """ c, u = s["commodity"], s["unit"] # Product of the input efficiency [energy / activity units] and emissions # intensity for the input commodity [mass / energy] → [mass / activity units] uq = ( registry.Quantity(1.0, u) * registry(EI_TEMP.get((species, c), "0 g / J")) ).to(units_out) return pd.Series(dict(**s, _ef=uq.magnitude, _unit_out=f"{uq.units:~}")) # Generate emissions_factor data # - Create a message_ix-ready data frame; fill `species` as the "emissions" label. # - Add the input commodity. # - Merge columns (_ef, _unit_out) computed by _ef_and_unit(). This function runs on # only the unique combinations of (commodity, unit) in `input_data`, or less than # 10 rows. # - Compute the product of the `input` value and `ef` column. # - Restore the expected dimensions. df = ( make_df("emission_factor", **input_data, emission=species) .assign(commodity=input_data["commodity"]) .merge( input_data[["commodity", "unit"]] .drop_duplicates() .apply(_ef_and_unit, axis=1), on=["commodity", "unit"], ) .eval("value = value * _ef") .drop(["_ef", "commodity", "unit"], axis=1) .rename(columns={"_unit_out": "unit"}) ) result = dict(emission_factor=df) # Emissions intensity values excerpted from existing scenarios ei = get_intensity(context).sel(emission=species, drop=True) # Name of the relation relation = "CO2_trp" if species == "CO2" else f"{species}_Emission" if not context.transport.emission_relations: pass elif not len(ei): log.info(f"No emissions intensity values for {relation!r}; skip") else: # Convert `input` to quantity # TODO provide a general function somewhere that does this units = input_data["unit"].unique() assert 1 == len(units) dims = list(filter(lambda c: c not in ("value", "unit"), input_data.columns)) input_qty = Quantity(input_data.set_index(dims)["value"], units=units[0]) # Convert units # FIXME these units are hard-coded, particular to CO2 in MESSAGEix-GLOBIOM ra = convert_units(mul(input_qty, ei), "Mt / (Gv km)") # - Convert to pd.DataFrame. # - Ensure year_act is integer. # - Populate node_rel and year_rel from node_loc and year_act, respectively. # NB eval() approach does not work for strings in node_rel, for some reason. # - Drop duplicates. tmp = ( ra.to_series() .reset_index() .astype({"year_act": int}) .assign(node_rel=lambda df: df["node_loc"]) .eval("year_rel = year_act") .drop_duplicates( subset="node_rel year_rel node_loc technology year_act mode".split() ) ) name = "relation_activity" result[name] = make_df(name, **tmp, relation=relation, unit=f"{ra.units:~}") return result