Skip to content

Commit 6ea300f

Browse files
fix: CBC output parsing, parse runtime & duality gap (#446)
* Fix CBC output parsing and parse runtime & duality gap * Add release note
1 parent 353d45f commit 6ea300f

File tree

2 files changed

+29
-9
lines changed

2 files changed

+29
-9
lines changed

doc/release_notes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Upcoming Version
66

77
**Bug Fixes**
88

9+
* Fix the parsing of solutions returned by the CBC solver when setting a MIP duality
10+
gap tolerance.
911
* Improve the mapping of termination conditions for the SCIP solver
1012

1113
Version 0.5.3

linopy/solvers.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import subprocess as sub
1515
import sys
1616
from abc import ABC, abstractmethod
17+
from collections import namedtuple
1718
from collections.abc import Generator
1819
from pathlib import Path
1920
from typing import TYPE_CHECKING, Any, Callable
@@ -438,28 +439,32 @@ def solve_problem_from_file(
438439
p.stdout.close()
439440
p.wait()
440441
else:
441-
log_f = open(log_fn, "w")
442-
p = sub.Popen(command.split(" "), stdout=log_f, stderr=log_f)
443-
p.wait()
442+
with open(log_fn, "w") as log_f:
443+
p = sub.Popen(command.split(" "), stdout=log_f, stderr=log_f)
444+
p.wait()
444445

445446
with open(solution_fn) as f:
446-
data = f.readline()
447+
first_line = f.readline()
447448

448-
if data.startswith("Optimal - objective value"):
449+
if first_line.startswith("Optimal "):
449450
status = Status.from_termination_condition("optimal")
450-
elif "Infeasible" in data:
451+
elif "Infeasible" in first_line:
451452
status = Status.from_termination_condition("infeasible")
452453
else:
453454
status = Status(SolverStatus.warning, TerminationCondition.unknown)
454-
status.legacy_status = data
455+
status.legacy_status = first_line
455456

456457
# Use HiGHS to parse the problem file and find the set of variable names, needed to parse solution
457458
h = highspy.Highs()
458459
h.readModel(path_to_string(problem_fn))
459460
variables = {v.name for v in h.getVariables()}
460461

461462
def get_solver_solution() -> Solution:
462-
objective = float(data[len("Optimal - objective value ") :])
463+
m = re.match(r"Optimal.* - objective value (\d+\.?\d*)$", first_line)
464+
if m and len(m.groups()) == 1:
465+
objective = float(m.group(1))
466+
else:
467+
objective = np.nan
463468

464469
with open(solution_fn, "rb") as f:
465470
trimmed_sol_fn = re.sub(rb"\*\*\s+", b"", f.read())
@@ -482,7 +487,20 @@ def get_solver_solution() -> Solution:
482487
solution = self.safe_get_solution(status=status, func=get_solver_solution)
483488
solution = maybe_adjust_objective_sign(solution, io_api, sense)
484489

485-
return Result(status, solution)
490+
# Parse the output and get duality gap and solver runtime
491+
mip_gap, runtime = None, None
492+
if log_fn is not None:
493+
with open(log_fn) as log_f:
494+
output = "".join(log_f.readlines())
495+
m = re.search(r"\nGap: +(\d+\.?\d*)\n", output)
496+
if m and len(m.groups()) == 1:
497+
mip_gap = float(m.group(1))
498+
m = re.search(r"\nTime \(Wallclock seconds\): +(\d+\.?\d*)\n", output)
499+
if m and len(m.groups()) == 1:
500+
runtime = float(m.group(1))
501+
CbcModel = namedtuple("CbcModel", ["mip_gap", "runtime"])
502+
503+
return Result(status, solution, CbcModel(mip_gap, runtime))
486504

487505

488506
class GLPK(Solver):

0 commit comments

Comments
 (0)