From d54cf3016320a1dce36bf4a0459959a6430a4640 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Tue, 1 Sep 2026 17:27:52 +0200 Subject: [PATCH 1/2] =?UTF-8?q?imrpove=20MASTR=20CHP=20assignment;=20impro?= =?UTF-8?q?ve=20MASTR=20"W=C3=A4rme"=20assignment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- powerplantmatching/data.py | 57 +++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/powerplantmatching/data.py b/powerplantmatching/data.py index 08fd526..e1551ed 100644 --- a/powerplantmatching/data.py +++ b/powerplantmatching/data.py @@ -18,6 +18,7 @@ import pycountry import requests from deprecation import deprecated +from scipy import optimize from .cleaning import ( clean_name, @@ -2337,6 +2338,45 @@ def MASTR( """ + def _assign_PP_or_CHP(df): + df.loc[:,"Set"] = "PP" + + # KWK plants consist of multiple units, all of which share the same MastrKwkNummer + grouped_kwk = df[df["KwkMastrNummer"].notna()].groupby("KwkMastrNummer", sort=False) + for _, kwk_units in grouped_kwk: + n_units = len(kwk_units) + sizes = kwk_units.Capacity.mul(1e3).to_numpy() + capacity = kwk_units.ElektrischeKwkLeistung.unique().item() + if np.isnan(capacity): + capacity = kwk_units.ThermischeNutzleistung.unique().item() + if np.isnan(capacity): + continue + # Solve Knapsack problem to select units that sum up to the capacity, minimizing the deviation from the target capacity. + # One binary variable per unit, plus a non-negative deviation variable. + objective = np.r_[np.zeros(n_units), 1] + integrality = np.r_[np.ones(n_units), 0] + bounds = optimize.Bounds( + lb=np.zeros(n_units + 1), + ub=np.r_[np.ones(n_units), np.inf], + ) + # capacity - deviation <= selected capacity <= capacity + deviation + constraints = optimize.LinearConstraint( + A=[np.r_[sizes, -1], np.r_[sizes, 1]], + lb=[-np.inf, capacity], + ub=[capacity, np.inf], + ) + # Minimize the deviation variable. + res = optimize.milp( + c=objective, + constraints=constraints, + integrality=integrality, + bounds=bounds, + ) + # NB: If the smallest size is at least twice the capacity, nothing gets selected. + selected = res.x[:-1] > 0.5 + df.loc[kwk_units.index[selected], "Set"] = "CHP" + return df + config = get_config() if config is None else config THRESHOLD_KW = config["MASTR"].get("capacity_threshold", 0.1) * 1e3 # noqa: F841 @@ -2388,6 +2428,7 @@ def MASTR( available_columns = pd.read_csv(file.open(name), nrows=0).columns target_columns = [ "GeplantesInbetriebnahmedatum", + "ElektrischeKwkLeistung", "ThermischeNutzleistung", "KwkMastrNummer", "Batterietechnologie", @@ -2448,6 +2489,16 @@ def MASTR( df["PLZ_lat"] = df.Postleitzahl.map(PLZ_map.lat) df["PLZ_lon"] = df.Postleitzahl.map(PLZ_map.lon) + # change the Energietraeger from Waerme to the main fuel type of the unit if there are multiple units with the same KwkMastrNummer + for unit in df.query("Energietraeger == 'Wärme'").iterrows(): + kwk_units = df[df.KwkMastrNummer == unit[1].KwkMastrNummer] + energietraeger = "Wärme" + if len(kwk_units) > 1: + # If Wärme is the dominant fuel type, keep it as Wärme, otherwise use the main fuel type of the unit + energietraeger = kwk_units.groupby("Energietraeger").Nettonennleistung.sum().sort_values(ascending=False).index[0] + waermeunits = kwk_units.query("Energietraeger == 'Wärme'").index + df.loc[waermeunits, "Energietraeger"] = energietraeger + df_processed = ( df.rename(columns=RENAME_COLUMNS) .query("Status in @status_list") @@ -2481,10 +2532,8 @@ def MASTR( config=config, parse_columns=PARSE_COLUMNS, ) - .assign( - Set=lambda df: df["Set"].where( - df["KwkMastrNummer"].isna() & df["ThermischeNutzleistung"].isna(), "CHP" - ), + .pipe( + _assign_PP_or_CHP ) ) From aaa3796fe76b352739e644697bd30b1ccb835b60 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:56:35 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- powerplantmatching/data.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/powerplantmatching/data.py b/powerplantmatching/data.py index e1551ed..07cd344 100644 --- a/powerplantmatching/data.py +++ b/powerplantmatching/data.py @@ -2339,10 +2339,12 @@ def MASTR( """ def _assign_PP_or_CHP(df): - df.loc[:,"Set"] = "PP" + df.loc[:, "Set"] = "PP" # KWK plants consist of multiple units, all of which share the same MastrKwkNummer - grouped_kwk = df[df["KwkMastrNummer"].notna()].groupby("KwkMastrNummer", sort=False) + grouped_kwk = df[df["KwkMastrNummer"].notna()].groupby( + "KwkMastrNummer", sort=False + ) for _, kwk_units in grouped_kwk: n_units = len(kwk_units) sizes = kwk_units.Capacity.mul(1e3).to_numpy() @@ -2492,13 +2494,18 @@ def _assign_PP_or_CHP(df): # change the Energietraeger from Waerme to the main fuel type of the unit if there are multiple units with the same KwkMastrNummer for unit in df.query("Energietraeger == 'Wärme'").iterrows(): kwk_units = df[df.KwkMastrNummer == unit[1].KwkMastrNummer] - energietraeger = "Wärme" + energietraeger = "Wärme" if len(kwk_units) > 1: # If Wärme is the dominant fuel type, keep it as Wärme, otherwise use the main fuel type of the unit - energietraeger = kwk_units.groupby("Energietraeger").Nettonennleistung.sum().sort_values(ascending=False).index[0] + energietraeger = ( + kwk_units.groupby("Energietraeger") + .Nettonennleistung.sum() + .sort_values(ascending=False) + .index[0] + ) waermeunits = kwk_units.query("Energietraeger == 'Wärme'").index df.loc[waermeunits, "Energietraeger"] = energietraeger - + df_processed = ( df.rename(columns=RENAME_COLUMNS) .query("Status in @status_list") @@ -2532,9 +2539,7 @@ def _assign_PP_or_CHP(df): config=config, parse_columns=PARSE_COLUMNS, ) - .pipe( - _assign_PP_or_CHP - ) + .pipe(_assign_PP_or_CHP) ) psw = df_processed.query(