# 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 logging from abc import ABC, abstractmethod logger = logging.getLogger(__name__) class ExtractedConstraint(ABC): pass class InternalSolver(ABC): """ Abstract class representing the MIP solver used internally by LearningSolver. """ @abstractmethod def solve_lp(self, tee=False): """ Solves the LP relaxation of the currently loaded instance. After this method finishes, the solution can be retrieved by calling `get_solution`. Parameters ---------- tee: bool If true, prints the solver log to the screen. Returns ------- dict A dictionary of solver statistics containing the following keys: "Optimal value". """ pass @abstractmethod def get_solution(self): """ Returns current solution found by the solver. If called after `solve`, returns the best primal solution found during the search. If called after `solve_lp`, returns the optimal solution to the LP relaxation. The solution is a dictionary `sol`, where the optimal value of `var[idx]` is given by `sol[var][idx]`. """ pass @abstractmethod def set_warm_start(self, solution): """ Sets the warm start to be used by the solver. The solution should be a dictionary following the same format as the one produced by `get_solution`. Only one warm start is supported. Calling this function when a warm start already exists will remove the previous warm start. """ pass @abstractmethod def clear_warm_start(self): """ Removes any existing warm start from the solver. """ pass @abstractmethod def set_instance(self, instance, model=None): """ Loads the given instance into the solver. Parameters ---------- instance: miplearn.Instance The instance to be loaded. model: The concrete optimization model corresponding to this instance (e.g. JuMP.Model or pyomo.core.ConcreteModel). If not provided, it will be generated by calling `instance.to_model()`. """ pass @abstractmethod def fix(self, solution): """ Fixes the values of a subset of decision variables. The values should be provided in the dictionary format generated by `get_solution`. Missing values in the solution indicate variables that should be left free. """ pass @abstractmethod def set_branching_priorities(self, priorities): """ Sets the branching priorities for the given decision variables. When the MIP solver needs to decide on which variable to branch, variables with higher priority are picked first, given that they are fractional. Ties are solved arbitrarily. By default, all variables have priority zero. The priorities should be provided in the dictionary format generated by `get_solution`. Missing values indicate variables whose priorities should not be modified. """ pass @abstractmethod def add_constraint(self, constraint): """ Adds a single constraint to the model. """ pass @abstractmethod def solve(self, tee=False, iteration_cb=None, lazy_cb=None): """ Solves the currently loaded instance. After this method finishes, the best solution found can be retrieved by calling `get_solution`. Parameters ---------- iteration_cb: () -> Bool By default, InternalSolver makes a single call to the native `solve` method and returns the result. If an iteration callback is provided instead, InternalSolver enters a loop, where `solve` and `iteration_cb` are called alternatively. To stop the loop, `iteration_cb` should return False. Any other result causes the solver to loop again. lazy_cb: (internal_solver, model) -> None This function is called whenever the solver finds a new candidate solution and can be used to add lazy constraints to the model. Only the following operations within the callback are allowed: - Querying the value of a variable, through `get_value(var, idx)` - Querying if a constraint is satisfied, through `is_constraint_satisfied(cobj)` - Adding a new constraint to the problem, through `add_constraint` Additional operations may be allowed by specific subclasses. tee: Bool If true, prints the solver log to the screen. Returns ------- dict A dictionary of solver statistics containing the following keys: "Lower bound", "Upper bound", "Wallclock time", "Nodes", "Sense", "Log" and "Warm start value". """ pass @abstractmethod def get_value(self, var_name, index): """ Returns the current value of a decision variable. """ pass @abstractmethod def get_constraint_ids(self): """ Returns a list of ids, which uniquely identify each constraint in the model. """ pass @abstractmethod def extract_constraint(self, cid): """ Removes a given constraint from the model and returns an object `cobj` which can be used to verify if the removed constraint is still satisfied by the current solution, using `is_constraint_satisfied(cobj)`, and can potentially be re-added to the model using `add_constraint(cobj)`. """ pass @abstractmethod def relax(self): """ Drops all integrality constraints from the model. """ pass @abstractmethod def get_constraint_slacks(self): """ Returns a dictionary mapping constraint name to the constraint slack in the current solution. """ pass @abstractmethod def is_constraint_satisfied(self, cobj): pass @abstractmethod def set_constraint_sense(self, cid, sense): pass @abstractmethod def set_constraint_rhs(self, cid, rhs): pass @abstractmethod def set_threads(self, threads): pass @abstractmethod def set_time_limit(self, time_limit): pass @abstractmethod def set_node_limit(self, node_limit): pass @abstractmethod def set_gap_tolerance(self, gap_tolerance): pass @abstractmethod def get_variables(self): pass def get_empty_solution(self): solution = {} for (var, indices) in self.get_variables().items(): solution[var] = {} for idx in indices: solution[var][idx] = 0.0 return solution