Skip to content

Commit 8b81a61

Browse files
committed
Standardize how to treat material double-counting
1 parent 1569031 commit 8b81a61

File tree

3 files changed

+142
-43
lines changed

3 files changed

+142
-43
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
"""BMT runs."""
2+
3+
from .utils import subtract_material_demand
4+
5+
__all__ = ["subtract_material_demand"]
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""Utility functions for MESSAGEix-BMT (Buildings, Materials, Transport) integration."""
2+
3+
import logging
4+
from typing import TYPE_CHECKING
5+
6+
import pandas as pd
7+
8+
if TYPE_CHECKING:
9+
from message_ix import Scenario
10+
11+
from message_ix_models import ScenarioInfo
12+
13+
log = logging.getLogger(__name__)
14+
15+
16+
def subtract_material_demand(
17+
scenario: "Scenario",
18+
info: "ScenarioInfo",
19+
sturm_r: pd.DataFrame,
20+
sturm_c: pd.DataFrame,
21+
method: str = "bm_subtraction",
22+
) -> pd.DataFrame:
23+
"""Subtract inter-sector material demand from existing demands in scenario.
24+
25+
This function provides different approaches for subtracting inter-sector material
26+
demand from the original material demand, due to BM (material inputs for residential
27+
and commercial building construction), PM (material inputs for power capacity), IM
28+
(material inputs for infrastructures), TM (material inputs for new vehicles) links.
29+
30+
Parameters
31+
----------
32+
scenario : message_ix.Scenario
33+
The scenario to modify
34+
info : ScenarioInfo
35+
Scenario information
36+
sturm_r : pd.DataFrame
37+
Residential STURM data
38+
sturm_c : pd.DataFrame
39+
Commercial STURM data
40+
method : str, optional
41+
Method to use for subtraction:
42+
- "bm_subtraction": default, substract entire trajectory
43+
- "im_subtraction": substract base year and rerun material demand projection
44+
- "pm_subtraction": to be determined (currently treated as additional demand)
45+
- "tm_subtraction": to be determined
46+
47+
Returns
48+
-------
49+
pd.DataFrame
50+
Modified demand data with material demand subtracted
51+
"""
52+
# Retrieve data once
53+
mat_demand = scenario.par("demand", {"level": "demand"})
54+
index_cols = ["node", "year", "commodity"]
55+
56+
if method == "bm_subtraction":
57+
# Subtract the building material demand trajectory from existing demands
58+
for rc, base_data, how in (
59+
("resid", sturm_r, "right"),
60+
("comm", sturm_c, "outer"),
61+
):
62+
new_col = f"demand_{rc}_const"
63+
64+
# - Drop columns.
65+
# - Rename "value" to e.g. "demand_resid_const".
66+
# - Extract MESSAGEix-Materials commodity name from STURM commodity name.
67+
# - Drop other rows.
68+
# - Set index.
69+
df = (
70+
base_data.drop(columns=["level", "time", "unit"])
71+
.rename(columns={"value": new_col})
72+
.assign(
73+
commodity=lambda _df: _df.commodity.str.extract(
74+
f"{rc}_mat_demand_(cement|steel|aluminum)", expand=False
75+
)
76+
)
77+
.dropna(subset=["commodity"])
78+
.set_index(index_cols)
79+
)
80+
81+
# Merge existing demands at level "demand".
82+
# - how="right": drop all rows in par("demand", …) with no match in `df`.
83+
# - how="outer": keep the union of rows in `mat_demand` (e.g. from sturm_r)
84+
# and in `df` (from sturm_c); fill NA with zeroes.
85+
mat_demand = mat_demand.join(df, on=index_cols, how=how).fillna(0)
86+
87+
# False if main() is being run for the second time on `scenario`
88+
first_pass = "construction_resid_build" not in info.set["technology"]
89+
90+
# If not on the first pass, this modification is already performed; skip
91+
if first_pass:
92+
# - Compute new value = (existing value - STURM values), but no less than 0.
93+
# - Drop intermediate column.
94+
mat_demand = (
95+
mat_demand.eval(
96+
"value = value - demand_comm_const - demand_resid_const"
97+
)
98+
.assign(value=lambda df: df["value"].clip(0))
99+
.drop(columns=["demand_comm_const", "demand_resid_const"])
100+
)
101+
102+
elif method == "im_subtraction":
103+
# TODO: to be implemented
104+
log.warning("Method 'im_subtraction' not implemented yet, using bm_subtraction")
105+
return subtract_material_demand(
106+
scenario, info, sturm_r, sturm_c, "bm_subtraction"
107+
)
108+
109+
elif method == "pm_subtraction":
110+
# TODO: Implement alternative method 2
111+
log.warning("Method 'pm_subtraction' not implemented yet, using bm_subtraction")
112+
return subtract_material_demand(
113+
scenario, info, sturm_r, sturm_c, "bm_subtraction"
114+
)
115+
116+
elif method == "tm_subtraction":
117+
# TODO: Implement alternative method 3
118+
log.warning("Method 'tm_subtraction' not implemented yet, using bm_subtraction")
119+
return subtract_material_demand(
120+
scenario, info, sturm_r, sturm_c, "bm_subtraction"
121+
)
122+
123+
else:
124+
raise ValueError(f"Unknown method: {method}")
125+
126+
return mat_demand

message_ix_models/model/buildings/build.py

Lines changed: 12 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from message_ix_models import Context, ScenarioInfo, Spec
2727
from message_ix_models.model import build
28+
from message_ix_models.model.bmt.utils import subtract_material_demand
2829
from message_ix_models.model.structure import (
2930
generate_set_elements,
3031
get_codes,
@@ -839,50 +840,18 @@ def materials(
839840
for name, df in data.items():
840841
result[name].append(df)
841842

842-
# Retrieve data once
843-
mat_demand = scenario.par("demand", {"level": "demand"})
844-
index_cols = ["node", "year", "commodity"]
845-
846-
# Subtract building material demand from existing demands in scenario
847-
for rc, base_data, how in (("resid", sturm_r, "right"), ("comm", sturm_c, "outer")):
848-
new_col = f"demand_{rc}_const"
849-
850-
# - Drop columns.
851-
# - Rename "value" to e.g. "demand_resid_const".
852-
# - Extract MESSAGEix-Materials commodity name from STURM commodity name.
853-
# - Drop other rows.
854-
# - Set index.
855-
df = (
856-
base_data.drop(columns=["level", "time", "unit"])
857-
.rename(columns={"value": new_col})
858-
.assign(
859-
commodity=lambda _df: _df.commodity.str.extract(
860-
f"{rc}_mat_demand_(cement|steel|aluminum)", expand=False
861-
)
862-
)
863-
.dropna(subset=["commodity"])
864-
.set_index(index_cols)
865-
)
843+
# Use the reusable function to subtract material demand
844+
# One can change the method parameter to use different approaches:
845+
# - "bm_subtraction": Building material subtraction (default)
846+
# - "im_subtraction": Infrastructure material subtraction (to be implemented)
847+
# - "pm_subtraction": Power material subtraction (to be implemented)
848+
# - "tm_subtraction": Transport material subtraction (to be implemented)
849+
mat_demand = subtract_material_demand(
850+
scenario, info, sturm_r, sturm_c, method="bm_subtraction"
851+
)
866852

867-
# Merge existing demands at level "demand".
868-
# - how="right": drop all rows in par("demand", …) that have no match in `df`.
869-
# - how="outer": keep the union of rows in `mat_demand` (e.g. from sturm_r) and
870-
# in `df` (from sturm_c); fill NA with zeroes.
871-
mat_demand = mat_demand.join(df, on=index_cols, how=how).fillna(0)
872-
873-
# False if main() is being run for the second time on `scenario`
874-
first_pass = "construction_resid_build" not in info.set["technology"]
875-
876-
# If not on the first pass, this modification is already performed; skip
877-
if first_pass:
878-
# - Compute new value = (existing value - STURM values), but no less than 0.
879-
# - Drop intermediate column.
880-
# - Add to combined data.
881-
result["demand"].append(
882-
mat_demand.eval("value = value - demand_comm_const - demand_resid_const")
883-
.assign(value=lambda df: df["value"].clip(0))
884-
.drop(columns=["demand_comm_const", "demand_resid_const"])
885-
)
853+
# Add the modified demand to results
854+
result["demand"].append(mat_demand)
886855

887856
# Concatenate data frames together
888857
return {k: pd.concat(v) for k, v in result.items()}

0 commit comments

Comments
 (0)