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 _build_equality_set(m): """Construct an equality set map. Maps all variables to the set of variables that are linked to them by equality. Mapping takes place using id(). That is, if you have x = y, then you would have id(x) -> ComponentSet([x, y]) and id(y) -> ComponentSet([x, y]) in the mapping. """ #: dict: map of var UID to the set of all equality-linked var UIDs eq_var_map = ComponentMap() relevant_vars = ComponentSet() for constr in m.component_data_objects(ctype=Constraint, active=True, descend_into=True): # Check to make sure the constraint is of form v1 - v2 == 0 if (value(constr.lower) == 0 and value(constr.upper) == 0 and constr.body.polynomial_degree() == 1): repn = generate_standard_repn(constr.body) # only take the variables with nonzero coefficients vars_ = [v for i, v in enumerate(repn.linear_vars) if repn.linear_coefs[i]] if (len(vars_) == 2 and sorted(l for l in repn.linear_coefs if l) == [-1, 1]): # this is an a == b constraint. v1 = vars_[0] v2 = vars_[1] set1 = eq_var_map.get(v1, ComponentSet([v1])) set2 = eq_var_map.get(v2, ComponentSet([v2])) relevant_vars.update([v1, v2]) set1.update(set2) # set1 is now the union for v in set1: eq_var_map[v] = set1 return eq_var_map, relevant_vars
def disjunctive_obbt(model, solver): """Provides Optimality-based bounds tightening to a model using a solver.""" model._disjuncts_to_process = list(model.component_data_objects( ctype=Disjunct, active=True, descend_into=(Block, Disjunct), descent_order=TraversalStrategy.BreadthFirstSearch)) if model.type() == Disjunct: model._disjuncts_to_process.insert(0, model) linear_var_set = ComponentSet() for constr in model.component_data_objects( Constraint, active=True, descend_into=(Block, Disjunct)): if constr.body.polynomial_degree() in linear_degrees: linear_var_set.update(identify_variables(constr.body, include_fixed=False)) model._disj_bnds_linear_vars = list(linear_var_set) for disj_idx, disjunct in enumerate(model._disjuncts_to_process): var_bnds = obbt_disjunct(model, disj_idx, solver) if var_bnds is not None: # Add bounds to the disjunct if not hasattr(disjunct, '_disj_var_bounds'): # No bounds had been computed before. Attach the bounds dictionary. disjunct._disj_var_bounds = var_bnds else: # Update the bounds dictionary. for var, new_bnds in var_bnds.items(): old_lb, old_ub = disjunct._disj_var_bounds.get(var, (-inf, inf)) new_lb, new_ub = new_bnds disjunct._disj_var_bounds[var] = (max(old_lb, new_lb), min(old_ub, new_ub)) else: disjunct.deactivate() # prune disjunct
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 detect_unfixed_discrete_vars(model): """Detect unfixed discrete variables in use on the model.""" var_set = ComponentSet() for constr in model.component_data_objects( Constraint, active=True, descend_into=True): var_set.update( v for v in EXPR.identify_variables( constr.body, include_fixed=False) if not v.is_continuous()) return var_set
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 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 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 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 obbt_disjunct(orig_model, idx, solver): model = orig_model.clone() # Fix the disjunct to be active disjunct = model._disjuncts_to_process[idx] disjunct.indicator_var.fix(1) for obj in model.component_data_objects(Objective, active=True): obj.deactivate() # Deactivate nonlinear constraints for constr in model.component_data_objects( Constraint, active=True, descend_into=(Block, Disjunct)): if constr.body.polynomial_degree() not in linear_degrees: constr.deactivate() # Only look at the variables participating in active constraints within the scope relevant_var_set = ComponentSet() for constr in disjunct.component_data_objects(Constraint, active=True): relevant_var_set.update(identify_variables(constr.body, include_fixed=False)) TransformationFactory('gdp.bigm').apply_to(model) model._var_bounding_obj = Objective(expr=1, sense=minimize) for var in relevant_var_set: model._var_bounding_obj.set_value(expr=var) var_lb = solve_bounding_problem(model, solver) if var_lb is None: return None # bounding problem infeasible model._var_bounding_obj.set_value(expr=-var) var_ub = solve_bounding_problem(model, solver) if var_ub is None: return None # bounding problem infeasible else: var_ub = -var_ub # sign correction var.setlb(var_lb) var.setub(var_ub) # Maps original variable --> (new computed LB, new computed UB) var_bnds = ComponentMap( ((orig_var, ( clone_var.lb if clone_var.has_lb() else -inf, clone_var.ub if clone_var.has_ub() else inf)) for orig_var, clone_var in zip( orig_model._disj_bnds_linear_vars, model._disj_bnds_linear_vars) if clone_var in relevant_var_set) ) return var_bnds
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_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 _apply_to(self, instance, **kwds): config = self.CONFIG(kwds) if config.tmp and not hasattr(instance, '_tmp_propagate_original_bounds'): instance._tmp_propagate_original_bounds = Suffix( direction=Suffix.LOCAL) eq_var_map, relevant_vars = _build_equality_set(instance) processed = ComponentSet() # Go through each variable in an equality set to propagate the variable # bounds to all equality-linked variables. for var in relevant_vars: # If we have already processed the variable, skip it. if var in processed: continue var_equality_set = eq_var_map.get(var, ComponentSet([var])) #: variable lower bounds in the equality set lbs = [v.lb for v in var_equality_set if v.has_lb()] max_lb = max(lbs) if len(lbs) > 0 else None #: variable upper bounds in the equality set ubs = [v.ub for v in var_equality_set if v.has_ub()] min_ub = min(ubs) if len(ubs) > 0 else None # Check for error due to bound cross-over if max_lb is not None and min_ub is not None and max_lb > min_ub: # the lower bound is above the upper bound. Raise a ValueError. # get variable with the highest lower bound v1 = next(v for v in var_equality_set if v.lb == max_lb) # get variable with the lowest upper bound v2 = next(v for v in var_equality_set if v.ub == min_ub) raise ValueError( 'Variable {} has a lower bound {} ' ' > the upper bound {} of variable {}, ' 'but they are linked by equality constraints.' .format(v1.name, value(v1.lb), value(v2.ub), v2.name)) for v in var_equality_set: if config.tmp: # TODO warn if overwriting instance._tmp_propagate_original_bounds[v] = ( v.lb, v.ub) v.setlb(max_lb) v.setub(min_ub) processed.update(var_equality_set)
def __init__(self, comp): self._vars = ComponentSet() self._saved_bounds = list() if comp.type() == Constraint: if comp.is_indexed(): for c in comp.values(): self._vars.update(identify_variables(c.body)) else: self._vars.update(identify_variables(comp.body)) else: for c in comp.component_data_objects(Constraint, descend_into=True, active=True, sort=True): self._vars.update(identify_variables(c.body))
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 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 _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 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 test_eq(self): cset1 = ComponentSet() self.assertEqual(cset1, set()) self.assertTrue(cset1 == set()) self.assertNotEqual(cset1, list()) self.assertFalse(cset1 == list()) self.assertNotEqual(cset1, tuple()) self.assertFalse(cset1 == tuple()) self.assertNotEqual(cset1, dict()) self.assertFalse(cset1 == dict()) cset1.update(self._components) self.assertNotEqual(cset1, set()) self.assertFalse(cset1 == set()) self.assertNotEqual(cset1, list()) self.assertFalse(cset1 == list()) self.assertNotEqual(cset1, tuple()) self.assertFalse(cset1 == tuple()) self.assertNotEqual(cset1, dict()) self.assertFalse(cset1 == dict()) self.assertTrue(cset1 == cset1) self.assertEqual(cset1, cset1) cset2 = ComponentSet(self._components) self.assertTrue(cset2 == cset1) self.assertFalse(cset2 != cset1) self.assertEqual(cset2, cset1) self.assertTrue(cset1 == cset2) self.assertFalse(cset1 != cset2) self.assertEqual(cset1, cset2) cset2.remove(self._components[0]) self.assertFalse(cset2 == cset1) self.assertTrue(cset2 != cset1) self.assertNotEqual(cset2, cset1) self.assertFalse(cset1 == cset2) self.assertTrue(cset1 != cset2) self.assertNotEqual(cset1, cset2)
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 _set_input(self, relaxation_side=RelaxationSide.BOTH, persistent_solvers=None, use_linear_relaxation=True, large_eval_tol=math.inf): self._oa_points = list() self._saved_oa_points = list() self._persistent_solvers = persistent_solvers if self._persistent_solvers is None: self._persistent_solvers = ComponentSet() if not isinstance(self._persistent_solvers, Iterable): self._persistent_solvers = ComponentSet([self._persistent_solvers]) else: self._persistent_solvers = ComponentSet(self._persistent_solvers) self._relaxation_side = relaxation_side assert self._relaxation_side in RelaxationSide self.use_linear_relaxation = use_linear_relaxation self.large_eval_tol = large_eval_tol
def detect_unfixed_discrete_vars(model): """Detect unfixed discrete variables in use on the model.""" var_set = ComponentSet() for constr in model.component_data_objects(Constraint, active=True, descend_into=True): var_set.update( v for v in EXPR.identify_variables(constr.body, include_fixed=False) if not v.is_continuous()) for obj in model.component_data_objects(Objective, active=True): var_set.update( v for v in EXPR.identify_variables(obj.expr, include_fixed=False) if not v.is_continuous()) return var_set
def _load_slacks(self, cons_to_load=None): if not hasattr(self._pyomo_model, 'slack'): self._pyomo_model.slack = Suffix(direction=Suffix.IMPORT) con_map = self._pyomo_con_to_solver_con_map slack = self._pyomo_model.slack if cons_to_load is None: cons_to_load = ComponentSet(con_map.keys()) reverse_con_map = {} for pyomo_con, con in con_map.items(): reverse_con_map[con] = pyomo_con for cplex_con in self._solver_model.linear_constraints.get_names(): pyomo_con = reverse_con_map[cplex_con] if pyomo_con in cons_to_load: slack[pyomo_con] = self._solver_model.solution.get_linear_slacks(cplex_con) for cplex_con in self._solver_model.quadratic_constraints.get_names(): pyomo_con = reverse_con_map[cplex_con] if pyomo_con in cons_to_load: slack[pyomo_con] = self._solver_model.solution.get_quadratic_slacks(cplex_con)
def pass_tear_wegstein(self, G, tears, x): """ Set the destination value of all tear edges to the corresponding value in the numpy array x. """ fixed_inputs = self.fixed_inputs() edge_list = self.idx_to_edge(G) i = 0 for tear in tears: arc = G.edges[edge_list[tear]]["arc"] src, dest = arc.src, arc.dest dest_unit = dest.parent_block() if dest_unit not in fixed_inputs: fixed_inputs[dest_unit] = ComponentSet() for name, index, mem in src.iter_vars(names=True): peer = self.source_dest_peer(arc, name, index) self.pass_single_value(dest, name, peer, x[i], fixed_inputs[dest_unit]) i += 1
def _load_slacks(self, cons_to_load=None): if not hasattr(self._pyomo_model, 'slack'): self._pyomo_model.slack = Suffix(direction=Suffix.IMPORT) con_map = self._pyomo_con_to_solver_con_map slack = self._pyomo_model.slack if cons_to_load is None: cons_to_load = ComponentSet(con_map.keys()) reverse_con_map = {} for pyomo_con, con in con_map.items(): reverse_con_map[con] = pyomo_con for gurobi_con in self._solver_model.getConstrs(): pyomo_con = reverse_con_map[gurobi_con] if pyomo_con in cons_to_load: slack[pyomo_con] = gurobi_con.Slack if self._version_major >= 5: for gurobi_con in self._solver_model.getQConstrs(): pyomo_con = reverse_con_map[gurobi_con] if pyomo_con in cons_to_load: slack[pyomo_con] = gurobi_con.QCSlack
def _add_sos_constraint(self, con): if not con.active: return None conname = self._symbol_map.getSymbol(con, self._labeler) level = con.level if level == 1: sos_type = self._gurobipy.GRB.SOS_TYPE1 elif level == 2: sos_type = self._gurobipy.GRB.SOS_TYPE2 else: raise ValueError("Solver does not support SOS " "level {0} constraints".format(level)) gurobi_vars = [] weights = [] self._vars_referenced_by_con[con] = ComponentSet() if hasattr(con, 'get_items'): # aml sos constraint sos_items = list(con.get_items()) else: # kernel sos constraint sos_items = list(con.items()) for v, w in sos_items: self._vars_referenced_by_con[con].add(v) gurobi_vars.append(self._pyomo_var_to_solver_var_map[v]) self._referenced_variables[v] += 1 weights.append(w) gurobipy_con = self._solver_model.addSOS(sos_type, gurobi_vars, weights) self._pyomo_con_to_solver_con_map[con] = gurobipy_con self._solver_con_to_pyomo_con_map[gurobipy_con] = con self._needs_updated = True
def _set_objective(self, obj): if self._objective is not None: for var in self._vars_referenced_by_obj: self._referenced_variables[var] -= 1 self._vars_referenced_by_obj = ComponentSet() self._objective = None self._solver_model.objective.set_linear([(i, 0.0) for i in range(len(self._pyomo_var_to_solver_var_map.values()))]) self._solver_model.objective.set_quadratic([[[0], [0]] for i in self._pyomo_var_to_solver_var_map.keys()]) if obj.active is False: raise ValueError('Cannot add inactive objective to solver.') if obj.sense == minimize: sense = self._solver_model.objective.sense.minimize elif obj.sense == maximize: sense = self._solver_model.objective.sense.maximize else: raise ValueError('Objective sense is not recognized: {0}'.format(obj.sense)) cplex_expr, referenced_vars = self._get_expr_from_pyomo_expr(obj.expr, self._max_obj_degree) for i in range(len(cplex_expr.q_coefficients)): cplex_expr.q_coefficients[i] *= 2 for var in referenced_vars: self._referenced_variables[var] += 1 self._solver_model.objective.set_sense(sense) if hasattr(self._solver_model.objective, 'set_offset'): self._solver_model.objective.set_offset(cplex_expr.offset) if len(cplex_expr.coefficients) != 0: self._solver_model.objective.set_linear(list(zip(cplex_expr.variables, cplex_expr.coefficients))) if len(cplex_expr.q_coefficients) != 0: self._solver_model.objective.set_quadratic_coefficients(list(zip(cplex_expr.q_variables1, cplex_expr.q_variables2, cplex_expr.q_coefficients))) self._objective = obj self._vars_referenced_by_obj = referenced_vars
def is_explicitly_indexed_by(comp, *sets, **kwargs): """ Function for determining whether a pyomo component is indexed by a set or group of sets. Args: comp : Some Pyomo component, possibly indexed sets : Pyomo Sets to check indexing by expand_all_set_operators : Whether or not to expand all set operators in the subsets method Returns: A bool that is True if comp is directly indexed by every set in sets. """ if not comp.is_indexed(): return False for s in sets: if isinstance(s, SetProduct): msg = ('Checking for explicit indexing by a SetProduct ' 'is not supported') raise TypeError(msg) expand_all_set_operators = kwargs.pop('expand_all_set_operators', False) if kwargs: keys = kwargs.keys() raise ValueError('Unrecognized keyword arguments: %s' % str(keys)) projected_subsets = comp.index_set().subsets( expand_all_set_operators=expand_all_set_operators) # Expanding all set operators here can be dangerous because it will not # distinguish between operators that contain their operands (e.g. union, # where you might consider the component to be considered indexed by # the operands) and operators that don't. # Ideally would like to check for containment by inclusion and containment # by product in one search of the set operators. subset_set = ComponentSet(projected_subsets) return all([_ in subset_set for _ in sets])
def _add_sos_constraint(self, con): if not con.active: return None conname = self._symbol_map.getSymbol(con, self._labeler) level = con.level if level == 1: sos_type = self._solver_model.SOS.type.SOS1 elif level == 2: sos_type = self._solver_model.SOS.type.SOS2 else: raise ValueError( 'Solver does not support SOS level {0} constraints'.format( level)) cplex_vars = [] weights = [] self._vars_referenced_by_con[con] = ComponentSet() if hasattr(con, 'get_items'): # aml sos constraint sos_items = list(con.get_items()) else: # kernel sos constraint sos_items = list(con.items()) for v, w in sos_items: self._vars_referenced_by_con[con].add(v) cplex_vars.append(self._pyomo_var_to_solver_var_map[v]) self._referenced_variables[v] += 1 weights.append(w) self._solver_model.SOS.add(type=sos_type, SOS=[cplex_vars, weights], name=conname) self._pyomo_con_to_solver_con_map[con] = conname self._solver_con_to_pyomo_con_map[conname] = con
def build(self, **kwargs): self._persistent_solvers = kwargs.pop('persistent_solvers', None) if self._persistent_solvers is None: self._persistent_solvers = ComponentSet() if not isinstance(self._persistent_solvers, Iterable): self._persistent_solvers = ComponentSet([self._persistent_solvers]) else: self._persistent_solvers = ComponentSet(self._persistent_solvers) self._relaxation_side = kwargs.pop('relaxation_side', RelaxationSide.BOTH) assert self._relaxation_side in RelaxationSide self._set_input(kwargs) self.rebuild() if len(kwargs) != 0: msg = 'Unexpected keyword arguments in build:\n' for k, v in kwargs.items(): msg += '\t{0}: {1}\n'.format(k, v) raise ValueError(msg)
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 activated_blocks_set(block): """ Method to return a ComponentSet of all activated Block components in a model. Args: block : model to be studied Returns: A ComponentSet including all activated Block components in block (including block itself) """ block_set = ComponentSet() if block.active: block_set.add(block) for b in block.component_data_objects( ctype=Block, active=True, descend_into=True): block_set.add(b) return block_set
def build_ordered_component_lists(model, prefix='working'): """Define lists used for future data transfer.""" GDPopt = model.GDPopt_utils var_set = ComponentSet() setattr( GDPopt, '%s_constraints_list' % prefix, list( model.component_data_objects( ctype=Constraint, active=True, descend_into=(Block, Disjunct)))) setattr( GDPopt, '%s_disjuncts_list' % prefix, list( model.component_data_objects( ctype=Disjunct, descend_into=(Block, Disjunct)))) setattr( GDPopt, '%s_disjunctions_list' % prefix, list( model.component_data_objects( ctype=Disjunction, active=True, descend_into=(Disjunct, Block)))) # Identify the non-fixed variables in (potentially) active constraints for constr in getattr(GDPopt, '%s_constraints_list' % prefix): for v in EXPR.identify_variables(constr.body, 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(GDPopt, '%s_disjuncts_list' % prefix): var_set.add(disj.indicator_var) # We use component_data_objects rather than list(var_set) in order to # preserve a deterministic ordering. setattr( GDPopt, '%s_var_list' % prefix, list( v for v in model.component_data_objects( ctype=Var, descend_into=(Block, Disjunct)) if v in var_set)) setattr( GDPopt, '%s_nonlinear_constraints' % prefix, [ v for v in getattr(GDPopt, '%s_constraints_list' % prefix) if v.body.polynomial_degree() not in (0, 1)])
def large_residuals_set(block, tol=1e-5): """ Method to return a ComponentSet of all Constraint components with a residual greater than a given threshold which appear in a model. Args: block : model to be studied tol : residual threshold for inclusion in ComponentSet Returns: A ComponentSet including all Constraint components with a residual greater than tol which appear in block """ large_residuals_set = ComponentSet() for c in block.component_data_objects( ctype=Constraint, active=True, descend_into=True): if c.active and value(c.lower - c.body()) > tol: large_residuals_set.add(c) elif c.active and value(c.body() - c.upper) > tol: large_residuals_set.add(c) return large_residuals_set
class BoundsManager(object): def __init__(self, comp): self._vars = ComponentSet() self._saved_bounds = list() if comp.type() == Constraint: if comp.is_indexed(): for c in comp.values(): self._vars.update(identify_variables(c.body)) else: self._vars.update(identify_variables(comp.body)) else: for c in comp.component_data_objects(Constraint, descend_into=True, active=True, sort=True): self._vars.update(identify_variables(c.body)) def save_bounds(self): bnds = ComponentMap() for v in self._vars: bnds[v] = (v.lb, v.ub) self._saved_bounds.append(bnds) def pop_bounds(self, ndx=-1): bnds = self._saved_bounds.pop(ndx) for v, _bnds in bnds.items(): lb, ub = _bnds v.setlb(lb) v.setub(ub) def load_bounds(self, bnds, save_current_bounds=True): if save_current_bounds: self.save_bounds() for v, _bnds in bnds.items(): if v in self._vars: lb, ub = _bnds v.setlb(lb) v.setub(ub)
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 _build_equality_set(m): """Construct an equality set map. Maps all variables to the set of variables that are linked to them by equality. Mapping takes place using id(). That is, if you have x = y, then you would have id(x) -> ComponentSet([x, y]) and id(y) -> ComponentSet([x, y]) in the mapping. """ #: dict: map of var UID to the set of all equality-linked var UIDs eq_var_map = ComponentMap() relevant_vars = ComponentSet() for constr in m.component_data_objects(ctype=Constraint, active=True, descend_into=True): # Check to make sure the constraint is of form v1 - v2 == 0 if (value(constr.lower) == 0 and value(constr.upper) == 0 and constr.body.polynomial_degree() == 1): repn = generate_standard_repn(constr.body) # only take the variables with nonzero coefficients vars_ = [ v for i, v in enumerate(repn.linear_vars) if repn.linear_coefs[i] ] if (len(vars_) == 2 and sorted(l for l in repn.linear_coefs if l) == [-1, 1]): # this is an a == b constraint. v1 = vars_[0] v2 = vars_[1] set1 = eq_var_map.get(v1, ComponentSet([v1])) set2 = eq_var_map.get(v2, ComponentSet([v2])) relevant_vars.update([v1, v2]) set1.update(set2) # set1 is now the union for v in set1: eq_var_map[v] = set1 return eq_var_map, relevant_vars
def _apply_to(self, instance, **kwds): config = self.CONFIG(kwds) if config.tmp and not hasattr(instance, '_tmp_propagate_fixed'): instance._tmp_propagate_fixed = ComponentSet() eq_var_map, relevant_vars = _build_equality_set(instance) #: ComponentSet: The set of all fixed variables fixed_vars = ComponentSet((v for v in relevant_vars if v.fixed)) newly_fixed = _detect_fixed_variables(instance) if config.tmp: instance._tmp_propagate_fixed.update(newly_fixed) fixed_vars.update(newly_fixed) processed = ComponentSet() # Go through each fixed variable to propagate the 'fixed' status to all # equality-linked variabes. for v1 in fixed_vars: # If we have already processed the variable, skip it. if v1 in processed: continue eq_set = eq_var_map.get(v1, ComponentSet([v1])) for v2 in eq_set: if (v2.fixed and value(v1) != value(v2)): raise ValueError( 'Variables {} and {} have conflicting fixed ' 'values of {} and {}, but are linked by ' 'equality constraints.' .format(v1.name, v2.name, value(v1), value(v2))) elif not v2.fixed: v2.fix(value(v1)) if config.tmp: instance._tmp_propagate_fixed.add(v2) # Add all variables in the equality set to the set of processed # variables. processed.update(eq_set)
class BoundsManager(object): def __init__(self, comp): self._vars = ComponentSet() self._saved_bounds = list() if comp.type() == Constraint: if comp.is_indexed(): for c in comp.values(): self._vars.update(identify_variables(c.body)) else: self._vars.update(identify_variables(comp.body)) else: for c in comp.component_data_objects(Constraint, descend_into=True, active=True, sort=True): self._vars.update(identify_variables(c.body)) def save_bounds(self): bnds = ComponentMap() for v in self._vars: bnds[v] = (v.lb, v.ub) self._saved_bounds.append(bnds) def pop_bounds(self, ndx=-1): bnds = self._saved_bounds.pop(ndx) for v, _bnds in bnds.items(): lb, ub = _bnds v.setlb(lb) v.setub(ub) def load_bounds(self, bnds, save_current_bounds=True): if save_current_bounds: self.save_bounds() for v, _bnds in bnds.items(): if v in self._vars: lb, ub = _bnds v.setlb(lb) v.setub(ub)
def __init__(self, subproblem_solver): self._subproblem_solver = pe.SolverFactory(subproblem_solver) if isinstance(self._subproblem_solver, PersistentSolver): self._using_persistent_solver = True else: self._using_persistent_solver = False self._relaxations = ComponentSet() self._relaxations_not_tracking_solver = ComponentSet() self._relaxations_with_added_cuts = ComponentSet() self._pyomo_model = None self.options = ConfigBlock() self.options.declare( 'feasibility_tol', ConfigValue(default=1e-6, domain=NonNegativeFloat, doc='Tolerance below which cuts will not be added')) self.options.declare( 'max_iter', ConfigValue(default=30, domain=NonNegativeInt, doc='Maximum number of iterations')) self.options.declare( 'keep_cuts', ConfigValue( default=False, domain=In([True, False]), doc='Whether or not to keep the cuts generated after the solve' )) self.options.declare( 'time_limit', ConfigValue(default=float('inf'), domain=NonNegativeFloat, doc='Time limit in seconds')) self.subproblem_solver_options = ConfigBlock(implicit=True)
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 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 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 _apply_to_impl(self, instance, **kwds): config = self.CONFIG(kwds.pop('options', {})) # We will let args override suffixes and estimate as a last # resort. More specific args/suffixes override ones anywhere in # the tree. Suffixes lower down in the tree override ones higher # up. if 'default_bigM' in kwds: logger.warn("DEPRECATED: the 'default_bigM=' argument has been " "replaced by 'bigM='") config.bigM = kwds.pop('default_bigM') config.set_value(kwds) bigM = config.bigM targets = config.targets if targets is None: targets = (instance, ) _HACK_transform_whole_instance = True else: _HACK_transform_whole_instance = False # We need to check that all the targets are in fact on instance. As we # do this, we will use the set below to cache components we know to be # in the tree rooted at instance. knownBlocks = {} for t in targets: # check that t is in fact a child of instance if not is_child_of(parent=instance, child=t, knownBlocks=knownBlocks): raise GDP_Error("Target %s is not a component on instance %s!" % (t.name, instance.name)) elif t.ctype is Disjunction: if t.parent_component() is t: self._transform_disjunction(t, bigM) else: self._transform_disjunctionData( t, bigM, t.index()) elif t.ctype in (Block, Disjunct): if t.parent_component() is t: self._transform_block(t, bigM) else: self._transform_blockData(t, bigM) else: raise GDP_Error( "Target %s was not a Block, Disjunct, or Disjunction. " "It was of type %s and can't be transformed." % (t.name, type(t))) # issue warnings about anything that was in the bigM args dict that we # didn't use if bigM is not None: unused_args = ComponentSet(bigM.keys()) - \ ComponentSet(self.used_args.keys()) if len(unused_args) > 0: warning_msg = ("Unused arguments in the bigM map! " "These arguments were not used by the " "transformation:\n") for component in unused_args: if hasattr(component, 'name'): warning_msg += "\t%s\n" % component.name else: warning_msg += "\t%s\n" % component logger.warn(warning_msg) # HACK for backwards compatibility with the older GDP transformations # # Until the writers are updated to find variables on things # other than active blocks, we need to reclassify the Disjuncts # as Blocks after transformation so that the writer will pick up # all the variables that it needs (in this case, indicator_vars). if _HACK_transform_whole_instance: HACK_GDP_Disjunct_Reclassifier().apply_to(instance)
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() indicator_vars = ComponentSet(disj.indicator_var for disj in GDPopt.disjunct_list) 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)) # Note: FBBT may cause some disjuncts to be fathomed, which can cause # a fixed variable to be different than the subproblem value. # In this case, we simply construct the integer cut as usual with # the subproblem value rather than its fixed value. if val is None: val = var.value if not config.force_subproblem_nlp: # By default (config.force_subproblem_nlp = False), we only want # the integer cuts to be over disjunct indicator vars. if var not in indicator_vars: 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 # Exclude the current binary combination config.logger.info('Adding integer cut') GDPopt.integer_cuts.add(expr=int_cut) if config.calc_disjunctive_bounds: with time_code(solve_data.timing, "disjunctive variable bounding"): TransformationFactory('contrib.compute_disj_var_bounds').apply_to( m, solver=config.mip_solver if config.obbt_disjunctive_bounds else None )
def __init__(self, **kwds): OptSolver.__init__(self, **kwds) self._pyomo_model = None """The pyomo model being solved.""" self._solver_model = None """The python instance of the solver model (e.g., the gurobipy Model instance).""" self._symbol_map = SymbolMap() """A symbol map used to map between pyomo components and their names used with the solver.""" self._labeler = None """The labeler for creating names for the solver model components.""" self._pyomo_var_to_solver_var_map = ComponentMap() """A dictionary mapping pyomo Var's to the solver variables.""" self._pyomo_con_to_solver_con_map = ComponentMap() """A dictionary mapping pyomo constraints to solver constraints.""" self._vars_referenced_by_con = ComponentMap() """A dictionary mapping constraints to a ComponentSet containt the pyomo variables referenced by that constraint. This is primarily needed for the persistent solvers. When a constraint is deleted, we need to decrement the number of times those variables are referenced (see self._referenced_variables).""" self._vars_referenced_by_obj = ComponentSet() """A set containing the pyomo variables referenced by that the objective. This is primarily needed for the persistent solvers. When a the objective is deleted, we need to decrement the number of times those variables are referenced (see self._referenced_variables).""" self._objective = None """The pyomo Objective object currently being used with the solver.""" self.results = None """A results object return from the solve method.""" self._skip_trivial_constraints = False """A bool. If True, then any constraints with a constant body will not be added to the solver model. Be careful with this. If a trivial constraint is skipped then that constraint cannot be removed from a persistent solver (an error will be raised if a user tries to remove a non-existent constraint).""" self._output_fixed_variable_bounds = False """A bool. If False then an error will be raised if a fixed variable is used in one of the solver constraints. This is useful for catching bugs. Ordinarily a fixed variable should appear as a constant value in the solver constraints. If True, then the error will not be raised.""" self._python_api_exists = False """A bool indicating whether or not the python api is available for the specified solver.""" self._version = None """The version of the solver.""" self._version_major = None """The major version of the solver. For example, if using Gurobi 7.0.2, then _version_major is 7.""" self._symbolic_solver_labels = False """A bool. If true then the solver components will be given names corresponding to the pyomo component names.""" self._capabilites = Options() self._referenced_variables = ComponentMap() """dict: {var: count} where count is the number of constraints/objective referencing the var""" self._keepfiles = False """A bool. If True, then the solver log will be saved.""" self._save_results = True """A bool. This is used for backwards compatability. If True, the solution will be loaded into the Solution
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 _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 _apply_to(self, model, detect_fixed_vars=True): """Apply the transformation to the given model.""" # Generate the equality sets eq_var_map = _build_equality_set(model) # Detect and process fixed variables. if detect_fixed_vars: _fix_equality_fixed_variables(model) # Generate aggregation infrastructure model._var_aggregator_info = Block( doc="Holds information for the variable aggregation " "transformation system.") z = model._var_aggregator_info.z = VarList(doc="Aggregated variables.") # Map of the aggregate var to the equalty set (ComponentSet) z_to_vars = model._var_aggregator_info.z_to_vars = ComponentMap() # Map of variables to their corresponding aggregate var var_to_z = model._var_aggregator_info.var_to_z = ComponentMap() processed_vars = ComponentSet() # TODO This iteritems is sorted by the variable name of the key in # order to preserve determinism. Unfortunately, var.name() is an # expensive operation right now. for var, eq_set in sorted(eq_var_map.items(), key=lambda tup: tup[0].name): if var in processed_vars: continue # Skip already-process variables # This would be weird. The variable hasn't been processed, but is # in the map. Raise an exception. assert var_to_z.get(var, None) is None z_agg = z.add() z_to_vars[z_agg] = eq_set var_to_z.update(ComponentMap((v, z_agg) for v in eq_set)) # Set the bounds of the aggregate variable based on the bounds of # the variables in its equality set. z_agg.setlb(max_if_not_None(v.lb for v in eq_set if v.has_lb())) z_agg.setub(min_if_not_None(v.ub for v in eq_set if v.has_ub())) # Set the fixed status of the aggregate var fixed_vars = [v for v in eq_set if v.fixed] if fixed_vars: # Check to make sure all the fixed values are the same. if any(var.value != fixed_vars[0].value for var in fixed_vars[1:]): raise ValueError( "Aggregate variable for equality set is fixed to " "multiple different values: %s" % (fixed_vars,)) z_agg.fix(fixed_vars[0].value) # Check that the fixed value lies within bounds. if z_agg.has_lb() and z_agg.value < value(z_agg.lb): raise ValueError( "Aggregate variable for equality set is fixed to " "a value less than its lower bound: %s < LB %s" % (z_agg.value, value(z_agg.lb)) ) if z_agg.has_ub() and z_agg.value > value(z_agg.ub): raise ValueError( "Aggregate variable for equality set is fixed to " "a value greater than its upper bound: %s > UB %s" % (z_agg.value, value(z_agg.ub)) ) else: # Set the value to be the average of the values within the # bounds only if the value is not already fixed. values_within_bounds = [ v.value for v in eq_set if ( v.value is not None and ((z_agg.has_lb() and v.value >= value(z_agg.lb)) or not z_agg.has_lb()) and ((z_agg.has_ub() and v.value <= value(z_agg.ub)) or not z_agg.has_ub()) )] num_vals = len(values_within_bounds) z_agg.value = ( sum(val for val in values_within_bounds) / num_vals) \ if num_vals > 0 else None processed_vars.update(eq_set) # Do the substitution substitution_map = {id(var): z_var for var, z_var in var_to_z.items()} for constr in model.component_data_objects( ctype=Constraint, active=True ): new_body = ExpressionReplacementVisitor( substitute=substitution_map ).dfs_postorder_stack(constr.body) constr.set_value((constr.lower, new_body, constr.upper)) for objective in model.component_data_objects( ctype=Objective, active=True ): new_expr = ExpressionReplacementVisitor( substitute=substitution_map ).dfs_postorder_stack(objective.expr) objective.set_value(new_expr)
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 fbbt_block(m, tol=1e-4): """ 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 """ 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) improved_vars = ComponentSet() for c in m.component_data_objects(ctype=Constraint, active=True, descend_into=True, sort=True): fbbt_con(c) for v in identify_variables(c.body): if v.lb is not None: if value(v.lb) > var_lbs[v] + tol: improved_vars.add(v) var_lbs[v] = value(v.lb) if v.ub is not None: if value(v.ub) < var_ubs[v] - tol: improved_vars.add(v) var_ubs[v] = value(v.ub) while len(improved_vars) > 0: v = improved_vars.pop() for c in var_to_con_map[v]: fbbt_con(c) for _v in identify_variables(c.body): if _v.lb is not None: if value(_v.lb) > var_lbs[_v] + tol: improved_vars.add(_v) var_lbs[_v] = value(_v.lb) if _v.ub is not None: if value(_v.ub) < var_ubs[_v] - tol: improved_vars.add(_v) var_ubs[_v] = value(_v.ub)
def get_inconsistent_initial_conditions(model, time, tol=1e-8, t0=None, allow_skip=True, suppress_warnings=False): """Finds constraints of the model that are implicitly or explicitly indexed by time and checks if they are consistent to within a tolerance at the initial value of time. Args: model: Model whose constraints to check time: Set whose initial condition will be checked tol: Maximum constraint violation t0: Point in time at which to check constraints Returns: List of constraint data objects that were found to be inconsistent. """ if t0 is None: t0 = time.first() inconsistent = ComponentSet() for con in model.component_objects(Constraint, active=True): if not is_explicitly_indexed_by(con, time): continue if is_in_block_indexed_by(con, time): continue info = get_index_set_except(con, time) non_time_set = info['set_except'] index_getter = info['index_getter'] for non_time_index in non_time_set: index = index_getter(non_time_index, t0) try: condata = con[index] except KeyError: # To allow Constraint.Skip if not suppress_warnings: print(index_warning(con.name, index)) if not allow_skip: raise continue if (value(condata.body) - value(condata.upper) > tol or value(condata.lower) - value(condata.body) > tol): inconsistent.add(condata) for blk in model.component_objects(Block, active=True): # What if there are time-indexed blocks at multiple levels # of a hierarchy? # My preferred convention is to only check the first (highest- # level) time index, but distinguishing between different-level # time indices is an expensive operation. if not is_explicitly_indexed_by(blk, time): continue if is_in_block_indexed_by(blk, time): continue info = get_index_set_except(blk, time) non_time_set = info['set_except'] index_getter = info['index_getter'] for non_time_index in non_time_set: index = index_getter(non_time_index, t0) blkdata = blk[index] for condata in blkdata.component_data_objects(Constraint, active=True): if (value(condata.body) - value(condata.upper) > tol or value(condata.lower) - value(condata.body) > tol): if condata in inconsistent: raise ValueError( '%s has already been visited. The only way this ' 'should happen is if the model has nested time-' 'indexed blocks, which is not supported.') inconsistent.add(condata) return list(inconsistent)
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
def build_model_size_report(model): """Build a model size report object.""" report = ModelSizeReport() activated_disjunctions = ComponentSet() activated_disjuncts = ComponentSet() fixed_true_disjuncts = ComponentSet() activated_constraints = ComponentSet() activated_vars = ComponentSet() new_containers = (model,) while new_containers: new_activated_disjunctions = ComponentSet() new_activated_disjuncts = ComponentSet() new_fixed_true_disjuncts = ComponentSet() new_activated_constraints = ComponentSet() for container in new_containers: (next_activated_disjunctions, next_fixed_true_disjuncts, next_activated_disjuncts, next_activated_constraints ) = _process_activated_container(container) new_activated_disjunctions.update(next_activated_disjunctions) new_activated_disjuncts.update(next_activated_disjuncts) new_fixed_true_disjuncts.update(next_fixed_true_disjuncts) new_activated_constraints.update(next_activated_constraints) new_containers = ((new_activated_disjuncts - activated_disjuncts) | (new_fixed_true_disjuncts - fixed_true_disjuncts)) activated_disjunctions.update(new_activated_disjunctions) activated_disjuncts.update(new_activated_disjuncts) fixed_true_disjuncts.update(new_fixed_true_disjuncts) activated_constraints.update(new_activated_constraints) activated_vars.update( var for constr in activated_constraints for var in EXPR.identify_variables( constr.body, include_fixed=False)) activated_vars.update( disj.indicator_var for disj in activated_disjuncts) report.activated = Container() report.activated.variables = len(activated_vars) report.activated.binary_variables = sum( 1 for v in activated_vars if v.is_binary()) report.activated.integer_variables = sum( 1 for v in activated_vars if v.is_integer()) report.activated.continuous_variables = sum( 1 for v in activated_vars if v.is_continuous()) report.activated.disjunctions = len(activated_disjunctions) report.activated.disjuncts = len(activated_disjuncts) report.activated.constraints = len(activated_constraints) report.activated.nonlinear_constraints = sum( 1 for c in activated_constraints if c.body.polynomial_degree() not in (1, 0)) report.overall = Container() block_like = (Block, Disjunct) all_vars = ComponentSet( model.component_data_objects(Var, descend_into=block_like)) report.overall.variables = len(all_vars) report.overall.binary_variables = sum(1 for v in all_vars if v.is_binary()) report.overall.integer_variables = sum( 1 for v in all_vars if v.is_integer()) report.overall.continuous_variables = sum( 1 for v in all_vars if v.is_continuous()) report.overall.disjunctions = sum( 1 for d in model.component_data_objects( Disjunction, descend_into=block_like)) report.overall.disjuncts = sum( 1 for d in model.component_data_objects( Disjunct, descend_into=block_like)) report.overall.constraints = sum( 1 for c in model.component_data_objects( Constraint, descend_into=block_like)) report.overall.nonlinear_constraints = sum( 1 for c in model.component_data_objects( Constraint, descend_into=block_like) if c.body.polynomial_degree() not in (1, 0)) report.warning = Container() report.warning.unassociated_disjuncts = sum( 1 for d in model.component_data_objects( Disjunct, descend_into=block_like) if not d.indicator_var.fixed and d not in activated_disjuncts) return report
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 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 _apply_to(self, model, **kwds): """Apply the transformation to the given model.""" config = self.CONFIG(kwds.pop('options', {})) config.set_value(kwds) integer_vars = list( v for v in model.component_data_objects( ctype=Var, descend_into=(Block, Disjunct)) if v.is_integer() and not v.fixed) if len(integer_vars) == 0: logger.info("Model has no free integer variables. No reformulation needed.") return vars_on_constr = ComponentSet() for c in model.component_data_objects( ctype=Constraint, descend_into=(Block, Disjunct), active=True): vars_on_constr.update(v for v in identify_variables(c.body, include_fixed=False) if v.is_integer()) if config.ignore_unused: num_vars_not_on_constr = len(integer_vars) - len(vars_on_constr) if num_vars_not_on_constr > 0: logger.info( "%s integer variables on the model are not attached to any constraints. " "Ignoring unused variables." ) integer_vars = list(vars_on_constr) logger.info( "Reformulating integer variables using the %s strategy." % config.strategy) # Set up reformulation block blk_name = unique_component_name(model, "_int_to_binary_reform") reform_block = Block( doc="Holds variables and constraints for reformulating " "integer variables to binary variables." ) setattr(model, blk_name, reform_block) reform_block.int_var_set = RangeSet(0, len(integer_vars) - 1) reform_block.new_binary_var = Var( Any, domain=Binary, dense=False, doc="Binary variable with index (int_var_idx, idx)") reform_block.integer_to_binary_constraint = Constraint( reform_block.int_var_set, doc="Equality constraints mapping the binary variable values " "to the integer variable value.") # check that variables are bounded and non-negative for idx, int_var in enumerate(integer_vars): if not (int_var.has_lb() and int_var.has_ub()): raise ValueError( "Integer variable %s is missing an " "upper or lower bound. LB: %s; UB: %s. " "Integer to binary reformulation does not support unbounded integer variables." % (int_var.name, int_var.lb, int_var.ub)) if int_var.lb < 0: raise ValueError( "Integer variable %s can be negative. " "Integer to binary reformulation currently only supports non-negative integer " "variables." % (int_var.name,) ) # do the reformulation highest_power = int(floor(log(value(int_var.ub), 2))) # TODO potentially fragile due to floating point reform_block.integer_to_binary_constraint.add( idx, expr=int_var == sum( reform_block.new_binary_var[idx, pwr] * (2 ** pwr) for pwr in range(0, highest_power + 1))) # Relax the original integer variable int_var.domain = NonNegativeReals logger.info( "Reformulated %s integer variables using " "%s binary variables and %s constraints." % (len(integer_vars), len(reform_block.new_binary_var), len(reform_block.integer_to_binary_constraint)))