def enforce(self): """Calls the enforce methods of the constraint handlers for all violated constraints in the registered current instance. The prepare methods of the constraint handlers that are not part of the relaxation are called. The order is given by the enforce priority order (smaller priority first). All indices of constraints belonging to the handler are passed using the sets object. Before calling a plugin function, a current data object is appended to the _curdata stack. Unaccepted user operations: None. :return: A UserInputStatus. """ assert self._cur_instance is not None instance = self._cur_instance # Iterate over all constraint handlers in the specified order. for (_, hdlr) in self._hdlrs_enf: if not hdlr.relax() and instance.nviolated(hdlr) > 0: # Generate constraint handler sets. sets = instance.violated_sets(hdlr) # Only possible to do inside if resolve after every # conshandler that performs changes. model = instance.relax_model() # Set up current data if not already existing. curdata = _CurrentData.create(hdlr) # Call enforce method. self._curdata.append(curdata) self._get_hdlr(hdlr).enforce(sets, model, self._solver) self._curdata.append(curdata) Stats.separate(self, curdata) # Handle user input. if curdata.infeasible: return UserInputStatus.INFEASIBLE elif curdata.branched: self._bnb_tree.register_child_instances(curdata.children) return UserInputStatus.BRANCHED elif curdata.ncuts > 0 or curdata.ntighten > 0: return UserInputStatus.RESOLVE raise UserInputError('Enforcement failed. No constraint handler ' 'called an interface function.')
def _create_result_output(self, filename, is_min, is_quad, fake_bounds, res): """Creates a string summarising the solving process of an instance. """ # Get additional data. if self._sol_file is not None: instance_name = (filename.split('/')[-1]).split('.')[0] lower, upper = self._read_bounds(instance_name) else: upper = 'unknown' lower = 'unknown' out = '\n--------------------------------------------------\n' out += 'Instance: \t {}\n'.format(filename) out += 'Objective: \t {} {}\n'.format('min' if is_min else 'max', 'quad' if is_quad else 'lin') out += 'Upper bound: \t {}\n'.format(upper) out += 'Lower bound: \t {}\n'.format(lower) out += 'Constraints: \t {}\n'.format( Stats.get_bounding_initial_nconss()) out += 'Variables: \t {}\n'.format(res.problem.number_of_variables) out += 'Nonzeros: \t {}\n'.format(res.problem.number_of_nonzeros) out += '\n' out += 'Solver time: \t {:.3f}\n'.format(res.solver.wallclock_time) out += 'Solution status: {}\n'.format( res.solver.termination_condition) out += 'Upper bound: \t {}\n'.format(res.problem.upper_bound) out += 'Lower bound: \t {}\n'.format(res.problem.lower_bound) key = 'Number of created subproblems' nsubprob = res.solver.statistics.branch_and_bound[key] out += 'Created nodes: \t {}\n'.format(nsubprob) key = 'Number of considered subproblems' nsubprob = res.solver.statistics.branch_and_bound[key] out += 'Handled nodes: \t {}\n'.format(nsubprob) out += 'Constraints:\n' hdlr_list = Stats.get_initial_hdlr_constraints() for (hdlr, nconss) in hdlr_list: out += ' {} \t {}\n'.format(nconss, hdlr) out += 'Fake varbounds: {}\n'.format(fake_bounds) out += '--------------------------------------------------\n\n' return out
def identify(self): """Calls the identify methods of the constraint handlers for all unclassified constraints in the registered current instance. The identify methods of the constraint handlers are called. The order is given by the identify priority order (smaller priority first). All indices of unclassified constraints are passed using the sets parameter. Before calling a plugin function, a current data object is appended to the _curdata stack. Unaccepted user operations: Branching, bounds tightening, adding constraints, declaring infeasibility. :return: A UserInputStatus. """ assert self._cur_instance is not None instance = self._cur_instance # Iterate over all constraint handlers in the specified order. for (_, hdlr) in self._hdlrs_ident: # Generate unclassified constraint sets. sets = {} model = instance.model() for constype in hdlr.constypes(): sets[constype] = instance.unclassified(constype) # Set up current data. curdata = _CurrentData.create(hdlr) # Call identify method. self._curdata.append(curdata) self._get_hdlr(hdlr).identify(sets, model, self._solver) self._curdata.pop() Stats.identify(self, curdata) # Handle user input. if curdata.ncuts > 0 or curdata.ntighten > 0 or curdata.branched\ or curdata.infeasible: raise UserInputError('Unallowed interface operation during ' 'identify process.') if instance.nunclassified() == 0: return UserInputStatus.OK else: raise UserInputError('After calling all constraint handlers, ' 'there are still unclassified constraints.')
def solve(self, py_model): """Initiates the solving process. Calls the branch and bound algorithm. :param py_model: Model to solve. """ Stats.initialise(verbosity=self._verbosity) self._py_model = py_model # Create instance. instance = Instance.create_instance(py_model.clone()) # Create bnb_tree. self._bnb_tree = BranchAndBound.create(self) self._bnb_tree.register_root_instance(instance) # Check sanity of user setup. self._check_setup_sanity(instance) # Start solving process. self._stage = SolvingStage.SOLVING res = self._bnb_tree.execute(self._gap_epsilon) self._stage = SolvingStage.DONE self._postprocess(res) self._stage = SolvingStage.SETUP return self._result
def prepare(self): """Calls the prepare methods of the constraint handlers for all of their constraints in the registered current instance. The prepare methods of the constraint handlers are called. The order is given by the enforce priority order (smaller priority first). All indices of constraints belonging to the handler are passed using the sets parameter. Before calling a plugin function, a current data object is appended to the _curdata stack. Unaccepted user operations: None. :return: A UserInputStatus. """ assert self._cur_instance is not None instance = self._cur_instance # Iterate over all constraint handlers in the specified order. for (_, hdlr) in self._hdlrs_enf: # Generate constraint handler sets. sets = instance.conshandler_sets(hdlr) model = instance.model() # Set up current data. curdata = _CurrentData.create(hdlr) # Call prepare method. self._curdata.append(curdata) self._get_hdlr(hdlr).prepare(sets, model, self._solver) self._curdata.pop() Stats.prepare(self, curdata) # Handle user input. if curdata.infeasible: return UserInputStatus.INFEASIBLE elif curdata.branched: return UserInputStatus.BRANCHED elif curdata.nmatched > 0: raise UserInputError('Unallowed interface operation during ' 'prepare process.') return UserInputStatus.OK
def solving_stage_test(self): """Test the call of different interface functions in the solving stage. """ self.solver = PyMINLP() solver = self.solver self.coord = solver._coordinator coord = self.coord coord._bnb_tree = BranchAndBound.create(coord) Stats.initialise(1) # Test identify. solver.use_constraint_handler('INTTEST_Hdlr_1', ['Data'], 1, 1, True) solver.use_constraint_handler('INTTEST_Hdlr_2', ['InfDynamics', 'SusDynamics'], 2, 2) coord._stage = SolvingStage.SOLVING self.assertRaises(SolvingStageError, solver.set_epsilon) coord.set_instance(self.int_inst) self.assertRaises(UserInputError, coord.identify) # Test prepare. INTTEST_Hdlr_1._mode = 1 INTTEST_Hdlr_2._mode = 1 self.assertEqual(coord.prepare(), UserInputStatus.BRANCHED) INTTEST_Hdlr_1._mode = 2 INTTEST_Hdlr_2._mode = 2 self.assertEqual(coord.prepare(), UserInputStatus.INFEASIBLE) INTTEST_Hdlr_1._mode = 3 INTTEST_Hdlr_2._mode = 3 self.assertEqual(coord.prepare(), UserInputStatus.OK) INTTEST_Hdlr_1._mode = 4 INTTEST_Hdlr_2._mode = 4 self.assertRaises(UserInputError, coord.prepare)
def execute(self, epsilon=None): """Performs the branch and bound algorithm. Precondition: An instance is registered. :param epsilon: The gap epsilon for finishing the solving process. :return: The result (BnBResult).""" assert self._root_node is not None Stats.start_bnb(self) # Initialize data. self._lower_bound = -np.inf self._upper_bound = np.inf open_nodes = self._open_nodes heapq.heappush(open_nodes, (self._root_node.lower_bound, self._root_node)) self._node_count = 0 # Outer loop for all generated nodes. while len(open_nodes) > 0: # Update global lower bound if possible. lower, _ = open_nodes[0] if lower > self._lower_bound: self._lower_bound = lower # Check if current solution is good enough. if epsilon is not None \ and self._upper_bound - self._lower_bound < epsilon: break # Perform node selection and register the chosen instance. # TODO maybe offer a plugin for this. _, self._cur_node = heapq.heappop(open_nodes) node = self._cur_node self._interface.set_instance(node.instance) self._child_nodes = [] self._node_count += 1 # Read the minimal lower bound of all other nodes. if len(open_nodes) > 0: minimal_lower, _ = open_nodes[0] else: minimal_lower = None # Check time limit. if self._interface.time_limit_reached(): Stats.finish_bnb(self) return BnBResult.TIMEOUT Stats.start_node(self) # Assign all constraints of this node to a constraint # handler. result = self._interface.identify() if result != UserInputStatus.OK: raise ValueError('Status after identify method call is {}. ' 'Expected OK.'.format(result)) # Call preparation routines. result = self._interface.prepare() if result == UserInputStatus.OK: node_done = False elif result == UserInputStatus.INFEASIBLE: node_done = True elif result == UserInputStatus.BRANCHED: if len(self._child_nodes) == 0: raise ValueError('Branching performed but no child nodes ' 'were given.') for node in self._child_nodes: heapq.heappush(self._open_nodes, (node.lower_bound, node)) self._cur_node.set_branched() node_done = True else: raise ValueError('Status after preparation method call is {}. ' 'Expected OK, INFEASIBLE, ' 'BRANCHED.'.format(result)) # Inner loop for a single node. while not node_done: # Check time limit. if self._interface.time_limit_reached(): Stats.finish_bnb(self) return BnBResult.TIMEOUT Stats.start_rel_sol(self) # Solve relaxation. result = self._interface.solve_relaxation() # Handle the user input. if result != UserInputStatus.OK: raise ValueError( 'Status after solve_relaxation method call is {}. ' 'Expected OK.'.format(result)) Stats.finish_rel_sol(self) # Evaluate result of the solution of the relaxation. if node.has_optimal_solution(): node.update_lower_bound() # Update global lower bound if possible. if node.lower_bound > self._lower_bound: if minimal_lower is None: self._lower_bound = node.lower_bound elif minimal_lower > self._lower_bound: self._lower_bound = min(minimal_lower, node.lower_bound) elif node.relax_infeasible(): break else: raise ValueError( 'The relaxation solver terminated with ' 'termination condition {}.'.format( node.instance.relax_termination_condition())) # Check dual bound. sol = node.rel_sol_value() if sol >= self._upper_bound: # Pruning. node_done = True else: # Feasibility checking. if node.feasible(): node_done = True # Update the global bounds. self._upper_bound = sol self._best_node = node # Check whether new solution prunes any other # nodes. del_count = 0 for i in reversed(range(len(open_nodes))): _, open_node = open_nodes[i] if open_node.lower_bound >= self._upper_bound: open_nodes.pop(i) del_count += 1 else: # Enforce the violated constraints. result = self._interface.enforce() # Handle user decision. if result == UserInputStatus.INFEASIBLE: node_done = True elif result == UserInputStatus.BRANCHED: if len(self._child_nodes) == 0: raise ValueError('Branching performed but no ' 'child nodes were given.') for node in self._child_nodes: heapq.heappush(self._open_nodes, (node.lower_bound, node)) self._cur_node.set_branched() node_done = True elif result == UserInputStatus.RESOLVE: node_done = False else: raise ValueError( 'Status after solve_relaxation method' 'call is {}. Expected OK, RESOLVE ' 'or INFEASIBLE.'.format(result)) Stats.finish_node(self) if len(open_nodes) == 0 and self._best_node is not None: self._lower_bound = self._best_node.lower_bound Stats.finish_bnb(self) if self._best_node is None: return BnBResult.INFEASIBLE else: return BnBResult.OPTIMAL
def _postprocess(self, result): """Create a SolverResult object, save it as _result and write solution data onto the user given Pyomo object (_py_model). :param result: The BnBResult indicating the result of the branch and bound process. """ assert self._stage == SolvingStage.DONE assert type(result) is BnBResult # Copy solution to model. optinst = self._bnb_tree.best_instance() if optinst is not None: optinst.write_solution(self._py_model) # Create result object. self._result = SolverResults() soln = Solution() # Set some result meta data. self._result.solver.name = ('PyMINLP') self._result.solver.wallclock_time = Stats.get_solver_time() # Set solver status. if result == BnBResult.INFEASIBLE: self._result.solver.status = SolverStatus.ok self._result.solver.termination_condition \ = TerminationCondition.infeasible soln.status = SolutionStatus.infeasible elif result == BnBResult.OPTIMAL: self._result.solver.status = SolverStatus.ok self._result.solver.termination_condition \ = TerminationCondition.optimal soln.status = SolutionStatus.optimal elif result == BnBResult.TIMEOUT: self._result.solver.status = SolverStatus.ok self._result.solver.termination_condition \ = TerminationCondition.maxTimeLimit soln.status = SolutionStatus.stoppedByLimit if result == BnBResult.OPTIMAL or result == BnBResult.TIMEOUT: # Set solution data. self._result.problem.sense = pyomo.core.kernel.minimize self._result.problem.lower_bound = self._bnb_tree.lower_bound() self._result.problem.upper_bound = self._bnb_tree.upper_bound() soln.gap = self._bnb_tree.upper_bound() \ - self._bnb_tree.lower_bound() if optinst is not None: # Set objective data. obj = self._py_model.component_objects(Objective) for o in obj: obj_name = o.name obj_val = value(o) break soln.objective[obj_name] = {'Value': obj_val} # Set variable data. #for vartype in self._py_model.component_objects(Var): # for v in vartype: # name = vartype[v].name # val = value(vartype[v]) # soln.variable[name] = {'Value': val} # Set problem instance data. self._result.problem.name = self._py_model.name self._result.problem.number_of_constraints = \ self._py_model.nconstraints() # self._result.problem.number_of_nonzeros = None self._result.problem.number_of_variables = self._py_model.nvariables() # self._result.problem.number_of_binary_variables = None # self._result.problem.number_of_integer_variables = None # self._result.problem.number_of_continuous_variables = None self._result.problem.number_of_objectives = \ self._py_model.nobjectives() # Set branch and bound data. nsubprob = 'Number of created subproblems' self._result.solver.statistics.branch_and_bound[nsubprob] = \ self._bnb_tree.nopennodes() + self._bnb_tree.nconsiderednodes() nsubprob = 'Number of considered subproblems' self._result.solver.statistics.branch_and_bound[nsubprob] = \ self._bnb_tree.nconsiderednodes() self._result.solution.insert(soln)
def time_limit_reached(self): return Stats.get_solver_time() > self._time_limit