def test_inactive_constraints(self): m = ConcreteModel() m.x = Var() m.c1 = Constraint(expr=m.x == 1) m.c2 = Constraint(expr=m.x == 2) m.o = Objective(expr=m.x) self.assertFalse(satisfiable(m)) m.c2.deactivate() self.assertTrue(satisfiable(m))
def test_8PP_deactive(self): exfile = import_file( join(exdir, 'eight_process', 'eight_proc_model.py')) m = exfile.build_eight_process_flowsheet() for djn in m.component_data_objects(ctype=Disjunction): djn.deactivate() self.assertTrue(satisfiable(m) is not False)
def _prescreen_node(node_data, node_model, solve_data): config = solve_data.config # Check node for satisfiability if sat-solver is enabled if config.check_sat and satisfiable(node_model, config.logger) is False: if node_data.node_count == 0: config.logger.info( "Root node is not satisfiable. Problem is infeasible.") else: config.logger.info("SAT solver pruned node %s" % node_data.node_count) new_lb = new_ub = float('inf') else: # Solve model subproblem if config.solve_local_rnGDP: solve_data.config.logger.debug( "Screening node %s with LB %.10g and %s inactive disjunctions." % (node_data.node_count, node_data.obj_lb, node_data.num_unbranched_disjunctions)) new_lb, new_ub = _solve_local_rnGDP_subproblem( node_model, solve_data) else: new_lb, new_ub = float('-inf'), float('inf') new_lb = max(node_data.obj_lb, new_lb) new_node_data = node_data._replace(obj_lb=new_lb, obj_ub=new_ub, is_screened=True) return new_node_data
def test_simple_unsat_model(self): m = ConcreteModel() m.x = Var() m.c1 = Constraint(expr=1 == m.x) m.c2 = Constraint(expr=2 == m.x) m.o = Objective(expr=m.x) self.assertFalse(satisfiable(m))
def test_binary_expressions(self): m = ConcreteModel() m.x = Var() m.y = Var() m.z = Var() m.c = Constraint(expr=0 <= m.x + m.y - m.z * m.y / m.x + 7) m.o = Objective(expr=m.x) self.assertTrue(satisfiable(m))
def test_disjunction_unsat(self): m = ConcreteModel() m.x1 = Var(bounds=(0, 8)) m.x2 = Var(bounds=(0, 8)) m.obj = Objective(expr=m.x1 + m.x2, sense=minimize) m.y1 = Disjunct() m.y2 = Disjunct() m.y1.c1 = Constraint(expr=m.x1 >= 9) m.y1.c2 = Constraint(expr=m.x2 >= 2) m.y2.c1 = Constraint(expr=m.x1 >= 3) m.y2.c2 = Constraint(expr=m.x2 >= 9) m.djn = Disjunction(expr=[m.y1, m.y2]) self.assertFalse(satisfiable(m))
def test_unary_expressions(self): m = ConcreteModel() m.x = Var() m.y = Var() m.z = Var() m.a = Var() m.b = Var() m.c = Var() m.d = Var() m.c1 = Constraint(expr=0 <= sin(m.x)) m.c2 = Constraint(expr=0 <= cos(m.y)) m.c3 = Constraint(expr=0 <= tan(m.z)) m.c4 = Constraint(expr=0 <= asin(m.a)) m.c5 = Constraint(expr=0 <= acos(m.b)) m.c6 = Constraint(expr=0 <= atan(m.c)) m.c7 = Constraint(expr=0 <= sqrt(m.d)) m.o = Objective(expr=m.x) self.assertTrue(satisfiable(m) is not False)
def test_multiple_disjunctions_sat(self): m = ConcreteModel() m.x1 = Var(bounds=(0, 8)) m.x2 = Var(bounds=(0, 8)) m.obj = Objective(expr=m.x1 + m.x2, sense=minimize) m.y1 = Disjunct() m.y2 = Disjunct() m.y1.c1 = Constraint(expr=m.x1 >= 2) m.y1.c2 = Constraint(expr=m.x2 >= 2) m.y2.c1 = Constraint(expr=m.x1 >= 1) m.y2.c2 = Constraint(expr=m.x2 >= 1) m.djn1 = Disjunction(expr=[m.y1, m.y2]) m.z1 = Disjunct() m.z2 = Disjunct() m.z1.c1 = Constraint(expr=m.x1 <= 1) m.z1.c2 = Constraint(expr=m.x2 <= 1) m.z2.c1 = Constraint(expr=m.x1 <= 0) m.z2.c2 = Constraint(expr=m.x2 <= 0) m.djn2 = Disjunction(expr=[m.z1, m.z2]) self.assertTrue(satisfiable(m))
def test_binary_domains(self): m = ConcreteModel() m.x1 = Var(domain=Binary) m.c1 = Constraint(expr=m.x1 == 2) self.assertFalse(satisfiable(m))
def test_bounds_sat(self): m = ConcreteModel() m.x = Var(bounds=(0, 5)) m.c1 = Constraint(expr=4.99 == m.x) m.o = Objective(expr=m.x) self.assertTrue(satisfiable(m))
def solve(self, model, **kwds): config = self.CONFIG(kwds.pop('options', {})) config.set_value(kwds) return SolverFactory('gdpopt').solve( model, strategy='LBB', minlp_solver=config.solver, minlp_solver_args=config.solver_args, tee=config.tee, check_sat=config.check_sat, logger=config.logger, time_limit=config.time_limit) # Validate model to be used with gdpbb self.validate_model(model) # Set solver as an MINLP solve_data = GDPbbSolveData() solve_data.timing = Container() solve_data.original_model = model solve_data.results = SolverResults() old_logger_level = config.logger.getEffectiveLevel() with time_code(solve_data.timing, 'total', is_main_timer=True), \ restore_logger_level(config.logger), \ create_utility_block(model, 'GDPbb_utils', solve_data): if config.tee and old_logger_level > logging.INFO: # If the logger does not already include INFO, include it. config.logger.setLevel(logging.INFO) config.logger.info( "Starting GDPbb version %s using %s as subsolver" % (".".join(map(str, self.version())), config.solver)) # Setup results solve_data.results.solver.name = 'GDPbb - %s' % (str( config.solver)) setup_results_object(solve_data, config) # clone original model for root node of branch and bound root = solve_data.working_model = solve_data.original_model.clone() # get objective sense process_objective(solve_data, config) objectives = solve_data.original_model.component_data_objects( Objective, active=True) obj = next(objectives, None) solve_data.results.problem.sense = obj.sense # set up lists to keep track of which disjunctions have been covered. # this list keeps track of the relaxed disjunctions root.GDPbb_utils.unenforced_disjunctions = list( disjunction for disjunction in root.GDPbb_utils.disjunction_list if disjunction.active) root.GDPbb_utils.deactivated_constraints = ComponentSet([ constr for disjunction in root.GDPbb_utils.unenforced_disjunctions for disjunct in disjunction.disjuncts for constr in disjunct.component_data_objects(ctype=Constraint, active=True) if constr.body.polynomial_degree() not in (1, 0) ]) # Deactivate nonlinear constraints in unenforced disjunctions for constr in root.GDPbb_utils.deactivated_constraints: constr.deactivate() # Add the BigM suffix if it does not already exist. Used later during nonlinear constraint activation. if not hasattr(root, 'BigM'): root.BigM = Suffix() # Pre-screen that none of the disjunctions are already predetermined due to the disjuncts being fixed # to True/False values. # TODO this should also be done within the loop, but we aren't handling it right now. # Should affect efficiency, but not correctness. root.GDPbb_utils.disjuncts_fixed_True = ComponentSet() # Only find top-level (non-nested) disjunctions for disjunction in root.component_data_objects(Disjunction, active=True): fixed_true_disjuncts = [ disjunct for disjunct in disjunction.disjuncts if disjunct.indicator_var.fixed and disjunct.indicator_var.value == 1 ] fixed_false_disjuncts = [ disjunct for disjunct in disjunction.disjuncts if disjunct.indicator_var.fixed and disjunct.indicator_var.value == 0 ] for disjunct in fixed_false_disjuncts: disjunct.deactivate() if len(fixed_false_disjuncts) == len( disjunction.disjuncts) - 1: # all but one disjunct in the disjunction is fixed to False. Remaining one must be true. if not fixed_true_disjuncts: fixed_true_disjuncts = [ disjunct for disjunct in disjunction.disjuncts if disjunct not in fixed_false_disjuncts ] # Reactivate the fixed-true disjuncts for disjunct in fixed_true_disjuncts: newly_activated = ComponentSet() for constr in disjunct.component_data_objects(Constraint): if constr in root.GDPbb_utils.deactivated_constraints: newly_activated.add(constr) constr.activate() # Set the big M value for the constraint root.BigM[constr] = 1 # Note: we use a default big M value of 1 # because all non-selected disjuncts should be deactivated. # Therefore, none of the big M transformed nonlinear constraints will need to be relaxed. # The default M value should therefore be irrelevant. root.GDPbb_utils.deactivated_constraints -= newly_activated root.GDPbb_utils.disjuncts_fixed_True.add(disjunct) if fixed_true_disjuncts: assert disjunction.xor, "GDPbb only handles disjunctions in which one term can be selected. " \ "%s violates this assumption." % (disjunction.name, ) root.GDPbb_utils.unenforced_disjunctions.remove( disjunction) # Check satisfiability if config.check_sat and satisfiable(root, config.logger) is False: # Problem is not satisfiable. Problem is infeasible. obj_value = obj_sign * float('inf') else: # solve the root node config.logger.info("Solving the root node.") obj_value, result, var_values = self.subproblem_solve( root, config) if obj_sign * obj_value == float('inf'): config.logger.info( "Model was found to be infeasible at the root node. Elapsed %.2f seconds." % get_main_elapsed_time(solve_data.timing)) if solve_data.results.problem.sense == minimize: solve_data.results.problem.lower_bound = float('inf') solve_data.results.problem.upper_bound = None else: solve_data.results.problem.lower_bound = None solve_data.results.problem.upper_bound = float('-inf') solve_data.results.solver.timing = solve_data.timing solve_data.results.solver.iterations = 0 solve_data.results.solver.termination_condition = tc.infeasible return solve_data.results # initialize minheap for Branch and Bound algorithm # Heap structure: (ordering tuple, model) # Ordering tuple: (objective value, disjunctions_left, -total_nodes_counter) # - select solutions with lower objective value, # then fewer disjunctions left to explore (depth first), # then more recently encountered (tiebreaker) heap = [] total_nodes_counter = 0 disjunctions_left = len(root.GDPbb_utils.unenforced_disjunctions) heapq.heappush(heap, ((obj_sign * obj_value, disjunctions_left, -total_nodes_counter), root, result, var_values)) # loop to branch through the tree while len(heap) > 0: # pop best model off of heap sort_tuple, incumbent_model, incumbent_results, incumbent_var_values = heapq.heappop( heap) incumbent_obj_value, disjunctions_left, _ = sort_tuple config.logger.info( "Exploring node with LB %.10g and %s inactive disjunctions." % (incumbent_obj_value, disjunctions_left)) # if all the originally active disjunctions are active, solve and # return solution if disjunctions_left == 0: config.logger.info("Model solved.") # Model is solved. Copy over solution values. original_model = solve_data.original_model for orig_var, val in zip( original_model.GDPbb_utils.variable_list, incumbent_var_values): orig_var.value = val solve_data.results.problem.lower_bound = incumbent_results.problem.lower_bound solve_data.results.problem.upper_bound = incumbent_results.problem.upper_bound solve_data.results.solver.timing = solve_data.timing solve_data.results.solver.iterations = total_nodes_counter solve_data.results.solver.termination_condition = incumbent_results.solver.termination_condition return solve_data.results # Pick the next disjunction to branch on next_disjunction = incumbent_model.GDPbb_utils.unenforced_disjunctions[ 0] config.logger.info("Branching on disjunction %s" % next_disjunction.name) assert next_disjunction.xor, "GDPbb only handles disjunctions in which one term can be selected. " \ "%s violates this assumption." % (next_disjunction.name, ) new_nodes_counter = 0 for i, disjunct in enumerate(next_disjunction.disjuncts): # Create one branch for each of the disjuncts on the disjunction if any(disj.indicator_var.fixed and disj.indicator_var.value == 1 for disj in next_disjunction.disjuncts if disj is not disjunct): # If any other disjunct is fixed to 1 and an xor relationship applies, # then this disjunct cannot be activated. continue # Check time limit if get_main_elapsed_time( solve_data.timing) >= config.time_limit: if solve_data.results.problem.sense == minimize: solve_data.results.problem.lower_bound = incumbent_obj_value solve_data.results.problem.upper_bound = float( 'inf') else: solve_data.results.problem.lower_bound = float( '-inf') solve_data.results.problem.upper_bound = incumbent_obj_value config.logger.info('GDPopt unable to converge bounds ' 'before time limit of {} seconds. ' 'Elapsed: {} seconds'.format( config.time_limit, get_main_elapsed_time( solve_data.timing))) config.logger.info( 'Final bound values: LB: {} UB: {}'.format( solve_data.results.problem.lower_bound, solve_data.results.problem.upper_bound)) solve_data.results.solver.timing = solve_data.timing solve_data.results.solver.iterations = total_nodes_counter solve_data.results.solver.termination_condition = tc.maxTimeLimit return solve_data.results # Branch on the disjunct child = incumbent_model.clone() # TODO I am leaving the old branching system in place, but there should be # something better, ideally that deals with nested disjunctions as well. disjunction_to_branch = child.GDPbb_utils.unenforced_disjunctions.pop( 0) child_disjunct = disjunction_to_branch.disjuncts[i] child_disjunct.indicator_var.fix(1) # Deactivate (and fix to 0) other disjuncts on the disjunction for disj in disjunction_to_branch.disjuncts: if disj is not child_disjunct: disj.deactivate() # Activate nonlinear constraints on the newly fixed child disjunct newly_activated = ComponentSet() for constr in child_disjunct.component_data_objects( Constraint): if constr in child.GDPbb_utils.deactivated_constraints: newly_activated.add(constr) constr.activate() # Set the big M value for the constraint child.BigM[constr] = 1 # Note: we use a default big M value of 1 # because all non-selected disjuncts should be deactivated. # Therefore, none of the big M transformed nonlinear constraints will need to be relaxed. # The default M value should therefore be irrelevant. child.GDPbb_utils.deactivated_constraints -= newly_activated child.GDPbb_utils.disjuncts_fixed_True.add(child_disjunct) if disjunct in incumbent_model.GDPbb_utils.disjuncts_fixed_True: # If the disjunct was already branched to True from a parent disjunct branching, just pass # through the incumbent value without resolving. The solution should be the same as the parent. total_nodes_counter += 1 ordering_tuple = (obj_sign * incumbent_obj_value, disjunctions_left - 1, -total_nodes_counter) heapq.heappush(heap, (ordering_tuple, child, result, incumbent_var_values)) new_nodes_counter += 1 continue if config.check_sat and satisfiable( child, config.logger) is False: # Problem is not satisfiable. Skip this disjunct. continue obj_value, result, var_values = self.subproblem_solve( child, config) total_nodes_counter += 1 ordering_tuple = (obj_sign * obj_value, disjunctions_left - 1, -total_nodes_counter) heapq.heappush(heap, (ordering_tuple, child, result, var_values)) new_nodes_counter += 1 config.logger.info( "Added %s new nodes with %s relaxed disjunctions to the heap. Size now %s." % (new_nodes_counter, disjunctions_left - 1, len(heap)))
def test_simple_sat_model(self): m = ConcreteModel() m.x = Var() m.c = Constraint(expr=1 == m.x) m.o = Objective(expr=m.x) self.assertTrue(satisfiable(m))
def test_lower_bound_unsat(self): m = ConcreteModel() m.x = Var(bounds=(0, 5)) m.c = Constraint(expr=-0.01 == m.x) m.o = Objective(expr=m.x) self.assertFalse(satisfiable(m))
def test_constrained_layout(self): exfile = import_file( join(exdir, 'constrained_layout', 'cons_layout_model.py')) m = exfile.build_constrained_layout_model() self.assertTrue(satisfiable(m) is not False)
def test_8PP(self): exfile = import_file( join(exdir, 'eight_process', 'eight_proc_model.py')) m = exfile.build_eight_process_flowsheet() self.assertTrue(satisfiable(m) is not False)
def solve(self, model, **kwds): config = self.CONFIG(kwds.pop('options', {})) config.set_value(kwds) # Validate model to be used with gdpbb self.validate_model(model) # Set solver as an MINLP solver = SolverFactory(config.solver) solve_data = GDPbbSolveData() solve_data.timing = Container() solve_data.original_model = model solve_data.results = SolverResults() old_logger_level = config.logger.getEffectiveLevel() with time_code(solve_data.timing, 'total'), \ restore_logger_level(config.logger), \ create_utility_block(model, 'GDPbb_utils', solve_data): if config.tee and old_logger_level > logging.INFO: # If the logger does not already include INFO, include it. config.logger.setLevel(logging.INFO) config.logger.info( "Starting GDPbb version %s using %s as subsolver" % (".".join(map(str, self.version())), config.solver)) # Setup results solve_data.results.solver.name = 'GDPbb - %s' % (str( config.solver)) setup_results_object(solve_data, config) # Initialize list containing indicator vars for reupdating model after solving indicator_list_name = unique_component_name( model, "_indicator_list") indicator_vars = [] for disjunction in model.component_data_objects(ctype=Disjunction, active=True): for disjunct in disjunction.disjuncts: indicator_vars.append(disjunct.indicator_var) setattr(model, indicator_list_name, indicator_vars) # get objective sense objectives = model.component_data_objects(Objective, active=True) obj = next(objectives, None) obj_sign = 1 if obj.sense == minimize else -1 solve_data.results.problem.sense = obj.sense # clone original model for root node of branch and bound root = model.clone() # set up lists to keep track of which disjunctions have been covered. # this list keeps track of the original disjunctions that were active and are soon to be inactive root.GDPbb_utils.unenforced_disjunctions = list( disjunction for disjunction in root.GDPbb_utils.disjunction_list if disjunction.active) # this list keeps track of the disjunctions that have been activated by the branch and bound root.GDPbb_utils.curr_active_disjunctions = [] # deactivate all disjunctions in the model # self.indicate(root) for djn in root.GDPbb_utils.unenforced_disjunctions: djn.deactivate() # Deactivate all disjuncts in model. To be reactivated when disjunction # is reactivated. for disj in root.component_data_objects(Disjunct, active=True): disj._deactivate_without_fixing_indicator() # Satisfiability check would go here # solve the root node config.logger.info("Solving the root node.") obj_value, result, _ = self.subproblem_solve(root, solver, config) # initialize minheap for Branch and Bound algorithm # Heap structure: (ordering tuple, model) # Ordering tuple: (objective value, disjunctions_left, -counter) # - select solutions with lower objective value, # then fewer disjunctions left to explore (depth first), # then more recently encountered (tiebreaker) heap = [] counter = 0 disjunctions_left = len(root.GDPbb_utils.unenforced_disjunctions) heapq.heappush( heap, ((obj_sign * obj_value, disjunctions_left, -counter), root, result, root.GDPbb_utils.variable_list)) # loop to branch through the tree while len(heap) > 0: # pop best model off of heap sort_tup, mdl, mdl_results, vars = heapq.heappop(heap) old_obj_val, disjunctions_left, _ = sort_tup config.logger.info( "Exploring node with LB %.10g and %s inactive disjunctions." % (old_obj_val, disjunctions_left)) # if all the originally active disjunctions are active, solve and # return solution if disjunctions_left == 0: config.logger.info("Model solved.") # Model is solved. Copy over solution values. for orig_var, soln_var in zip( model.GDPbb_utils.variable_list, vars): orig_var.value = soln_var.value solve_data.results.problem.lower_bound = mdl_results.problem.lower_bound solve_data.results.problem.upper_bound = mdl_results.problem.upper_bound solve_data.results.solver.timing = solve_data.timing solve_data.results.solver.termination_condition = mdl_results.solver.termination_condition return solve_data.results next_disjunction = mdl.GDPbb_utils.unenforced_disjunctions.pop( 0) config.logger.info("Activating disjunction %s" % next_disjunction.name) next_disjunction.activate() mdl.GDPbb_utils.curr_active_disjunctions.append( next_disjunction) djn_left = len(mdl.GDPbb_utils.unenforced_disjunctions) for disj in next_disjunction.disjuncts: disj._activate_without_unfixing_indicator() if not disj.indicator_var.fixed: disj.indicator_var = 0 # initially set all indicator vars to zero added_disj_counter = 0 for disj in next_disjunction.disjuncts: if not disj.indicator_var.fixed: disj.indicator_var = 1 mnew = mdl.clone() if not disj.indicator_var.fixed: disj.indicator_var = 0 # Check feasibility if config.check_sat and satisfiable( mnew, config.logger) is False: # problem is not satisfiable. Skip this disjunct. continue obj_value, result, vars = self.subproblem_solve( mnew, solver, config) counter += 1 ordering_tuple = (obj_sign * obj_value, djn_left, -counter) heapq.heappush(heap, (ordering_tuple, mnew, result, vars)) added_disj_counter = added_disj_counter + 1 config.logger.info( "Added %s new nodes with %s relaxed disjunctions to the heap. Size now %s." % (added_disj_counter, djn_left, len(heap)))
def test_strip_pack(self): exfile = import_file( join(exdir, 'strip_packing', 'strip_packing_concrete.py')) m = exfile.build_rect_strip_packing_model() self.assertTrue(satisfiable(m))
def test_abs_expressions(self): m = ConcreteModel() m.x = Var() m.c1 = Constraint(expr=-0.001 >= abs(m.x)) m.o = Objective(expr=m.x) self.assertFalse(satisfiable(m))
def test_unhandled_expressions(self): m = ConcreteModel() m.x = Var() m.c1 = Constraint(expr=0 <= log(m.x)) self.assertTrue(satisfiable(m))
def test_real_domains(self): m = ConcreteModel() m.x1 = Var(domain=NonNegativeReals) m.c1 = Constraint(expr=m.x1 == -1.3) self.assertFalse(satisfiable(m))
def test_integer_domains(self): m = ConcreteModel() m.x1 = Var(domain=PositiveIntegers) m.c1 = Constraint(expr=m.x1 == 0.5) self.assertFalse(satisfiable(m))
def test_ex_633_trespalacios(self): exfile = import_file(join(exdir, 'small_lit', 'ex_633_trespalacios.py')) m = exfile.build_simple_nonconvex_gdp() self.assertTrue(satisfiable(m) is not False)
def test_unhandled_expressions(self): m = ConcreteModel() m.x = Var() m.c1 = Constraint(expr= 0 <= log(m.x)) self.assertTrue(satisfiable(m))
def test_integer_domains(self): m = ConcreteModel() m.x1 = Var(domain = PositiveIntegers) m.c1 = Constraint(expr = m.x1 == 0.5) self.assertFalse(satisfiable(m))
def test_real_domains(self): m = ConcreteModel() m.x1 = Var(domain = NonNegativeReals) m.c1 = Constraint(expr = m.x1 == -1.3) self.assertFalse(satisfiable(m))
def test_binary_domains(self): m = ConcreteModel() m.x1 = Var(domain = Binary) m.c1 = Constraint(expr = m.x1 == 2) self.assertFalse(satisfiable(m))
def solve(self, model, **kwds): config = self.CONFIG(kwds.pop('options', {})) config.set_value(kwds) # Validate model to be used with gdpbb self.validate_model(model) # Set solver as an MINLP solve_data = GDPbbSolveData() solve_data.timing = Container() solve_data.original_model = model solve_data.results = SolverResults() old_logger_level = config.logger.getEffectiveLevel() with time_code(solve_data.timing, 'total', is_main_timer=True), \ restore_logger_level(config.logger), \ create_utility_block(model, 'GDPbb_utils', solve_data): if config.tee and old_logger_level > logging.INFO: # If the logger does not already include INFO, include it. config.logger.setLevel(logging.INFO) config.logger.info( "Starting GDPbb version %s using %s as subsolver" % (".".join(map(str, self.version())), config.solver) ) # Setup results solve_data.results.solver.name = 'GDPbb - %s' % (str(config.solver)) setup_results_object(solve_data, config) # clone original model for root node of branch and bound root = solve_data.working_model = solve_data.original_model.clone() # get objective sense process_objective(solve_data, config) objectives = solve_data.original_model.component_data_objects(Objective, active=True) obj = next(objectives, None) obj_sign = 1 if obj.sense == minimize else -1 solve_data.results.problem.sense = obj.sense # set up lists to keep track of which disjunctions have been covered. # this list keeps track of the relaxed disjunctions root.GDPbb_utils.unenforced_disjunctions = list( disjunction for disjunction in root.GDPbb_utils.disjunction_list if disjunction.active ) root.GDPbb_utils.deactivated_constraints = ComponentSet([ constr for disjunction in root.GDPbb_utils.unenforced_disjunctions for disjunct in disjunction.disjuncts for constr in disjunct.component_data_objects(ctype=Constraint, active=True) if constr.body.polynomial_degree() not in (1, 0) ]) # Deactivate nonlinear constraints in unenforced disjunctions for constr in root.GDPbb_utils.deactivated_constraints: constr.deactivate() # Add the BigM suffix if it does not already exist. Used later during nonlinear constraint activation. if not hasattr(root, 'BigM'): root.BigM = Suffix() # Pre-screen that none of the disjunctions are already predetermined due to the disjuncts being fixed # to True/False values. # TODO this should also be done within the loop, but we aren't handling it right now. # Should affect efficiency, but not correctness. root.GDPbb_utils.disjuncts_fixed_True = ComponentSet() # Only find top-level (non-nested) disjunctions for disjunction in root.component_data_objects(Disjunction, active=True): fixed_true_disjuncts = [disjunct for disjunct in disjunction.disjuncts if disjunct.indicator_var.fixed and disjunct.indicator_var.value == 1] fixed_false_disjuncts = [disjunct for disjunct in disjunction.disjuncts if disjunct.indicator_var.fixed and disjunct.indicator_var.value == 0] for disjunct in fixed_false_disjuncts: disjunct.deactivate() if len(fixed_false_disjuncts) == len(disjunction.disjuncts) - 1: # all but one disjunct in the disjunction is fixed to False. Remaining one must be true. if not fixed_true_disjuncts: fixed_true_disjuncts = [disjunct for disjunct in disjunction.disjuncts if disjunct not in fixed_false_disjuncts] # Reactivate the fixed-true disjuncts for disjunct in fixed_true_disjuncts: newly_activated = ComponentSet() for constr in disjunct.component_data_objects(Constraint): if constr in root.GDPbb_utils.deactivated_constraints: newly_activated.add(constr) constr.activate() # Set the big M value for the constraint root.BigM[constr] = 1 # Note: we use a default big M value of 1 # because all non-selected disjuncts should be deactivated. # Therefore, none of the big M transformed nonlinear constraints will need to be relaxed. # The default M value should therefore be irrelevant. root.GDPbb_utils.deactivated_constraints -= newly_activated root.GDPbb_utils.disjuncts_fixed_True.add(disjunct) if fixed_true_disjuncts: assert disjunction.xor, "GDPbb only handles disjunctions in which one term can be selected. " \ "%s violates this assumption." % (disjunction.name, ) root.GDPbb_utils.unenforced_disjunctions.remove(disjunction) # Check satisfiability if config.check_sat and satisfiable(root, config.logger) is False: # Problem is not satisfiable. Problem is infeasible. obj_value = obj_sign * float('inf') else: # solve the root node config.logger.info("Solving the root node.") obj_value, result, var_values = self.subproblem_solve(root, config) if obj_sign * obj_value == float('inf'): config.logger.info("Model was found to be infeasible at the root node. Elapsed %.2f seconds." % get_main_elapsed_time(solve_data.timing)) if solve_data.results.problem.sense == minimize: solve_data.results.problem.lower_bound = float('inf') solve_data.results.problem.upper_bound = None else: solve_data.results.problem.lower_bound = None solve_data.results.problem.upper_bound = float('-inf') solve_data.results.solver.timing = solve_data.timing solve_data.results.solver.iterations = 0 solve_data.results.solver.termination_condition = tc.infeasible return solve_data.results # initialize minheap for Branch and Bound algorithm # Heap structure: (ordering tuple, model) # Ordering tuple: (objective value, disjunctions_left, -total_nodes_counter) # - select solutions with lower objective value, # then fewer disjunctions left to explore (depth first), # then more recently encountered (tiebreaker) heap = [] total_nodes_counter = 0 disjunctions_left = len(root.GDPbb_utils.unenforced_disjunctions) heapq.heappush( heap, ( (obj_sign * obj_value, disjunctions_left, -total_nodes_counter), root, result, var_values)) # loop to branch through the tree while len(heap) > 0: # pop best model off of heap sort_tuple, incumbent_model, incumbent_results, incumbent_var_values = heapq.heappop(heap) incumbent_obj_value, disjunctions_left, _ = sort_tuple config.logger.info("Exploring node with LB %.10g and %s inactive disjunctions." % ( incumbent_obj_value, disjunctions_left )) # if all the originally active disjunctions are active, solve and # return solution if disjunctions_left == 0: config.logger.info("Model solved.") # Model is solved. Copy over solution values. original_model = solve_data.original_model for orig_var, val in zip(original_model.GDPbb_utils.variable_list, incumbent_var_values): orig_var.value = val solve_data.results.problem.lower_bound = incumbent_results.problem.lower_bound solve_data.results.problem.upper_bound = incumbent_results.problem.upper_bound solve_data.results.solver.timing = solve_data.timing solve_data.results.solver.iterations = total_nodes_counter solve_data.results.solver.termination_condition = incumbent_results.solver.termination_condition return solve_data.results # Pick the next disjunction to branch on next_disjunction = incumbent_model.GDPbb_utils.unenforced_disjunctions[0] config.logger.info("Branching on disjunction %s" % next_disjunction.name) assert next_disjunction.xor, "GDPbb only handles disjunctions in which one term can be selected. " \ "%s violates this assumption." % (next_disjunction.name, ) new_nodes_counter = 0 for i, disjunct in enumerate(next_disjunction.disjuncts): # Create one branch for each of the disjuncts on the disjunction if any(disj.indicator_var.fixed and disj.indicator_var.value == 1 for disj in next_disjunction.disjuncts if disj is not disjunct): # If any other disjunct is fixed to 1 and an xor relationship applies, # then this disjunct cannot be activated. continue # Check time limit if get_main_elapsed_time(solve_data.timing) >= config.time_limit: if solve_data.results.problem.sense == minimize: solve_data.results.problem.lower_bound = incumbent_obj_value solve_data.results.problem.upper_bound = float('inf') else: solve_data.results.problem.lower_bound = float('-inf') solve_data.results.problem.upper_bound = incumbent_obj_value config.logger.info( 'GDPopt unable to converge bounds ' 'before time limit of {} seconds. ' 'Elapsed: {} seconds' .format(config.time_limit, get_main_elapsed_time(solve_data.timing))) config.logger.info( 'Final bound values: LB: {} UB: {}'. format(solve_data.results.problem.lower_bound, solve_data.results.problem.upper_bound)) solve_data.results.solver.timing = solve_data.timing solve_data.results.solver.iterations = total_nodes_counter solve_data.results.solver.termination_condition = tc.maxTimeLimit return solve_data.results # Branch on the disjunct child = incumbent_model.clone() # TODO I am leaving the old branching system in place, but there should be # something better, ideally that deals with nested disjunctions as well. disjunction_to_branch = child.GDPbb_utils.unenforced_disjunctions.pop(0) child_disjunct = disjunction_to_branch.disjuncts[i] child_disjunct.indicator_var.fix(1) # Deactivate (and fix to 0) other disjuncts on the disjunction for disj in disjunction_to_branch.disjuncts: if disj is not child_disjunct: disj.deactivate() # Activate nonlinear constraints on the newly fixed child disjunct newly_activated = ComponentSet() for constr in child_disjunct.component_data_objects(Constraint): if constr in child.GDPbb_utils.deactivated_constraints: newly_activated.add(constr) constr.activate() # Set the big M value for the constraint child.BigM[constr] = 1 # Note: we use a default big M value of 1 # because all non-selected disjuncts should be deactivated. # Therefore, none of the big M transformed nonlinear constraints will need to be relaxed. # The default M value should therefore be irrelevant. child.GDPbb_utils.deactivated_constraints -= newly_activated child.GDPbb_utils.disjuncts_fixed_True.add(child_disjunct) if disjunct in incumbent_model.GDPbb_utils.disjuncts_fixed_True: # If the disjunct was already branched to True from a parent disjunct branching, just pass # through the incumbent value without resolving. The solution should be the same as the parent. total_nodes_counter += 1 ordering_tuple = (obj_sign * incumbent_obj_value, disjunctions_left - 1, -total_nodes_counter) heapq.heappush(heap, (ordering_tuple, child, result, incumbent_var_values)) new_nodes_counter += 1 continue if config.check_sat and satisfiable(child, config.logger) is False: # Problem is not satisfiable. Skip this disjunct. continue obj_value, result, var_values = self.subproblem_solve(child, config) total_nodes_counter += 1 ordering_tuple = (obj_sign * obj_value, disjunctions_left - 1, -total_nodes_counter) heapq.heappush(heap, (ordering_tuple, child, result, var_values)) new_nodes_counter += 1 config.logger.info("Added %s new nodes with %s relaxed disjunctions to the heap. Size now %s." % ( new_nodes_counter, disjunctions_left - 1, len(heap)))
def solve(self, model, **kwds): config = self.CONFIG(kwds.pop('options', {})) config.set_value(kwds) # Validate model to be used with gdpbb self.validate_model(model) # Set solver as an MINLP solver = SolverFactory(config.solver) solve_data = GDPbbSolveData() solve_data.timing = Container() solve_data.original_model = model solve_data.results = SolverResults() old_logger_level = config.logger.getEffectiveLevel() with time_code(solve_data.timing, 'total'), \ restore_logger_level(config.logger), \ create_utility_block(model, 'GDPbb_utils', solve_data): if config.tee and old_logger_level > logging.INFO: # If the logger does not already include INFO, include it. config.logger.setLevel(logging.INFO) config.logger.info( "Starting GDPbb version %s using %s as subsolver" % (".".join(map(str, self.version())), config.solver) ) # Setup results solve_data.results.solver.name = 'GDPbb - %s' % (str(config.solver)) setup_results_object(solve_data, config) # Initialize list containing indicator vars for reupdating model after solving indicator_list_name = unique_component_name(model, "_indicator_list") indicator_vars = [] for disjunction in model.component_data_objects( ctype=Disjunction, active=True): for disjunct in disjunction.disjuncts: indicator_vars.append(disjunct.indicator_var) setattr(model, indicator_list_name, indicator_vars) # get objective sense objectives = model.component_data_objects(Objective, active=True) obj = next(objectives, None) obj_sign = 1 if obj.sense == minimize else -1 solve_data.results.problem.sense = obj.sense # clone original model for root node of branch and bound root = model.clone() # set up lists to keep track of which disjunctions have been covered. # this list keeps track of the original disjunctions that were active and are soon to be inactive root.GDPbb_utils.unenforced_disjunctions = list( disjunction for disjunction in root.GDPbb_utils.disjunction_list if disjunction.active ) # this list keeps track of the disjunctions that have been activated by the branch and bound root.GDPbb_utils.curr_active_disjunctions = [] # deactivate all disjunctions in the model # self.indicate(root) for djn in root.GDPbb_utils.unenforced_disjunctions: djn.deactivate() # Deactivate all disjuncts in model. To be reactivated when disjunction # is reactivated. for disj in root.component_data_objects(Disjunct, active=True): disj._deactivate_without_fixing_indicator() # Satisfiability check would go here # solve the root node config.logger.info("Solving the root node.") obj_value, result, _ = self.subproblem_solve(root, solver, config) # initialize minheap for Branch and Bound algorithm # Heap structure: (ordering tuple, model) # Ordering tuple: (objective value, disjunctions_left, -counter) # - select solutions with lower objective value, # then fewer disjunctions left to explore (depth first), # then more recently encountered (tiebreaker) heap = [] counter = 0 disjunctions_left = len(root.GDPbb_utils.unenforced_disjunctions) heapq.heappush(heap, ((obj_sign * obj_value, disjunctions_left, -counter), root, result, root.GDPbb_utils.variable_list)) # loop to branch through the tree while len(heap) > 0: # pop best model off of heap sort_tup, mdl, mdl_results, vars = heapq.heappop(heap) old_obj_val, disjunctions_left, _ = sort_tup config.logger.info("Exploring node with LB %.10g and %s inactive disjunctions." % ( old_obj_val, disjunctions_left )) # if all the originally active disjunctions are active, solve and # return solution if disjunctions_left == 0: config.logger.info("Model solved.") # Model is solved. Copy over solution values. for orig_var, soln_var in zip(model.GDPbb_utils.variable_list, vars): orig_var.value = soln_var.value solve_data.results.problem.lower_bound = mdl_results.problem.lower_bound solve_data.results.problem.upper_bound = mdl_results.problem.upper_bound solve_data.results.solver.timing = solve_data.timing solve_data.results.solver.termination_condition = mdl_results.solver.termination_condition return solve_data.results next_disjunction = mdl.GDPbb_utils.unenforced_disjunctions.pop(0) config.logger.info("Activating disjunction %s" % next_disjunction.name) next_disjunction.activate() mdl.GDPbb_utils.curr_active_disjunctions.append(next_disjunction) djn_left = len(mdl.GDPbb_utils.unenforced_disjunctions) for disj in next_disjunction.disjuncts: disj._activate_without_unfixing_indicator() if not disj.indicator_var.fixed: disj.indicator_var = 0 # initially set all indicator vars to zero added_disj_counter = 0 for disj in next_disjunction.disjuncts: if not disj.indicator_var.fixed: disj.indicator_var = 1 mnew = mdl.clone() if not disj.indicator_var.fixed: disj.indicator_var = 0 # Check feasibility if config.check_sat and satisfiable(mnew, config.logger) is False: # problem is not satisfiable. Skip this disjunct. continue obj_value, result, vars = self.subproblem_solve(mnew, solver, config) counter += 1 ordering_tuple = (obj_sign * obj_value, djn_left, -counter) heapq.heappush(heap, (ordering_tuple, mnew, result, vars)) added_disj_counter = added_disj_counter + 1 config.logger.info("Added %s new nodes with %s relaxed disjunctions to the heap. Size now %s." % ( added_disj_counter, djn_left, len(heap)))