From 3e7d0fe2b9807e659683f1448187a72882232360 Mon Sep 17 00:00:00 2001 From: Clifford Ondieki Date: Thu, 19 Feb 2026 20:48:59 +0100 Subject: [PATCH 1/4] fix: update config and add functionality for pypsa-de --- config/config.de.yaml | 5 ++++- scripts/pypsa-de/additional_functionality.py | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/config/config.de.yaml b/config/config.de.yaml index 42e014928..8ab1e2dfb 100644 --- a/config/config.de.yaml +++ b/config/config.de.yaml @@ -445,7 +445,10 @@ solving: gas pipeline new: 0.3 H2 pipeline: 0.05 H2 pipeline retrofitted: 0.05 - fractional_last_unit_size: true + fractional_last_unit_size: true + solver: + name: highs + options: highs-default constraints: # The default CO2 budget uses the KSG targets, and the non CO2 emissions from the REMIND model in the KN2045_Mix scenario co2_budget_national: diff --git a/scripts/pypsa-de/additional_functionality.py b/scripts/pypsa-de/additional_functionality.py index e49bd55c3..0a457fbf6 100644 --- a/scripts/pypsa-de/additional_functionality.py +++ b/scripts/pypsa-de/additional_functionality.py @@ -228,6 +228,18 @@ def h2_import_limits(n, investment_year, limits_volume_max): & (n.links.bus1.str[:2] != ct) ] + if incoming.empty and outgoing.empty: + logger.warning( + f"No hydrogen import/export links found for {ct}; skipping limit enforcement." + ) + continue + + if incoming.empty and outgoing.empty: + logger.warning( + f"No hydrogen import/export links found for {ct}; skipping limit enforcement." + ) + continue + incoming_p = ( n.model["Link-p"].loc[:, incoming] * n.snapshot_weightings.generators ).sum() From 223634bc5643c67bca0a592df2067a97a421dad6 Mon Sep 17 00:00:00 2001 From: Clifford Ondieki Date: Thu, 19 Feb 2026 21:24:35 +0100 Subject: [PATCH 2/4] fix: update snakefile for pypsa-de --- scripts/pypsa-de/additional_functionality.py | 81 +++++++++++++++----- 1 file changed, 63 insertions(+), 18 deletions(-) diff --git a/scripts/pypsa-de/additional_functionality.py b/scripts/pypsa-de/additional_functionality.py index 0a457fbf6..de784cd47 100644 --- a/scripts/pypsa-de/additional_functionality.py +++ b/scripts/pypsa-de/additional_functionality.py @@ -9,6 +9,33 @@ logger = logging.getLogger(__name__) +def h2_import_limits_enabled(config): + return config.get("pypsa-de", {}).get("h2_import_limits", {}).get("enable", True) + + +def safe_add_constraint(model, expr, rhs, sense, name): + """Wrap solver call to skip constant-constant constraints.""" + try: + if sense == "<=": + model.add_constraints(expr <= rhs, name=name) + elif sense == ">=": + model.add_constraints(expr >= rhs, name=name) + else: + raise ValueError(f"Unsupported sense '{sense}'") + return True + except ValueError as exc: + if "Both sides of the constraint are constant" in str(exc): + logger.debug( + "Skipping constraint %s because both sides are constant (%s %s %s)", + name, + expr, + sense, + rhs, + ) + return False + raise + + def add_capacity_limits(n, investment_year, limits_capacity, sense="maximum"): for c in n.iterate_components(limits_capacity): logger.info(f"Adding {sense} constraints for {c.list_name}") @@ -208,6 +235,10 @@ def add_pos_neg_aux_variables(n, idx, var_name, infix): def h2_import_limits(n, investment_year, limits_volume_max): + if not h2_import_limits_enabled(n.config): + logger.info("Skipping H2 import limit constraints because pypsa-de.h2_import_limits.enable is False.") + return + for ct in limits_volume_max["h2_import"]: limit = limits_volume_max["h2_import"][ct][investment_year] * 1e6 @@ -251,7 +282,13 @@ def h2_import_limits(n, investment_year, limits_volume_max): cname = f"H2_import_limit-{ct}" - n.model.add_constraints(lhs <= limit, name=f"GlobalConstraint-{cname}") + added = safe_add_constraint( + n.model, + lhs, + limit, + "<=", + name=f"GlobalConstraint-{cname}", + ) if cname in n.global_constraints.index: logger.warning( @@ -259,20 +296,27 @@ def h2_import_limits(n, investment_year, limits_volume_max): ) n.global_constraints.drop(cname, inplace=True) - n.add( - "GlobalConstraint", - cname, - constant=limit, - sense="<=", - type="", - carrier_attribute="", - ) + if added: + n.add( + "GlobalConstraint", + cname, + constant=limit, + sense="<=", + type="", + carrier_attribute="", + ) logger.info("Adding H2 export ban") cname = f"H2_export_ban-{ct}" - n.model.add_constraints(lhs >= 0, name=f"GlobalConstraint-{cname}") + added_export = safe_add_constraint( + n.model, + lhs, + 0, + ">=", + name=f"GlobalConstraint-{cname}", + ) if cname in n.global_constraints.index: logger.warning( @@ -280,14 +324,15 @@ def h2_import_limits(n, investment_year, limits_volume_max): ) n.global_constraints.drop(cname, inplace=True) - n.add( - "GlobalConstraint", - cname, - constant=0, - sense=">=", - type="", - carrier_attribute="", - ) + if added_export: + n.add( + "GlobalConstraint", + cname, + constant=0, + sense=">=", + type="", + carrier_attribute="", + ) def h2_production_limits(n, investment_year, limits_volume_min, limits_volume_max): From 395c1f525f9842800c1425f65acfb2cc827b5554 Mon Sep 17 00:00:00 2001 From: Clifford Ondieki Date: Mon, 7 Sep 2026 18:39:55 +0200 Subject: [PATCH 3/4] fix: prevent custom config dictionaries from being overwritten (Issue #30) --- scripts/pypsa-de/build_scenarios.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/pypsa-de/build_scenarios.py b/scripts/pypsa-de/build_scenarios.py index 5a2bea181..f58ccab85 100644 --- a/scripts/pypsa-de/build_scenarios.py +++ b/scripts/pypsa-de/build_scenarios.py @@ -268,7 +268,7 @@ def write_to_scenario_yaml(input, output, scenarios, df): general_df = general_df.xs(reference_scenario, level="scenario") co2_budget_fractions = get_co2_budget(general_df, co2_budget_source) - if not config[scenario].get("sector"): + if "sector" not in config[scenario]: config[scenario]["sector"] = {} if config[scenario]["sector"].get("aviation_demand_factor") is not None: @@ -285,7 +285,7 @@ def write_to_scenario_yaml(input, output, scenarios, df): st_primary_fraction = get_primary_steel_share(scenario_df, planning_horizons) dri_fraction = get_DRI_share(scenario_df, planning_horizons) - if not config[scenario].get("industry"): + if "industry" not in config[scenario]: config[scenario]["industry"] = {} if config[scenario]["industry"].get("St_primary_fraction") is not None: @@ -307,7 +307,7 @@ def write_to_scenario_yaml(input, output, scenarios, df): config[scenario]["industry"]["DRI_fraction"][year] = round( dri_fraction.loc["DRI_Steel_Share", year].item(), 4 ) - if not config[scenario].get("solving"): + if "solving" not in config[scenario]: config[scenario]["solving"] = {} if not config[scenario]["solving"].get("constraints"): config[scenario]["solving"]["constraints"] = {} From 4a024b03c7d19d5424b8b5632627d0676f22fb96 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:08:15 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/pypsa-de/additional_functionality.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/pypsa-de/additional_functionality.py b/scripts/pypsa-de/additional_functionality.py index de784cd47..8b0e5a95b 100644 --- a/scripts/pypsa-de/additional_functionality.py +++ b/scripts/pypsa-de/additional_functionality.py @@ -236,7 +236,9 @@ def add_pos_neg_aux_variables(n, idx, var_name, infix): def h2_import_limits(n, investment_year, limits_volume_max): if not h2_import_limits_enabled(n.config): - logger.info("Skipping H2 import limit constraints because pypsa-de.h2_import_limits.enable is False.") + logger.info( + "Skipping H2 import limit constraints because pypsa-de.h2_import_limits.enable is False." + ) return for ct in limits_volume_max["h2_import"]: