From ff6ff8a9060235cea4d798b40e293957943af56e Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Fri, 22 May 2026 16:58:18 +0200 Subject: [PATCH 01/19] feat: release license environment after each rolling horizon --- scripts/cba/solve_cba_network.py | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index eb21fb57d0..fe3be5a2e0 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -17,6 +17,7 @@ """ import copy +import gc import importlib import logging import os @@ -177,6 +178,28 @@ def optimize_with_rolling_horizon( c.static.loc[comp, "e_sum_max"] = window_energy status, condition = n.optimize(sns, **kwargs) # type: ignore + + # Explicitly dispose of Gurobi environment after each rolling horizon window + # This is critical for WLS license which doesn't allow multiple environments + # in the same process, even sequentially + if hasattr(n, "model") and n.model is not None: + try: + # Access the Gurobi model if it exists + if hasattr(n.model, "solver_model") and n.model.solver_model is not None: + gurobi_model = n.model.solver_model + if hasattr(gurobi_model, "dispose"): + gurobi_model.dispose() + # Also try to dispose the environment + if hasattr(gurobi_model, "_env") and gurobi_model._env is not None: + gurobi_model._env.dispose() + # Clear the solver_model reference + n.model.solver_model = None + # Force garbage collection to ensure cleanup + # (Note: n.model is a read-only property, can't be deleted/set) + gc.collect() + except Exception as e: + logger.warning(f"Failed to dispose Gurobi environment: {e}") + if status != "ok": logger.warning( f"Optimization failed with status {status} and condition {condition}" @@ -191,6 +214,21 @@ def optimize_with_rolling_horizon( retry_kwargs["solver_name"] = fallback_solver["name"] retry_kwargs["solver_options"] = fallback_solver.get("options", {}) status, condition = n.optimize(sns, **retry_kwargs) # type: ignore + + # Cleanup after fallback solver too + if hasattr(n, "model") and n.model is not None: + try: + if hasattr(n.model, "solver_model") and n.model.solver_model is not None: + gurobi_model = n.model.solver_model + if hasattr(gurobi_model, "dispose"): + gurobi_model.dispose() + if hasattr(gurobi_model, "_env") and gurobi_model._env is not None: + gurobi_model._env.dispose() + n.model.solver_model = None + gc.collect() + except Exception as e: + logger.warning(f"Failed to dispose Gurobi environment after fallback: {e}") + if status != "ok": logger.warning(f"Fallback also failed: {status} / {condition}") return status, condition From 2ddf1d8a23ca251f85e8798639a9f944eb9b218a Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Tue, 26 May 2026 15:03:54 +0200 Subject: [PATCH 02/19] fix: only discard gurobi license after printing infeasibility, if infeasible --- scripts/cba/solve_cba_network.py | 85 +++++++++++++++++++------------- 1 file changed, 51 insertions(+), 34 deletions(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index fe3be5a2e0..306a598a94 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -52,6 +52,44 @@ logger = logging.getLogger(__name__) +def dispose_gurobi_model(n: pypsa.Network) -> None: + """ + Explicitly dispose of Gurobi environment. + + This is critical for WLS license which doesn't allow multiple environments + in the same process, even sequentially. Without it, if using Gurobi, what happens is: + - First solve (project 1's first rolling horizon): Gurobi environment is created and holds the license. + - After solve completes, environment is not disposed, so license is still held. + - Second solve (project 2's first rolling horizon): Gurobi tries to create a new environment, + but WLS license server denies it because the previous environment is still active. + This results in an error and the second solve fails immediately with a license error, + even though the first solve completed successfully. + + What this function does is explicitly dispose of the Gurobi model and environment after each solve, + which releases the license and allows subsequent solves to create new environments without issue. + + This function should only be called after: + - A successful solve, OR + - Computing and printing infeasibilities for a failed solve + """ + if hasattr(n, "model") and n.model is not None: + try: + # Access the Gurobi model if it exists + if hasattr(n.model, "solver_model") and n.model.solver_model is not None: + gurobi_model = n.model.solver_model + if hasattr(gurobi_model, "dispose"): + gurobi_model.dispose() + # Also try to dispose the environment + if hasattr(gurobi_model, "_env") and gurobi_model._env is not None: + gurobi_model._env.dispose() + # Clear the solver_model reference + n.model.solver_model = None + # Force garbage collection to ensure cleanup + gc.collect() + except Exception as e: + logger.warning(f"Failed to dispose Gurobi environment: {e}") + + def extra_functionality( n: pypsa.Network, snapshots: pd.DatetimeIndex, @@ -179,26 +217,10 @@ def optimize_with_rolling_horizon( status, condition = n.optimize(sns, **kwargs) # type: ignore - # Explicitly dispose of Gurobi environment after each rolling horizon window - # This is critical for WLS license which doesn't allow multiple environments - # in the same process, even sequentially - if hasattr(n, "model") and n.model is not None: - try: - # Access the Gurobi model if it exists - if hasattr(n.model, "solver_model") and n.model.solver_model is not None: - gurobi_model = n.model.solver_model - if hasattr(gurobi_model, "dispose"): - gurobi_model.dispose() - # Also try to dispose the environment - if hasattr(gurobi_model, "_env") and gurobi_model._env is not None: - gurobi_model._env.dispose() - # Clear the solver_model reference - n.model.solver_model = None - # Force garbage collection to ensure cleanup - # (Note: n.model is a read-only property, can't be deleted/set) - gc.collect() - except Exception as e: - logger.warning(f"Failed to dispose Gurobi environment: {e}") + # Only dispose Gurobi model if solve succeeded + # If solve failed, keep model for infeasibility computation + if status == "ok": + dispose_gurobi_model(n) if status != "ok": logger.warning( @@ -215,20 +237,10 @@ def optimize_with_rolling_horizon( retry_kwargs["solver_options"] = fallback_solver.get("options", {}) status, condition = n.optimize(sns, **retry_kwargs) # type: ignore - # Cleanup after fallback solver too - if hasattr(n, "model") and n.model is not None: - try: - if hasattr(n.model, "solver_model") and n.model.solver_model is not None: - gurobi_model = n.model.solver_model - if hasattr(gurobi_model, "dispose"): - gurobi_model.dispose() - if hasattr(gurobi_model, "_env") and gurobi_model._env is not None: - gurobi_model._env.dispose() - n.model.solver_model = None - gc.collect() - except Exception as e: - logger.warning(f"Failed to dispose Gurobi environment after fallback: {e}") - + # Only dispose after fallback if it succeeded + if status == "ok": + dispose_gurobi_model(n) + if status != "ok": logger.warning(f"Fallback also failed: {status} / {condition}") return status, condition @@ -330,7 +342,12 @@ def solve_network( labels = n.model.compute_infeasibilities() logger.info(f"Labels:\n{labels}") n.model.print_infeasibilities() + # Now safe to dispose after computing infeasibilities + dispose_gurobi_model(n) raise RuntimeError("Solving status 'infeasible'. Infeasibilities computed.") + + # If solve succeeded - dispose the model + dispose_gurobi_model(n) if __name__ == "__main__": From 9d81b95f6cfb0cd0978b30bf1c72d87b97188a9a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:02:10 +0000 Subject: [PATCH 03/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/cba/solve_cba_network.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index 306a598a94..d728a83b4c 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -55,19 +55,19 @@ def dispose_gurobi_model(n: pypsa.Network) -> None: """ Explicitly dispose of Gurobi environment. - + This is critical for WLS license which doesn't allow multiple environments in the same process, even sequentially. Without it, if using Gurobi, what happens is: - First solve (project 1's first rolling horizon): Gurobi environment is created and holds the license. - After solve completes, environment is not disposed, so license is still held. - - Second solve (project 2's first rolling horizon): Gurobi tries to create a new environment, - but WLS license server denies it because the previous environment is still active. - This results in an error and the second solve fails immediately with a license error, + - Second solve (project 2's first rolling horizon): Gurobi tries to create a new environment, + but WLS license server denies it because the previous environment is still active. + This results in an error and the second solve fails immediately with a license error, even though the first solve completed successfully. What this function does is explicitly dispose of the Gurobi model and environment after each solve, which releases the license and allows subsequent solves to create new environments without issue. - + This function should only be called after: - A successful solve, OR - Computing and printing infeasibilities for a failed solve @@ -216,12 +216,12 @@ def optimize_with_rolling_horizon( c.static.loc[comp, "e_sum_max"] = window_energy status, condition = n.optimize(sns, **kwargs) # type: ignore - + # Only dispose Gurobi model if solve succeeded # If solve failed, keep model for infeasibility computation if status == "ok": dispose_gurobi_model(n) - + if status != "ok": logger.warning( f"Optimization failed with status {status} and condition {condition}" @@ -236,11 +236,11 @@ def optimize_with_rolling_horizon( retry_kwargs["solver_name"] = fallback_solver["name"] retry_kwargs["solver_options"] = fallback_solver.get("options", {}) status, condition = n.optimize(sns, **retry_kwargs) # type: ignore - + # Only dispose after fallback if it succeeded if status == "ok": dispose_gurobi_model(n) - + if status != "ok": logger.warning(f"Fallback also failed: {status} / {condition}") return status, condition @@ -345,7 +345,7 @@ def solve_network( # Now safe to dispose after computing infeasibilities dispose_gurobi_model(n) raise RuntimeError("Solving status 'infeasible'. Infeasibilities computed.") - + # If solve succeeded - dispose the model dispose_gurobi_model(n) From 5d33afd9d9ce840afe5dd84ffc4d5eeefa14cef6 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Fri, 19 Jun 2026 14:20:45 +0200 Subject: [PATCH 04/19] feat: remove garbage collection and `_env.dispose()` --- scripts/cba/solve_cba_network.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index d728a83b4c..8cc8b57622 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -17,7 +17,6 @@ """ import copy -import gc import importlib import logging import os @@ -79,13 +78,8 @@ def dispose_gurobi_model(n: pypsa.Network) -> None: gurobi_model = n.model.solver_model if hasattr(gurobi_model, "dispose"): gurobi_model.dispose() - # Also try to dispose the environment - if hasattr(gurobi_model, "_env") and gurobi_model._env is not None: - gurobi_model._env.dispose() # Clear the solver_model reference n.model.solver_model = None - # Force garbage collection to ensure cleanup - gc.collect() except Exception as e: logger.warning(f"Failed to dispose Gurobi environment: {e}") From d8772b59624299e847683f4a707186cd15a7d1fa Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Fri, 19 Jun 2026 14:24:05 +0200 Subject: [PATCH 05/19] docs: edit docstrings --- scripts/cba/solve_cba_network.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index 8cc8b57622..b49e2891b8 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -55,14 +55,14 @@ def dispose_gurobi_model(n: pypsa.Network) -> None: """ Explicitly dispose of Gurobi environment. - This is critical for WLS license which doesn't allow multiple environments - in the same process, even sequentially. Without it, if using Gurobi, what happens is: + This function is relevant when using Gurobi for the CBA rolling horizon optimization. + Without this function, what happens is: - First solve (project 1's first rolling horizon): Gurobi environment is created and holds the license. - After solve completes, environment is not disposed, so license is still held. - - Second solve (project 2's first rolling horizon): Gurobi tries to create a new environment, + - Second solve (project 1's second rolling horizon, or project 2's first rolling horizon): Gurobi tries to create a new environment, but WLS license server denies it because the previous environment is still active. - This results in an error and the second solve fails immediately with a license error, - even though the first solve completed successfully. + + This situation results the second solve failing with a license error, causing the workflow to fail. What this function does is explicitly dispose of the Gurobi model and environment after each solve, which releases the license and allows subsequent solves to create new environments without issue. @@ -76,6 +76,7 @@ def dispose_gurobi_model(n: pypsa.Network) -> None: # Access the Gurobi model if it exists if hasattr(n.model, "solver_model") and n.model.solver_model is not None: gurobi_model = n.model.solver_model + # Dispose of the Gurobi model to release the license if hasattr(gurobi_model, "dispose"): gurobi_model.dispose() # Clear the solver_model reference From 5438244f2dd8b7239f8c70781abaaacbd591e275 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Fri, 19 Jun 2026 16:21:16 +0200 Subject: [PATCH 06/19] chore: edit comments --- scripts/cba/solve_cba_network.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index b49e2891b8..e8c43f1962 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -212,11 +212,11 @@ def optimize_with_rolling_horizon( status, condition = n.optimize(sns, **kwargs) # type: ignore - # Only dispose Gurobi model if solve succeeded - # If solve failed, keep model for infeasibility computation + # If solve is successful, dispose of Gurobi model to release license before next rolling horizon if status == "ok": dispose_gurobi_model(n) + # If solve failed, hold on to license until after IIS is computed in solve_network() if status != "ok": logger.warning( f"Optimization failed with status {status} and condition {condition}" @@ -337,11 +337,11 @@ def solve_network( labels = n.model.compute_infeasibilities() logger.info(f"Labels:\n{labels}") n.model.print_infeasibilities() - # Now safe to dispose after computing infeasibilities + # Dispose of Gurobi license after computing infeasibilities dispose_gurobi_model(n) raise RuntimeError("Solving status 'infeasible'. Infeasibilities computed.") - # If solve succeeded - dispose the model + # Final disposal of Gurobi environment (if needed) dispose_gurobi_model(n) From 000bc51ba50fe3dcfcbf46360e04ceca6c909b66 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Fri, 19 Jun 2026 16:28:20 +0200 Subject: [PATCH 07/19] docs: add #756 to doc/release_notes --- doc/release_notes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 05c2ce4933..8845279123 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -35,6 +35,8 @@ Upcoming Open-TYNDP Release **Bugfixes and Compatibility** +* Dispose of Gurobi model after each rolling horizon optimization to prevent license conflicts (https://github.com/open-energy-transition/open-tyndp/pull/756). + **Documentation** * Update benchmarking documentation tables and figures for v0.7.1 (https://github.com/open-energy-transition/open-tyndp/pull/711). From d5f7f31522f3bac8016288a457e3049216b392c8 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Fri, 19 Jun 2026 16:39:33 +0200 Subject: [PATCH 08/19] feat: remove final disposal of gurobi model (unneeded) --- scripts/cba/solve_cba_network.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index e8c43f1962..63c69789d6 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -341,9 +341,6 @@ def solve_network( dispose_gurobi_model(n) raise RuntimeError("Solving status 'infeasible'. Infeasibilities computed.") - # Final disposal of Gurobi environment (if needed) - dispose_gurobi_model(n) - if __name__ == "__main__": if "snakemake" not in globals(): From 0bb7f248cefb1626289d9e7e79678ae480867e74 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Fri, 19 Jun 2026 16:45:18 +0200 Subject: [PATCH 09/19] feat: raise error if status is not ok, not only for infeasible in condition --- scripts/cba/solve_cba_network.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index 63c69789d6..bc49d00d3e 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -326,20 +326,23 @@ def solve_network( logger.warning( f"Solving status '{status}' with termination condition '{condition}'" ) - check_objective_value(n, solving) - - if "warning" in condition: - raise RuntimeError("Solving status 'warning'. Discarding solution.") + # If infeasible and using Gurobi or Xpress, compute and log infeasibilities before raising error + if "infeasible" in condition: + solver_name = solving["solver"]["name"] + if solver_name in ["gurobi", "xpress"]: + labels = n.model.compute_infeasibilities() + logger.info(f"Labels:\n{labels}") + n.model.print_infeasibilities() + # Dispose of Gurobi license after computing infeasibilities + dispose_gurobi_model(n) + raise RuntimeError( + "Solving status 'infeasible'. Infeasibilities computed." + ) + raise RuntimeError( + f"Solving status '{status}' with termination condition '{condition}'." + ) - if "infeasible" in condition: - solver_name = solving["solver"]["name"] - if solver_name in ["gurobi", "xpress"]: - labels = n.model.compute_infeasibilities() - logger.info(f"Labels:\n{labels}") - n.model.print_infeasibilities() - # Dispose of Gurobi license after computing infeasibilities - dispose_gurobi_model(n) - raise RuntimeError("Solving status 'infeasible'. Infeasibilities computed.") + check_objective_value(n, solving) if __name__ == "__main__": From 111a95987a9b78dd3ccc3f3b6da085dcc96f7439 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Sun, 21 Jun 2026 17:09:21 +0200 Subject: [PATCH 10/19] docs: edit release note description --- doc/release_notes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 8845279123..706e11635f 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -35,7 +35,7 @@ Upcoming Open-TYNDP Release **Bugfixes and Compatibility** -* Dispose of Gurobi model after each rolling horizon optimization to prevent license conflicts (https://github.com/open-energy-transition/open-tyndp/pull/756). +* Fix CBA workflow to (a) release Gurobi license after each successful rolling horizon optimization or after computing infeasibilities and (b) crash if rolling horizon fails when using HiGHS (https://github.com/open-energy-transition/open-tyndp/pull/756). **Documentation** From df4d0c726e9eb6cfc630d5b90f5d297175fbfa28 Mon Sep 17 00:00:00 2001 From: meas Date: Mon, 6 Jul 2026 17:20:18 +0200 Subject: [PATCH 11/19] refac: rework `dispose_gurobi_model()` to solver-agnostic `dispose_model()` Co-authored-by: Thomas Gilon --- scripts/cba/solve_cba_network.py | 41 +++++++++++++++----------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index bc49d00d3e..8952e798ce 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -51,38 +51,35 @@ logger = logging.getLogger(__name__) -def dispose_gurobi_model(n: pypsa.Network) -> None: +def dispose_model(n: pypsa.Network) -> None: """ - Explicitly dispose of Gurobi environment. + Explicitly dispose of Model object. - This function is relevant when using Gurobi for the CBA rolling horizon optimization. + This function is relevant when using a single-use license solver for the CBA rolling horizon optimization. Without this function, what happens is: - - First solve (project 1's first rolling horizon): Gurobi environment is created and holds the license. - - After solve completes, environment is not disposed, so license is still held. - - Second solve (project 1's second rolling horizon, or project 2's first rolling horizon): Gurobi tries to create a new environment, - but WLS license server denies it because the previous environment is still active. + - First solve (project 1's first rolling horizon): Model object is created and holds the license. + - After solve completes, Model will be garbage-collected, but license may still be held until next solve. + - Second solve (project 1's second rolling horizon, or project 2's first rolling horizon): Solver tries + to create a new Model object, but the license server denies it because the previous object is still active. - This situation results the second solve failing with a license error, causing the workflow to fail. + This situation results in the second solve failing with a single-use license error, causing the workflow to fail. - What this function does is explicitly dispose of the Gurobi model and environment after each solve, - which releases the license and allows subsequent solves to create new environments without issue. + What this function does is explicitly dispose of the Model after each solve, which releases the license + and allows subsequent solves to create new Model objects without issue. This function should only be called after: - A successful solve, OR - Computing and printing infeasibilities for a failed solve """ - if hasattr(n, "model") and n.model is not None: - try: - # Access the Gurobi model if it exists - if hasattr(n.model, "solver_model") and n.model.solver_model is not None: - gurobi_model = n.model.solver_model - # Dispose of the Gurobi model to release the license - if hasattr(gurobi_model, "dispose"): - gurobi_model.dispose() - # Clear the solver_model reference - n.model.solver_model = None - except Exception as e: - logger.warning(f"Failed to dispose Gurobi environment: {e}") + try: + if ( + n.model is not None + and hasattr(n.model, "solver_model") + and n.model.solver_model is not None + ): + n.model.solver_model = None + except Exception as e: + logger.warning(f"Failed to dispose Model object: {e}") def extra_functionality( From abd2acd880ecec62fd922284b57e973ccc56265e Mon Sep 17 00:00:00 2001 From: meas Date: Mon, 6 Jul 2026 17:21:05 +0200 Subject: [PATCH 12/19] refac: use `dispose_model()` instead of `dispose_gurobi_model()` Co-authored-by: Thomas Gilon --- scripts/cba/solve_cba_network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index 8952e798ce..25df3857c8 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -211,7 +211,7 @@ def optimize_with_rolling_horizon( # If solve is successful, dispose of Gurobi model to release license before next rolling horizon if status == "ok": - dispose_gurobi_model(n) + dispose_model(n) # If solve failed, hold on to license until after IIS is computed in solve_network() if status != "ok": @@ -231,7 +231,7 @@ def optimize_with_rolling_horizon( # Only dispose after fallback if it succeeded if status == "ok": - dispose_gurobi_model(n) + dispose_model(n) if status != "ok": logger.warning(f"Fallback also failed: {status} / {condition}") From a20005aef6f32cf75cc371ba29287cce06c43cd2 Mon Sep 17 00:00:00 2001 From: meas Date: Mon, 6 Jul 2026 17:22:09 +0200 Subject: [PATCH 13/19] refac: dispose of previous Model object if `fallback_solver` is used Co-authored-by: Thomas Gilon --- scripts/cba/solve_cba_network.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index 25df3857c8..d84d409866 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -220,6 +220,10 @@ def optimize_with_rolling_horizon( ) # Retry with fallback solver if configured if fallback_solver: + if fallback_solver: + # If solve failed and fallback is configured, dispose of Model before creating a new one. + dispose_model(n) + logger.info( f"Retrying window {i + 1}/{len(starting_points)} " f"with fallback solver '{fallback_solver['name']}'" From 8e2605ee3af137eac6ae75bc7a34475b01622770 Mon Sep 17 00:00:00 2001 From: meas Date: Mon, 6 Jul 2026 17:22:35 +0200 Subject: [PATCH 14/19] refac: remove unneeded `dispose` Co-authored-by: Thomas Gilon --- scripts/cba/solve_cba_network.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index d84d409866..60c755fba6 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -334,8 +334,6 @@ def solve_network( labels = n.model.compute_infeasibilities() logger.info(f"Labels:\n{labels}") n.model.print_infeasibilities() - # Dispose of Gurobi license after computing infeasibilities - dispose_gurobi_model(n) raise RuntimeError( "Solving status 'infeasible'. Infeasibilities computed." ) From e83d29552d9a1addf1c4a8aead86ca6b615798c0 Mon Sep 17 00:00:00 2001 From: meas Date: Mon, 6 Jul 2026 17:23:03 +0200 Subject: [PATCH 15/19] chore: change wording in commented code Co-authored-by: Thomas Gilon --- scripts/cba/solve_cba_network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index 60c755fba6..5a56c891fa 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -209,11 +209,11 @@ def optimize_with_rolling_horizon( status, condition = n.optimize(sns, **kwargs) # type: ignore - # If solve is successful, dispose of Gurobi model to release license before next rolling horizon + # If solve is successful, dispose of Model object to release license before next rolling horizon if status == "ok": dispose_model(n) - # If solve failed, hold on to license until after IIS is computed in solve_network() + # If solve failed, hold on to the Model object until after IIS is computed in solve_network() if status != "ok": logger.warning( f"Optimization failed with status {status} and condition {condition}" From 9c438cd851655a17ccae2b6ab460b0e2d99f8444 Mon Sep 17 00:00:00 2001 From: meas Date: Mon, 6 Jul 2026 17:23:25 +0200 Subject: [PATCH 16/19] docs: edit PR description in doc/release_notes.rst Co-authored-by: Thomas Gilon --- doc/release_notes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 706e11635f..08cb3d081b 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -35,7 +35,7 @@ Upcoming Open-TYNDP Release **Bugfixes and Compatibility** -* Fix CBA workflow to (a) release Gurobi license after each successful rolling horizon optimization or after computing infeasibilities and (b) crash if rolling horizon fails when using HiGHS (https://github.com/open-energy-transition/open-tyndp/pull/756). +* Fix CBA workflow to (a) release solver license after each successful rolling horizon optimization or after computing infeasibilities and (b) raise an error if rolling horizon fails when using HiGHS (https://github.com/open-energy-transition/open-tyndp/pull/756). **Documentation** From 0a3b0183873e97c765fb1ba489339d32b73467c1 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Mon, 6 Jul 2026 17:25:49 +0200 Subject: [PATCH 17/19] docs: add #756 to doc/release_notes --- doc/release_notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/release_notes.md b/doc/release_notes.md index 79c6bf77cc..e7bc027f74 100644 --- a/doc/release_notes.md +++ b/doc/release_notes.md @@ -31,6 +31,8 @@ **Bugfixes and Compatibility** +* Fix CBA workflow to (a) release solver license after each successful rolling horizon optimization or after computing infeasibilities and (b) raise an error if rolling horizon fails when using HiGHS (https://github.com/open-energy-transition/open-tyndp/pull/756). + **Documentation** * Update benchmarking documentation tables and figures for v0.7.1 ([#711](https://github.com/open-energy-transition/open-tyndp/pull/711)). From a62bdefac8f2e8a8e541b8c9afda71410c81e019 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Mon, 6 Jul 2026 17:29:25 +0200 Subject: [PATCH 18/19] fix: remove extra `if fallback_solver` --- scripts/cba/solve_cba_network.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index 5a56c891fa..76c66f5bc1 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -64,7 +64,7 @@ def dispose_model(n: pypsa.Network) -> None: This situation results in the second solve failing with a single-use license error, causing the workflow to fail. - What this function does is explicitly dispose of the Model after each solve, which releases the license + What this function does is explicitly dispose of the Model after each solve, which releases the license and allows subsequent solves to create new Model objects without issue. This function should only be called after: @@ -73,9 +73,9 @@ def dispose_model(n: pypsa.Network) -> None: """ try: if ( - n.model is not None - and hasattr(n.model, "solver_model") - and n.model.solver_model is not None + n.model is not None + and hasattr(n.model, "solver_model") + and n.model.solver_model is not None ): n.model.solver_model = None except Exception as e: @@ -219,7 +219,6 @@ def optimize_with_rolling_horizon( f"Optimization failed with status {status} and condition {condition}" ) # Retry with fallback solver if configured - if fallback_solver: if fallback_solver: # If solve failed and fallback is configured, dispose of Model before creating a new one. dispose_model(n) From 3f74288910319d821bd646e750fdb4e823f1ca38 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Wed, 8 Jul 2026 13:03:35 +0200 Subject: [PATCH 19/19] docs: edit link to PR in doc/release_notes --- doc/release_notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release_notes.md b/doc/release_notes.md index fa863f8540..a0f10af131 100644 --- a/doc/release_notes.md +++ b/doc/release_notes.md @@ -33,7 +33,7 @@ * Rename bus for `t339` project (Tyrrhenian) from ITSI to ITVI ([#751](https://github.com/open-energy-transition/open-tyndp/pull/751)). -* Fix CBA workflow to (a) release solver license after each successful rolling horizon optimization or after computing infeasibilities and (b) raise an error if rolling horizon fails when using HiGHS (https://github.com/open-energy-transition/open-tyndp/pull/756). +* Fix CBA workflow to (a) release solver license after each successful rolling horizon optimization or after computing infeasibilities and (b) raise an error if rolling horizon fails when using HiGHS ([#756](https://github.com/open-energy-transition/open-tyndp/pull/756)). **Documentation**