Skip to content

Update add_AFOLU_CO2_accounting() per message_data:ssp_dev #354

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 67 additions & 3 deletions message_ix_models/tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ def scenario(request: "FixtureRequest", test_context: "Context") -> "Scenario":
return bare_res(request, test_context, solved=False)


def test_add_AFOLU_CO2_accounting(scenario: "Scenario") -> None:
@pytest.mark.xfail(reason="Invalidated by changes in #354")
def test_add_AFOLU_CO2_accounting_0(scenario: "Scenario") -> None:
"""Previous method."""
info = ScenarioInfo(scenario)

commodity = ["LU_CO2"]
Expand Down Expand Up @@ -63,11 +65,73 @@ def test_add_AFOLU_CO2_accounting(scenario: "Scenario") -> None:
add_AFOLU_CO2_accounting.add_AFOLU_CO2_accounting(
scenario,
relation_name="CO2_Emission_Global_Total",
reg="R12_GLB",
glb_reg="R12_GLB", # NB Previously 'reg'
constraint_value=1.0,
)

# TODO Add assertions about modified structure & data

def test_add_AFOLU_CO2_accounting_1(scenario: "Scenario") -> None:
"""Current method."""
info = ScenarioInfo(scenario)

# commodity in expected land_output data = `emission` parameter to the function
commodity = "LU_CO2_orig"
land_scenario = ["BIO00GHG000", "BIO06GHG3000"]

land_output = make_df(
"land_output",
commodity=commodity,
level="primary",
value=123.4,
unit="-",
time="year",
).pipe(broadcast, year=info.Y, node=info.N, land_scenario=land_scenario)

with scenario.transact("Prepare for test of add_AFOLU_CO2_accounting()"):
scenario.add_set("commodity", commodity)
scenario.add_set("land_scenario", land_scenario)
scenario.add_par("land_output", land_output)

# Other parameter values
relation_name = "CO2_Emission_Global_Total"
level = "LU"
suffix = "_foo"

# Function runs without error
add_AFOLU_CO2_accounting.add_AFOLU_CO2_accounting(
scenario,
relation_name=relation_name,
emission=commodity,
level=level,
suffix=suffix,
)

exp = [f"{x}{suffix}" for x in land_scenario]

# relation_name is present
assert relation_name in set(scenario.set("relation"))

# Commodity and technology sets have expected added elements
assert set(exp) <= set(scenario.set("commodity"))
assert set(exp) <= set(scenario.set("technology"))

# balance_quality entries are present
pdt.assert_frame_equal(
pd.DataFrame([[c, level] for c in exp], columns=["commodity", "level"]),
scenario.set("balance_equality").sort_values("commodity"),
)

data_post = scenario.par("land_output", filters={"commodity": list(exp)})

# 1 row of data was added for every row of original land_input
assert len(land_output) == len(data_post)

# 'level' and 'value' as expected
assert {level} == set(data_post.level.unique())
assert (1.0 == data_post.value).all()

# 'commodity' corresponds to 'land_scenario'
assert (data_post.land_scenario + suffix == data_post.commodity).all()


def test_add_CO2_emission_constraint(scenario: "Scenario") -> None:
Expand Down
214 changes: 107 additions & 107 deletions message_ix_models/tools/add_AFOLU_CO2_accounting.py
Original file line number Diff line number Diff line change
@@ -1,133 +1,133 @@
"""Add regional CO2 entries from AFOLU to a generic relation in a specified region.

.. caution:: |gh-350|
"""
"""Add ``land_output`` and set entries for accounting AFOLU emissions of CO2."""

import logging
from typing import TYPE_CHECKING, Optional

import pandas as pd

from message_ix_models import ScenarioInfo
from message_ix_models.util import nodes_ex_world

from .add_CO2_emission_constraint import main as add_CO2_emission_constraint

if TYPE_CHECKING:
from message_ix import Scenario

log = logging.getLogger(__name__)


def add_AFOLU_CO2_accounting(
scen: "Scenario",
relation_name: str,
reg: str = "R11_GLB",
emission: str = "LU_CO2_orig",
level: str = "LU",
suffix: Optional[str] = None,
*,
# Unused/deprecated parameters
constraint_value: Optional[float] = None,
glb_reg: Optional[str] = None,
reg: Optional[str] = None,
) -> None:
"""Add regional CO2 entries from AFOLU to a generic relation in a specified region.
"""Add ``land_output`` and set entries for accounting AFOLU emissions of CO2.

The function has the following effects on `scen`:

1. A ``relation`` set member `relation_name` is added. However, **no data** for this
relation is added or changed.
2. A ``level`` set member `level` is added.
3. For every member of set ``land_scenario``:

a. Members with the same ID are added to the sets ``commodity`` and
``technology``. If `suffix` is given, it is appended to these added members.
b. A ``balance_equality`` set member is added for the commodity and `level`.

4. Data in ``land_output`` are:

- Retrieved where :py:`commodity=emission`. With the default value of `emission`
"LU_CO2_orig", the configuration.
- Modified to set `level`, value 1.0, unit "%", and replace the commodity label
with ``{land_scenario}{suffix}``, using the value of land_scenario from the
respective row.
- Added to `scen`.

Specifically for the land_use sceanrios: For each land_use scenario a new commodity
is created on a new `level` "LU". Each land_use scenario has an output of "1" onto
their commodity. For each of these commodities (which are set to = 0), there is a
corresponding new technology which has an input of "1" and an entry into the
relation, which corresponds to the the CO2 emissions of the land_use pathway. This
complicated setup is required, because Land-use scenarios only have a single entry
in the emission factor TCE, which is the sum of all land-use related emissions.
This structure and data interact with other data that are **not** configured by this
function, whereby:

- The technologies added in 3(a) receive ``input`` from the respective commodities.
This, combined with the ``balance_equality`` setting, ensure that the ``ACT`` of
these technologies is exactly equal to the corresponding ``LAND``.
- The technologies in turn have entries into a relation that is used for other
purposes.

This complicated setup is required, because land-use scenarios only have a single
entry in the emission factor TCE, which is the sum of all land-use related
emissions.

.. versionchanged:: :pull:`354`

- The function no longer sets values of ``relation_activity`` or ``input``.
- Parameters `constraint_value` and `glb_reg` are ignored.

Parameters
----------
scen :
Scenario to which changes should be applied.
relation_name :
Name of the generic relation for which the limit should be set.
glb_reg :
Node in `scen` to which constraint should be applied.
Name of a generic relation.
emission :
Commodity name for filtering ``land_output`` data.
level :
Level for added ``land_output`` data.
suffix :
Optional suffix for added commodity and level names.

Other parameters
----------------
constraint_value :
Value for which the lower constraint should be set.
Deprecated, unused.
glb_reg :
Deprecated, unused.
reg :
Deprecated, unused.

Raises
------
ValueError
if there is no ``land_output`` data for :py:`commodity=emission`.
"""
glb_reg = reg
if constraint_value is not None:
log.warning(
f"Argument add_AFOLU_CO2_accounting(…, constraint_value={constraint_value})"
" is ignored"
)
if glb_reg is not None:
log.warning(
f"Argument add_AFOLU_CO2_accounting(…, glb_reg={constraint_value}) is"
" ignored"
)

if relation_name not in scen.set("relation").tolist():
with scen.transact(
f"relation {relation_name!r} for limiting regional CO2 emissions at the "
"global level added"
):
with scen.transact("Add land_output entries for "):
# Add relation to set
if relation_name not in scen.set("relation").tolist():
scen.add_set("relation", relation_name)

if constraint_value:
add_CO2_emission_constraint(
scen, relation_name, constraint_value, type_rel="lower", reg=reg
# Add land-use level to set
if level not in scen.set("level"):
scen.add_set("level", level)

# Add commodities and technologies to sets
ls = [f"{s}{suffix or ''}" for s in scen.set("land_scenario").tolist()]
scen.add_set("commodity", ls)
scen.add_set("technology", ls)

# Enforce commodity balance equal (implicitly, to zero)
for commodity in ls:
scen.add_set("balance_equality", [commodity, level])

# - Retrieve land-use TCE emissions
# - Add land-use scenario `output` parameter onto new level/commodity
name = "land_output"
data = scen.par(name, filters={"commodity": [emission]}).assign(
commodity=lambda df: df.land_scenario + (suffix or ""),
level=level,
value=1.0,
unit="%",
)

# Add entires into sets required for the generation of the constraint
# new level - "LU"
# new commodities (set to `equal`) - name = LU scenario
# new technologies - name = LU scenario
scen.check_out()
scen.add_set("level", "LU")
ls = scen.set("land_scenario").tolist()
scen.add_set("commodity", ls)
for commodity in ls:
scen.add_set("balance_equality", [commodity, "LU"])
scen.add_set("technology", ls)

# Retrieve LU_CO2 emissions
loutput = scen.par("land_output", filters={"commodity": ["LU_CO2"]})
if loutput.empty:
raise ValueError("'land_output' not available for commodity 'LU_CO2'")

# Add land-use scenario `output` parameter onto new level/commodity
df_land_output = loutput.copy()
df_land_output.commodity = df_land_output.land_scenario
df_land_output.level = "LU"
df_land_output.value = 1
df_land_output.unit = "%"
scen.add_par("land_output", df_land_output)

# Add technology `input` and `relation_activity` parameter
info = ScenarioInfo(scen)
for reg in nodes_ex_world(info.N):
if reg == glb_reg:
continue
for y in info.Y:
for s in ls:
if s.find("BIO0N") >= 0:
continue

df = pd.DataFrame(
{
"node_loc": [reg],
"technology": s,
"year_vtg": y,
"year_act": y,
"mode": "M1",
"node_origin": reg,
"commodity": s,
"level": "LU",
"time": "year",
"time_origin": "year",
"value": 1,
"unit": "???",
}
)

scen.add_par("input", df)

df = pd.DataFrame(
{
"relation": [relation_name],
"node_rel": glb_reg,
"year_rel": y,
"node_loc": reg,
"technology": s,
"year_act": y,
"mode": "M1",
"value": loutput[
(loutput.node == reg)
& (loutput.year == y)
& (loutput.land_scenario == s)
].value,
"unit": "???",
}
)

scen.add_par("relation_activity", df)
scen.commit("Added technology to mimic land-use technologies")
if data.empty:
raise ValueError(f"No {name!r} data for commodity {emission}")

scen.add_par(name, data)
Loading