def _get_expr_from_pyomo_repn(self, repn, max_degree=2): referenced_vars = ComponentSet() degree = repn.polynomial_degree() if (degree is None) or (degree > max_degree): raise DegreeError( 'GurobiDirect does not support expressions of degree {0}.'. format(degree)) if len(repn.linear_vars) > 0: referenced_vars.update(repn.linear_vars) new_expr = self._gurobipy.LinExpr(repn.linear_coefs, [ self._pyomo_var_to_solver_var_map[i] for i in repn.linear_vars ]) else: new_expr = 0.0 for i, v in enumerate(repn.quadratic_vars): x, y = v new_expr += repn.quadratic_coefs[ i] * self._pyomo_var_to_solver_var_map[ x] * self._pyomo_var_to_solver_var_map[y] referenced_vars.add(x) referenced_vars.add(y) new_expr += repn.constant return new_expr, referenced_vars
def _process_container(blk, config): if not hasattr(blk, '_induced_linearity_info'): blk._induced_linearity_info = Block() else: assert blk._induced_linearity_info.type() == Block eff_discr_vars = detect_effectively_discrete_vars( blk, config.equality_tolerance) # TODO will need to go through this for each disjunct, since it does # not (should not) descend into Disjuncts. # Determine the valid values for the effectively discrete variables possible_var_values = determine_valid_values(blk, eff_discr_vars, config) # Collect find bilinear expressions that can be reformulated using # knowledge of effectively discrete variables bilinear_map = _bilinear_expressions(blk) # Relevant constraints are those with bilinear terms that involve # effectively_discrete_vars processed_pairs = ComponentSet() for v1, var_values in possible_var_values.items(): v1_pairs = bilinear_map.get(v1, ()) for v2, bilinear_constrs in v1_pairs.items(): if (v1, v2) in processed_pairs: continue _process_bilinear_constraints(blk, v1, v2, var_values, bilinear_constrs) processed_pairs.add((v2, v1))
def _get_expr_from_pyomo_repn(self, repn, max_degree=2): referenced_vars = ComponentSet() degree = repn.polynomial_degree() if (degree is None) or (degree > max_degree): raise DegreeError( 'Mosek does not support expressions of degree {0}.'.format( degree)) # if len(repn.linear_vars) > 0: referenced_vars.update(repn.linear_vars) indexes = [] [ indexes.append(self._pyomo_var_to_solver_var_map[i]) for i in repn.linear_vars ] new_expr = [list(repn.linear_coefs), indexes, repn.constant] qsubi = [] qsubj = [] qval = [] for i, v in enumerate(repn.quadratic_vars): x, y = v qsubj.append(self._pyomo_var_to_solver_var_map[x]) qsubi.append(self._pyomo_var_to_solver_var_map[y]) qval.append(repn.quadratic_coefs[i] * ((qsubi == qsubj) + 1)) referenced_vars.add(x) referenced_vars.add(y) new_expr.extend([qval, qsubi, qsubj]) return new_expr, referenced_vars
def _get_expr_from_pyomo_repn(self, repn, max_degree=2): referenced_vars = ComponentSet() degree = repn.polynomial_degree() if (degree is None) or (degree > max_degree): raise DegreeError( 'CPLEXDirect does not support expressions of degree {0}.'. format(degree)) new_expr = _CplexExpr() if len(repn.linear_vars) > 0: referenced_vars.update(repn.linear_vars) new_expr.variables.extend(self._pyomo_var_to_ndx_map[i] for i in repn.linear_vars) new_expr.coefficients.extend(repn.linear_coefs) for i, v in enumerate(repn.quadratic_vars): x, y = v new_expr.q_coefficients.append(repn.quadratic_coefs[i]) new_expr.q_variables1.append(self._pyomo_var_to_ndx_map[x]) new_expr.q_variables2.append(self._pyomo_var_to_ndx_map[y]) referenced_vars.add(x) referenced_vars.add(y) new_expr.offset = repn.constant return new_expr, referenced_vars
def _process_container(blk, config): if not hasattr(blk, '_induced_linearity_info'): blk._induced_linearity_info = Block() else: assert blk._induced_linearity_info.type() == Block eff_discr_vars = detect_effectively_discrete_vars( blk, config.equality_tolerance) # TODO will need to go through this for each disjunct, since it does # not (should not) descend into Disjuncts. # Determine the valid values for the effectively discrete variables possible_var_values = determine_valid_values(blk, eff_discr_vars, config) # Collect find bilinear expressions that can be reformulated using # knowledge of effectively discrete variables bilinear_map = _bilinear_expressions(blk) # Relevant constraints are those with bilinear terms that involve # effectively_discrete_vars processed_pairs = ComponentSet() for v1, var_values in possible_var_values.items(): v1_pairs = bilinear_map.get(v1, ()) for v2, bilinear_constrs in v1_pairs.items(): if (v1, v2) in processed_pairs: continue _process_bilinear_constraints( blk, v1, v2, var_values, bilinear_constrs) processed_pairs.add((v2, v1))
def _get_expr_from_pyomo_repn(self, repn, max_degree=2): degree = repn.polynomial_degree() if degree is None or degree > max_degree: raise DegreeError( "CPLEXDirect does not support expressions of degree {0}.". format(degree)) referenced_vars = ComponentSet(repn.linear_vars) q_coefficients = [] q_variables1 = [] q_variables2 = [] for i, v in enumerate(repn.quadratic_vars): x, y = v q_coefficients.append(repn.quadratic_coefs[i]) q_variables1.append(self._pyomo_var_to_ndx_map[x]) q_variables2.append(self._pyomo_var_to_ndx_map[y]) referenced_vars.add(x) referenced_vars.add(y) return ( _CplexExpr( variables=[ self._pyomo_var_to_ndx_map[var] for var in repn.linear_vars ], coefficients=repn.linear_coefs, offset=repn.constant, q_variables1=q_variables1, q_variables2=q_variables2, q_coefficients=q_coefficients, ), referenced_vars, )
def _get_expr_from_pyomo_repn(self, repn, max_degree=2): referenced_vars = ComponentSet() degree = repn.polynomial_degree() if (degree is None) or (degree > max_degree): raise DegreeError( 'XpressDirect does not support expressions of degree {0}.'. format(degree)) # NOTE: xpress's python interface only allows for expresions # with native numeric types. Others, like numpy.float64, # will cause an exception when constructing expressions if len(repn.linear_vars) > 0: referenced_vars.update(repn.linear_vars) new_expr = self._xpress.Sum( float(coef) * self._pyomo_var_to_solver_var_map[var] for coef, var in zip(repn.linear_coefs, repn.linear_vars)) else: new_expr = 0.0 for coef, (x, y) in zip(repn.quadratic_coefs, repn.quadratic_vars): new_expr += float(coef) * self._pyomo_var_to_solver_var_map[ x] * self._pyomo_var_to_solver_var_map[y] referenced_vars.add(x) referenced_vars.add(y) new_expr += repn.constant return new_expr, referenced_vars
def run_order(self, G, order, function, ignore=None, use_guesses=False): """ Run computations in the order provided by calling the function Arguments --------- G A networkx graph corresponding to order order The order in which to run each node in the graph function The function to be called on each block/node ignore Edge indexes to ignore when passing values use_guesses If True, will check the guesses dict when fixing free variables before calling function """ fixed_inputs = self.fixed_inputs() fixed_outputs = ComponentSet() edge_map = self.edge_to_idx(G) guesses = self.options["guesses"] default = self.options["default_guess"] for lev in order: for unit in lev: if unit not in fixed_inputs: fixed_inputs[unit] = ComponentSet() fixed_ins = fixed_inputs[unit] # make sure all inputs are fixed for port in unit.component_data_objects(Port): if not len(port.sources()): continue if use_guesses and port in guesses: self.load_guesses(guesses, port, fixed_ins) self.load_values(port, default, fixed_ins, use_guesses) function(unit) # free the inputs that were not already fixed for var in fixed_ins: var.free() fixed_ins.clear() # pass the values downstream for all outlet ports for port in unit.component_data_objects(Port): dests = port.dests() if not len(dests): continue for var in port.iter_vars(expr_vars=True, fixed=False): fixed_outputs.add(var) var.fix() for arc in dests: arc_map = self.arc_to_edge(G) if edge_map[arc_map[arc]] not in ignore: self.pass_values(arc, fixed_inputs) for var in fixed_outputs: var.free() fixed_outputs.clear()
def run_order(self, G, order, function, ignore=None, use_guesses=False): """ Run computations in the order provided by calling the function Arguments --------- G A networkx graph corresponding to order order The order in which to run each node in the graph function The function to be called on each block/node ignore Edge indexes to ignore when passing values use_guesses If True, will check the guesses dict when fixing free variables before calling function """ fixed_inputs = self.fixed_inputs() fixed_outputs = ComponentSet() edge_map = self.edge_to_idx(G) guesses = self.options["guesses"] default = self.options["default_guess"] for lev in order: for unit in lev: if unit not in fixed_inputs: fixed_inputs[unit] = ComponentSet() fixed_ins = fixed_inputs[unit] # make sure all inputs are fixed for port in unit.component_data_objects(Port): if not len(port.sources()): continue if use_guesses and port in guesses: self.load_guesses(guesses, port, fixed_ins) self.load_values(port, default, fixed_ins, use_guesses) function(unit) # free the inputs that were not already fixed for var in fixed_ins: var.free() fixed_ins.clear() # pass the values downstream for all outlet ports for port in unit.component_data_objects(Port): dests = port.dests() if not len(dests): continue for var in port.iter_vars(expr_vars=True, fixed=False): fixed_outputs.add(var) var.fix() for arc in dests: arc_map = self.arc_to_edge(G) if edge_map[arc_map[arc]] not in ignore: self.pass_values(arc, fixed_inputs) for var in fixed_outputs: var.free() fixed_outputs.clear()
def free_variables_in_active_equalities_set(blk): """ Return a set of variables that are contined in active equalities. """ vin = ComponentSet() for c in active_equalities(blk): for v in identify_variables(c.body): if not v.fixed: vin.add(v) return vin
def free_variables_in_active_equalities_set(blk): """ Return a set of variables that are contined in active equalities. """ vin = ComponentSet() for c in active_equalities(blk): for v in identify_variables(c.body): if not v.fixed: vin.add(v) return vin
def add_integer_cut(var_values, target_model, solve_data, config, feasible=False): """Add an integer cut to the target GDP model.""" with time_code(solve_data.timing, 'integer cut generation'): m = target_model GDPopt = m.GDPopt_utils var_value_is_one = ComponentSet() var_value_is_zero = ComponentSet() for var, val in zip(GDPopt.variable_list, var_values): if not var.is_binary(): continue if var.fixed: if val is not None and var.value != val: # val needs to be None or match var.value. Otherwise, we have a # contradiction. raise ValueError( "Fixed variable %s has value %s != " "provided value of %s." % (var.name, var.value, val)) val = var.value if not config.force_subproblem_nlp: # Skip indicator variables # TODO we should implement this as a check among Disjuncts instead if not (var.local_name == 'indicator_var' and var.parent_block().type() == Disjunct): continue if fabs(val - 1) <= config.integer_tolerance: var_value_is_one.add(var) elif fabs(val) <= config.integer_tolerance: var_value_is_zero.add(var) else: raise ValueError( 'Binary %s = %s is not 0 or 1' % (var.name, val)) if not (var_value_is_one or var_value_is_zero): # if no remaining binary variables, then terminate algorithm. config.logger.info( 'Adding integer cut to a model without discrete variables. ' 'Model is now infeasible.') if solve_data.objective_sense == minimize: solve_data.LB = float('inf') else: solve_data.UB = float('-inf') return False int_cut = (sum(1 - v for v in var_value_is_one) + sum(v for v in var_value_is_zero)) >= 1 if not feasible: config.logger.info('Adding integer cut') GDPopt.integer_cuts.add(expr=int_cut) else: backtracking_enabled = ( "disabled" if GDPopt.no_backtracking.active else "allowed") config.logger.info( 'Registering explored configuration. ' 'Backtracking is currently %s.' % backtracking_enabled) GDPopt.no_backtracking.add(expr=int_cut)
def add_integer_cut(var_values, target_model, solve_data, config, feasible=False): """Add an integer cut to the target GDP model.""" with time_code(solve_data.timing, 'integer cut generation'): m = target_model GDPopt = m.GDPopt_utils var_value_is_one = ComponentSet() var_value_is_zero = ComponentSet() for var, val in zip(GDPopt.variable_list, var_values): if not var.is_binary(): continue if var.fixed: if val is not None and var.value != val: # val needs to be None or match var.value. Otherwise, we have a # contradiction. raise ValueError( "Fixed variable %s has value %s != " "provided value of %s." % (var.name, var.value, val)) val = var.value if not config.force_subproblem_nlp: # Skip indicator variables # TODO we should implement this as a check among Disjuncts instead if not (var.local_name == 'indicator_var' and var.parent_block().type() == Disjunct): continue if fabs(val - 1) <= config.integer_tolerance: var_value_is_one.add(var) elif fabs(val) <= config.integer_tolerance: var_value_is_zero.add(var) else: raise ValueError( 'Binary %s = %s is not 0 or 1' % (var.name, val)) if not (var_value_is_one or var_value_is_zero): # if no remaining binary variables, then terminate algorithm. config.logger.info( 'Adding integer cut to a model without discrete variables. ' 'Model is now infeasible.') if solve_data.objective_sense == minimize: solve_data.LB = float('inf') else: solve_data.UB = float('-inf') return False int_cut = (sum(1 - v for v in var_value_is_one) + sum(v for v in var_value_is_zero)) >= 1 if not feasible: config.logger.info('Adding integer cut') GDPopt.integer_cuts.add(expr=int_cut) else: backtracking_enabled = ( "disabled" if GDPopt.no_backtracking.active else "allowed") config.logger.info( 'Registering explored configuration. ' 'Backtracking is currently %s.' % backtracking_enabled) GDPopt.no_backtracking.add(expr=int_cut)
def active_equality_set(blk): """ Generator returning active equality constraints in a model. Args: blk: a Pyomo block in which to look for variables. """ ac = ComponentSet() for c in active_equalities(blk): ac.add(c) return ac
def active_equality_set(blk): """ Generator returning active equality constraints in a model. Args: blk: a Pyomo block in which to look for variables. """ ac = ComponentSet() for c in active_equalities(blk): ac.add(c) return ac
def test_isdisjoint(self): cset1 = ComponentSet() cset2 = ComponentSet() self.assertTrue(cset1.isdisjoint(cset2)) self.assertTrue(cset2.isdisjoint(cset1)) v = variable() cset1.add(v) self.assertTrue(cset1.isdisjoint(cset2)) self.assertTrue(cset2.isdisjoint(cset1)) cset2.add(v) self.assertFalse(cset1.isdisjoint(cset2)) self.assertFalse(cset2.isdisjoint(cset1))
def pass_edges(self, G, edges): """Call pass values for a list of edge indexes""" fixed_outputs = ComponentSet() edge_list = self.idx_to_edge(G) for ei in edges: arc = G.edges[edge_list[ei]]["arc"] for var in arc.src.iter_vars(expr_vars=True, fixed=False): fixed_outputs.add(var) var.fix() self.pass_values(arc, self.fixed_inputs()) for var in fixed_outputs: var.free() fixed_outputs.clear()
def test_pop(self): cset = ComponentSet() self.assertEqual(len(cset), 0) with self.assertRaises(KeyError): cset.pop() v = variable() cset.add(v) self.assertTrue(v in cset) self.assertEqual(len(cset), 1) v_ = cset.pop() self.assertIs(v, v_) self.assertTrue(v not in cset) self.assertEqual(len(cset), 0)
def pass_edges(self, G, edges): """Call pass values for a list of edge indexes""" fixed_outputs = ComponentSet() edge_list = self.idx_to_edge(G) for ei in edges: arc = G.edges[edge_list[ei]]["arc"] for var in arc.src.iter_vars(expr_vars=True, fixed=False): fixed_outputs.add(var) var.fix() self.pass_values(arc, self.fixed_inputs()) for var in fixed_outputs: var.free() fixed_outputs.clear()
def add_integer_cut(var_values, solve_data, config, feasible=False): """Add an integer cut to the linear GDP model.""" m = solve_data.linear_GDP GDPopt = m.GDPopt_utils var_value_is_one = ComponentSet() var_value_is_zero = ComponentSet() for var, val in zip(GDPopt.working_var_list, var_values): if not var.is_binary(): continue if var.fixed: if val is not None and var.value != val: # val needs to be None or match var.value. Otherwise, we have a # contradiction. raise ValueError( "Fixed variable %s has value %s != " "provided value of %s." % (var.name, var.value, val)) val = var.value # TODO we can also add a check to skip binary variables that are not an # indicator_var on disjuncts. if fabs(val - 1) <= config.integer_tolerance: var_value_is_one.add(var) elif fabs(val) <= config.integer_tolerance: var_value_is_zero.add(var) else: raise ValueError( 'Binary %s = %s is not 0 or 1' % (var.name, val)) if not (var_value_is_one or var_value_is_zero): # if no remaining binary variables, then terminate algorithm. config.logger.info( 'Adding integer cut to a model without binary variables. ' 'Model is now infeasible.') if solve_data.objective_sense == minimize: solve_data.LB = float('inf') else: solve_data.UB = float('-inf') return False int_cut = (sum(1 - v for v in var_value_is_one) + sum(v for v in var_value_is_zero)) >= 1 if not feasible: config.logger.info('Adding integer cut') GDPopt.integer_cuts.add(expr=int_cut) else: backtracking_enabled = ( "disabled" if GDPopt.no_backtracking.active else "allowed") config.logger.info( 'Registering explored configuration. ' 'Backtracking is currently %s.' % backtracking_enabled) GDPopt.no_backtracking.add(expr=int_cut)
def visit(self, node, values): if node.__class__ is not EXPR.ExternalFunctionExpression: return node if id(node._fcn) not in self.efSet: return node # At this point we know this is an ExternalFunctionExpression # node that we want to replace with an auliliary variable (y) new_args = [] seen = ComponentSet() # TODO: support more than PythonCallbackFunctions assert isinstance(node._fcn, PythonCallbackFunction) # # Note: the first argument to PythonCallbackFunction is the # function ID. Since we are going to complain about constant # parameters, we need to skip the first argument when processing # the argument list. This is really not good: we should allow # for constant arguments to the functions, and we should relax # the restriction that the external functions implement the # PythonCallbackFunction API (that restriction leads unfortunate # things later; i.e., accessing the private _fcn attribute # below). for arg in list(values)[1:]: if type(arg) in nonpyomo_leaf_types or arg.is_fixed(): # We currently do not allow constants or parameters for # the external functions. raise RuntimeError( "TrustRegion does not support black boxes with " "constant or parameter inputs\n\tExpression: %s" % (node,) ) if arg.is_expression_type(): # All expressions (including simple linear expressions) # are replaced with a single auxiliary variable (and # eventually an additional constraint equating the # auxiliary variable to the original expression) _x = self.trf.x.add() _x.set_value( value(arg) ) self.trf.conset.add(_x == arg) new_args.append(_x) else: # The only thing left is bare variables: check for duplicates. if arg in seen: raise RuntimeError( "TrustRegion does not support black boxes with " "duplicate input arguments\n\tExpression: %s" % (node,) ) seen.add(arg) new_args.append(arg) _y = self.trf.y.add() self.trf.external_fcns.append(node) self.trf.exfn_xvars.append(new_args) return _y
def visit(self, node, values): if node.__class__ is not EXPR.ExternalFunctionExpression: return node if id(node._fcn) not in self.efSet: return node # At this point we know this is an ExternalFunctionExpression # node that we want to replace with an auliliary variable (y) new_args = [] seen = ComponentSet() # TODO: support more than PythonCallbackFunctions assert isinstance(node._fcn, PythonCallbackFunction) # # Note: the first argument to PythonCallbackFunction is the # function ID. Since we are going to complain about constant # parameters, we need to skip the first argument when processing # the argument list. This is really not good: we should allow # for constant arguments to the functions, and we should relax # the restriction that the external functions implement the # PythonCallbackFunction API (that restriction leads unfortunate # things later; i.e., accessing the private _fcn attribute # below). for arg in list(values)[1:]: if type(arg) in nonpyomo_leaf_types or arg.is_fixed(): # We currently do not allow constants or parameters for # the external functions. raise RuntimeError( "TrustRegion does not support black boxes with " "constant or parameter inputs\n\tExpression: %s" % (node, )) if arg.is_expression_type(): # All expressions (including simple linear expressions) # are replaced with a single auxiliary variable (and # eventually an additional constraint equating the # auxiliary variable to the original expression) _x = self.trf.x.add() _x.set_value(value(arg)) self.trf.conset.add(_x == arg) new_args.append(_x) else: # The only thing left is bare variables: check for duplicates. if arg in seen: raise RuntimeError( "TrustRegion does not support black boxes with " "duplicate input arguments\n\tExpression: %s" % (node, )) seen.add(arg) new_args.append(arg) _y = self.trf.y.add() self.trf.external_fcns.append(node) self.trf.exfn_xvars.append(new_args) return _y
def set_add(self): cset = ComponentSet() self.assertEqual(len(cset), 0) for i, c in enumerate(self._components): self.assertTrue(c not in cset) cset.add(c) self.assertTrue(c in cset) self.assertEqual(len(cset), i + 1) self.assertEqual(len(cset), len(self._components)) for c in self._components: self.assertTrue(c in cset) cset.add(c) self.assertTrue(c in cset) self.assertEqual(len(cset), len(self._components))
def filter_variables_from_solution(candidate_variables_at_relaxation_solution, tolerance=1e-6): """ This function takes a set of candidate variables for OBBT and filters out the variables that are at their bounds in the provided solution to the relaxation. See Gleixner, Ambros M., et al. "Three enhancements for optimization-based bound tightening." Journal of Global Optimization 67.4 (2017): 731-757. for details on why this works. The basic idea is that if x = xl is feasible for the relaxation that will be used for OBBT, then minimizing x subject to that relaxation is guaranteed to result in an optimal solution of x* = xl. This function simply loops through candidate_variables_at_relaxation_solution and specifies which variables should be minimized and which variables should be maximized with OBBT. Parameters ---------- candidate_variables_at_relaxation_solution: iterable of _GeneralVarData This should be an iterable of the variables which are candidates for OBBT. The values of the variables should be feasible for the relaxation that would be used to perform OBBT on the variables. tolerance: float A float greater than or equal to zero. If the value of the variable is within tolerance of its lower bound, then that variable is filtered from the set of variables that should be minimized for OBBT. The same is true for upper bounds and variables that should be maximized. Returns ------- vars_to_minimize: ComponentSet of _GeneralVarData variables that should be considered for minimization vars_to_maximize: ComponentSet of _GeneralVarData variables that should be considered for maximization """ candidate_vars = ComponentSet(candidate_variables_at_relaxation_solution) vars_to_minimize = ComponentSet() vars_to_maximize = ComponentSet() for v in candidate_vars: if (not v.has_lb()) or (v.value - v.lb > tolerance): vars_to_minimize.add(v) if (not v.has_ub()) or (v.ub - v.value > tolerance): vars_to_maximize.add(v) return vars_to_minimize, vars_to_maximize
def constraints_in_True_disjuncts(model, config): """Yield constraints in disjuncts where the indicator value is set or fixed to True.""" for constr in model.component_data_objects(Constraint): yield constr observed_disjuncts = ComponentSet() for disjctn in model.component_data_objects(Disjunction): # get all the disjuncts in the disjunction. Check which ones are True. for disj in disjctn.disjuncts: if disj in observed_disjuncts: continue observed_disjuncts.add(disj) if fabs(disj.indicator_var.value - 1) <= config.integer_tolerance: for constr in disj.component_data_objects(Constraint): yield constr
def constraints_in_True_disjuncts(model, config): """Yield constraints in disjuncts where the indicator value is set or fixed to True.""" for constr in model.component_data_objects(Constraint): yield constr observed_disjuncts = ComponentSet() for disjctn in model.component_data_objects(Disjunction): # get all the disjuncts in the disjunction. Check which ones are True. for disj in disjctn.disjuncts: if disj in observed_disjuncts: continue observed_disjuncts.add(disj) if fabs(disj.indicator_var.value - 1) <= config.integer_tolerance: for constr in disj.component_data_objects(Constraint): yield constr
def pass_tear_direct(self, G, tears): """Pass values across all tears in the given tear set""" fixed_outputs = ComponentSet() edge_list = self.idx_to_edge(G) for tear in tears: # fix everything then call pass values arc = G.edges[edge_list[tear]]["arc"] for var in arc.src.iter_vars(expr_vars=True, fixed=False): fixed_outputs.add(var) var.fix() self.pass_values(arc, fixed_inputs=self.fixed_inputs()) for var in fixed_outputs: var.free() fixed_outputs.clear()
def pass_tear_direct(self, G, tears): """Pass values across all tears in the given tear set""" fixed_outputs = ComponentSet() edge_list = self.idx_to_edge(G) for tear in tears: # fix everything then call pass values arc = G.edges[edge_list[tear]]["arc"] for var in arc.src.iter_vars(expr_vars=True, fixed=False): fixed_outputs.add(var) var.fix() self.pass_values(arc, fixed_inputs=self.fixed_inputs()) for var in fixed_outputs: var.free() fixed_outputs.clear()
def _detect_fixed_variables(m): """Detect fixed variables due to constraints of form var = const.""" new_fixed_vars = ComponentSet() for constr in m.component_data_objects(ctype=Constraint, active=True, descend_into=True): if constr.equality and constr.body.polynomial_degree() == 1: repn = generate_canonical_repn(constr.body) if len(repn.variables) == 1 and repn.linear[0]: var = repn.variables[0] coef = float(repn.linear[0]) const = repn.constant if repn.constant is not None else 0 var_val = (value(constr.lower) - value(const)) / coef var.fix(var_val) new_fixed_vars.add(var) return new_fixed_vars
def vars_to_eliminate_list(x): if isinstance(x, (Var, _VarData)): if not x.is_indexed(): return ComponentSet([x]) ans = ComponentSet() for j in x.index_set(): ans.add(x[j]) return ans elif hasattr(x, '__iter__'): ans = ComponentSet() for i in x: ans.update(vars_to_eliminate_list(i)) return ans else: raise ValueError("Expected Var or list of Vars." "\n\tRecieved %s" % type(x))
def total_blocks_set(block): """ Method to return a ComponentSet of all Block components in a model. Args: block : model to be studied Returns: A ComponentSet including all Block components in block (including block itself) """ total_blocks_set = ComponentSet( block.component_data_objects( ctype=Block, active=None, descend_into=True)) total_blocks_set.add(block) return total_blocks_set
def _detect_fixed_variables(m): """Detect fixed variables due to constraints of form var = const.""" new_fixed_vars = ComponentSet() for constr in m.component_data_objects(ctype=Constraint, active=True, descend_into=True): if constr.equality and constr.body.polynomial_degree() == 1: repn = generate_standard_repn(constr.body) if len(repn.linear_vars) == 1 and repn.linear_coefs[0]: var = repn.linear_vars[0] coef = float(repn.linear_coefs[0]) const = repn.constant var_val = (value(constr.lower) - value(const)) / coef var.fix(var_val) new_fixed_vars.add(var) return new_fixed_vars
def _get_expr_from_pyomo_repn(self, repn, max_degree=2): referenced_vars = ComponentSet() degree = canonical_degree(repn) if (degree is None) or (degree > max_degree): raise DegreeError( 'CPLEXDirect does not support expressions of degree {0}.'. format(degree)) if isinstance(repn, LinearCanonicalRepn): new_expr = _CplexExpr() if repn.constant is not None: new_expr.offset = repn.constant if (repn.linear is not None) and (len(repn.linear) > 0): list(map(referenced_vars.add, repn.variables)) new_expr.variables.extend(self._pyomo_var_to_ndx_map[var] for var in repn.variables) new_expr.coefficients.extend(coeff for coeff in repn.linear) else: new_expr = _CplexExpr() if 0 in repn: new_expr.offset = repn[0][None] if 1 in repn: for ndx, coeff in repn[1].items(): new_expr.coefficients.append(coeff) var = repn[-1][ndx] new_expr.variables.append(self._pyomo_var_to_ndx_map[var]) referenced_vars.add(var) if 2 in repn: for key, coeff in repn[2].items(): new_expr.q_coefficients.append(coeff) indices = list(key.keys()) if len(indices) == 1: ndx = indices[0] var = repn[-1][ndx] referenced_vars.add(var) cplex_var = self._pyomo_var_to_ndx_map[var] new_expr.q_variables1.append(cplex_var) new_expr.q_variables2.append(cplex_var) else: ndx = indices[0] var = repn[-1][ndx] referenced_vars.add(var) cplex_var = self._pyomo_var_to_ndx_map[var] new_expr.q_variables1.append(cplex_var) ndx = indices[1] var = repn[-1][ndx] referenced_vars.add(var) cplex_var = self._pyomo_var_to_ndx_map[var] new_expr.q_variables2.append(cplex_var) return new_expr, referenced_vars
def fixed_unused_variables_set(block): """ Method to return a ComponentSet of all fixed Var components which do not appear within any activated Constraint in a model. Args: block : model to be studied Returns: A ComponentSet including all fixed Var components which do not appear within any Constraints in block """ var_set = ComponentSet() for v in unused_variables_set(block): if v.fixed: var_set.add(v) return var_set
def fixed_variables_only_in_inequalities(block): """ Method to return a ComponentSet of all fixed Var components which appear only within activated inequality Constraints in a model. Args: block : model to be studied Returns: A ComponentSet including all fixed Var components which appear only within activated inequality Constraints in block """ var_set = ComponentSet() for v in variables_only_in_inequalities(block): if v.fixed: var_set.add(v) return var_set
def variables_in_activated_inequalities_set(block): """ Method to return a ComponentSet of all Var components which appear within an inequality Constraint in a model. Args: block : model to be studied Returns: A ComponentSet including all Var components which appear within activated inequality Constraints in block """ var_set = ComponentSet() for c in activated_inequalities_generator(block): for v in identify_variables(c.body): var_set.add(v) return var_set
def test_pickle(self): c = ComponentSet() self.assertEqual(len(c), 0) cup = pickle.loads(pickle.dumps(c)) self.assertIsNot(cup, c) self.assertEqual(len(cup), 0) v = variable() c.add(v) self.assertEqual(len(c), 1) self.assertTrue(v in c) cup = pickle.loads(pickle.dumps(c)) vup = cup.pop() cup.add(vup) self.assertIsNot(cup, c) self.assertIsNot(vup, v) self.assertEqual(len(cup), 1) self.assertTrue(vup in cup) self.assertEqual(vup.parent, None) b = block() V = b.V = variable_list() b.V.append(v) b.c = c self.assertEqual(len(c), 1) self.assertTrue(v in c) self.assertIs(v.parent, b.V) self.assertIs(V.parent, b) self.assertIs(b.parent, None) bup = pickle.loads(pickle.dumps(b)) Vup = bup.V vup = Vup[0] cup = bup.c self.assertIsNot(cup, c) self.assertIsNot(vup, v) self.assertIsNot(Vup, V) self.assertIsNot(bup, b) self.assertEqual(len(cup), 1) self.assertTrue(vup in cup) self.assertIs(vup.parent, Vup) self.assertIs(Vup.parent, bup) self.assertIs(bup.parent, None) self.assertEqual(len(c), 1) self.assertTrue(v in c)
def active_variables_in_deactivated_blocks_set(block): """ Method to return a ComponentSet of any Var components which appear within an active Constraint but belong to a deacitvated Block in a model. Args: block : model to be studied Returns: A ComponentSet including any Var components which belong to a deacitvated Block but appear in an activate Constraint in block """ var_set = ComponentSet() block_set = activated_blocks_set(block) for v in variables_in_activated_constraints_set(block): if v.parent_block() not in block_set: var_set.add(v) return var_set
def variables_in_activated_constraints_set(block): """ Method to return a ComponentSet of all Var components which appear within a Constraint in a model. Args: block : model to be studied Returns: A ComponentSet including all Var components which appear within activated Constraints in block """ var_set = ComponentSet() for c in block.component_data_objects( ctype=Constraint, active=True, descend_into=True): for v in identify_variables(c.body): var_set.add(v) return var_set
def _get_expr_from_pyomo_repn(self, repn, max_degree=2): referenced_vars = ComponentSet() degree = canonical_degree(repn) if (degree is None) or (degree > max_degree): raise DegreeError( 'GurobiDirect does not support expressions of degree {0}.'. format(degree)) if isinstance(repn, LinearCanonicalRepn): if (repn.linear is not None) and (len(repn.linear) > 0): list(map(referenced_vars.add, repn.variables)) new_expr = self._gurobipy.LinExpr(repn.linear, [ self._pyomo_var_to_solver_var_map[i] for i in repn.variables ]) else: new_expr = 0 if repn.constant is not None: new_expr += repn.constant else: new_expr = 0 if 0 in repn: new_expr += repn[0][None] if 1 in repn: for ndx, coeff in repn[1].items(): new_expr += coeff * self._pyomo_var_to_solver_var_map[ repn[-1][ndx]] referenced_vars.add(repn[-1][ndx]) if 2 in repn: for key, coeff in repn[2].items(): tmp_expr = coeff for ndx, power in key.items(): referenced_vars.add(repn[-1][ndx]) for i in range(power): tmp_expr *= self._pyomo_var_to_solver_var_map[ repn[-1][ndx]] new_expr += tmp_expr return new_expr, referenced_vars
def _get_expr_from_pyomo_repn(self, repn, max_degree=2): referenced_vars = ComponentSet() degree = repn.polynomial_degree() if (degree is None) or (degree > max_degree): raise DegreeError('GurobiDirect does not support expressions of degree {0}.'.format(degree)) if len(repn.linear_vars) > 0: referenced_vars.update(repn.linear_vars) new_expr = self._gurobipy.LinExpr(repn.linear_coefs, [self._pyomo_var_to_solver_var_map[i] for i in repn.linear_vars]) else: new_expr = 0.0 for i,v in enumerate(repn.quadratic_vars): x,y = v new_expr += repn.quadratic_coefs[i] * self._pyomo_var_to_solver_var_map[x] * self._pyomo_var_to_solver_var_map[y] referenced_vars.add(x) referenced_vars.add(y) new_expr += repn.constant return new_expr, referenced_vars
def _get_expr_from_pyomo_repn(self, repn, max_degree=2): referenced_vars = ComponentSet() degree = repn.polynomial_degree() if (degree is None) or (degree > max_degree): raise DegreeError('CPLEXDirect does not support expressions of degree {0}.'.format(degree)) new_expr = _CplexExpr() if len(repn.linear_vars) > 0: referenced_vars.update(repn.linear_vars) new_expr.variables.extend(self._pyomo_var_to_ndx_map[i] for i in repn.linear_vars) new_expr.coefficients.extend(repn.linear_coefs) for i, v in enumerate(repn.quadratic_vars): x, y = v new_expr.q_coefficients.append(repn.quadratic_coefs[i]) new_expr.q_variables1.append(self._pyomo_var_to_ndx_map[x]) new_expr.q_variables2.append(self._pyomo_var_to_ndx_map[y]) referenced_vars.add(x) referenced_vars.add(y) new_expr.offset = repn.constant return new_expr, referenced_vars
def build_ordered_component_lists(model, solve_data): """Define lists used for future data transfer. Also attaches ordered lists of the variables, constraints, disjuncts, and disjunctions to the model so that they can be used for mapping back and forth. """ util_blk = getattr(model, solve_data.util_block_name) var_set = ComponentSet() setattr( util_blk, 'constraint_list', list( model.component_data_objects( ctype=Constraint, active=True, descend_into=(Block, Disjunct)))) setattr( util_blk, 'disjunct_list', list( model.component_data_objects( ctype=Disjunct, descend_into=(Block, Disjunct)))) setattr( util_blk, 'disjunction_list', list( model.component_data_objects( ctype=Disjunction, active=True, descend_into=(Disjunct, Block)))) # Identify the non-fixed variables in (potentially) active constraints and # objective functions for constr in getattr(util_blk, 'constraint_list'): for v in identify_variables(constr.body, include_fixed=False): var_set.add(v) for obj in model.component_data_objects(ctype=Objective, active=True): for v in identify_variables(obj.expr, include_fixed=False): var_set.add(v) # Disjunct indicator variables might not appear in active constraints. In # fact, if we consider them Logical variables, they should not appear in # active algebraic constraints. For now, they need to be added to the # variable set. for disj in getattr(util_blk, 'disjunct_list'): var_set.add(disj.indicator_var) # We use component_data_objects rather than list(var_set) in order to # preserve a deterministic ordering. var_list = list( v for v in model.component_data_objects( ctype=Var, descend_into=(Block, Disjunct)) if v in var_set) setattr(util_blk, 'variable_list', var_list)
def _apply_to(self, instance, **kwds): if __debug__ and logger.isEnabledFor(logging.DEBUG): #pragma:nocover logger.debug("Calling ConnectorExpander") connectorsFound = False for c in instance.component_data_objects(Connector): connectorsFound = True break if not connectorsFound: return if __debug__ and logger.isEnabledFor(logging.DEBUG): #pragma:nocover logger.debug(" Connectors found!") self._name_buffer = {} # # At this point, there are connectors in the model, so we must # look for constraints that involve connectors and expand them. # # List of the connectors in the order in which we found them # (this should be deterministic, provided that the user's model # is deterministic) connector_list = [] # list of constraints with connectors: tuple(constraint, connector_set) # (this should be deterministic, provided that the user's model # is deterministic) constraint_list = [] # ID of the next connector group (set of matched connectors) groupID = 0 # connector_groups stars out as a dict of {id(set): (groupID, set)} # If you sort by the groupID, then this will be deterministic. connector_groups = dict() # map of connector to the set of connectors that must match it matched_connectors = ComponentMap() # The set of connectors found in the current constraint found = ComponentSet() connector_types = set([SimpleConnector, _ConnectorData]) for constraint in instance.component_data_objects( Constraint, sort=SortComponents.deterministic): ref = None for c in EXPR.identify_components(constraint.body, connector_types): found.add(c) if c in matched_connectors: if ref is None: # The first connector in this constraint has # already been seen. We will use that Set as # the reference ref = matched_connectors[c] elif ref is not matched_connectors[c]: # We already have a reference group; merge this # new group into it. # # Optimization: this merge is linear in the size # of the src set. If the reference set is # smaller, save time by switching to a new # reference set. src = matched_connectors[c] if len(ref) < len(src): ref, src = src, ref ref.update(src) for _ in src: matched_connectors[_] = ref del connector_groups[id(src)] # else: pass # The new group *is* the reference group; # there is nothing to do. else: # The connector has not been seen before. connector_list.append(c) if ref is None: # This is the first connector in the constraint: # start a new reference set. ref = ComponentSet() connector_groups[id(ref)] = (groupID, ref) groupID += 1 # This connector hasn't been seen. Record it. ref.add(c) matched_connectors[c] = ref if ref is not None: constraint_list.append((constraint, found)) found = ComponentSet() # Validate all connector sets and expand the empty ones known_conn_sets = {} for groupID, conn_set in sorted(itervalues(connector_groups)): known_conn_sets[id(conn_set)] \ = self._validate_and_expand_connector_set(conn_set) # Expand each constraint for constraint, conn_set in constraint_list: cList = ConstraintList() constraint.parent_block().add_component( '%s.expanded' % ( constraint.getname( fully_qualified=False, name_buffer=self._name_buffer), ), cList ) connId = next(iter(conn_set)) ref = known_conn_sets[id(matched_connectors[connId])] for k,v in sorted(iteritems(ref)): if v[1] >= 0: _iter = v[0] else: _iter = (v[0],) for idx in _iter: substitution = {} for c in conn_set: if v[1] >= 0: new_v = c.vars[k][idx] elif k in c.aggregators: new_v = c.vars[k].add() else: new_v = c.vars[k] substitution[id(c)] = new_v cList.add(( constraint.lower, EXPR.clone_expression( constraint.body, substitution ), constraint.upper )) constraint.deactivate() # Now, go back and implement VarList aggregators for conn in connector_list: block = conn.parent_block() for var, aggregator in iteritems(conn.aggregators): c = Constraint(expr=aggregator(block, conn.vars[var])) block.add_component( '%s.%s.aggregate' % ( conn.getname( fully_qualified=True, name_buffer=self._name_buffer), var), c )
def _transformDisjunctionData(self, obj, transBlock, index): # Convex hull doesn't work if this is an or constraint. So if # xor is false, give up if not obj.xor: raise GDP_Error("Cannot do convex hull transformation for " "disjunction %s with or constraint. Must be an xor!" % obj.name) parent_component = obj.parent_component() transBlock.disjContainers.add(parent_component) orConstraint, disaggregationConstraint \ = self._getDisjunctionConstraints(parent_component) # We first go through and collect all the variables that we # are going to disaggregate. varOrder_set = ComponentSet() varOrder = [] varsByDisjunct = ComponentMap() for disjunct in obj.disjuncts: # This is crazy, but if the disjunct has been previously # relaxed, the disjunct *could* be deactivated. not_active = not disjunct.active if not_active: disjunct._activate_without_unfixing_indicator() try: disjunctVars = varsByDisjunct[disjunct] = ComponentSet() for cons in disjunct.component_data_objects( Constraint, active = True, sort=SortComponents.deterministic, descend_into=Block): # we aren't going to disaggregate fixed # variables. This means there is trouble if they are # unfixed later... for var in EXPR.identify_variables( cons.body, include_fixed=False): # Note the use of a list so that we will # eventually disaggregate the vars in a # deterministic order (the order that we found # them) disjunctVars.add(var) if var not in varOrder_set: varOrder.append(var) varOrder_set.add(var) finally: if not_active: disjunct._deactivate_without_fixing_indicator() # We will only disaggregate variables that # 1) appear in multiple disjuncts, or # 2) are not contained in this disjunct, or # 3) are not themselves disaggregated variables varSet = [] localVars = ComponentMap((d,[]) for d in obj.disjuncts) for var in varOrder: disjuncts = [d for d in varsByDisjunct if var in varsByDisjunct[d]] if len(disjuncts) > 1: varSet.append(var) elif self._contained_in(var, disjuncts[0]): localVars[disjuncts[0]].append(var) elif self._contained_in(var, transBlock): # There is nothing to do here: these are already # disaggregated vars that can/will be forced to 0 when # their disjunct is not active. pass else: varSet.append(var) # Now that we know who we need to disaggregate, we will do it # while we also transform the disjuncts. or_expr = 0 for disjunct in obj.disjuncts: or_expr += disjunct.indicator_var self._transform_disjunct(disjunct, transBlock, varSet, localVars[disjunct]) orConstraint.add(index, (or_expr, 1)) for i, var in enumerate(varSet): disaggregatedExpr = 0 for disjunct in obj.disjuncts: if 'chull' not in disjunct._gdp_transformation_info: if not disjunct.indicator_var.is_fixed() \ or value(disjunct.indicator_var) != 0: raise RuntimeError( "GDP chull: disjunct was not relaxed, but " "does not appear to be correctly deactivated.") continue disaggregatedVar = disjunct._gdp_transformation_info['chull'][ 'disaggregatedVars'][var] disaggregatedExpr += disaggregatedVar if type(index) is tuple: consIdx = index + (i,) elif parent_component.is_indexed(): consIdx = (index,) + (i,) else: consIdx = i disaggregationConstraint.add( consIdx, var == disaggregatedExpr)
def _collect_ports(self, instance): self._name_buffer = {} # List of the ports in the order in which we found them # (this should be deterministic, provided that the user's model # is deterministic) port_list = [] # ID of the next port group (set of matched ports) groupID = 0 # port_groups stars out as a dict of {id(set): (groupID, set)} # If you sort by the groupID, then this will be deterministic. port_groups = dict() # map of port to the set of ports that must match it matched_ports = ComponentMap() for arc in instance.component_data_objects(**obj_iter_kwds): ports = ComponentSet(arc.ports) ref = None for p in arc.ports: if p in matched_ports: if ref is None: # The first port in this arc has # already been seen. We will use that Set as # the reference ref = matched_ports[p] elif ref is not matched_ports[p]: # We already have a reference group; merge this # new group into it. # Optimization: this merge is linear in the size # of the src set. If the reference set is # smaller, save time by switching to a new # reference set. src = matched_ports[p] if len(ref) < len(src): ref, src = src, ref ref.update(src) for i in src: matched_ports[i] = ref del port_groups[id(src)] # else: pass # The new group *is* the reference group; # there is nothing to do. else: # The port has not been seen before. port_list.append(p) if ref is None: # This is the first port in the arc: # start a new reference set. ref = ComponentSet() port_groups[id(ref)] = (groupID, ref) groupID += 1 # This port hasn't been seen. Record it. ref.add(p) matched_ports[p] = ref # Validate all port sets and expand the empty ones known_port_sets = {} for groupID, port_set in sorted(itervalues(port_groups)): known_port_sets[id(port_set)] \ = self._validate_and_expand_port_set(port_set) return port_list, known_port_sets, matched_ports
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 fbbt_block(m, tol=1e-4, deactivate_satisfied_constraints=False, integer_tol=1e-5, infeasible_tol=1e-8): """ Feasibility based bounds tightening (FBBT) for a block or model. This loops through all of the constraints in the block and performs FBBT on each constraint (see the docstring for fbbt_con()). Through this processes, any variables whose bounds improve by more than tol are collected, and FBBT is performed again on all constraints involving those variables. This process is continued until no variable bounds are improved by more than tol. Parameters ---------- m: pyomo.core.base.block.Block or pyomo.core.base.PyomoModel.ConcreteModel tol: float deactivate_satisfied_constraints: bool If deactivate_satisfied_constraints is True and a constraint is always satisfied, then the constranit will be deactivated integer_tol: float If the lower bound computed on a binary variable is less than or equal to integer_tol, then the lower bound is left at 0. Otherwise, the lower bound is increased to 1. If the upper bound computed on a binary variable is greater than or equal to 1-integer_tol, then the upper bound is left at 1. Otherwise the upper bound is decreased to 0. infeasible_tol: float If the bounds computed on the body of a constraint violate the bounds of the constraint by more than infeasible_tol, then the constraint is considered infeasible and an exception is raised. Returns ------- new_var_bounds: ComponentMap A ComponentMap mapping from variables a tuple containing the lower and upper bounds, respectively, computed from FBBT. """ new_var_bounds = ComponentMap() var_to_con_map = ComponentMap() var_lbs = ComponentMap() var_ubs = ComponentMap() for c in m.component_data_objects(ctype=Constraint, active=True, descend_into=True, sort=True): for v in identify_variables(c.body): if v not in var_to_con_map: var_to_con_map[v] = list() if v.lb is None: var_lbs[v] = -math.inf else: var_lbs[v] = value(v.lb) if v.ub is None: var_ubs[v] = math.inf else: var_ubs[v] = value(v.ub) var_to_con_map[v].append(c) for _v in m.component_data_objects(ctype=Var, active=True, descend_into=True, sort=True): if _v.is_fixed(): _v.setlb(_v.value) _v.setub(_v.value) new_var_bounds[_v] = (_v.value, _v.value) improved_vars = ComponentSet() for c in m.component_data_objects(ctype=Constraint, active=True, descend_into=True, sort=True): _new_var_bounds = fbbt_con(c, deactivate_satisfied_constraints=deactivate_satisfied_constraints, integer_tol=integer_tol, infeasible_tol=infeasible_tol) new_var_bounds.update(_new_var_bounds) for v, bnds in _new_var_bounds.items(): vlb, vub = bnds if vlb is not None: if vlb > var_lbs[v] + tol: improved_vars.add(v) var_lbs[v] = vlb if vub is not None: if vub < var_ubs[v] - tol: improved_vars.add(v) var_ubs[v] = vub while len(improved_vars) > 0: v = improved_vars.pop() for c in var_to_con_map[v]: _new_var_bounds = fbbt_con(c, deactivate_satisfied_constraints=deactivate_satisfied_constraints, integer_tol=integer_tol, infeasible_tol=infeasible_tol) new_var_bounds.update(_new_var_bounds) for _v, bnds in _new_var_bounds.items(): _vlb, _vub = bnds if _vlb is not None: if _vlb > var_lbs[_v] + tol: improved_vars.add(_v) var_lbs[_v] = _vlb if _vub is not None: if _vub < var_ubs[_v] - tol: improved_vars.add(_v) var_ubs[_v] = _vub return new_var_bounds