diff --git a/config/config.de.yaml b/config/config.de.yaml index 21f8d2248..1927501b0 100644 --- a/config/config.de.yaml +++ b/config/config.de.yaml @@ -444,7 +444,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..8b0e5a95b 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,12 @@ 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 @@ -228,6 +261,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() @@ -239,7 +284,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( @@ -247,20 +298,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( @@ -268,14 +326,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): 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"] = {}