Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 6 additions & 2 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
Release Notes
=============

.. Upcoming Version
.. ----------------
Upcoming Version
----------------

**Bug Fixes**

* Fix the parsing of solutions returned by the CBC solver when setting a MIP duality
gap tolerance.

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