Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -6,6 +6,8 @@ Upcoming Version

**Bug Fixes**

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

Version 0.5.3
Expand Down
36 changes: 27 additions & 9 deletions linopy/solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import subprocess as sub
import sys
from abc import ABC, abstractmethod
from collections import namedtuple
from collections.abc import Generator
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable
Expand Down Expand Up @@ -438,28 +439,32 @@
p.stdout.close()
p.wait()
else:
log_f = open(log_fn, "w")
p = sub.Popen(command.split(" "), stdout=log_f, stderr=log_f)
p.wait()
with open(log_fn, "w") as log_f:
p = sub.Popen(command.split(" "), stdout=log_f, stderr=log_f)
p.wait()

with open(solution_fn) as f:
data = f.readline()
first_line = f.readline()

if data.startswith("Optimal - objective value"):
if first_line.startswith("Optimal "):
status = Status.from_termination_condition("optimal")
elif "Infeasible" in data:
elif "Infeasible" in first_line:
status = Status.from_termination_condition("infeasible")
else:
status = Status(SolverStatus.warning, TerminationCondition.unknown)
status.legacy_status = data
status.legacy_status = first_line

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

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

Check warning on line 467 in linopy/solvers.py

View check run for this annotation

Codecov / codecov/patch

linopy/solvers.py#L467

Added line #L467 was not covered by tests

with open(solution_fn, "rb") as f:
trimmed_sol_fn = re.sub(rb"\*\*\s+", b"", f.read())
Expand All @@ -482,7 +487,20 @@
solution = self.safe_get_solution(status=status, func=get_solver_solution)
solution = maybe_adjust_objective_sign(solution, io_api, sense)

return Result(status, solution)
# Parse the output and get duality gap and solver runtime
mip_gap, runtime = None, None
if log_fn is not None:
with open(log_fn) as log_f:
output = "".join(log_f.readlines())
m = re.search(r"\nGap: +(\d+\.?\d*)\n", output)
if m and len(m.groups()) == 1:
mip_gap = float(m.group(1))

Check warning on line 497 in linopy/solvers.py

View check run for this annotation

Codecov / codecov/patch

linopy/solvers.py#L497

Added line #L497 was not covered by tests
m = re.search(r"\nTime \(Wallclock seconds\): +(\d+\.?\d*)\n", output)
if m and len(m.groups()) == 1:
runtime = float(m.group(1))
CbcModel = namedtuple("CbcModel", ["mip_gap", "runtime"])

return Result(status, solution, CbcModel(mip_gap, runtime))


class GLPK(Solver):
Expand Down
Loading