1414import subprocess as sub
1515import sys
1616from abc import ABC , abstractmethod
17+ from collections import namedtuple
1718from collections .abc import Generator
1819from pathlib import Path
1920from 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
488506class GLPK (Solver ):
0 commit comments