From 90fd4f6011986cc5ed9e92a4dfebbdfbb918e817 Mon Sep 17 00:00:00 2001 From: beijingzyl <1772066848@qq.com> Date: Fri, 19 Dec 2025 13:10:12 +0100 Subject: [PATCH 1/4] fix nuclear constraint --- workflow/scripts/solve_network.py | 63 +++++++++++++++---------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/workflow/scripts/solve_network.py b/workflow/scripts/solve_network.py index 44fbc4e8..70ddbc9a 100644 --- a/workflow/scripts/solve_network.py +++ b/workflow/scripts/solve_network.py @@ -28,27 +28,30 @@ def calc_nuclear_expansion_limit( config: dict, planning_year: int, network_path: str, -) -> None: +) -> float | None: """ - Calculate and apply the nuclear expansion limit from configuration. + Calculate the nuclear expansion limit from configuration. Args: - n (pypsa.Network): the network object - config (dict): full configuration dictionary (mutated in place) - planning_year (int): target planning horizon year - network_path (str): path to the current network file, used to locate base year + n: the network object + config: full configuration dictionary + planning_year: target planning horizon year + network_path: path to the current network file + + Returns: + float | None: maximum allowed nuclear capacity in MW, or None if not applicable """ - nuclear_cfg = config.setdefault("nuclear_reactors", {}) + nuclear_cfg = config.get("nuclear_reactors", {}) if not nuclear_cfg.get("enable_growth_limit", True): - return + return None annual_addition = nuclear_cfg.get("max_annual_capacity_addition") base_year = nuclear_cfg.get("base_year", 2020) n_years = planning_year - base_year if not annual_addition or n_years <= 0: - return + return None base_capacity = nuclear_cfg.get("base_capacity") if base_capacity is None: @@ -60,15 +63,13 @@ def calc_nuclear_expansion_limit( base_capacity = n.generators[n.generators.carrier == "nuclear"]["p_nom"].sum() max_capacity = base_capacity + annual_addition * n_years - nuclear_gens_ext = n.generators[ - (n.generators.carrier == "nuclear") & (n.generators.p_nom_extendable == True) - ].index - if len(nuclear_gens_ext) > 0: - logger.info( - f"Nuclear expansion limit for {planning_year}: {max_capacity:.0f} MW " - f"[{base_capacity:.0f} + {annual_addition:.0f} × {n_years} years]" - ) + logger.info( + f"Nuclear expansion limit for {planning_year}: {max_capacity:.0f} MW " + f"[{base_capacity:.0f} + {annual_addition:.0f} × {n_years} years]" + ) + + return max_capacity def set_transmission_limit(n: pypsa.Network, kind: str, factor: float, n_years=1): @@ -307,18 +308,11 @@ def prepare_network( def add_nuclear_expansion_constraints(n: pypsa.Network): - """ - Add nuclear expansion limit constraint if configured. - - This function adds a global constraint limiting the total capacity of all - extendable nuclear generators. The limit is based on max_annual_capacity_addition. + """Add nuclear expansion limit constraint if configured.""" - Args: - n (pypsa.Network): the network object - """ - nuclear_config = n.config.get("nuclear_reactors", {}) + max_capacity = getattr(n, "nuclear_max_capacity", None) - if not nuclear_config.get("enable_growth_limit", True): + if max_capacity is None: return nuclear_gens_ext = n.generators[ @@ -326,17 +320,19 @@ def add_nuclear_expansion_constraints(n: pypsa.Network): ].index if len(nuclear_gens_ext) == 0: - return - - limit = nuclear_config.get("max_annual_capacity_addition") - if limit is None: + logger.info("No extendable nuclear generators found") return n.model.add_constraints( - n.model["Generator-p_nom"].loc[nuclear_gens_ext].sum() <= limit, + n.model["Generator-p_nom"].loc[nuclear_gens_ext].sum() <= max_capacity, name="nuclear_expansion_limit", ) + logger.info( + f"Added nuclear expansion constraint: " + f"{len(nuclear_gens_ext)} generators, total <= {max_capacity:.0f} MW" + ) + def add_battery_constraints(n: pypsa.Network): """ @@ -946,12 +942,13 @@ def solve_network( # # TODO: remove ugly hack # n.storage_units.p_nom_max = n.storage_units.p_nom * 1.05**exp_years - calc_nuclear_expansion_limit( + nuclear_limit = calc_nuclear_expansion_limit( n=n, config=config, planning_year=int(snakemake.wildcards.planning_horizons), network_path=snakemake.input.network_name, ) + n.nuclear_max_capacity = nuclear_limit if tunnel: logger.info(f"tunnel process alive? {tunnel.poll()}") From d61b0c049bb8bdfef55650bc7e474c8b63cbcbfe Mon Sep 17 00:00:00 2001 From: beijingzyl <1772066848@qq.com> Date: Fri, 19 Dec 2025 14:34:56 +0100 Subject: [PATCH 2/4] fix nuclear constraint --- workflow/scripts/solve_network.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/workflow/scripts/solve_network.py b/workflow/scripts/solve_network.py index 70ddbc9a..5cd727ce 100644 --- a/workflow/scripts/solve_network.py +++ b/workflow/scripts/solve_network.py @@ -310,7 +310,7 @@ def prepare_network( def add_nuclear_expansion_constraints(n: pypsa.Network): """Add nuclear expansion limit constraint if configured.""" - max_capacity = getattr(n, "nuclear_max_capacity", None) + max_capacity = n.config.get("nuclear_max_capacity") if max_capacity is None: return @@ -948,7 +948,8 @@ def solve_network( planning_year=int(snakemake.wildcards.planning_horizons), network_path=snakemake.input.network_name, ) - n.nuclear_max_capacity = nuclear_limit + if nuclear_limit is not None: + n.config["nuclear_max_capacity"] = nuclear_limit if tunnel: logger.info(f"tunnel process alive? {tunnel.poll()}") From 591d4e513fb29f696c8d8a7898b74f938d407fcf Mon Sep 17 00:00:00 2001 From: beijingzyl <1772066848@qq.com> Date: Fri, 19 Dec 2025 15:13:51 +0100 Subject: [PATCH 3/4] fix test error --- workflow/scripts/solve_network.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/workflow/scripts/solve_network.py b/workflow/scripts/solve_network.py index 5cd727ce..9c84ea7c 100644 --- a/workflow/scripts/solve_network.py +++ b/workflow/scripts/solve_network.py @@ -310,7 +310,8 @@ def prepare_network( def add_nuclear_expansion_constraints(n: pypsa.Network): """Add nuclear expansion limit constraint if configured.""" - max_capacity = n.config.get("nuclear_max_capacity") + config = getattr(n, "config", {}) + max_capacity = config.get("nuclear_max_capacity") if isinstance(config, dict) else None if max_capacity is None: return @@ -811,7 +812,7 @@ def extra_functionality(n: pypsa.Network, _) -> None: n (pypsa.Network): the network object to optimize _: dummy for compatibility with pypsa solve """ - config = n.config + config = getattr(n, "config", {}) add_battery_constraints(n) add_transmission_constraints(n) add_nuclear_expansion_constraints(n) @@ -949,7 +950,7 @@ def solve_network( network_path=snakemake.input.network_name, ) if nuclear_limit is not None: - n.config["nuclear_max_capacity"] = nuclear_limit + config["nuclear_max_capacity"] = nuclear_limit if tunnel: logger.info(f"tunnel process alive? {tunnel.poll()}") From f2fdc10c29223adc263d2c2184e17177f45605da Mon Sep 17 00:00:00 2001 From: beijingzyl <1772066848@qq.com> Date: Fri, 19 Dec 2025 15:29:56 +0100 Subject: [PATCH 4/4] update gurobi 12 --- workflow/envs/environment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow/envs/environment.yaml b/workflow/envs/environment.yaml index 6535e3f1..09716ea3 100644 --- a/workflow/envs/environment.yaml +++ b/workflow/envs/environment.yaml @@ -64,7 +64,7 @@ dependencies: - pip: - vresutils>=0.3.1 - tsam>=1.1.0 - - gurobipy==11.0.3 + - gurobipy==12.0.3 - jwt - remind-pypsa-coupling>=0.1.2 - mkdocs>=1.4.0