def _apply_to(self, instance, **kwargs): config = self.CONFIG(kwargs) if config.tmp and not hasattr(instance, '_tmp_trivial_deactivated_constrs'): instance._tmp_trivial_deactivated_constrs = ComponentSet() elif config.tmp: logger.warning( 'Deactivating trivial constraints on the block {} for which ' 'trivial constraints were previously deactivated. ' 'Reversion will affect all deactivated constraints.'.format( instance.name)) # Trivial constraints are those that do not contain any variables, ie. # the polynomial degree is 0 for constr in instance.component_data_objects(ctype=Constraint, active=True, descend_into=True): repn = generate_standard_repn(constr.body) if not repn.is_constant(): # This constraint is not trivial continue # We need to check each constraint to sure that it is not violated. constr_lb = value( constr.lower) if constr.has_lb() else float('-inf') constr_ub = value( constr.upper) if constr.has_ub() else float('inf') constr_value = repn.constant # Check if the lower bound is violated outside a given tolerance if (constr_value + config.tolerance <= constr_lb): if config.ignore_infeasible: continue else: raise InfeasibleConstraintException( 'Trivial constraint {} violates LB {} ≤ BODY {}.'. format(constr.name, constr_lb, constr_value)) # Check if the upper bound is violated outside a given tolerance if (constr_value >= constr_ub + config.tolerance): if config.ignore_infeasible: continue else: raise InfeasibleConstraintException( 'Trivial constraint {} violates BODY {} ≤ UB {}.'. format(constr.name, constr_value, constr_ub)) # Constraint is not infeasible. Deactivate it. if config.tmp: instance._tmp_trivial_deactivated_constrs.add(constr) config.return_trivial.append(constr) constr.deactivate()
def inv(xl, xu, feasibility_tol): """ The case where xl is very slightly positive but should be very slightly negative (or xu is very slightly negative but should be very slightly positive) should not be an issue. Suppose xu is 2 and xl is 1e-15 but should be -1e-15. The bounds obtained from this function will be [0.5, 1e15] or [0.5, inf), depending on the value of feasibility_tol. The true bounds are (-inf, -1e15] U [0.5, inf), where U is union. The exclusion of (-inf, -1e15] should be acceptable. Additionally, it very important to return a non-negative interval when xl is non-negative. """ if xu - xl <= -feasibility_tol: raise InfeasibleConstraintException( f'lower bound is greater than upper bound in inv; xl: {xl}; xu: {xu}' ) elif xu <= 0 <= xl: raise IntervalException(f'Division by zero in inv; xl: {xl}; xu: {xu}') elif 0 <= xl <= feasibility_tol: # xu must be strictly positive ub = inf lb = 1.0 / xu elif xl > feasibility_tol: # xl and xu must be strictly positive ub = 1.0 / xl lb = 1.0 / xu elif -feasibility_tol <= xu <= 0: # xl must be strictly negative lb = -inf ub = 1.0 / xl elif xu < -feasibility_tol: # xl and xu must be strictly negative ub = 1.0 / xl lb = 1.0 / xu else: # everything else lb = -inf ub = inf return lb, ub
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 InfeasibleConstraintException( '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)
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 an exception. # 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 InfeasibleConstraintException( '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 _inverse_power2(zl, zu, xl, xu, feasiblity_tol): """ z = x**y => compute bounds on y y = ln(z) / ln(x) This function assumes the exponent can be fractional, so x must be positive. This method should not be called if the exponent is an integer. """ if xu <= 0: raise IntervalException( 'Cannot raise a negative variable to a fractional power.') if (xl > 0 and zu <= 0) or (xl >= 0 and zu < 0): raise InfeasibleConstraintException( 'A positive variable raised to the power of anything must be positive.' ) lba, uba = log(zl, zu) lbb, ubb = log(xl, xu) yl, yu = div(lba, uba, lbb, ubb, feasiblity_tol) return yl, yu
def power(xl, xu, yl, yu, feasibility_tol): """ Compute bounds on x**y. """ if xl > 0: """ If x is always positive, things are simple. We only need to worry about the sign of y. """ if yl < 0 < yu: lb = min(xu**yl, xl**yu) ub = max(xl**yl, xu**yu) elif yl >= 0: lb = min(xl**yl, xl**yu) ub = max(xu**yl, xu**yu) else: # yu <= 0: lb = min(xu**yl, xu**yu) ub = max(xl**yl, xl**yu) elif xl == 0: if yl >= 0: lb = min(xl**yl, xl**yu) ub = max(xu**yl, xu**yu) elif yu <= 0: lb, ub = inv(*power(xl, xu, *sub(0, 0, yl, yu), feasibility_tol), feasibility_tol) else: lb1, ub1 = power(xl, xu, 0, yu, feasibility_tol) lb2, ub2 = power(xl, xu, yl, 0, feasibility_tol) lb = min(lb1, lb2) ub = max(ub1, ub2) elif yl == yu and yl == round(yl): # the exponent is an integer, so x can be negative """ The logic here depends on several things: 1) The sign of x 2) The sign of y 3) Whether y is even or odd. There are also special cases to avoid math domain errors. """ y = yl if xu <= 0: if y < 0: if y % 2 == 0: lb = xl**y if xu == 0: ub = inf else: ub = xu**y else: if xu == 0: lb = -inf ub = inf else: lb = xu**y ub = xl**y else: if y % 2 == 0: lb = xu**y ub = xl**y else: lb = xl**y ub = xu**y else: if y < 0: if y % 2 == 0: lb = min(xl**y, xu**y) ub = inf else: lb = -inf ub = inf else: if y % 2 == 0: lb = 0 ub = max(xl**y, xu**y) else: lb = xl**y ub = xu**y elif yl == yu: # the exponent has to be fractional, so x must be positive if xu < 0: msg = 'Cannot raise a negative number to the power of {0}.\n'.format( yl) msg += 'The upper bound of a variable raised to the power of {0} is {1}'.format( yl, xu) raise InfeasibleConstraintException(msg) xl = 0 lb, ub = power(xl, xu, yl, yu, feasibility_tol) else: lb = -inf ub = inf return lb, ub
def _inverse_power1(zl, zu, yl, yu, orig_xl, orig_xu, feasibility_tol): """ z = x**y => compute bounds on x. First, start by computing bounds on x with x = exp(ln(z) / y) However, if y is an integer, then x can be negative, so there are several special cases. See the docs below. """ xl, xu = log(zl, zu) xl, xu = div(xl, xu, yl, yu, feasibility_tol) xl, xu = exp(xl, xu) # if y is an integer, then x can be negative if yl == yu and yl == round(yl): # y is a fixed integer y = yl if y == 0: # Anything to the power of 0 is 1, so if y is 0, then x can be anything # (assuming zl <= 1 <= zu, which is enforced when traversing the tree in the other direction) xl = -inf xu = inf elif y % 2 == 0: """ if y is even, then there are two primary cases (note that it is much easier to walk through these while looking at plots): case 1: y is positive x**y is convex, positive, and symmetric. The bounds on x depend on the lower bound of z. If zl <= 0, then xl should simply be -xu. However, if zl > 0, then we may be able to say something better. For example, if the original lower bound on x is positive, then we can keep xl computed from x = exp(ln(z) / y). Furthermore, if the original lower bound on x is larger than -xl computed from x = exp(ln(z) / y), then we can still keep the xl computed from x = exp(ln(z) / y). Similar logic applies to the upper bound of x. case 2: y is negative The ideas are similar to case 1. """ if zu + feasibility_tol < 0: raise InfeasibleConstraintException( 'Infeasible. Anything to the power of {0} must be positive.' .format(y)) if y > 0: if zu <= 0: _xl = 0 _xu = 0 elif zl <= 0: _xl = -xu _xu = xu else: if orig_xl <= -xl + feasibility_tol: _xl = -xu else: _xl = xl if orig_xu < xl - feasibility_tol: _xu = -xl else: _xu = xu xl = _xl xu = _xu else: if zu == 0: raise InfeasibleConstraintException( 'Infeasible. Anything to the power of {0} must be positive.' .format(y)) elif zl <= 0: _xl = -inf _xu = inf else: if orig_xl <= -xl + feasibility_tol: _xl = -xu else: _xl = xl if orig_xu < xl - feasibility_tol: _xu = -xl else: _xu = xu xl = _xl xu = _xu else: # y % 2 == 1 """ y is odd. Case 1: y is positive x**y is monotonically increasing. If y is positive, then we can can compute the bounds on x using x = z**(1/y) and the signs on xl and xu depend on the signs of zl and zu. Case 2: y is negative Again, this is easier to visualize with a plot. x**y approaches zero when x approaches -inf or inf. Thus, if zl < 0 < zu, then no bounds can be inferred for x. If z is positive (zl >=0 ) then we can use the bounds computed from x = exp(ln(z) / y). If z is negative (zu <= 0), then we live in the bottom left quadrant, xl depends on zu, and xu depends on zl. """ if y > 0: xl = abs(zl)**(1.0 / y) xl = math.copysign(xl, zl) xu = abs(zu)**(1.0 / y) xu = math.copysign(xu, zu) else: if zl >= 0: pass elif zu <= 0: if zu == 0: xl = -inf else: xl = -abs(zu)**(1.0 / y) if zl == 0: xu = -inf else: xu = -abs(zl)**(1.0 / y) else: xl = -inf xu = inf return xl, xu
def fbbt_con(con, deactivate_satisfied_constraints=False, integer_tol=1e-5, infeasible_tol=1e-8): """ Feasibility based bounds tightening for a constraint. This function attempts to improve the bounds of each variable in the constraint based on the bounds of the constraint and the bounds of the other variables in the constraint. For example: >>> import pyomo.environ as pe >>> from pyomo.contrib.fbbt.fbbt import fbbt >>> m = pe.ConcreteModel() >>> m.x = pe.Var(bounds=(-1,1)) >>> m.y = pe.Var(bounds=(-2,2)) >>> m.z = pe.Var() >>> m.c = pe.Constraint(expr=m.x*m.y + m.z == 1) >>> fbbt(m.c) >>> print(m.z.lb, m.z.ub) -1.0 3.0 Parameters ---------- con: pyomo.core.base.constraint.Constraint constraint on which to perform fbbt deactivate_satisfied_constraints: bool If deactivate_satisfied_constraints is True and the 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. """ if not con.active: return ComponentMap() bnds_dict = ComponentMap( ) # a dictionary to store the bounds of every node in the tree # a walker to propagate bounds from the variables to the root visitorA = _FBBTVisitorLeafToRoot(bnds_dict) visitorA.dfs_postorder_stack(con.body) # Now we need to replace the bounds in bnds_dict for the root # node with the bounds on the constraint (if those bounds are # better). _lb = value(con.lower) _ub = value(con.upper) if _lb is None: _lb = -math.inf if _ub is None: _ub = math.inf lb, ub = bnds_dict[con.body] # check if the constraint is infeasible if lb > _ub + infeasible_tol or ub < _lb - infeasible_tol: raise InfeasibleConstraintException( 'Detected an infeasible constraint during FBBT: {0}'.format( str(con))) # check if the constraint is always satisfied if deactivate_satisfied_constraints: if lb >= _lb and ub <= _ub: con.deactivate() if _lb > lb: lb = _lb if _ub < ub: ub = _ub bnds_dict[con.body] = (lb, ub) # Now, propagate bounds back from the root to the variables visitorB = _FBBTVisitorRootToLeaf(bnds_dict, integer_tol=integer_tol) visitorB.dfs_postorder_stack(con.body) new_var_bounds = ComponentMap() for _node, _bnds in bnds_dict.items(): if _node.__class__ in nonpyomo_leaf_types: continue if _node.is_variable_type(): lb, ub = bnds_dict[_node] if lb == -math.inf: lb = None if ub == math.inf: ub = None new_var_bounds[_node] = (lb, ub) return new_var_bounds