Source code for benderslib.utils

# coding:utf-8
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2021-2026 Peng-Hui Guo <[email protected]>

import math


[docs] def draw_curve(result: 'BendersResult') -> None: """Draw the convergence curve of the Benders algorithm. This function plots the lower bound, upper bound, incumbent objective value, and gap over iterations. See this :doc:`example <../examples/basic/annotated_benders>` for a figure generated by this function. Parameters ---------- result : BendersResult The result object containing the convergence history of the Benders algorithm, including lists of lower bounds (``lb_list``), upper bounds (``ub_list``), and incumbent objective values (``obj_list``) at each iteration. Example ---------- .. code-block:: python from benderslib.utils import draw_curve BD = ClassicalBenders(mp, sp, complicating_vars) BD.solve() draw_curve(BD.result) """ try: import matplotlib.pyplot as plt except ImportError: # pragma: no cover raise ImportError("matplotlib is not installed. Please install it with: pip install benderslib[plot]") plt.style.use('seaborn-v0_8') # Draw convergence curve fig, ax1 = plt.subplots(dpi=600) ax1.plot(result.lb_list, label='Lower Bound') ax1.plot(result.ub_list, label='Upper Bound') ax1.plot(result.obj_list, label='Incumbent') ax1.set_xlabel('Iteration') ax1.set_ylabel('Objective') ax1.set_title('Benders Decomposition') ax1.grid(True) # Draw Gap on the right axis ax2 = ax1.twinx() gap = [ (obj - lb) / abs(obj) if abs(obj) > 1e-4 else float('inf') for lb, obj in zip(result.lb_list, result.obj_list) ] ax2.plot(gap, 'k--', label='Gap') ax2.set_ylabel('Gap') ax2.tick_params(axis='y') ax2.set_ylim(0, 1) ax2.grid(None) # To show the legend for the second axis lines, labels = ax1.get_legend_handles_labels() lines2, labels2 = ax2.get_legend_handles_labels() ax2.legend(lines + lines2, labels + labels2, loc='best') plt.tight_layout() plt.show() plt.close(fig)
[docs] def is_all_integer(vals: list[float], tol=1e-5) -> bool: """Check if all values in a list are integers within a specified tolerance. Sometimes solvers may return values that are very close to integers but not exactly integers due to numerical issues. This function checks if each value in the list is within a specified tolerance of an integer, to determine if the solution can be considered as integer feasible. Parameters ---------- vals : list of float The list of values to check. tol : float, optional The tolerance for checking if a value is close enough to an integer. Example ---------- .. code-block:: python from benderslib.utils import is_all_integer vals = [1.0, 2.00001, 3.99999] print(is_all_integer(vals)) """ for v in vals: if abs(v - round(v)) > tol: return False return True
[docs] def normalize_cut(cut: 'Cut', max_norm: float = 1e5) -> 'Cut': """ Normalize a Benders cut if its L2 norm exceeds a threshold. This function scales down the cut's coefficients and right-hand side if the L2 norm of the coefficient vector is greater than ``max_norm``. This can help improve numerical stability in the master problem solver. Parameters ---------- cut : Cut The Benders cut to normalize. max_norm : float, optional The maximum allowed L2 norm for the cut's coefficients. Example ---------- .. code-block:: python from benderslib.utils import normalize_cut cut = Cut( vars=['x1', 'x2', 'x3'], coefs=[2.0e5, 1.0e5, 4.5e6], rhs=1e6, sense=CST.LE ) norm_cut = normalize_cut(cut, max_norm=1e5) """ # L2 norm norm = math.sqrt(sum(c * c for c in cut.coefs)) if norm > max_norm: scale = max_norm / norm cut.coefs = [c * scale for c in cut.coefs] cut.rhs *= scale return cut