def compute_alpha(self, problem, expr, ctx): xs = self._collect_expr_variables(expr) x_bounds = [] for x in xs: x_view = problem.variable_view(x) x_l = x_view.lower_bound() x_u = x_view.upper_bound() if x_l is None or x_u is None: raise ValueError( 'Variables must be bounded: name={}, lb={}, ub={}'.format( x.name, x_l, x_u)) x_bounds.append(Interval(x_l, x_u)) tree_data = expr.expression_tree_data() f = tree_data.eval(x_bounds) H = f.hessian(x_bounds, [1.0]) n = len(xs) min_ = 0 H = [Interval.zero() + i for i in H] for i in range(n): h = H[i * n:(i + 1) * n] v = h[i].lower_bound - sum( max(abs(h[j].lower_bound), abs(h[j].upper_bound)) for j in range(n) if j != i) if v < min_: min_ = v alpha = max(0, -0.5 * min_) return alpha
def _univariate_term_bound(a, b, x_bound): """Return the bound of the univariate term ax^2 + bx""" lower_bound = x_bound.lower_bound upper_bound = x_bound.upper_bound if lower_bound is None or isinf(lower_bound): univariate_lower_bound = -inf else: univariate_lower_bound = a * lower_bound**2 + b * lower_bound if upper_bound is None or isinf(upper_bound): univariate_upper_bound = inf else: univariate_upper_bound = a * upper_bound**2 + b * upper_bound if -b / (2 * a) in x_bound: t = -(b**2) / (4 * a) new_lower_bound = min(univariate_lower_bound, univariate_upper_bound, t) new_upper_bound = max(univariate_lower_bound, univariate_upper_bound, t) return Interval(new_lower_bound, new_upper_bound) new_lower_bound = min(univariate_lower_bound, univariate_upper_bound) new_upper_bound = max(univariate_lower_bound, univariate_upper_bound) return Interval(new_lower_bound, new_upper_bound)
def perform_fbbt(problem, maxiter, timelimit, objective_upper_bound=None, branching_variable=None): """Perform FBBT on `problem` with the given `maxiter` and `timelimit`.""" bounds = ExpressionDict(problem) bounds_tightener = BoundsTightener( FBBTStopCriterion(max_iter=maxiter, timelimit=timelimit), ) for variable in problem.variables: lb = problem.lower_bound(variable) ub = problem.upper_bound(variable) bounds[variable] = Interval(lb, ub) # For all intents and purposes here an infinite upper bound is the same # as no upper bound. if objective_upper_bound is not None and is_inf(objective_upper_bound): objective_upper_bound = None if objective_upper_bound is not None: root_expr = problem.objective.root_expr expr_bounds = Interval(None, objective_upper_bound) if root_expr not in bounds: bounds[root_expr] = expr_bounds else: existing_bounds = bounds[root_expr] new_bounds = existing_bounds.intersect(expr_bounds) bounds[root_expr] = new_bounds def get_bound(b): def _f(expr): return b[expr] return _f def set_bound(b): def _f(expr, value): b[expr] = value return _f _initialize_bounds(problem, bounds, get_bound(bounds), set_bound(bounds)) try: with timeout(timelimit, 'Timeout in FBBT'): try: bounds_tightener.tighten( problem, bounds, branching_variable=branching_variable, objective_changed=objective_upper_bound is not None, ) except EmptyIntervalError: pass except TimeoutError: pass _manage_infinity_bounds( problem, bounds, get_bound(bounds), set_bound(bounds) ) return bounds
def test_objective_bound(visitor, a, b): lb, ub = min(a, b), max(a, b) assume(lb < ub) child = pe.Var() o = Objective('obj', children=[child]) bounds = ComponentMap() bounds[child] = Interval(lb, ub) matched, result = visitor.visit_expression(o, bounds) assert matched assert result == Interval(lb, ub)
def _inequality_bounds_and_expr(expr): if len(expr._args_) == 2: (lhs, rhs) = expr._args_ if is_numeric(lhs): return Interval(numeric_value(lhs), None), rhs else: return Interval(None, numeric_value(rhs)), lhs elif len(expr._args_) == 3: (lhs, ex, rhs) = expr._args_ return Interval(numeric_value(lhs), numeric_value(rhs)), ex else: raise ValueError('Malformed InequalityExpression')
def apply(self, expr, bounds): assert len(expr.args) == 2 _, expo = expr.args if type(expo) not in nonpyomo_leaf_types: if not expo.is_constant(): return Interval(None, None) expo = expo.value is_even = almosteq(expo % 2, 0) is_positive = expo > 0 if is_even and is_positive: return Interval(0, None) return Interval(None, None)
def apply(self, expr, convexity, _mono, bounds): child = expr.args[0] child_bounds = bounds[child] cvx = convexity[child] concave_domain = Interval(-1, 0) if child_bounds in concave_domain and cvx.is_concave(): return Convexity.Concave convex_domain = Interval(0, 1) if child_bounds in convex_domain and cvx.is_convex(): return Convexity.Convex return Convexity.Unknown
def test_constraint_bound(visitor, a, b, c, d): e_lb, e_ub = min(a, b), max(a, b) c_lb, c_ub = min(c, d), max(c, d) assume(max(e_lb, c_lb) <= min(e_ub, c_ub)) child = pe.Var() cons = Constraint('c0', lower_bound=c_lb, upper_bound=c_ub, children=[child]) bounds = ComponentMap() bounds[child] = Interval(e_lb, e_ub) matched, result = visitor.visit_expression(cons, bounds) expected = Interval(max(e_lb, c_lb), min(c_ub, e_ub)) assert result == expected
def intervals(draw, allow_infinity=True): a = draw(reals(allow_infinity=allow_infinity)) if np.isinf(a): allow_infinity = False b = draw(reals(allow_infinity=allow_infinity)) lb, ub = min(a, b), max(a, b) return Interval(lb, ub)
def handle_result(self, expr, value, bounds): if value is None: new_bounds = Interval(None, None) else: new_bounds = value bounds[expr] = new_bounds return True
def test_even_pow_negative_bound(visitor, base, n): c = -2 * n expr = base**c bounds = ComponentMap() matched, result = visitor.visit_expression(expr, bounds) assert matched assert result == Interval(None, None)
def test_even_pow_non_const_bound(visitor, base, expo): assume(not expo.is_constant()) expr = base**expo bounds = ComponentMap() matched, result = visitor.visit_expression(expr, bounds) assert matched assert result == Interval(None, None)
class TestAbs(object): @pytest.mark.parametrize('it,expected', [ (Interval(1, 2), Interval(1, 2)), (Interval(-3, -2), Interval(2, 3)), (Interval(-3, 2), Interval(0, 3)), (Interval(-2, 3), Interval(0, 3)), ]) def test_abs(self, it, expected): assert expected == abs(it)
def test_variable_bound(visitor, a, b): lb = min(a, b) ub = max(a, b) var = pe.Var(bounds=(lb, ub)) var.construct() matched, result = visitor.visit_expression(var, None) assert matched assert result == Interval(lb, ub)
def _variable_bound_from_univariate_lower_bound(a, b, c): t = c / a + (b**2) / (4 * a**2) if a > 0: return Interval(None, None) # if c + (b**2) / (4*a) <= 0: # return Interval(None, None) # new_upper_bound = Interval(None, -sqrt(t, RM.RN) - b/(2*a)) # new_lower_bound = Interval(sqrt(t, RM.RN) - b/(2*a), None) # return new_upper_bound.intersect(new_lower_bound) # a < 0 if c + (b**2) / (4 * a) > 0: # The bound would be empty which is something that shouldn't happen. # Return [-inf, inf] so we don't tighten the bounds return Interval(None, None) return Interval(-sqrt(t, RM.RN) - b / (2 * a), sqrt(t, RM.RN) - b / (2 * a))
def test_division_bound(visitor, bound, expected): num = pe.Var() child = pe.Var() expr = num / child bounds = ComponentMap() bounds[num] = Interval(1.0, 1.0) bounds[child] = bound matched, result = visitor.visit_expression(expr, bounds) assert matched assert result == expected
def apply(self, expr, bounds): expr_bound = bounds[expr] child_bounds = {} if len(expr.terms) > self.max_expr_children: return None terms = expr.terms for term_idx, term in enumerate(terms): var1 = term.var1 var2 = term.var2 siblings_bound = sum( self._term_bound(t, bounds) for i, t in enumerate(terms) if i != term_idx) term_bound = (expr_bound - siblings_bound) / term.coefficient if var1 is var2: term_bound = term_bound.intersect(Interval(0, None)) upper_bound = term_bound.sqrt().upper_bound new_bound = Interval(-upper_bound, upper_bound) if id(var1) in child_bounds: existing = child_bounds[id(var1)] child_bounds[id(var1)] = existing.intersect(new_bound) else: child_bounds[id(var1)] = Interval(new_bound.lower_bound, new_bound.upper_bound) else: new_bound_var1 = term_bound / bounds[var2] new_bound_var2 = term_bound / bounds[var1] if id(var1) in child_bounds: existing = child_bounds[id(var1)] child_bounds[id(var1)] = existing.intersect(new_bound_var1) else: child_bounds[id(var1)] = new_bound_var1 if id(var2) in child_bounds: existing = child_bounds[id(var2)] child_bounds[id(var2)] = existing.intersect(new_bound_var2) else: child_bounds[id(var2)] = new_bound_var2 return [child_bounds[id(v)] for v in expr.args]
def apply(self, expr, convexity, monotonicity, bounds): g = expr.args[0] cvx_f = Convexity.Linear cvx_g = convexity[g] mono_f = Monotonicity.Constant mono_g = monotonicity[g] bounds_f = Interval(1.0, 1.0) bounds_g = bounds[g] return _division_convexity(cvx_f, cvx_g, mono_f, mono_g, bounds_f, bounds_g)
def _underestimate_bilinear_term(self, problem, term, ctx): bilinear_aux_vars = ctx.metadata[BILINEAR_AUX_VAR_META] x_expr = term.var1 y_expr = term.var2 xy_tuple = self._bilinear_tuple(x_expr, y_expr) w = self._get_bilinear_aux_var(problem, ctx, xy_tuple) if w is None: x_l = problem.lower_bound(x_expr) x_u = problem.upper_bound(x_expr) y_l = problem.lower_bound(y_expr) y_u = problem.upper_bound(y_expr) if term.var1 == term.var2: assert np.isclose(x_l, y_l) and np.isclose(x_u, y_u) w_bounds = Interval(x_l, x_u) ** 2 else: w_bounds = Interval(x_l, x_u) * Interval(y_l, y_u) w = Variable( self._format_aux_name(term.var1, term.var2), w_bounds.lower_bound, w_bounds.upper_bound, Domain.REAL, ) reference = BilinearTermReference(x_expr, y_expr) w.reference = reference bilinear_aux_vars[xy_tuple] = w constraints = self._generate_envelope_constraints(problem, term, w) else: constraints = [] new_expr = LinearExpression([w], [term.coefficient], 0.0) return new_expr, constraints
def _initialize_bounds(problem, bounds, get_bound, set_bound): """Set bounds of root_expr to constraints bounds. Since GALINI doesn't consider constraints as expression we have to do this manually. """ for constraint in problem.constraints: root_expr = constraint.root_expr expr_bounds = Interval(constraint.lower_bound, constraint.upper_bound) if root_expr not in bounds: set_bound(root_expr, expr_bounds) else: existing_bounds = get_bound(root_expr) new_bounds = existing_bounds.intersect(expr_bounds) set_bound(root_expr, new_bounds)
def bound_description_to_bound(bound_str): if isinstance(bound_str, str): return { 'zero': Interval.zero(), 'nonpositive': Interval(None, 0), 'nonnegative': Interval(0, None), 'positive': Interval(1, None), 'negative': Interval(None, -1), 'unbounded': Interval(None, None), }[bound_str] elif isinstance(bound_str, Interval): return bound_str else: return Interval(bound_str, bound_str)
def handle_result(self, expr, value, bounds): if value is None: new_bounds = Interval(None, None) else: new_bounds = value old_bounds = bounds.get(expr, None) if old_bounds is not None: new_bounds = old_bounds.intersect( new_bounds, abs_eps=self.intersect_abs_eps, ) has_changed = old_bounds != new_bounds else: has_changed = True bounds[expr] = new_bounds return has_changed
def test_visitor_tightens_new_bounds(visitor, expr): bounds = ComponentMap() assert bounds.get(expr, None) is None assert visitor.handle_result(expr, Interval(0, 2), bounds) assert bounds[expr] == Interval(0, 2) assert not visitor.handle_result(expr, Interval(0, 2), bounds) assert bounds[expr] == Interval(0, 2) assert visitor.handle_result(expr, Interval(0, 1), bounds) assert bounds[expr] == Interval(0, 1)
def _manage_infinity_bounds(problem, _bounds, get_bound, set_bound): """In some cases variables bounds are numbers that are bigger than mc.infinity. Change them back to None. """ for variable in problem.variables: expr_bounds = get_bound(variable) lower_bound = expr_bounds.lower_bound upper_bound = expr_bounds.upper_bound if is_inf(lower_bound): new_lower_bound = None else: new_lower_bound = lower_bound if is_inf(upper_bound): new_upper_bound = None else: new_upper_bound = upper_bound set_bound(variable, Interval(new_lower_bound, new_upper_bound))
def apply(self, expr, bounds): # Look for Quadratic + Linear expressions if expr.nargs() != 2: return quadratic, linear = _quadratic_and_linear(expr.children) if quadratic is None or linear is None: return univariate_terms, quadratic_terms, linear_terms = \ _collect_expression_types(quadratic, linear) # Compute bounds of non univariate terms. We will use this to perform # tightening on univariate terms. quadratic_terms_bound = sum(t.coefficient * bounds[t.var1] * bounds[t.var2] for t in quadratic_terms) linear_terms_bound = sum(bounds[v] * c for v, c in linear_terms) expr_bound = bounds.get(expr, Interval(None, None)) non_univariate_bound = \ expr_bound - quadratic_terms_bound - linear_terms_bound univariate_terms_bounds = [ _univariate_term_bound(a, b, bounds[v]) for (v, a, b) in univariate_terms ] tightened_univariate_terms_bounds = [ _tighten_univariate_term_bound(term_idx, univariate_terms_bounds, non_univariate_bound) for term_idx, _ in enumerate(univariate_terms_bounds) ] vars_bounds = ComponentMap( (v, _variable_bound_from_univariate_bound(a, b, bound)) for bound, ( v, a, b) in zip(tightened_univariate_terms_bounds, univariate_terms)) return vars_bounds
def apply(self, expr, bounds): base, expo = expr.args if type(expo) not in nonpyomo_leaf_types: if not expo.is_constant(): return None expo = expo.value if not almosteq(expo, 2): return None expr_bound = bounds[expr] # the bound of a square number is never negative, but check anyway to # avoid unexpected crashes. if not expr_bound.is_nonnegative(): return None sqrt_bound = expr_bound.sqrt() return [ Interval(-sqrt_bound.upper_bound, sqrt_bound.upper_bound), None, ]
def intervals(draw, allow_infinity=True, lower_bound=None, upper_bound=None): if lower_bound is None and allow_infinity: lower = draw( st.one_of(st.none(), reals(max_value=upper_bound, allow_infinity=False))) else: lower = draw( reals(min_value=lower_bound, max_value=upper_bound, allow_infinity=False)) if upper_bound is None and allow_infinity: upper = draw( st.one_of(st.none(), reals(min_value=lower, allow_infinity=False))) else: upper = draw( reals( min_value=lower, max_value=upper_bound, allow_infinity=False, )) return Interval(lower, upper)
class AcosRule(_UnaryFunctionBoundsRule): """Bound propagation rule for acos.""" initial_bound = Interval(-1, 1)
class LogRule(_UnaryFunctionBoundsRule): """Bound propagation rule for log.""" initial_bound = Interval(0, None)
class SqrtRule(_UnaryFunctionBoundsRule): """Bound propagation rule for sqrt.""" initial_bound = Interval(0, None)