Source code for benderslib.result
# coding:utf-8
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2021-2026 Peng-Hui Guo <[email protected]>
import json
from dataclasses import dataclass, field, asdict
from .consts import BendersConsts as CST
[docs]
@dataclass
class BendersResult:
"""Results and statistics from the Benders Decomposition process.
Example
-----------
.. code-block:: python
BD = BendersSolver(...)
BD.solve()
print(BD.result.obj)
"""
lb: float = -float('Inf')
"""Lower bound on the objective value."""
lb_list: list = field(default_factory=list)
"""List of lower bounds over iterations."""
ub: float = float('Inf')
"""Upper bound on the objective value."""
ub_list: list = field(default_factory=list)
"""List of upper bounds over iterations."""
obj: float = float('Inf')
"""Best objective value found."""
obj_list: list = field(default_factory=list)
"""List of objective values of incumbent solutions found over iterations."""
gap_abs: float = float('Inf')
"""Absolute gap between upper and lower bounds, defined as ``abs(ub - lb)``."""
gap: float = float('Inf')
"""Relative gap between upper and lower bounds, defined as ``abs(ub - lb) / abs(ub)``."""
n_sol: int = 0
"""Number of feasible solutions found."""
n_iter: int = 0
"""Number of Benders iterations performed."""
runtime: float = 0.0
"""Total runtime of the Benders decomposition process."""
runtime_master: float = 0.0
"""Total runtime spent solving the master problem."""
runtime_sub: float = 0.0
"""Total runtime spent solving the subproblem."""
n_opt_cuts: int = 0
"""Number of optimality cuts added."""
n_feas_cuts: int = 0
"""Number of feasibility cuts added."""
n_cuts: int = 0
"""Total number of optimality cuts and feasibility cuts added."""
status = CST.UNSOLVED
"""Final status of the Benders decomposition process, see :class:`BendersConsts` for possible values."""
# Values of decision variables in the best solution
solution: dict = field(default_factory=dict)
"""Dictionary of variable names to their values in the best solution found."""
[docs]
def save(self, filename: str) -> None:
"""Save the result to a JSON file.
Parameters
-----------
filename : str
Path to the JSON file where the result will be saved.
"""
with open(filename, 'w') as f:
data = asdict(self)
if self.solution:
is_nested = isinstance(next(iter(self.solution.values())), dict)
if is_nested:
data['solution'] = {
s: {k: v for k, v in sol.items() if v != 0.0}
for s, sol in self.solution.items()
}
else:
data['solution'] = {k: v for k, v in self.solution.items() if v != 0.0}
json.dump(data, f, indent=4)
def __str__(self):
summary = (
f"Benders Result:\n"
f" - {'Status:'.ljust(CST.LOG_NAME_WIDTH)}{self.status}\n"
f" - {'Incumbent:'.ljust(CST.LOG_NAME_WIDTH)}{self.obj:.10e}\n"
f" - {'Bound:'.ljust(CST.LOG_NAME_WIDTH)}{self.lb:.10e}\n"
f" - {'Gap (abs.):'.ljust(CST.LOG_NAME_WIDTH)}{self.gap_abs:.10f}\n"
f" - {'Gap (rel.):'.ljust(CST.LOG_NAME_WIDTH)}{self.gap:.8%}\n"
f" - {'Solutions No.:'.ljust(CST.LOG_NAME_WIDTH)}{self.n_sol}\n"
f" - {'Iteration No.:'.ljust(CST.LOG_NAME_WIDTH)}{self.n_iter}\n"
f" - {'Cuts No.:'.ljust(CST.LOG_NAME_WIDTH)}{self.n_opt_cuts + self.n_feas_cuts}"
f" [Optimality: {self.n_opt_cuts}, Feasibility: {self.n_feas_cuts}]\n"
f" - {'Solve Time (sec.):'.ljust(CST.LOG_NAME_WIDTH)}{self.runtime:.4f}"
f" [Master: {self.runtime_master:.4f}, Sub: {self.runtime_sub:.4f}]"
)
return summary