Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ff6ff8a
feat: release license environment after each rolling horizon
measrainsey May 22, 2026
2ddf1d8
fix: only discard gurobi license after printing infeasibility, if inf…
measrainsey May 26, 2026
9d81b95
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 19, 2026
5d33afd
feat: remove garbage collection and `_env.dispose()`
measrainsey Jun 19, 2026
d8772b5
docs: edit docstrings
measrainsey Jun 19, 2026
5438244
chore: edit comments
measrainsey Jun 19, 2026
000bc51
docs: add #756 to doc/release_notes
measrainsey Jun 19, 2026
d5f7f31
feat: remove final disposal of gurobi model (unneeded)
measrainsey Jun 19, 2026
0bb7f24
feat: raise error if status is not ok, not only for infeasible in con…
measrainsey Jun 19, 2026
111a959
docs: edit release note description
measrainsey Jun 21, 2026
df4d0c7
refac: rework `dispose_gurobi_model()` to solver-agnostic `dispose_mo…
measrainsey Jul 6, 2026
abd2acd
refac: use `dispose_model()` instead of `dispose_gurobi_model()`
measrainsey Jul 6, 2026
a20005a
refac: dispose of previous Model object if `fallback_solver` is used
measrainsey Jul 6, 2026
8e2605e
refac: remove unneeded `dispose`
measrainsey Jul 6, 2026
e83d295
chore: change wording in commented code
measrainsey Jul 6, 2026
9c438cd
docs: edit PR description in doc/release_notes.rst
measrainsey Jul 6, 2026
16daa0d
Merge branch 'master' into fix/gurobi-license-env
measrainsey Jul 6, 2026
0a3b018
docs: add #756 to doc/release_notes
measrainsey Jul 6, 2026
a62bdef
fix: remove extra `if fallback_solver`
measrainsey Jul 6, 2026
4239cb1
Merge branch 'master' into fix/gurobi-license-env
measrainsey Jul 8, 2026
3f74288
docs: edit link to PR in doc/release_notes
measrainsey Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ 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).
Comment thread
measrainsey marked this conversation as resolved.
Outdated

**Documentation**

* Update benchmarking documentation tables and figures for v0.7.1 (https://github.com/open-energy-transition/open-tyndp/pull/711).
Expand Down
72 changes: 61 additions & 11 deletions scripts/cba/solve_cba_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,40 @@
logger = logging.getLogger(__name__)


def dispose_gurobi_model(n: pypsa.Network) -> None:
"""
Explicitly dispose of Gurobi environment.

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 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 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.

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}")
Comment thread
measrainsey marked this conversation as resolved.
Outdated


def extra_functionality(
n: pypsa.Network,
snapshots: pd.DatetimeIndex,
Expand Down Expand Up @@ -177,6 +211,12 @@ def optimize_with_rolling_horizon(
c.static.loc[comp, "e_sum_max"] = window_energy

status, condition = n.optimize(sns, **kwargs) # type: ignore

# If solve is successful, dispose of Gurobi model to release license before next rolling horizon
Comment thread
measrainsey marked this conversation as resolved.
Outdated
if status == "ok":
dispose_gurobi_model(n)
Comment thread
measrainsey marked this conversation as resolved.
Outdated

# If solve failed, hold on to license until after IIS is computed in solve_network()
Comment thread
measrainsey marked this conversation as resolved.
Outdated
if status != "ok":
logger.warning(
f"Optimization failed with status {status} and condition {condition}"
Expand All @@ -191,6 +231,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)
Comment thread
measrainsey marked this conversation as resolved.
Outdated

if status != "ok":
logger.warning(f"Fallback also failed: {status} / {condition}")
return status, condition
Expand Down Expand Up @@ -281,18 +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}'."
)
Comment thread
measrainsey marked this conversation as resolved.
Outdated

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()
raise RuntimeError("Solving status 'infeasible'. Infeasibilities computed.")
check_objective_value(n, solving)


if __name__ == "__main__":
Expand Down