Skip to content

Commit 10dd2ff

Browse files
measrainseypre-commit-ci[bot]tgilon
authored
fix: make CBA workflow release Gurobi license after each rolling horizon optimization and fail if optimization is infeasible when using HiGHS (#756)
* feat: release license environment after each rolling horizon * fix: only discard gurobi license after printing infeasibility, if infeasible * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * feat: remove garbage collection and `_env.dispose()` * docs: edit docstrings * chore: edit comments * docs: add #756 to doc/release_notes * feat: remove final disposal of gurobi model (unneeded) * feat: raise error if status is not ok, not only for infeasible in condition * docs: edit release note description * refac: rework `dispose_gurobi_model()` to solver-agnostic `dispose_model()` Co-authored-by: Thomas Gilon <thomas.gilon@openenergytransition.org> * refac: use `dispose_model()` instead of `dispose_gurobi_model()` Co-authored-by: Thomas Gilon <thomas.gilon@openenergytransition.org> * refac: dispose of previous Model object if `fallback_solver` is used Co-authored-by: Thomas Gilon <thomas.gilon@openenergytransition.org> * refac: remove unneeded `dispose` Co-authored-by: Thomas Gilon <thomas.gilon@openenergytransition.org> * chore: change wording in commented code Co-authored-by: Thomas Gilon <thomas.gilon@openenergytransition.org> * docs: edit PR description in doc/release_notes.rst Co-authored-by: Thomas Gilon <thomas.gilon@openenergytransition.org> * docs: add #756 to doc/release_notes * fix: remove extra `if fallback_solver` * docs: edit link to PR in doc/release_notes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Thomas Gilon <thomas.gilon@openenergytransition.org>
1 parent 5d135f2 commit 10dd2ff

2 files changed

Lines changed: 61 additions & 11 deletions

File tree

doc/release_notes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333

3434
* Rename bus for `t339` project (Tyrrhenian) from ITSI to ITVI ([#751](https://github.com/open-energy-transition/open-tyndp/pull/751)).
3535

36+
* 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)).
37+
3638
**Documentation**
3739

3840
* Update benchmarking documentation tables and figures for v0.7.1 ([#711](https://github.com/open-energy-transition/open-tyndp/pull/711)).

scripts/cba/solve_cba_network.py

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,37 @@
5151
logger = logging.getLogger(__name__)
5252

5353

54+
def dispose_model(n: pypsa.Network) -> None:
55+
"""
56+
Explicitly dispose of Model object.
57+
58+
This function is relevant when using a single-use license solver for the CBA rolling horizon optimization.
59+
Without this function, what happens is:
60+
- First solve (project 1's first rolling horizon): Model object is created and holds the license.
61+
- After solve completes, Model will be garbage-collected, but license may still be held until next solve.
62+
- Second solve (project 1's second rolling horizon, or project 2's first rolling horizon): Solver tries
63+
to create a new Model object, but the license server denies it because the previous object is still active.
64+
65+
This situation results in the second solve failing with a single-use license error, causing the workflow to fail.
66+
67+
What this function does is explicitly dispose of the Model after each solve, which releases the license
68+
and allows subsequent solves to create new Model objects without issue.
69+
70+
This function should only be called after:
71+
- A successful solve, OR
72+
- Computing and printing infeasibilities for a failed solve
73+
"""
74+
try:
75+
if (
76+
n.model is not None
77+
and hasattr(n.model, "solver_model")
78+
and n.model.solver_model is not None
79+
):
80+
n.model.solver_model = None
81+
except Exception as e:
82+
logger.warning(f"Failed to dispose Model object: {e}")
83+
84+
5485
def extra_functionality(
5586
n: pypsa.Network,
5687
snapshots: pd.DatetimeIndex,
@@ -177,12 +208,21 @@ def optimize_with_rolling_horizon(
177208
c.static.loc[comp, "e_sum_max"] = window_energy
178209

179210
status, condition = n.optimize(sns, **kwargs) # type: ignore
211+
212+
# If solve is successful, dispose of Model object to release license before next rolling horizon
213+
if status == "ok":
214+
dispose_model(n)
215+
216+
# If solve failed, hold on to the Model object until after IIS is computed in solve_network()
180217
if status != "ok":
181218
logger.warning(
182219
f"Optimization failed with status {status} and condition {condition}"
183220
)
184221
# Retry with fallback solver if configured
185222
if fallback_solver:
223+
# If solve failed and fallback is configured, dispose of Model before creating a new one.
224+
dispose_model(n)
225+
186226
logger.info(
187227
f"Retrying window {i + 1}/{len(starting_points)} "
188228
f"with fallback solver '{fallback_solver['name']}'"
@@ -191,6 +231,11 @@ def optimize_with_rolling_horizon(
191231
retry_kwargs["solver_name"] = fallback_solver["name"]
192232
retry_kwargs["solver_options"] = fallback_solver.get("options", {})
193233
status, condition = n.optimize(sns, **retry_kwargs) # type: ignore
234+
235+
# Only dispose after fallback if it succeeded
236+
if status == "ok":
237+
dispose_model(n)
238+
194239
if status != "ok":
195240
logger.warning(f"Fallback also failed: {status} / {condition}")
196241
return status, condition
@@ -281,18 +326,21 @@ def solve_network(
281326
logger.warning(
282327
f"Solving status '{status}' with termination condition '{condition}'"
283328
)
284-
check_objective_value(n, solving)
285-
286-
if "warning" in condition:
287-
raise RuntimeError("Solving status 'warning'. Discarding solution.")
329+
# If infeasible and using Gurobi or Xpress, compute and log infeasibilities before raising error
330+
if "infeasible" in condition:
331+
solver_name = solving["solver"]["name"]
332+
if solver_name in ["gurobi", "xpress"]:
333+
labels = n.model.compute_infeasibilities()
334+
logger.info(f"Labels:\n{labels}")
335+
n.model.print_infeasibilities()
336+
raise RuntimeError(
337+
"Solving status 'infeasible'. Infeasibilities computed."
338+
)
339+
raise RuntimeError(
340+
f"Solving status '{status}' with termination condition '{condition}'."
341+
)
288342

289-
if "infeasible" in condition:
290-
solver_name = solving["solver"]["name"]
291-
if solver_name in ["gurobi", "xpress"]:
292-
labels = n.model.compute_infeasibilities()
293-
logger.info(f"Labels:\n{labels}")
294-
n.model.print_infeasibilities()
295-
raise RuntimeError("Solving status 'infeasible'. Infeasibilities computed.")
343+
check_objective_value(n, solving)
296344

297345

298346
if __name__ == "__main__":

0 commit comments

Comments
 (0)