|
| 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 |
0 commit comments