Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@

* 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 ([#756](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)).
Expand Down
70 changes: 59 additions & 11 deletions scripts/cba/solve_cba_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,37 @@
logger = logging.getLogger(__name__)


def dispose_model(n: pypsa.Network) -> None:
"""
Explicitly dispose of Model object.

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): 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 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
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
"""
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(
n: pypsa.Network,
snapshots: pd.DatetimeIndex,
Expand Down Expand Up @@ -177,12 +208,21 @@ 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 Model object to release license before next rolling horizon
if status == "ok":
dispose_model(n)

# 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}"
)
# Retry with fallback solver if configured
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']}'"
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_model(n)

if status != "ok":
logger.warning(f"Fallback also failed: {status} / {condition}")
return status, condition
Expand Down Expand Up @@ -281,18 +326,21 @@ 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()
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()
raise RuntimeError("Solving status 'infeasible'. Infeasibilities computed.")
check_objective_value(n, solving)


if __name__ == "__main__":
Expand Down