Module miplearn.solvers.learning

Expand source code
#  MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
#  Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
#  Released under the modified BSD license. See COPYING.md for more details.

import gzip
import logging
import os
import pickle
import tempfile
from typing import Optional, List, Any, IO, cast, BinaryIO, Union, Callable, Dict

from p_tqdm import p_map

from miplearn.components.component import Component
from miplearn.components.cuts import UserCutsComponent
from miplearn.components.lazy_dynamic import DynamicLazyConstraintsComponent
from miplearn.components.objective import ObjectiveValueComponent
from miplearn.components.primal import PrimalSolutionComponent
from miplearn.instance import Instance
from miplearn.solvers import _RedirectOutput
from miplearn.solvers.internal import InternalSolver
from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver
from miplearn.types import MIPSolveStats, TrainingSample, LearningSolveStats

logger = logging.getLogger(__name__)


class _GlobalVariables:
    def __init__(self) -> None:
        self.solver: Optional[LearningSolver] = None
        self.instances: Optional[Union[List[str], List[Instance]]] = None
        self.output_filenames: Optional[List[str]] = None
        self.discard_outputs: bool = False


# Global variables used for multiprocessing. Global variables are copied by the
# operating system when the process forks. Local variables are copied through
# serialization, which is a much slower process.
_GLOBAL = [_GlobalVariables()]


def _parallel_solve(idx):
    solver = _GLOBAL[0].solver
    instances = _GLOBAL[0].instances
    output_filenames = _GLOBAL[0].output_filenames
    discard_outputs = _GLOBAL[0].discard_outputs
    if output_filenames is None:
        output_filename = None
    else:
        output_filename = output_filenames[idx]
    stats = solver.solve(
        instances[idx],
        output_filename=output_filename,
        discard_output=discard_outputs,
    )
    return stats, instances[idx]


class LearningSolver:
    """
    Mixed-Integer Linear Programming (MIP) solver that extracts information
    from previous runs and uses Machine Learning methods to accelerate the
    solution of new (yet unseen) instances.

    Parameters
    ----------
    components: List[Component]
        Set of components in the solver. By default, includes
        `ObjectiveValueComponent`, `PrimalSolutionComponent`,
        `DynamicLazyConstraintsComponent` and `UserCutsComponent`.
    mode: str
        If "exact", solves problem to optimality, keeping all optimality
        guarantees provided by the MIP solver. If "heuristic", uses machine
        learning more aggressively, and may return suboptimal solutions.
    solver: Callable[[], InternalSolver]
        A callable that constructs the internal solver. If None is provided,
        use GurobiPyomoSolver.
    use_lazy_cb: bool
        If true, use native solver callbacks for enforcing lazy constraints,
        instead of a simple loop. May not be supported by all solvers.
    solve_lp_first: bool
        If true, solve LP relaxation first, then solve original MILP. This
        option should be activated if the LP relaxation is not very
        expensive to solve and if it provides good hints for the integer
        solution.
    simulate_perfect: bool
        If true, each call to solve actually performs three actions: solve
        the original problem, train the ML models on the data that was just
        collected, and solve the problem again. This is useful for evaluating
        the theoretical performance of perfect ML models.
    """

    def __init__(
        self,
        components: List[Component] = None,
        mode: str = "exact",
        solver: Callable[[], InternalSolver] = None,
        use_lazy_cb: bool = False,
        solve_lp_first: bool = True,
        simulate_perfect: bool = False,
    ):
        if solver is None:
            solver = GurobiPyomoSolver
        assert callable(solver), f"Callable expected. Found {solver.__class__} instead."
        self.components: Dict[str, Component] = {}
        self.internal_solver: Optional[InternalSolver] = None
        self.mode: str = mode
        self.simulate_perfect: bool = simulate_perfect
        self.solve_lp_first: bool = solve_lp_first
        self.solver_factory: Callable[[], InternalSolver] = solver
        self.tee = False
        self.use_lazy_cb: bool = use_lazy_cb
        if components is not None:
            for comp in components:
                self._add_component(comp)
        else:
            self._add_component(ObjectiveValueComponent())
            self._add_component(PrimalSolutionComponent(mode=mode))
            self._add_component(DynamicLazyConstraintsComponent())
            self._add_component(UserCutsComponent())
        assert self.mode in ["exact", "heuristic"]

    def _solve(
        self,
        instance: Union[Instance, str],
        model: Any = None,
        output_filename: Optional[str] = None,
        discard_output: bool = False,
        tee: bool = False,
    ) -> LearningSolveStats:

        # Load instance from file, if necessary
        filename = None
        fileformat = None
        file: Union[BinaryIO, gzip.GzipFile]
        if isinstance(instance, str):
            filename = instance
            logger.info("Reading: %s" % filename)
            if filename.endswith(".gz"):
                fileformat = "pickle-gz"
                with gzip.GzipFile(filename, "rb") as file:
                    instance = pickle.load(cast(IO[bytes], file))
            else:
                fileformat = "pickle"
                with open(filename, "rb") as file:
                    instance = pickle.load(cast(IO[bytes], file))
        assert isinstance(instance, Instance)

        # Generate model
        if model is None:
            with _RedirectOutput([]):
                model = instance.to_model()

        # Initialize training sample
        training_sample: TrainingSample = {}
        if not hasattr(instance, "training_data"):
            instance.training_data = []
        instance.training_data += [training_sample]

        # Initialize internal solver
        self.tee = tee
        self.internal_solver = self.solver_factory()
        assert self.internal_solver is not None
        assert isinstance(self.internal_solver, InternalSolver)
        self.internal_solver.set_instance(instance, model)

        # Solve linear relaxation
        if self.solve_lp_first:
            logger.info("Solving LP relaxation...")
            lp_stats = self.internal_solver.solve_lp(tee=tee)
            training_sample["LP solution"] = self.internal_solver.get_solution()
            training_sample["LP value"] = lp_stats["Optimal value"]
            training_sample["LP log"] = lp_stats["Log"]
        else:
            training_sample["LP solution"] = self.internal_solver.get_empty_solution()
            training_sample["LP value"] = 0.0

        # Before-solve callbacks
        logger.debug("Running before_solve callbacks...")
        for component in self.components.values():
            component.before_solve(self, instance, model)

        # Define wrappers
        def iteration_cb_wrapper() -> bool:
            should_repeat = False
            assert isinstance(instance, Instance)
            for comp in self.components.values():
                if comp.iteration_cb(self, instance, model):
                    should_repeat = True
            return should_repeat

        def lazy_cb_wrapper(
            cb_solver: LearningSolver,
            cb_model: Any,
        ) -> None:
            assert isinstance(instance, Instance)
            for comp in self.components.values():
                comp.lazy_cb(self, instance, model)

        lazy_cb = None
        if self.use_lazy_cb:
            lazy_cb = lazy_cb_wrapper

        # Solve MILP
        logger.info("Solving MILP...")
        stats = cast(
            LearningSolveStats,
            self.internal_solver.solve(
                tee=tee,
                iteration_cb=iteration_cb_wrapper,
                lazy_cb=lazy_cb,
            ),
        )
        if "LP value" in training_sample.keys():
            stats["LP value"] = training_sample["LP value"]
        stats["Solver"] = "default"
        stats["Gap"] = self._compute_gap(
            ub=stats["Upper bound"],
            lb=stats["Lower bound"],
        )
        stats["Mode"] = self.mode

        # Add some information to training_sample
        training_sample["Lower bound"] = stats["Lower bound"]
        training_sample["Upper bound"] = stats["Upper bound"]
        training_sample["MIP log"] = stats["Log"]
        training_sample["Solution"] = self.internal_solver.get_solution()

        # After-solve callbacks
        logger.debug("Calling after_solve callbacks...")
        for component in self.components.values():
            component.after_solve(self, instance, model, stats, training_sample)

        # Write to file, if necessary
        if not discard_output and filename is not None:
            if output_filename is None:
                output_filename = filename
            logger.info("Writing: %s" % output_filename)
            if fileformat == "pickle":
                with open(output_filename, "wb") as file:
                    pickle.dump(instance, cast(IO[bytes], file))
            else:
                with gzip.GzipFile(output_filename, "wb") as file:
                    pickle.dump(instance, cast(IO[bytes], file))
        return stats

    def solve(
        self,
        instance: Union[Instance, str],
        model: Any = None,
        output_filename: Optional[str] = None,
        discard_output: bool = False,
        tee: bool = False,
    ) -> LearningSolveStats:
        """
        Solves the given instance. If trained machine-learning models are
        available, they will be used to accelerate the solution process.

        The argument `instance` may be either an Instance object or a
        filename pointing to a pickled Instance object.

        This method adds a new training sample to `instance.training_sample`.
        If a filename is provided, then the file is modified in-place. That is,
        the original file is overwritten.

        If `solver.solve_lp_first` is False, the properties lp_solution and
        lp_value will be set to dummy values.

        Parameters
        ----------
        instance: Union[Instance, str]
            The instance to be solved, or a filename.
        model: Any
            The corresponding Pyomo model. If not provided, it will be created.
        output_filename: Optional[str]
            If instance is a filename and output_filename is provided, write the
            modified instance to this file, instead of replacing the original one. If
            output_filename is None (the default), modified the original file in-place.
        discard_output: bool
            If True, do not write the modified instances anywhere; simply discard
            them. Useful during benchmarking.
        tee: bool
            If true, prints solver log to screen.

        Returns
        -------
        LearningSolveStats
            A dictionary of solver statistics containing at least the following
            keys: "Lower bound", "Upper bound", "Wallclock time", "Nodes",
            "Sense", "Log", "Warm start value" and "LP value".

            Additional components may generate additional keys. For example,
            ObjectiveValueComponent adds the keys "Predicted LB" and
            "Predicted UB". See the documentation of each component for more
            details.
        """
        if self.simulate_perfect:
            if not isinstance(instance, str):
                raise Exception("Not implemented")
            with tempfile.NamedTemporaryFile(suffix=os.path.basename(instance)) as tmp:
                self._solve(
                    instance=instance,
                    model=model,
                    output_filename=tmp.name,
                    tee=tee,
                )
                self.fit([tmp.name])
        return self._solve(
            instance=instance,
            model=model,
            output_filename=output_filename,
            discard_output=discard_output,
            tee=tee,
        )

    def parallel_solve(
        self,
        instances: Union[List[str], List[Instance]],
        n_jobs: int = 4,
        label: str = "Solve",
        output_filenames: Optional[List[str]] = None,
        discard_outputs: bool = False,
    ) -> List[LearningSolveStats]:
        """
        Solves multiple instances in parallel.

        This method is equivalent to calling `solve` for each item on the list,
        but it processes multiple instances at the same time. Like `solve`, this
        method modifies each instance in place. Also like `solve`, a list of
        filenames may be provided.

        Parameters
        ----------
        output_filenames: Optional[List[str]]
            If instances are file names and output_filenames is provided, write the
            modified instances to these files, instead of replacing the original
            files. If output_filenames is None, modifies the instances in-place.
        discard_outputs: bool
            If True, do not write the modified instances anywhere; simply discard
            them instead. Useful during benchmarking.
        label: str
            Label to show in the progress bar.
        instances: Union[List[str], List[Instance]]
            The instances to be solved
        n_jobs: int
            Number of instances to solve in parallel at a time.

        Returns
        -------
        List[LearningSolveStats]
            List of solver statistics, with one entry for each provided instance.
            The list is the same you would obtain by calling
            `[solver.solve(p) for p in instances]`
        """
        self.internal_solver = None
        self._silence_miplearn_logger()
        _GLOBAL[0].solver = self
        _GLOBAL[0].output_filenames = output_filenames
        _GLOBAL[0].instances = instances
        _GLOBAL[0].discard_outputs = discard_outputs
        results = p_map(
            _parallel_solve,
            list(range(len(instances))),
            num_cpus=n_jobs,
            desc=label,
        )
        stats = []
        for (idx, (s, instance)) in enumerate(results):
            stats.append(s)
            instances[idx] = instance
        self._restore_miplearn_logger()
        return stats

    def fit(self, training_instances: Union[List[str], List[Instance]]) -> None:
        if len(training_instances) == 0:
            return
        for component in self.components.values():
            component.fit(training_instances)

    def _add_component(self, component: Component) -> None:
        name = component.__class__.__name__
        self.components[name] = component

    def _silence_miplearn_logger(self) -> None:
        miplearn_logger = logging.getLogger("miplearn")
        self.prev_log_level = miplearn_logger.getEffectiveLevel()
        miplearn_logger.setLevel(logging.WARNING)

    def _restore_miplearn_logger(self) -> None:
        miplearn_logger = logging.getLogger("miplearn")
        miplearn_logger.setLevel(self.prev_log_level)

    def __getstate__(self) -> Dict:
        self.internal_solver = None
        return self.__dict__

    @staticmethod
    def _compute_gap(ub: Optional[float], lb: Optional[float]) -> Optional[float]:
        if lb is None or ub is None or lb * ub < 0:
            # solver did not find a solution and/or bound
            return None
        elif abs(ub - lb) < 1e-6:
            # avoid division by zero when ub = lb = 0
            return 0.0
        else:
            # divide by max(abs(ub),abs(lb)) to ensure gap <= 1
            return (ub - lb) / max(abs(ub), abs(lb))

Classes

class LearningSolver (components=None, mode='exact', solver=None, use_lazy_cb=False, solve_lp_first=True, simulate_perfect=False)

Mixed-Integer Linear Programming (MIP) solver that extracts information from previous runs and uses Machine Learning methods to accelerate the solution of new (yet unseen) instances.

Parameters

components : List[Component]
Set of components in the solver. By default, includes ObjectiveValueComponent, PrimalSolutionComponent, DynamicLazyConstraintsComponent and UserCutsComponent.
mode : str
If "exact", solves problem to optimality, keeping all optimality guarantees provided by the MIP solver. If "heuristic", uses machine learning more aggressively, and may return suboptimal solutions.
solver : Callable[[], InternalSolver]
A callable that constructs the internal solver. If None is provided, use GurobiPyomoSolver.
use_lazy_cb : bool
If true, use native solver callbacks for enforcing lazy constraints, instead of a simple loop. May not be supported by all solvers.
solve_lp_first : bool
If true, solve LP relaxation first, then solve original MILP. This option should be activated if the LP relaxation is not very expensive to solve and if it provides good hints for the integer solution.
simulate_perfect : bool
If true, each call to solve actually performs three actions: solve the original problem, train the ML models on the data that was just collected, and solve the problem again. This is useful for evaluating the theoretical performance of perfect ML models.
Expand source code
class LearningSolver:
    """
    Mixed-Integer Linear Programming (MIP) solver that extracts information
    from previous runs and uses Machine Learning methods to accelerate the
    solution of new (yet unseen) instances.

    Parameters
    ----------
    components: List[Component]
        Set of components in the solver. By default, includes
        `ObjectiveValueComponent`, `PrimalSolutionComponent`,
        `DynamicLazyConstraintsComponent` and `UserCutsComponent`.
    mode: str
        If "exact", solves problem to optimality, keeping all optimality
        guarantees provided by the MIP solver. If "heuristic", uses machine
        learning more aggressively, and may return suboptimal solutions.
    solver: Callable[[], InternalSolver]
        A callable that constructs the internal solver. If None is provided,
        use GurobiPyomoSolver.
    use_lazy_cb: bool
        If true, use native solver callbacks for enforcing lazy constraints,
        instead of a simple loop. May not be supported by all solvers.
    solve_lp_first: bool
        If true, solve LP relaxation first, then solve original MILP. This
        option should be activated if the LP relaxation is not very
        expensive to solve and if it provides good hints for the integer
        solution.
    simulate_perfect: bool
        If true, each call to solve actually performs three actions: solve
        the original problem, train the ML models on the data that was just
        collected, and solve the problem again. This is useful for evaluating
        the theoretical performance of perfect ML models.
    """

    def __init__(
        self,
        components: List[Component] = None,
        mode: str = "exact",
        solver: Callable[[], InternalSolver] = None,
        use_lazy_cb: bool = False,
        solve_lp_first: bool = True,
        simulate_perfect: bool = False,
    ):
        if solver is None:
            solver = GurobiPyomoSolver
        assert callable(solver), f"Callable expected. Found {solver.__class__} instead."
        self.components: Dict[str, Component] = {}
        self.internal_solver: Optional[InternalSolver] = None
        self.mode: str = mode
        self.simulate_perfect: bool = simulate_perfect
        self.solve_lp_first: bool = solve_lp_first
        self.solver_factory: Callable[[], InternalSolver] = solver
        self.tee = False
        self.use_lazy_cb: bool = use_lazy_cb
        if components is not None:
            for comp in components:
                self._add_component(comp)
        else:
            self._add_component(ObjectiveValueComponent())
            self._add_component(PrimalSolutionComponent(mode=mode))
            self._add_component(DynamicLazyConstraintsComponent())
            self._add_component(UserCutsComponent())
        assert self.mode in ["exact", "heuristic"]

    def _solve(
        self,
        instance: Union[Instance, str],
        model: Any = None,
        output_filename: Optional[str] = None,
        discard_output: bool = False,
        tee: bool = False,
    ) -> LearningSolveStats:

        # Load instance from file, if necessary
        filename = None
        fileformat = None
        file: Union[BinaryIO, gzip.GzipFile]
        if isinstance(instance, str):
            filename = instance
            logger.info("Reading: %s" % filename)
            if filename.endswith(".gz"):
                fileformat = "pickle-gz"
                with gzip.GzipFile(filename, "rb") as file:
                    instance = pickle.load(cast(IO[bytes], file))
            else:
                fileformat = "pickle"
                with open(filename, "rb") as file:
                    instance = pickle.load(cast(IO[bytes], file))
        assert isinstance(instance, Instance)

        # Generate model
        if model is None:
            with _RedirectOutput([]):
                model = instance.to_model()

        # Initialize training sample
        training_sample: TrainingSample = {}
        if not hasattr(instance, "training_data"):
            instance.training_data = []
        instance.training_data += [training_sample]

        # Initialize internal solver
        self.tee = tee
        self.internal_solver = self.solver_factory()
        assert self.internal_solver is not None
        assert isinstance(self.internal_solver, InternalSolver)
        self.internal_solver.set_instance(instance, model)

        # Solve linear relaxation
        if self.solve_lp_first:
            logger.info("Solving LP relaxation...")
            lp_stats = self.internal_solver.solve_lp(tee=tee)
            training_sample["LP solution"] = self.internal_solver.get_solution()
            training_sample["LP value"] = lp_stats["Optimal value"]
            training_sample["LP log"] = lp_stats["Log"]
        else:
            training_sample["LP solution"] = self.internal_solver.get_empty_solution()
            training_sample["LP value"] = 0.0

        # Before-solve callbacks
        logger.debug("Running before_solve callbacks...")
        for component in self.components.values():
            component.before_solve(self, instance, model)

        # Define wrappers
        def iteration_cb_wrapper() -> bool:
            should_repeat = False
            assert isinstance(instance, Instance)
            for comp in self.components.values():
                if comp.iteration_cb(self, instance, model):
                    should_repeat = True
            return should_repeat

        def lazy_cb_wrapper(
            cb_solver: LearningSolver,
            cb_model: Any,
        ) -> None:
            assert isinstance(instance, Instance)
            for comp in self.components.values():
                comp.lazy_cb(self, instance, model)

        lazy_cb = None
        if self.use_lazy_cb:
            lazy_cb = lazy_cb_wrapper

        # Solve MILP
        logger.info("Solving MILP...")
        stats = cast(
            LearningSolveStats,
            self.internal_solver.solve(
                tee=tee,
                iteration_cb=iteration_cb_wrapper,
                lazy_cb=lazy_cb,
            ),
        )
        if "LP value" in training_sample.keys():
            stats["LP value"] = training_sample["LP value"]
        stats["Solver"] = "default"
        stats["Gap"] = self._compute_gap(
            ub=stats["Upper bound"],
            lb=stats["Lower bound"],
        )
        stats["Mode"] = self.mode

        # Add some information to training_sample
        training_sample["Lower bound"] = stats["Lower bound"]
        training_sample["Upper bound"] = stats["Upper bound"]
        training_sample["MIP log"] = stats["Log"]
        training_sample["Solution"] = self.internal_solver.get_solution()

        # After-solve callbacks
        logger.debug("Calling after_solve callbacks...")
        for component in self.components.values():
            component.after_solve(self, instance, model, stats, training_sample)

        # Write to file, if necessary
        if not discard_output and filename is not None:
            if output_filename is None:
                output_filename = filename
            logger.info("Writing: %s" % output_filename)
            if fileformat == "pickle":
                with open(output_filename, "wb") as file:
                    pickle.dump(instance, cast(IO[bytes], file))
            else:
                with gzip.GzipFile(output_filename, "wb") as file:
                    pickle.dump(instance, cast(IO[bytes], file))
        return stats

    def solve(
        self,
        instance: Union[Instance, str],
        model: Any = None,
        output_filename: Optional[str] = None,
        discard_output: bool = False,
        tee: bool = False,
    ) -> LearningSolveStats:
        """
        Solves the given instance. If trained machine-learning models are
        available, they will be used to accelerate the solution process.

        The argument `instance` may be either an Instance object or a
        filename pointing to a pickled Instance object.

        This method adds a new training sample to `instance.training_sample`.
        If a filename is provided, then the file is modified in-place. That is,
        the original file is overwritten.

        If `solver.solve_lp_first` is False, the properties lp_solution and
        lp_value will be set to dummy values.

        Parameters
        ----------
        instance: Union[Instance, str]
            The instance to be solved, or a filename.
        model: Any
            The corresponding Pyomo model. If not provided, it will be created.
        output_filename: Optional[str]
            If instance is a filename and output_filename is provided, write the
            modified instance to this file, instead of replacing the original one. If
            output_filename is None (the default), modified the original file in-place.
        discard_output: bool
            If True, do not write the modified instances anywhere; simply discard
            them. Useful during benchmarking.
        tee: bool
            If true, prints solver log to screen.

        Returns
        -------
        LearningSolveStats
            A dictionary of solver statistics containing at least the following
            keys: "Lower bound", "Upper bound", "Wallclock time", "Nodes",
            "Sense", "Log", "Warm start value" and "LP value".

            Additional components may generate additional keys. For example,
            ObjectiveValueComponent adds the keys "Predicted LB" and
            "Predicted UB". See the documentation of each component for more
            details.
        """
        if self.simulate_perfect:
            if not isinstance(instance, str):
                raise Exception("Not implemented")
            with tempfile.NamedTemporaryFile(suffix=os.path.basename(instance)) as tmp:
                self._solve(
                    instance=instance,
                    model=model,
                    output_filename=tmp.name,
                    tee=tee,
                )
                self.fit([tmp.name])
        return self._solve(
            instance=instance,
            model=model,
            output_filename=output_filename,
            discard_output=discard_output,
            tee=tee,
        )

    def parallel_solve(
        self,
        instances: Union[List[str], List[Instance]],
        n_jobs: int = 4,
        label: str = "Solve",
        output_filenames: Optional[List[str]] = None,
        discard_outputs: bool = False,
    ) -> List[LearningSolveStats]:
        """
        Solves multiple instances in parallel.

        This method is equivalent to calling `solve` for each item on the list,
        but it processes multiple instances at the same time. Like `solve`, this
        method modifies each instance in place. Also like `solve`, a list of
        filenames may be provided.

        Parameters
        ----------
        output_filenames: Optional[List[str]]
            If instances are file names and output_filenames is provided, write the
            modified instances to these files, instead of replacing the original
            files. If output_filenames is None, modifies the instances in-place.
        discard_outputs: bool
            If True, do not write the modified instances anywhere; simply discard
            them instead. Useful during benchmarking.
        label: str
            Label to show in the progress bar.
        instances: Union[List[str], List[Instance]]
            The instances to be solved
        n_jobs: int
            Number of instances to solve in parallel at a time.

        Returns
        -------
        List[LearningSolveStats]
            List of solver statistics, with one entry for each provided instance.
            The list is the same you would obtain by calling
            `[solver.solve(p) for p in instances]`
        """
        self.internal_solver = None
        self._silence_miplearn_logger()
        _GLOBAL[0].solver = self
        _GLOBAL[0].output_filenames = output_filenames
        _GLOBAL[0].instances = instances
        _GLOBAL[0].discard_outputs = discard_outputs
        results = p_map(
            _parallel_solve,
            list(range(len(instances))),
            num_cpus=n_jobs,
            desc=label,
        )
        stats = []
        for (idx, (s, instance)) in enumerate(results):
            stats.append(s)
            instances[idx] = instance
        self._restore_miplearn_logger()
        return stats

    def fit(self, training_instances: Union[List[str], List[Instance]]) -> None:
        if len(training_instances) == 0:
            return
        for component in self.components.values():
            component.fit(training_instances)

    def _add_component(self, component: Component) -> None:
        name = component.__class__.__name__
        self.components[name] = component

    def _silence_miplearn_logger(self) -> None:
        miplearn_logger = logging.getLogger("miplearn")
        self.prev_log_level = miplearn_logger.getEffectiveLevel()
        miplearn_logger.setLevel(logging.WARNING)

    def _restore_miplearn_logger(self) -> None:
        miplearn_logger = logging.getLogger("miplearn")
        miplearn_logger.setLevel(self.prev_log_level)

    def __getstate__(self) -> Dict:
        self.internal_solver = None
        return self.__dict__

    @staticmethod
    def _compute_gap(ub: Optional[float], lb: Optional[float]) -> Optional[float]:
        if lb is None or ub is None or lb * ub < 0:
            # solver did not find a solution and/or bound
            return None
        elif abs(ub - lb) < 1e-6:
            # avoid division by zero when ub = lb = 0
            return 0.0
        else:
            # divide by max(abs(ub),abs(lb)) to ensure gap <= 1
            return (ub - lb) / max(abs(ub), abs(lb))

Methods

def fit(self, training_instances)
Expand source code
def fit(self, training_instances: Union[List[str], List[Instance]]) -> None:
    if len(training_instances) == 0:
        return
    for component in self.components.values():
        component.fit(training_instances)
def parallel_solve(self, instances, n_jobs=4, label='Solve', output_filenames=None, discard_outputs=False)

Solves multiple instances in parallel.

This method is equivalent to calling solve for each item on the list, but it processes multiple instances at the same time. Like solve, this method modifies each instance in place. Also like solve, a list of filenames may be provided.

Parameters

output_filenames : Optional[List[str]]
If instances are file names and output_filenames is provided, write the modified instances to these files, instead of replacing the original files. If output_filenames is None, modifies the instances in-place.
discard_outputs : bool
If True, do not write the modified instances anywhere; simply discard them instead. Useful during benchmarking.
label : str
Label to show in the progress bar.
instances : Union[List[str], List[Instance]]
The instances to be solved
n_jobs : int
Number of instances to solve in parallel at a time.

Returns

List[LearningSolveStats]
List of solver statistics, with one entry for each provided instance. The list is the same you would obtain by calling [solver.solve(p) for p in instances]
Expand source code
def parallel_solve(
    self,
    instances: Union[List[str], List[Instance]],
    n_jobs: int = 4,
    label: str = "Solve",
    output_filenames: Optional[List[str]] = None,
    discard_outputs: bool = False,
) -> List[LearningSolveStats]:
    """
    Solves multiple instances in parallel.

    This method is equivalent to calling `solve` for each item on the list,
    but it processes multiple instances at the same time. Like `solve`, this
    method modifies each instance in place. Also like `solve`, a list of
    filenames may be provided.

    Parameters
    ----------
    output_filenames: Optional[List[str]]
        If instances are file names and output_filenames is provided, write the
        modified instances to these files, instead of replacing the original
        files. If output_filenames is None, modifies the instances in-place.
    discard_outputs: bool
        If True, do not write the modified instances anywhere; simply discard
        them instead. Useful during benchmarking.
    label: str
        Label to show in the progress bar.
    instances: Union[List[str], List[Instance]]
        The instances to be solved
    n_jobs: int
        Number of instances to solve in parallel at a time.

    Returns
    -------
    List[LearningSolveStats]
        List of solver statistics, with one entry for each provided instance.
        The list is the same you would obtain by calling
        `[solver.solve(p) for p in instances]`
    """
    self.internal_solver = None
    self._silence_miplearn_logger()
    _GLOBAL[0].solver = self
    _GLOBAL[0].output_filenames = output_filenames
    _GLOBAL[0].instances = instances
    _GLOBAL[0].discard_outputs = discard_outputs
    results = p_map(
        _parallel_solve,
        list(range(len(instances))),
        num_cpus=n_jobs,
        desc=label,
    )
    stats = []
    for (idx, (s, instance)) in enumerate(results):
        stats.append(s)
        instances[idx] = instance
    self._restore_miplearn_logger()
    return stats
def solve(self, instance, model=None, output_filename=None, discard_output=False, tee=False)

Solves the given instance. If trained machine-learning models are available, they will be used to accelerate the solution process.

The argument instance may be either an Instance object or a filename pointing to a pickled Instance object.

This method adds a new training sample to instance.training_sample. If a filename is provided, then the file is modified in-place. That is, the original file is overwritten.

If solver.solve_lp_first is False, the properties lp_solution and lp_value will be set to dummy values.

Parameters

instance : Union[Instance, str]
The instance to be solved, or a filename.
model : Any
The corresponding Pyomo model. If not provided, it will be created.
output_filename : Optional[str]
If instance is a filename and output_filename is provided, write the modified instance to this file, instead of replacing the original one. If output_filename is None (the default), modified the original file in-place.
discard_output : bool
If True, do not write the modified instances anywhere; simply discard them. Useful during benchmarking.
tee : bool
If true, prints solver log to screen.

Returns

LearningSolveStats

A dictionary of solver statistics containing at least the following keys: "Lower bound", "Upper bound", "Wallclock time", "Nodes", "Sense", "Log", "Warm start value" and "LP value".

Additional components may generate additional keys. For example, ObjectiveValueComponent adds the keys "Predicted LB" and "Predicted UB". See the documentation of each component for more details.

Expand source code
def solve(
    self,
    instance: Union[Instance, str],
    model: Any = None,
    output_filename: Optional[str] = None,
    discard_output: bool = False,
    tee: bool = False,
) -> LearningSolveStats:
    """
    Solves the given instance. If trained machine-learning models are
    available, they will be used to accelerate the solution process.

    The argument `instance` may be either an Instance object or a
    filename pointing to a pickled Instance object.

    This method adds a new training sample to `instance.training_sample`.
    If a filename is provided, then the file is modified in-place. That is,
    the original file is overwritten.

    If `solver.solve_lp_first` is False, the properties lp_solution and
    lp_value will be set to dummy values.

    Parameters
    ----------
    instance: Union[Instance, str]
        The instance to be solved, or a filename.
    model: Any
        The corresponding Pyomo model. If not provided, it will be created.
    output_filename: Optional[str]
        If instance is a filename and output_filename is provided, write the
        modified instance to this file, instead of replacing the original one. If
        output_filename is None (the default), modified the original file in-place.
    discard_output: bool
        If True, do not write the modified instances anywhere; simply discard
        them. Useful during benchmarking.
    tee: bool
        If true, prints solver log to screen.

    Returns
    -------
    LearningSolveStats
        A dictionary of solver statistics containing at least the following
        keys: "Lower bound", "Upper bound", "Wallclock time", "Nodes",
        "Sense", "Log", "Warm start value" and "LP value".

        Additional components may generate additional keys. For example,
        ObjectiveValueComponent adds the keys "Predicted LB" and
        "Predicted UB". See the documentation of each component for more
        details.
    """
    if self.simulate_perfect:
        if not isinstance(instance, str):
            raise Exception("Not implemented")
        with tempfile.NamedTemporaryFile(suffix=os.path.basename(instance)) as tmp:
            self._solve(
                instance=instance,
                model=model,
                output_filename=tmp.name,
                tee=tee,
            )
            self.fit([tmp.name])
    return self._solve(
        instance=instance,
        model=model,
        output_filename=output_filename,
        discard_output=discard_output,
        tee=tee,
    )