def _relax_leaf_to_root_ReciprocalExpression(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): arg = values[0] degree = degree_map[arg] if degree == 0: res = 1/arg degree_map[res] = 0 return res elif (id(arg), 'reciprocal') in aux_var_map: _aux_var, relaxation = aux_var_map[id(arg), 'reciprocal'] relaxation_side = relaxation_side_map[node] if relaxation_side != relaxation.relaxation_side: relaxation.relaxation_side = RelaxationSide.BOTH return _aux_var else: _aux_var = _get_aux_var(parent_block, 1/arg) arg = replace_sub_expression_with_aux_var(arg, parent_block) relaxation_side = relaxation_side_map[node] degree_map[_aux_var] = 1 if compute_bounds_on_expr(arg)[0] > 0: relaxation = PWUnivariateRelaxation() relaxation.set_input(x=arg, aux_var=_aux_var, relaxation_side=relaxation_side, f_x_expr=1/arg, shape=FunctionShape.CONVEX) elif compute_bounds_on_expr(arg)[1] < 0: relaxation = PWUnivariateRelaxation() relaxation.set_input(x=arg, aux_var=_aux_var, relaxation_side=relaxation_side, f_x_expr=1/arg, shape=FunctionShape.CONCAVE) else: _one = parent_block.aux_vars.add() _one.fix(1.0) relaxation = PWMcCormickRelaxation() relaxation.set_input(x1=arg, x2=_aux_var, aux_var=_one, relaxation_side=relaxation_side) aux_var_map[id(arg), 'reciprocal'] = (_aux_var, relaxation) setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) counter.increment() return _aux_var
def epigraph_reformulation(exp, slack_var_list, constraint_list, use_mcpp, sense): """Epigraph reformulation. Generate the epigraph reformuation for objective expressions. Parameters ---------- slack_var_list : VarList Slack vars for epigraph reformulation. constraint_list : ConstraintList Epigraph constraint list. use_mcpp : Bool Whether to use mcpp to tighten the bound of slack variables. exp : Expression The expression to reformualte. sense : objective sense The objective sense. """ slack_var = slack_var_list.add() if mcpp_available() and use_mcpp: mc_obj = McCormick(exp) slack_var.setub(mc_obj.upper()) slack_var.setlb(mc_obj.lower()) else: # Use Pyomo's contrib.fbbt package lb, ub = compute_bounds_on_expr(exp) if sense == minimize: slack_var.setlb(lb) else: slack_var.setub(ub) if sense == minimize: constraint_list.add(expr=slack_var >= exp) else: constraint_list.add(expr=slack_var <= exp)
def _estimate_M(self, expr, name): # If there are fixed variables here, unfix them for this calculation, # and we'll restore them at the end. fixed_vars = ComponentMap() if not self.assume_fixed_vars_permanent: for v in EXPR.identify_variables(expr, include_fixed=True): if v.fixed: fixed_vars[v] = value(v) v.fixed = False expr_lb, expr_ub = compute_bounds_on_expr(expr) if expr_lb is None or expr_ub is None: raise GDP_Error("Cannot estimate M for unbounded " "expressions.\n\t(found while processing " "constraint '%s'). Please specify a value of M " "or ensure all variables that appear in the " "constraint are bounded." % name) else: M = (expr_lb, expr_ub) # clean up if we unfixed things (fixed_vars is empty if we were assuming # fixed vars are fixed for life) for v, val in fixed_vars.items(): v.fix(val) return tuple(M)
def test_inf_bounds_on_expr(self): m = pyo.ConcreteModel() m.x = pyo.Var(bounds=(-1, 1)) m.y = pyo.Var() lb, ub = compute_bounds_on_expr(m.x + m.y) self.assertEqual(lb, None) self.assertEqual(ub, None)
def solve_with_gdp_opt(): m = MethanolModel().model for _d in m.component_data_objects(gdp.Disjunct, descend_into=True, active=True, sort=True): _d.BigM = pe.Suffix() for _c in _d.component_data_objects(pe.Constraint, descend_into=True, active=True, sort=True): lb, ub = compute_bounds_on_expr(_c.body) _d.BigM[_c] = max(abs(lb), abs(ub)) opt = pe.SolverFactory('gdpopt') opt.CONFIG.strategy = 'LOA' opt.CONFIG.mip_solver = 'gams' opt.CONFIG.nlp_solver = 'gams' opt.CONFIG.tee = True res = opt.solve(m) for d in m.component_data_objects(ctype=gdp.Disjunct, active=True, sort=True, descend_into=True): if d.indicator_var.value == 1: print(d.name) print(res) return m
def compute_fbbt_bounds(expr, global_constraints, opt): """ Calls fbbt on expr and returns the lower and upper bounds on the expression based on the bounds of the Vars that appear in the expression. Ignores the global_constraints and opt arguments. """ return compute_bounds_on_expr(expr)
def compute_float_bounds_on_expr(expr): lb, ub = compute_bounds_on_expr(expr) if lb is None: lb = -math.inf if ub is None: ub = math.inf return lb, ub
def _compute_alpha(xs, f_x_expr): hess = _hessian(xs, f_x_expr) alpha = 0.0 for i, x in enumerate(xs): a_ii_expr = hess[x][x] a_ii = compute_bounds_on_expr(a_ii_expr) tot = a_ii[0] for j, y in enumerate(xs): if i == j: continue a_ij_expr = hess[x][y] a_ij = compute_bounds_on_expr(a_ij_expr) tot -= max(abs(a_ij[0]), abs(a_ij[1])) tot = -0.5 * tot if tot > alpha: alpha = tot return alpha
def test_negative_power(self): m = pyo.ConcreteModel() m.x = pyo.Var() m.y = pyo.Var() e = (m.x**2 + m.y**2)**(-0.5) lb, ub = compute_bounds_on_expr(e) self.assertAlmostEqual(lb, 0) self.assertIsNone(ub)
def test_compute_expr_bounds(self): m = pyo.ConcreteModel() m.x = pyo.Var(bounds=(-1, 1)) m.y = pyo.Var(bounds=(-1, 1)) e = m.x + m.y lb, ub = compute_bounds_on_expr(e) self.assertAlmostEqual(lb, -2, 14) self.assertAlmostEqual(ub, 2, 14)
def test_compute_expr_bounds(self): m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1,1)) m.y = pe.Var(bounds=(-1,1)) e = m.x + m.y lb, ub = compute_bounds_on_expr(e) self.assertAlmostEqual(lb, -2, 14) self.assertAlmostEqual(ub, 2, 14)
def _get_subs(self, instance): subs = {} # Substitute one var for another from a * x + b * y + c = 0 subs_map = {} # id -> var fixes = [] # fix a variable from a * x + c = 0 cnstr = set() # constraints to deactivate rset = set() # variables used in a sub or fixed for c in instance.component_data_objects(pyo.Constraint, active=True): if ( pyo.value(c.lower) is not None and pyo.value(c.lower) == pyo.value(c.upper) and c.body.polynomial_degree() == 1 ): repn = generate_standard_repn(c.body) if len(repn.nonlinear_vars) != 0 or len(repn.quadratic_vars) != 0: continue if len(repn.linear_vars) > 2: continue elif len(repn.linear_vars) < 1: _log.warning("Constraint with no vars {}: {}".format(c, c.expr)) continue b0 = repn.constant - pyo.value(c.upper) v0 = repn.linear_vars[0] a0 = repn.linear_coefs[0] if id(v0) in rset: continue elif len(repn.linear_vars) == 1: fixes.append((v0, -b0/a0)) rset.add(id(v0)) cnstr.add(c) continue v1 = repn.linear_vars[1] a1 = repn.linear_coefs[1] if id(v1) in rset: continue subs[id(v0)] = -(b0 + a1 * v1) / a0 cnstr.add(c) rset.add(id(v0)) rset.add(id(v1)) if self.reversible: subs_map[id(v0)] = v0 _log.debug("Sub: {} = {}".format(v0, subs[id(v0)])) # Use the tightest set of bounds from v0 and v1 lb, ub = compute_bounds_on_expr(-(b0 + a0 * v0) / a1) if lb is not None: if v1.lb is None or lb > v1.lb: v1.setlb(lb) if ub is not None: if v1.ub is None or ub < v1.ub: v1.setub(ub) return subs, cnstr, fixes, subs_map
def test_quadratic_as_product(self): m = pyo.ConcreteModel() m.x = pyo.Var([1, 2], bounds=(-2, 6)) e1 = m.x[1] * m.x[1] + m.x[2] * m.x[2] e2 = m.x[1]**2 + m.x[2]**2 lb1, ub1 = compute_bounds_on_expr(e1) lb2, ub2 = compute_bounds_on_expr(e2) self.assertAlmostEqual(lb1, lb2) self.assertAlmostEqual(ub1, ub2) m.c = pyo.Constraint(expr=m.x[1] * m.x[1] + m.x[2] * m.x[2] == 0) fbbt(m.c) self.assertAlmostEqual(m.x[1].lb, 0) self.assertAlmostEqual(m.x[1].ub, 0) self.assertAlmostEqual(m.x[2].lb, 0) self.assertAlmostEqual(m.x[2].ub, 0)
def exitNode(self, node, values): if type(node) == AndExpression: return list((v if type(v) in _numeric_relational_types else v == 1) for v in values) elif type(node) == OrExpression: return sum(values) >= 1 elif type(node) == NotExpression: return 1 - values[0] # Note: the following special atoms should only be encountered as root nodes. # If they are encountered otherwise, something went wrong. sum_values = sum(values[1:]) num_args = node.nargs() - 1 # number of logical arguments if self._indicator is None: if type(node) == AtLeastExpression: return sum_values >= values[0] elif type(node) == AtMostExpression: return sum_values <= values[0] elif type(node) == ExactlyExpression: return sum_values == values[0] else: rhs_lb, rhs_ub = compute_bounds_on_expr(values[0]) if rhs_lb == float('-inf') or rhs_ub == float('inf'): raise ValueError( "Cannnot generate linear constraints for %s([N, *logical_args]) with unbounded N. " "Detected %s <= N <= %s." % (type(node).__name__, rhs_lb, rhs_ub)) indicator_binary = self._indicator.get_associated_binary() if type(node) == AtLeastExpression: return [ sum_values >= values[0] - rhs_ub * (1 - indicator_binary), sum_values <= values[0] - 1 + (-(rhs_lb - 1) + num_args) * indicator_binary ] elif type(node) == AtMostExpression: return [ sum_values <= values[0] + (-rhs_lb + num_args) * (1 - indicator_binary), sum_values >= (values[0] + 1) - (rhs_ub + 1) * indicator_binary ] elif type(node) == ExactlyExpression: less_than_binary = self._binary_varlist.add() more_than_binary = self._binary_varlist.add() return [ sum_values <= values[0] + (-rhs_lb + num_args) * (1 - indicator_binary), sum_values >= values[0] - rhs_ub * (1 - indicator_binary), indicator_binary + less_than_binary + more_than_binary >= 1, sum_values <= values[0] - 1 + (-(rhs_lb - 1) + num_args) * (1 - less_than_binary), sum_values >= values[0] + 1 - (rhs_ub + 1) * (1 - more_than_binary), ] pass
def _estimate_M(self, expr, name): # If there are fixed variables here, unfix them for this calculation, # and we'll restore them at the end. fixed_vars = ComponentMap() if not self.assume_fixed_vars_permanent: for v in EXPR.identify_variables(expr, include_fixed=True): if v.fixed: fixed_vars[v] = value(v) v.fixed = False # Calculate a best guess at M repn = generate_standard_repn(expr, quadratic=False) M = [0, 0] if not repn.is_nonlinear(): if repn.constant is not None: for i in (0, 1): if M[i] is not None: M[i] += repn.constant for i, coef in enumerate(repn.linear_coefs or []): var = repn.linear_vars[i] bounds = (value(var.lb), value(var.ub)) for i in (0, 1): # reverse the bounds if the coefficient is negative if coef > 0: j = i else: j = 1 - i if bounds[i] is not None: M[j] += value(bounds[i]) * coef else: raise GDP_Error( "Cannot estimate M for " "expressions with unbounded variables." "\n\t(found unbounded var '%s' while processing " "constraint '%s')" % (var.name, name)) else: # expression is nonlinear. Try using `contrib.fbbt` to estimate. expr_lb, expr_ub = compute_bounds_on_expr(expr) if expr_lb is None or expr_ub is None: raise GDP_Error("Cannot estimate M for unbounded nonlinear " "expressions.\n\t(found while processing " "constraint '%s')" % name) else: M = (expr_lb, expr_ub) # clean up if we unfixed things (fixed_vars is empty if we were assuming # fixed vars are fixed for life) for v, val in fixed_vars.items(): v.fix(val) return tuple(M)
def _get_aux_var(parent_block, expr): _aux_var = parent_block.aux_vars.add() lb, ub = compute_bounds_on_expr(expr) _aux_var.setlb(lb) _aux_var.setub(ub) try: expr_value = pe.value(expr, exception=False) except ArithmeticError: expr_value = None if expr_value is not None and pe.value(_aux_var, exception=False) is None: _aux_var.set_value(expr_value) return _aux_var
def _check_coefficents(comp, expr, too_large, too_small, largs_coef_map, small_coef_map): ders = reverse_sd(expr) for _v, _der in ders.items(): if isinstance(_v, _GeneralVarData): if _v.is_fixed(): continue der_lb, der_ub = compute_bounds_on_expr(_der) der_lb, der_ub = _bounds_to_float(der_lb, der_ub) if der_lb <= -too_large or der_ub >= too_large: if comp not in largs_coef_map: largs_coef_map[comp] = list() largs_coef_map[comp].append((_v, der_lb, der_ub)) if abs(der_lb) <= too_small and abs(der_ub) < too_small: if der_lb != 0 or der_ub != 0: if comp not in small_coef_map: small_coef_map[comp] = list() small_coef_map[comp].append((_v, der_lb, der_ub))
def _estimate_M(self, expr, name): # Calculate a best guess at M repn = generate_standard_repn(expr, quadratic=False) M = [0, 0] if not repn.is_nonlinear(): if repn.constant is not None: for i in (0, 1): if M[i] is not None: M[i] += repn.constant for i, coef in enumerate(repn.linear_coefs or []): var = repn.linear_vars[i] bounds = (value(var.lb), value(var.ub)) for i in (0, 1): # reverse the bounds if the coefficient is negative if coef > 0: j = i else: j = 1 - i if bounds[i] is not None: M[j] += value(bounds[i]) * coef else: raise GDP_Error( "Cannot estimate M for " "expressions with unbounded variables." "\n\t(found unbounded var %s while processing " "constraint %s)" % (var.name, name)) else: # expression is nonlinear. Try using `contrib.fbbt` to estimate. expr_lb, expr_ub = compute_bounds_on_expr(expr) if expr_lb is None or expr_ub is None: raise GDP_Error("Cannot estimate M for unbounded nonlinear " "expressions.\n\t(found while processing " "constraint %s)" % name) else: M = (expr_lb, expr_ub) return tuple(M)
def _relax_leaf_to_root_QuadraticExpression(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): relaxation_side = relaxation_side_map[node] term_graph = nx.Graph() term_graph.add_edges_from((id(term.var1), id(term.var2), { 'term': term }) for term in node.terms) disaggregated_exprs = [] aux_sum = 0.0 for connected_component in nx.connected_components(term_graph): connected_graph = term_graph.subgraph(connected_component) expr = 0.0 aux_var_expr = 0.0 bilinear_count = 0 for _, _, data in connected_graph.edges(data=True): bilinear_count += 1 term = data['term'] var1_id = id(term.var1) var2_id = id(term.var2) if term.var1 is term.var2: aux_var, _ = aux_var_map[var1_id, 'quadratic'] else: aux_var, _ = aux_var_map.get((var1_id, var2_id, 'mul'), (None, None)) if aux_var is None: aux_var, _ = aux_var_map.get((var2_id, var1_id, 'mul'), (None, None)) assert aux_var is not None expr += term.coefficient * term.var1 * term.var2 aux_var_expr += term.coefficient * aux_var if bilinear_count == 1: aux_sum += aux_var_expr continue expr = QuadraticExpression(expr) cvx = _convexity_rule.apply(expr, None, None, None) if relaxation_side == RelaxationSide.UNDER and cvx.is_convex(): disaggregated_exprs.append((expr, aux_var_expr)) elif relaxation_side == RelaxationSide.OVER and cvx.is_concave(): disaggregated_exprs.append((expr, aux_var_expr)) elif relaxation_side == RelaxationSide.BOTH and cvx.is_convex(): disaggregated_exprs.append((expr, aux_var_expr)) else: aux_sum += aux_var_expr if len(disaggregated_exprs) == 0: res = sum(values) degree_map[res] = max(degree_map[arg] for arg in values) return res # replace convex nonlinear expressions with an auxiliary variable res = aux_sum for expr, aux_var_expr in disaggregated_exprs: lb, ub = compute_bounds_on_expr(aux_var_expr) new_aux_var = parent_block.aux_vars.add() new_aux_var.setlb(lb) new_aux_var.setub(ub) new_aux_var.value = 0.0 degree_map[new_aux_var] = 1.0 relaxation = FactorableConvexExpressionRelaxation() relaxation.build( aux_var=new_aux_var, relaxed_f_x_expr=aux_var_expr, original_f_x_expr=expr, ) setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) counter.increment() res += new_aux_var degree_map[res] = 1.0 return res
def enumerate_solutions(): import time feed_choices = ['cheap', 'expensive'] feed_compressor_choices = ['single_stage', 'two_stage'] reactor_choices = ['cheap', 'expensive'] recycle_compressor_choices = ['single_stage', 'two_stage'] print('{0:<20}{1:<20}{2:<20}{3:<20}{4:<20}{5:<20}'.format( 'feed choice', 'feed compressor', 'reactor choice', 'recycle compressor', 'termination cond', 'profit')) since = time.time() for feed_choice in feed_choices: for feed_compressor_choice in feed_compressor_choices: for reactor_choice in reactor_choices: for recycle_compressor_choice in recycle_compressor_choices: m = MethanolModel() m = m.model for _d in m.component_data_objects(gdp.Disjunct, descend_into=True, active=True, sort=True): _d.BigM = pe.Suffix() for _c in _d.component_data_objects(pe.Constraint, descend_into=True, active=True, sort=True): lb, ub = compute_bounds_on_expr(_c.body) _d.BigM[_c] = max(abs(lb), abs(ub)) if feed_choice == 'cheap': m.cheap_feed_disjunct.indicator_var.fix(1) m.expensive_feed_disjunct.indicator_var.fix(0) else: m.cheap_feed_disjunct.indicator_var.fix(0) m.expensive_feed_disjunct.indicator_var.fix(1) if feed_compressor_choice == 'single_stage': m.single_stage_feed_compressor_disjunct.indicator_var.fix( 1) m.two_stage_feed_compressor_disjunct.indicator_var.fix( 0) else: m.single_stage_feed_compressor_disjunct.indicator_var.fix( 0) m.two_stage_feed_compressor_disjunct.indicator_var.fix( 1) if reactor_choice == 'cheap': m.cheap_reactor.indicator_var.fix(1) m.expensive_reactor.indicator_var.fix(0) else: m.cheap_reactor.indicator_var.fix(0) m.expensive_reactor.indicator_var.fix(1) if recycle_compressor_choice == 'single_stage': m.single_stage_recycle_compressor_disjunct.indicator_var.fix( 1) m.two_stage_recycle_compressor_disjunct.indicator_var.fix( 0) else: m.single_stage_recycle_compressor_disjunct.indicator_var.fix( 0) m.two_stage_recycle_compressor_disjunct.indicator_var.fix( 1) pe.TransformationFactory('gdp.fix_disjuncts').apply_to(m) fbbt(m, deactivate_satisfied_constraints=True) fix_vars_with_equal_bounds(m) for v in m.component_data_objects(pe.Var, descend_into=True): if not v.is_fixed(): if v.has_lb() and v.has_ub(): v.value = 0.5 * (v.lb + v.ub) else: v.value = 1.0 opt = pe.SolverFactory('ipopt') res = opt.solve(m, tee=False) print('{0:<20}{1:<20}{2:<20}{3:<20}{4:<20}{5:<20}'.format( feed_choice, feed_compressor_choice, reactor_choice, recycle_compressor_choice, str(res.solver.termination_condition), str(-pe.value(m.objective)))) time_elapsed = time.time() - since print('The code run {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60)) return m
def process_objective(solve_data, config, move_linear_objective=False, use_mcpp=True, updata_var_con_list=True): """Process model objective function. Check that the model has only 1 valid objective. If the objective is nonlinear, move it into the constraints. If no objective function exists, emit a warning and create a dummy objective. Parameters ---------- solve_data (GDPoptSolveData): solver environment data class config (ConfigBlock): solver configuration options move_linear_objective (bool): if True, move even linear objective functions to the constraints """ m = solve_data.working_model util_blk = getattr(m, solve_data.util_block_name) # Handle missing or multiple objectives active_objectives = list(m.component_data_objects( ctype=Objective, active=True, descend_into=True)) solve_data.results.problem.number_of_objectives = len(active_objectives) if len(active_objectives) == 0: config.logger.warning( 'Model has no active objectives. Adding dummy objective.') util_blk.dummy_objective = Objective(expr=1) main_obj = util_blk.dummy_objective elif len(active_objectives) > 1: raise ValueError('Model has multiple active objectives.') else: main_obj = active_objectives[0] solve_data.results.problem.sense = ProblemSense.minimize if main_obj.sense == 1 else ProblemSense.maximize solve_data.objective_sense = main_obj.sense # Move the objective to the constraints if it is nonlinear if main_obj.expr.polynomial_degree() not in (1, 0) \ or move_linear_objective: if move_linear_objective: config.logger.info("Moving objective to constraint set.") else: config.logger.info( "Objective is nonlinear. Moving it to constraint set.") util_blk.objective_value = Var(domain=Reals, initialize=0) if mcpp_available() and use_mcpp: mc_obj = McCormick(main_obj.expr) util_blk.objective_value.setub(mc_obj.upper()) util_blk.objective_value.setlb(mc_obj.lower()) else: # Use Pyomo's contrib.fbbt package lb, ub = compute_bounds_on_expr(main_obj.expr) if solve_data.results.problem.sense == ProblemSense.minimize: util_blk.objective_value.setlb(lb) else: util_blk.objective_value.setub(ub) if main_obj.sense == minimize: util_blk.objective_constr = Constraint( expr=util_blk.objective_value >= main_obj.expr) else: util_blk.objective_constr = Constraint( expr=util_blk.objective_value <= main_obj.expr) # Deactivate the original objective and add this new one. main_obj.deactivate() util_blk.objective = Objective( expr=util_blk.objective_value, sense=main_obj.sense) # Add the new variable and constraint to the working lists if main_obj.expr.polynomial_degree() not in (1, 0) or (move_linear_objective and updata_var_con_list): util_blk.variable_list.append(util_blk.objective_value) util_blk.continuous_variable_list.append(util_blk.objective_value) util_blk.constraint_list.append(util_blk.objective_constr) util_blk.objective_list.append(util_blk.objective) if util_blk.objective_constr.body.polynomial_degree() in (0, 1): util_blk.linear_constraint_list.append(util_blk.objective_constr) else: util_blk.nonlinear_constraint_list.append( util_blk.objective_constr)
def report_scaling(m: _BlockData, too_large: float = 5e4, too_small: float = 1e-6) -> bool: """ This function logs potentially poorly scaled parts of the model. It requires that all variables be bounded. It is important to note that this check is neither necessary nor sufficient to ensure a well-scaled model. However, it is a useful tool to help identify problematic parts of a model. This function uses symbolic differentiation and interval arithmetic to compute bounds on each entry in the jacobian of the constraints. Note that logging has to be turned on to get the output Parameters ---------- m: _BlockData The pyomo model or block too_large: float Values above too_large will generate a log entry too_small: float Coefficients below too_small will generate a log entry Returns ------- success: bool Returns False if any potentially poorly scaled components were found """ vars_without_bounds, vars_with_large_bounds = _check_var_bounds( m, too_large) cons_with_large_bounds = dict() cons_with_large_coefficients = dict() cons_with_small_coefficients = dict() objs_with_large_coefficients = pyo.ComponentMap() objs_with_small_coefficients = pyo.ComponentMap() for c in m.component_data_objects(pyo.Constraint, active=True, descend_into=True): _check_coefficents(c, c.body, too_large, too_small, cons_with_large_coefficients, cons_with_small_coefficients) for c in m.component_data_objects(pyo.Constraint, active=True, descend_into=True): c_lb, c_ub = compute_bounds_on_expr(c.body) c_lb, c_ub = _bounds_to_float(c_lb, c_ub) if c_lb <= -too_large or c_ub >= too_large: cons_with_large_bounds[c] = (c_lb, c_ub) for c in m.component_data_objects(pyo.Objective, active=True, descend_into=True): _check_coefficents(c, c.expr, too_large, too_small, objs_with_large_coefficients, objs_with_small_coefficients) s = '\n\n' if len(vars_without_bounds) > 0: s += 'The following variables are not bounded. Please add bounds.\n' s += _print_var_set(vars_without_bounds) if len(vars_with_large_bounds) > 0: s += 'The following variables have large bounds. Please scale them.\n' s += _print_var_set(vars_with_large_bounds) if len(objs_with_large_coefficients) > 0: s += 'The following objectives have potentially large coefficients. Please scale them.\n' s += _print_coefficients(objs_with_large_coefficients) if len(objs_with_small_coefficients) > 0: s += 'The following objectives have small coefficients.\n' s += _print_coefficients(objs_with_small_coefficients) if len(cons_with_large_coefficients) > 0: s += 'The following constraints have potentially large coefficients. Please scale them.\n' s += _print_coefficients(cons_with_large_coefficients) if len(cons_with_small_coefficients) > 0: s += 'The following constraints have small coefficients.\n' s += _print_coefficients(cons_with_small_coefficients) if len(cons_with_large_bounds) > 0: s += 'The following constraints have bodies with large bounds. Please scale them.\n' s += f'{"LB":>12}{"UB":>12} Constraint\n' for c, (c_lb, c_ub) in cons_with_large_bounds.items(): s += f'{c_lb:>12.2e}{c_ub:>12.2e} {str(c)}\n' if (len(vars_without_bounds) > 0 or len(vars_with_large_bounds) > 0 or len(cons_with_large_coefficients) > 0 or len(cons_with_small_coefficients) > 0 or len(objs_with_small_coefficients) > 0 or len(objs_with_large_coefficients) > 0 or len(cons_with_large_bounds) > 0): logger.info(s) return False return True
ub = pe.value(nlp.obj) # Build the relaxation """ Reformulate the NLP as min x4 - 3*x2 + x s.t. x2 = x**2 x4 = x2**2 Then relax the two constraints with PWXSquaredRelaxation objects. """ rel = pe.ConcreteModel() rel.x = pe.Var(bounds=(-2, 2)) rel.x2 = pe.Var(bounds=compute_bounds_on_expr(rel.x**2)) rel.x4 = pe.Var(bounds=compute_bounds_on_expr(rel.x2**2)) rel.x2_con = coramin.relaxations.PWXSquaredRelaxation() rel.x2_con.build(x=rel.x, aux_var=rel.x2, use_linear_relaxation=True) rel.x4_con = coramin.relaxations.PWXSquaredRelaxation() rel.x4_con.build(x=rel.x2, aux_var=rel.x4, use_linear_relaxation=True) rel.obj = pe.Objective(expr=rel.x4 - 3 * rel.x2 + rel.x) # Now solve the relaxation and refine the convex sides of the constraints with add_cut print('*********************************') print('OA Cut Generation') print('*********************************') opt = pe.SolverFactory('gurobi_direct') res = opt.solve(rel) lb = pe.value(rel.obj) print('gap: ' + str(100 * abs(ub - lb) / abs(ub)) + ' %')
def _relax_leaf_to_root_DivisionExpression(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): arg1, arg2 = values if arg1.__class__ == numeric_expr.MonomialTermExpression: coef1, arg1 = arg1.args else: coef1 = 1 if arg2.__class__ == numeric_expr.MonomialTermExpression: coef2, arg2 = arg2.args else: coef2 = 1 coef = coef1/coef2 degree_1 = degree_map[arg1] degree_2 = degree_map[arg2] if degree_2 == 0: res = (coef / arg2) * arg1 degree_map[res] = degree_1 return res elif (id(arg1), id(arg2), 'div') in aux_var_map: _aux_var, relaxation = aux_var_map[id(arg1), id(arg2), 'div'] relaxation_side = relaxation_side_map[node] if relaxation_side != relaxation.relaxation_side: relaxation.relaxation_side = RelaxationSide.BOTH res = coef * _aux_var degree_map[_aux_var] = 1 degree_map[res] = 1 return res elif degree_1 == 0: if (id(arg2), 'reciprocal') in aux_var_map: _aux_var, relaxation = aux_var_map[id(arg2), 'reciprocal'] relaxation_side = relaxation_side_map[node] if relaxation_side != relaxation.relaxation_side: relaxation.relaxation_side = RelaxationSide.BOTH res = coef * arg1 * _aux_var degree_map[_aux_var] = 1 degree_map[res] = 1 return res else: _aux_var = _get_aux_var(parent_block, 1/arg2) arg2 = replace_sub_expression_with_aux_var(arg2, parent_block) relaxation_side = relaxation_side_map[node] degree_map[_aux_var] = 1 if compute_bounds_on_expr(arg2)[0] > 0: relaxation = PWUnivariateRelaxation() relaxation.set_input(x=arg2, aux_var=_aux_var, relaxation_side=relaxation_side, f_x_expr=1/arg2, shape=FunctionShape.CONVEX) elif compute_bounds_on_expr(arg2)[1] < 0: relaxation = PWUnivariateRelaxation() relaxation.set_input(x=arg2, aux_var=_aux_var, relaxation_side=relaxation_side, f_x_expr=1/arg2, shape=FunctionShape.CONCAVE) else: _one = parent_block.aux_vars.add() _one.fix(1) relaxation = PWMcCormickRelaxation() relaxation.set_input(x1=arg2, x2=_aux_var, aux_var=_one, relaxation_side=relaxation_side) aux_var_map[id(arg2), 'reciprocal'] = (_aux_var, relaxation) setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) counter.increment() res = coef * arg1 * _aux_var degree_map[res] = 1 return res else: _aux_var = _get_aux_var(parent_block, arg1 / arg2) arg1 = replace_sub_expression_with_aux_var(arg1, parent_block) arg2 = replace_sub_expression_with_aux_var(arg2, parent_block) relaxation_side = relaxation_side_map[node] relaxation = PWMcCormickRelaxation() relaxation.set_input(x1=arg2, x2=_aux_var, aux_var=arg1, relaxation_side=relaxation_side) aux_var_map[id(arg1), id(arg2), 'div'] = (_aux_var, relaxation) setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) counter.increment() res = coef * _aux_var degree_map[_aux_var] = 1 degree_map[res] = 1 return res
def _get_aux_var(parent_block, expr): _aux_var = parent_block.aux_vars.add() lb, ub = compute_bounds_on_expr(expr) _aux_var.setlb(lb) _aux_var.setub(ub) return _aux_var
def _relax_leaf_to_root_PowExpression(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): arg1, arg2 = values degree1 = degree_map[arg1] degree2 = degree_map[arg2] if degree2 == 0: if degree1 == 0: res = arg1 ** arg2 degree_map[res] = 0 return res if not is_constant(arg2): logger.warning('Only constant exponents are supported: ' + str(arg1**arg2) + '\nReplacing ' + str(arg2) + ' with its value.') arg2 = pe.value(arg2) if arg2 == 1: return arg1 elif arg2 == 0: res = 1 degree_map[res] = 0 return res elif arg2 == 2: return _relax_quadratic(arg1=arg1, aux_var_map=aux_var_map, relaxation_side=relaxation_side_map[node], degree_map=degree_map, parent_block=parent_block, counter=counter) elif arg2 >= 0: if arg2 == round(arg2): if arg2 % 2 == 0 or compute_bounds_on_expr(arg1)[0] >= 0: return _relax_convex_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, relaxation_side=relaxation_side_map[node], degree_map=degree_map, parent_block=parent_block, counter=counter) elif compute_bounds_on_expr(arg1)[1] <= 0: return _relax_concave_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, relaxation_side=relaxation_side_map[node], degree_map=degree_map, parent_block=parent_block, counter=counter) else: # reformulate arg1 ** arg2 as arg1 * arg1 ** (arg2 - 1) _new_relaxation_side_map = ComponentMap() _reformulated = arg1 * arg1 ** (arg2 - 1) _new_relaxation_side_map[_reformulated] = relaxation_side_map[node] res = _relax_expr(expr=_reformulated, aux_var_map=aux_var_map, parent_block=parent_block, relaxation_side_map=_new_relaxation_side_map, counter=counter, degree_map=degree_map) degree_map[res] = 1 return res else: if arg2 < 1: return _relax_concave_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, relaxation_side=relaxation_side_map[node], degree_map=degree_map, parent_block=parent_block, counter=counter) else: return _relax_convex_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, relaxation_side=relaxation_side_map[node], degree_map=degree_map, parent_block=parent_block, counter=counter) else: if arg2 == round(arg2): if compute_bounds_on_expr(arg1)[0] >= 0: return _relax_convex_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, relaxation_side=relaxation_side_map[node], degree_map=degree_map, parent_block=parent_block, counter=counter) elif compute_bounds_on_expr(arg1)[1] <= 0: if arg2 % 2 == 0: return _relax_convex_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, relaxation_side=relaxation_side_map[node], degree_map=degree_map, parent_block=parent_block, counter=counter) else: return _relax_concave_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, relaxation_side=relaxation_side_map[node], degree_map=degree_map, parent_block=parent_block, counter=counter) else: # reformulate arg1 ** arg2 as 1 / arg1 ** (-arg2) _new_relaxation_side_map = ComponentMap() _reformulated = 1 / (arg1 ** (-arg2)) _new_relaxation_side_map[_reformulated] = relaxation_side_map[node] res = _relax_expr(expr=_reformulated, aux_var_map=aux_var_map, parent_block=parent_block, relaxation_side_map=_new_relaxation_side_map, counter=counter, degree_map=degree_map) degree_map[res] = 1 return res else: assert compute_bounds_on_expr(arg1)[0] >= 0 return _relax_convex_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, relaxation_side=relaxation_side_map[node], degree_map=degree_map, parent_block=parent_block, counter=counter) elif degree1 == 0: if not is_constant(arg1): logger.warning('Found {0} raised to a variable power. However, {0} does not appear to be constant (maybe ' 'it is or depends on a mutable Param?). Replacing {0} with its value.'.format(str(arg1))) arg1 = pe.value(arg1) if arg1 < 0: raise ValueError('Cannot raise a negative base to a variable exponent: ' + str(arg1**arg2)) return _relax_convex_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, relaxation_side=relaxation_side_map[node], degree_map=degree_map, parent_block=parent_block, counter=counter, swap=True) else: if (id(arg1), id(arg2), 'pow') in aux_var_map: _aux_var, relaxation = aux_var_map[id(arg1), id(arg2), 'pow'] if relaxation_side_map[node] != relaxation.relaxation_side: relaxation.relaxation_side = RelaxationSide.BOTH return _aux_var else: assert compute_bounds_on_expr(arg1)[0] >= 0 _new_relaxation_side_map = ComponentMap() _reformulated = pe.exp(arg2 * pe.log(arg1)) _new_relaxation_side_map[_reformulated] = relaxation_side_map[node] res = _relax_expr(expr=_reformulated, aux_var_map=aux_var_map, parent_block=parent_block, relaxation_side_map=_new_relaxation_side_map, counter=counter, degree_map=degree_map) degree_map[res] = 1 return res