From 4ffc85b7a56991beff390b43190f0fee28c37bb7 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 28 Sep 2023 13:45:48 -0600 Subject: [PATCH 01/36] Moving the fbbt leaf-to-root visitor onto StreamBasedExpressionVisitor, which may or may not matter for this exercise... --- pyomo/contrib/fbbt/fbbt.py | 239 +++++++++++++++++++++++++++---------- 1 file changed, 176 insertions(+), 63 deletions(-) diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index 5c486488540..62abb83b1ac 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -9,9 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from collections import defaultdict from pyomo.common.collections import ComponentMap, ComponentSet import pyomo.core.expr.numeric_expr as numeric_expr -from pyomo.core.expr.visitor import ExpressionValueVisitor, identify_variables +from pyomo.core.expr.visitor import ( + ExpressionValueVisitor, identify_variables, StreamBasedExpressionVisitor +) from pyomo.core.expr.numvalue import nonpyomo_leaf_types, value from pyomo.core.expr.numvalue import is_fixed import pyomo.contrib.fbbt.interval as interval @@ -450,8 +453,10 @@ def _prop_bnds_leaf_to_root_GeneralExpression(node, bnds_dict, feasibility_tol): expr_lb, expr_ub = bnds_dict[expr] bnds_dict[node] = (expr_lb, expr_ub) +def _prop_no_bounds(node, bnds_dict, feasibility_tol): + bnds_dict[node] = (-interval.inf, interval.inf) -_prop_bnds_leaf_to_root_map = dict() +_prop_bnds_leaf_to_root_map = defaultdict(lambda: _prop_no_bounds) _prop_bnds_leaf_to_root_map[ numeric_expr.ProductExpression ] = _prop_bnds_leaf_to_root_ProductExpression @@ -1031,7 +1036,6 @@ def _prop_bnds_root_to_leaf_GeneralExpression(node, bnds_dict, feasibility_tol): expr_lb, expr_ub = bnds_dict[node] bnds_dict[node.expr] = (expr_lb, expr_ub) - _prop_bnds_root_to_leaf_map = dict() _prop_bnds_root_to_leaf_map[ numeric_expr.ProductExpression @@ -1083,13 +1087,70 @@ def _check_and_reset_bounds(var, lb, ub): ub = orig_ub return lb, ub +def _before_constant(visitor, child): + visitor.bnds_dict[child] = (child, child) + return False, None + +def _before_var(visitor, child): + if child in visitor.bnds_dict: + return False, None + elif child.is_fixed() and not visitor.ignore_fixed: + lb = value(child.value) + ub = lb + else: + lb = value(child.lb) + ub = value(child.ub) + if lb is None: + lb = -interval.inf + if ub is None: + ub = interval.inf + if lb - visitor.feasibility_tol > ub: + raise InfeasibleConstraintException( + 'Variable has a lower bound that is larger than its ' + 'upper bound: {0}'.format( + str(child) + ) + ) + visitor.bnds_dict[child] = (lb, ub) + return False, None + +def _before_NPV(visitor, child): + val = value(child) + visitor.bnds_dict[child] = (val, val) + return False, None + +def _before_other(visitor, child): + return True, None + +def _before_external_function(visitor, child): + # TODO: provide some mechanism for users to provide interval + # arithmetic callback functions for general external + # functions + visitor.bnds_dict[child] = (-interval.inf, interval.inf) + return False, None + +def _register_new_before_child_handler(visitor, child): + handlers = _before_child_handlers + child_type = child.__class__ + if child.is_variable_type(): + handlers[child_type] = _before_var + elif not child.is_potentially_variable(): + handlers[child_type] = _before_NPV + else: + handlers[child_type] = _before_other + return handlers[child_type](visitor, child) + +_before_child_handlers = defaultdict(lambda: _register_new_before_child_handler) +_before_child_handlers[ + numeric_expr.ExternalFunctionExpression] = _before_external_function +for _type in nonpyomo_leaf_types: + _before_child_handlers[_type] = _before_constant -class _FBBTVisitorLeafToRoot(ExpressionValueVisitor): +class _FBBTVisitorLeafToRoot(StreamBasedExpressionVisitor): """ This walker propagates bounds from the variables to each node in the expression tree (all the way to the root node). """ - def __init__( self, bnds_dict, integer_tol=1e-4, feasibility_tol=1e-8, ignore_fixed=False ): @@ -1099,68 +1160,120 @@ def __init__( bnds_dict: ComponentMap integer_tol: float feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + If the bounds computed on the body of a constraint violate the bounds of + the constraint by more than feasibility_tol, then the constraint is + considered infeasible and an exception is raised. This tolerance is also + used when performing certain interval arithmetic operations to ensure that + none of the feasible region is removed due to floating point arithmetic and + to prevent math domain errors (a larger value is more conservative). """ + super().__init__() self.bnds_dict = bnds_dict self.integer_tol = integer_tol self.feasibility_tol = feasibility_tol self.ignore_fixed = ignore_fixed - def visit(self, node, values): - if node.__class__ in _prop_bnds_leaf_to_root_map: - _prop_bnds_leaf_to_root_map[node.__class__]( - node, self.bnds_dict, self.feasibility_tol - ) - else: - self.bnds_dict[node] = (-interval.inf, interval.inf) - return None - - def visiting_potential_leaf(self, node): - if node.__class__ in nonpyomo_leaf_types: - self.bnds_dict[node] = (node, node) - return True, None - - if node.is_variable_type(): - if node in self.bnds_dict: - return True, None - if node.is_fixed() and not self.ignore_fixed: - lb = value(node.value) - ub = lb - else: - lb = value(node.lb) - ub = value(node.ub) - if lb is None: - lb = -interval.inf - if ub is None: - ub = interval.inf - if lb - self.feasibility_tol > ub: - raise InfeasibleConstraintException( - 'Variable has a lower bound that is larger than its upper bound: {0}'.format( - str(node) - ) - ) - self.bnds_dict[node] = (lb, ub) - return True, None - - if not node.is_potentially_variable(): - # NPV nodes are effectively constant leaves. Evaluate it - # and return the value. - val = value(node) - self.bnds_dict[node] = (val, val) - return True, None - - if node.__class__ is numeric_expr.ExternalFunctionExpression: - # TODO: provide some mechanism for users to provide interval - # arithmetic callback functions for general external - # functions - self.bnds_dict[node] = (-interval.inf, interval.inf) - return True, None - - return False, None + def initializeWalker(self, expr): + walk, result = self.beforeChild(None, expr, 0) + if not walk: + return False, result#self.finalizeResult(result) + return True, expr + + def beforeChild(self, node, child, child_idx): + return _before_child_handlers[child.__class__](self, child) + + def exitNode(self, node, data): + _prop_bnds_leaf_to_root_map[node.__class__](node, self.bnds_dict, + self.feasibility_tol) + # if node.__class__ in _prop_bnds_leaf_to_root_map: + # _prop_bnds_leaf_to_root_map[node.__class__]( + # node, self.bnds_dict, self.feasibility_tol + # ) + # else: + # self.bnds_dict[node] = (-interval.inf, interval.inf) + # return None + + # def finalizeResult(self, result): + # return result + + +# class _FBBTVisitorLeafToRoot(ExpressionValueVisitor): +# """ +# This walker propagates bounds from the variables to each node in +# the expression tree (all the way to the root node). +# """ + +# def __init__( +# self, bnds_dict, integer_tol=1e-4, feasibility_tol=1e-8, ignore_fixed=False +# ): +# """ +# Parameters +# ---------- +# bnds_dict: ComponentMap +# integer_tol: float +# feasibility_tol: float +# If the bounds computed on the body of a constraint violate the bounds of the constraint by more than +# feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance +# is also used when performing certain interval arithmetic operations to ensure that none of the feasible +# region is removed due to floating point arithmetic and to prevent math domain errors (a larger value +# is more conservative). +# """ +# self.bnds_dict = bnds_dict +# self.integer_tol = integer_tol +# self.feasibility_tol = feasibility_tol +# self.ignore_fixed = ignore_fixed + +# def visit(self, node, values): +# if node.__class__ in _prop_bnds_leaf_to_root_map: +# _prop_bnds_leaf_to_root_map[node.__class__]( +# node, self.bnds_dict, self.feasibility_tol +# ) +# else: +# self.bnds_dict[node] = (-interval.inf, interval.inf) +# return None + +# def visiting_potential_leaf(self, node): +# if node.__class__ in nonpyomo_leaf_types: +# self.bnds_dict[node] = (node, node) +# return True, None + +# if node.is_variable_type(): +# if node in self.bnds_dict: +# return True, None +# if node.is_fixed() and not self.ignore_fixed: +# lb = value(node.value) +# ub = lb +# else: +# lb = value(node.lb) +# ub = value(node.ub) +# if lb is None: +# lb = -interval.inf +# if ub is None: +# ub = interval.inf +# if lb - self.feasibility_tol > ub: +# raise InfeasibleConstraintException( +# 'Variable has a lower bound that is larger than its upper bound: {0}'.format( +# str(node) +# ) +# ) +# self.bnds_dict[node] = (lb, ub) +# return True, None + +# if not node.is_potentially_variable(): +# # NPV nodes are effectively constant leaves. Evaluate it +# # and return the value. +# val = value(node) +# self.bnds_dict[node] = (val, val) +# return True, None + +# if node.__class__ is numeric_expr.ExternalFunctionExpression: +# # TODO: provide some mechanism for users to provide interval +# # arithmetic callback functions for general external +# # functions +# self.bnds_dict[node] = (-interval.inf, interval.inf) +# return True, None + +# return False, None class _FBBTVisitorRootToLeaf(ExpressionValueVisitor): @@ -1331,7 +1444,7 @@ def _fbbt_con(con, config): # a walker to propagate bounds from the variables to the root visitorA = _FBBTVisitorLeafToRoot(bnds_dict, feasibility_tol=config.feasibility_tol) - visitorA.dfs_postorder_stack(con.body) + visitorA.walk_expression(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 @@ -1582,7 +1695,7 @@ def compute_bounds_on_expr(expr, ignore_fixed=False): """ bnds_dict = ComponentMap() visitor = _FBBTVisitorLeafToRoot(bnds_dict, ignore_fixed=ignore_fixed) - visitor.dfs_postorder_stack(expr) + visitor.walk_expression(expr) lb, ub = bnds_dict[expr] if lb == -interval.inf: lb = None From 231b26c69561e0222177e946c6696dff5603aae7 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 28 Sep 2023 13:46:41 -0600 Subject: [PATCH 02/36] Keeping the fbbt visitor (and it's bounds dictionary) around during bigm, which basically means caching Var bounds info for the whole transformation --- pyomo/gdp/plugins/bigm.py | 5 +++++ pyomo/gdp/plugins/bigm_mixin.py | 13 ++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index bb731363898..0402a8c69fa 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -20,6 +20,7 @@ from pyomo.contrib.cp.transform.logical_to_disjunctive_program import ( LogicalToDisjunctive, ) +from pyomo.contrib.fbbt.fbbt import _FBBTVisitorLeafToRoot from pyomo.core import ( Block, BooleanVar, @@ -178,6 +179,10 @@ def _apply_to(self, instance, **kwds): def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) + bnds_dict = ComponentMap() + self._fbbt_visitor = _FBBTVisitorLeafToRoot( + bnds_dict, ignore_fixed=not self._config.assume_fixed_vars_permanent) + # filter out inactive targets and handle case where targets aren't # specified. targets = self._filter_targets(instance) diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index ba25dfeffd0..45395e213db 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -11,7 +11,8 @@ from pyomo.gdp import GDP_Error from pyomo.common.collections import ComponentSet -from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +import pyomo.contrib.fbbt.interval as interval +#from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.core import Suffix @@ -210,10 +211,12 @@ def _get_M_from_args(self, constraint, bigMargs, arg_list, lower, upper): return lower, upper def _estimate_M(self, expr, constraint): - expr_lb, expr_ub = compute_bounds_on_expr( - expr, ignore_fixed=not self._config.assume_fixed_vars_permanent - ) - if expr_lb is None or expr_ub is None: + # expr_lb, expr_ub = compute_bounds_on_expr( + # expr, ignore_fixed=not self._config.assume_fixed_vars_permanent + # ) + self._fbbt_visitor.walk_expression(expr) + expr_lb, expr_ub = self._fbbt_visitor.bnds_dict[expr] + if expr_lb == -interval.inf or expr_ub == interval.inf: raise GDP_Error( "Cannot estimate M for unbounded " "expressions.\n\t(found while processing " From 35641b0f3824a768cc1f17336b83314d35c30e0b Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 28 Sep 2023 14:41:22 -0600 Subject: [PATCH 03/36] Passing args to the handlers to avoid the assertions --- pyomo/contrib/fbbt/fbbt.py | 127 ++++++++++++++----------------------- 1 file changed, 49 insertions(+), 78 deletions(-) diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index 62abb83b1ac..011a998d4c4 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -76,7 +76,7 @@ class FBBTException(PyomoException): pass -def _prop_bnds_leaf_to_root_ProductExpression(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_ProductExpression(visitor, node, arg1, arg2): """ Parameters @@ -90,17 +90,16 @@ def _prop_bnds_leaf_to_root_ProductExpression(node, bnds_dict, feasibility_tol): region is removed due to floating point arithmetic and to prevent math domain errors (a larger value is more conservative). """ - assert len(node.args) == 2 - arg1, arg2 = node.args + bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg1] lb2, ub2 = bnds_dict[arg2] if arg1 is arg2: - bnds_dict[node] = interval.power(lb1, ub1, 2, 2, feasibility_tol) + bnds_dict[node] = interval.power(lb1, ub1, 2, 2, visitor.feasibility_tol) else: bnds_dict[node] = interval.mul(lb1, ub1, lb2, ub2) -def _prop_bnds_leaf_to_root_SumExpression(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_SumExpression(visitor, node, *args): """ Parameters @@ -114,13 +113,14 @@ def _prop_bnds_leaf_to_root_SumExpression(node, bnds_dict, feasibility_tol): region is removed due to floating point arithmetic and to prevent math domain errors (a larger value is more conservative). """ + bnds_dict = visitor.bnds_dict bnds = (0, 0) - for arg in node.args: + for arg in args: bnds = interval.add(*bnds, *bnds_dict[arg]) bnds_dict[node] = bnds -def _prop_bnds_leaf_to_root_DivisionExpression(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_DivisionExpression(visitor, node, arg1, arg2): """ Parameters @@ -134,14 +134,14 @@ def _prop_bnds_leaf_to_root_DivisionExpression(node, bnds_dict, feasibility_tol) region is removed due to floating point arithmetic and to prevent math domain errors (a larger value is more conservative). """ - assert len(node.args) == 2 - arg1, arg2 = node.args + bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg1] lb2, ub2 = bnds_dict[arg2] - bnds_dict[node] = interval.div(lb1, ub1, lb2, ub2, feasibility_tol=feasibility_tol) + bnds_dict[node] = interval.div(lb1, ub1, lb2, ub2, + feasibility_tol=visitor.feasibility_tol) -def _prop_bnds_leaf_to_root_PowExpression(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_PowExpression(visitor, node, arg1, arg2): """ Parameters @@ -155,16 +155,15 @@ def _prop_bnds_leaf_to_root_PowExpression(node, bnds_dict, feasibility_tol): region is removed due to floating point arithmetic and to prevent math domain errors (a larger value is more conservative). """ - assert len(node.args) == 2 - arg1, arg2 = node.args + bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg1] lb2, ub2 = bnds_dict[arg2] bnds_dict[node] = interval.power( - lb1, ub1, lb2, ub2, feasibility_tol=feasibility_tol + lb1, ub1, lb2, ub2, feasibility_tol=visitor.feasibility_tol ) -def _prop_bnds_leaf_to_root_NegationExpression(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_NegationExpression(visitor, node, arg): """ Parameters @@ -178,13 +177,12 @@ def _prop_bnds_leaf_to_root_NegationExpression(node, bnds_dict, feasibility_tol) region is removed due to floating point arithmetic and to prevent math domain errors (a larger value is more conservative). """ - assert len(node.args) == 1 - arg = node.args[0] + bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] bnds_dict[node] = interval.sub(0, 0, lb1, ub1) -def _prop_bnds_leaf_to_root_exp(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_exp(visitor, node, arg): """ Parameters @@ -198,13 +196,12 @@ def _prop_bnds_leaf_to_root_exp(node, bnds_dict, feasibility_tol): region is removed due to floating point arithmetic and to prevent math domain errors (a larger value is more conservative). """ - assert len(node.args) == 1 - arg = node.args[0] + bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] bnds_dict[node] = interval.exp(lb1, ub1) -def _prop_bnds_leaf_to_root_log(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_log(visitor, node, arg): """ Parameters @@ -218,13 +215,12 @@ def _prop_bnds_leaf_to_root_log(node, bnds_dict, feasibility_tol): region is removed due to floating point arithmetic and to prevent math domain errors (a larger value is more conservative). """ - assert len(node.args) == 1 - arg = node.args[0] + bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] bnds_dict[node] = interval.log(lb1, ub1) -def _prop_bnds_leaf_to_root_log10(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_log10(visitor, node, arg): """ Parameters @@ -238,13 +234,12 @@ def _prop_bnds_leaf_to_root_log10(node, bnds_dict, feasibility_tol): region is removed due to floating point arithmetic and to prevent math domain errors (a larger value is more conservative). """ - assert len(node.args) == 1 - arg = node.args[0] + bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] bnds_dict[node] = interval.log10(lb1, ub1) -def _prop_bnds_leaf_to_root_sin(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_sin(visitor, node, arg): """ Parameters @@ -258,13 +253,12 @@ def _prop_bnds_leaf_to_root_sin(node, bnds_dict, feasibility_tol): region is removed due to floating point arithmetic and to prevent math domain errors (a larger value is more conservative). """ - assert len(node.args) == 1 - arg = node.args[0] + bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] bnds_dict[node] = interval.sin(lb1, ub1) -def _prop_bnds_leaf_to_root_cos(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_cos(visitor, node, arg): """ Parameters @@ -278,13 +272,12 @@ def _prop_bnds_leaf_to_root_cos(node, bnds_dict, feasibility_tol): region is removed due to floating point arithmetic and to prevent math domain errors (a larger value is more conservative). """ - assert len(node.args) == 1 - arg = node.args[0] + bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] bnds_dict[node] = interval.cos(lb1, ub1) -def _prop_bnds_leaf_to_root_tan(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_tan(visitor, node, arg): """ Parameters @@ -298,13 +291,12 @@ def _prop_bnds_leaf_to_root_tan(node, bnds_dict, feasibility_tol): region is removed due to floating point arithmetic and to prevent math domain errors (a larger value is more conservative). """ - assert len(node.args) == 1 - arg = node.args[0] + bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] bnds_dict[node] = interval.tan(lb1, ub1) -def _prop_bnds_leaf_to_root_asin(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_asin(visitor, node, arg): """ Parameters @@ -318,15 +310,14 @@ def _prop_bnds_leaf_to_root_asin(node, bnds_dict, feasibility_tol): region is removed due to floating point arithmetic and to prevent math domain errors (a larger value is more conservative). """ - assert len(node.args) == 1 - arg = node.args[0] + bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] bnds_dict[node] = interval.asin( - lb1, ub1, -interval.inf, interval.inf, feasibility_tol + lb1, ub1, -interval.inf, interval.inf, visitor.feasibility_tol ) -def _prop_bnds_leaf_to_root_acos(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_acos(visitor, node, arg): """ Parameters @@ -340,15 +331,14 @@ def _prop_bnds_leaf_to_root_acos(node, bnds_dict, feasibility_tol): region is removed due to floating point arithmetic and to prevent math domain errors (a larger value is more conservative). """ - assert len(node.args) == 1 - arg = node.args[0] + bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] bnds_dict[node] = interval.acos( - lb1, ub1, -interval.inf, interval.inf, feasibility_tol + lb1, ub1, -interval.inf, interval.inf, visitor.feasibility_tol ) -def _prop_bnds_leaf_to_root_atan(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_atan(visitor, node, arg): """ Parameters @@ -362,13 +352,12 @@ def _prop_bnds_leaf_to_root_atan(node, bnds_dict, feasibility_tol): region is removed due to floating point arithmetic and to prevent math domain errors (a larger value is more conservative). """ - assert len(node.args) == 1 - arg = node.args[0] + bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] bnds_dict[node] = interval.atan(lb1, ub1, -interval.inf, interval.inf) -def _prop_bnds_leaf_to_root_sqrt(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_sqrt(visitor, node, arg): """ Parameters @@ -382,22 +371,22 @@ def _prop_bnds_leaf_to_root_sqrt(node, bnds_dict, feasibility_tol): region is removed due to floating point arithmetic and to prevent math domain errors (a larger value is more conservative). """ - assert len(node.args) == 1 - arg = node.args[0] + bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] bnds_dict[node] = interval.power( - lb1, ub1, 0.5, 0.5, feasibility_tol=feasibility_tol + lb1, ub1, 0.5, 0.5, feasibility_tol=visitor.feasibility_tol ) -def _prop_bnds_leaf_to_root_abs(node, bnds_dict, feasibility_tol): - assert len(node.args) == 1 - arg = node.args[0] +def _prop_bnds_leaf_to_root_abs(visitor, node, arg): + bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] bnds_dict[node] = interval.interval_abs(lb1, ub1) +def _prop_no_bounds(visitor, node, *args): + visitor.bnds_dict[node] = (-interval.inf, interval.inf) -_unary_leaf_to_root_map = dict() +_unary_leaf_to_root_map = defaultdict(lambda: _prop_no_bounds) _unary_leaf_to_root_map['exp'] = _prop_bnds_leaf_to_root_exp _unary_leaf_to_root_map['log'] = _prop_bnds_leaf_to_root_log _unary_leaf_to_root_map['log10'] = _prop_bnds_leaf_to_root_log10 @@ -411,7 +400,7 @@ def _prop_bnds_leaf_to_root_abs(node, bnds_dict, feasibility_tol): _unary_leaf_to_root_map['abs'] = _prop_bnds_leaf_to_root_abs -def _prop_bnds_leaf_to_root_UnaryFunctionExpression(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_UnaryFunctionExpression(visitor, node, arg): """ Parameters @@ -425,13 +414,10 @@ def _prop_bnds_leaf_to_root_UnaryFunctionExpression(node, bnds_dict, feasibility region is removed due to floating point arithmetic and to prevent math domain errors (a larger value is more conservative). """ - if node.getname() in _unary_leaf_to_root_map: - _unary_leaf_to_root_map[node.getname()](node, bnds_dict, feasibility_tol) - else: - bnds_dict[node] = (-interval.inf, interval.inf) + _unary_leaf_to_root_map[node.getname()](visitor, node, arg) -def _prop_bnds_leaf_to_root_GeneralExpression(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_GeneralExpression(visitor, node, expr): """ Propagate bounds from children to parent @@ -446,16 +432,13 @@ def _prop_bnds_leaf_to_root_GeneralExpression(node, bnds_dict, feasibility_tol): region is removed due to floating point arithmetic and to prevent math domain errors (a larger value is more conservative). """ - (expr,) = node.args + bnds_dict = visitor.bnds_dict if expr.__class__ in native_types: expr_lb = expr_ub = expr else: expr_lb, expr_ub = bnds_dict[expr] bnds_dict[node] = (expr_lb, expr_ub) -def _prop_no_bounds(node, bnds_dict, feasibility_tol): - bnds_dict[node] = (-interval.inf, interval.inf) - _prop_bnds_leaf_to_root_map = defaultdict(lambda: _prop_no_bounds) _prop_bnds_leaf_to_root_map[ numeric_expr.ProductExpression @@ -1176,26 +1159,14 @@ def __init__( def initializeWalker(self, expr): walk, result = self.beforeChild(None, expr, 0) if not walk: - return False, result#self.finalizeResult(result) + return False, result return True, expr def beforeChild(self, node, child, child_idx): return _before_child_handlers[child.__class__](self, child) def exitNode(self, node, data): - _prop_bnds_leaf_to_root_map[node.__class__](node, self.bnds_dict, - self.feasibility_tol) - # if node.__class__ in _prop_bnds_leaf_to_root_map: - # _prop_bnds_leaf_to_root_map[node.__class__]( - # node, self.bnds_dict, self.feasibility_tol - # ) - # else: - # self.bnds_dict[node] = (-interval.inf, interval.inf) - # return None - - # def finalizeResult(self, result): - # return result - + _prop_bnds_leaf_to_root_map[node.__class__](self, node, *node.args) # class _FBBTVisitorLeafToRoot(ExpressionValueVisitor): # """ From 78d3d1c25f45c7d883db372debf8a6d3394df40f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 28 Sep 2023 14:42:13 -0600 Subject: [PATCH 04/36] Running black --- pyomo/contrib/fbbt/fbbt.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index 011a998d4c4..36313597521 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -13,7 +13,9 @@ from pyomo.common.collections import ComponentMap, ComponentSet import pyomo.core.expr.numeric_expr as numeric_expr from pyomo.core.expr.visitor import ( - ExpressionValueVisitor, identify_variables, StreamBasedExpressionVisitor + ExpressionValueVisitor, + identify_variables, + StreamBasedExpressionVisitor, ) from pyomo.core.expr.numvalue import nonpyomo_leaf_types, value from pyomo.core.expr.numvalue import is_fixed @@ -137,8 +139,9 @@ def _prop_bnds_leaf_to_root_DivisionExpression(visitor, node, arg1, arg2): bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg1] lb2, ub2 = bnds_dict[arg2] - bnds_dict[node] = interval.div(lb1, ub1, lb2, ub2, - feasibility_tol=visitor.feasibility_tol) + bnds_dict[node] = interval.div( + lb1, ub1, lb2, ub2, feasibility_tol=visitor.feasibility_tol + ) def _prop_bnds_leaf_to_root_PowExpression(visitor, node, arg1, arg2): @@ -383,9 +386,11 @@ def _prop_bnds_leaf_to_root_abs(visitor, node, arg): lb1, ub1 = bnds_dict[arg] bnds_dict[node] = interval.interval_abs(lb1, ub1) + def _prop_no_bounds(visitor, node, *args): visitor.bnds_dict[node] = (-interval.inf, interval.inf) + _unary_leaf_to_root_map = defaultdict(lambda: _prop_no_bounds) _unary_leaf_to_root_map['exp'] = _prop_bnds_leaf_to_root_exp _unary_leaf_to_root_map['log'] = _prop_bnds_leaf_to_root_log @@ -439,6 +444,7 @@ def _prop_bnds_leaf_to_root_GeneralExpression(visitor, node, expr): expr_lb, expr_ub = bnds_dict[expr] bnds_dict[node] = (expr_lb, expr_ub) + _prop_bnds_leaf_to_root_map = defaultdict(lambda: _prop_no_bounds) _prop_bnds_leaf_to_root_map[ numeric_expr.ProductExpression @@ -1019,6 +1025,7 @@ def _prop_bnds_root_to_leaf_GeneralExpression(node, bnds_dict, feasibility_tol): expr_lb, expr_ub = bnds_dict[node] bnds_dict[node.expr] = (expr_lb, expr_ub) + _prop_bnds_root_to_leaf_map = dict() _prop_bnds_root_to_leaf_map[ numeric_expr.ProductExpression @@ -1070,10 +1077,12 @@ def _check_and_reset_bounds(var, lb, ub): ub = orig_ub return lb, ub + def _before_constant(visitor, child): visitor.bnds_dict[child] = (child, child) return False, None + def _before_var(visitor, child): if child in visitor.bnds_dict: return False, None @@ -1090,21 +1099,22 @@ def _before_var(visitor, child): if lb - visitor.feasibility_tol > ub: raise InfeasibleConstraintException( 'Variable has a lower bound that is larger than its ' - 'upper bound: {0}'.format( - str(child) - ) + 'upper bound: {0}'.format(str(child)) ) visitor.bnds_dict[child] = (lb, ub) return False, None + def _before_NPV(visitor, child): val = value(child) visitor.bnds_dict[child] = (val, val) return False, None + def _before_other(visitor, child): return True, None + def _before_external_function(visitor, child): # TODO: provide some mechanism for users to provide interval # arithmetic callback functions for general external @@ -1112,6 +1122,7 @@ def _before_external_function(visitor, child): visitor.bnds_dict[child] = (-interval.inf, interval.inf) return False, None + def _register_new_before_child_handler(visitor, child): handlers = _before_child_handlers child_type = child.__class__ @@ -1123,17 +1134,21 @@ def _register_new_before_child_handler(visitor, child): handlers[child_type] = _before_other return handlers[child_type](visitor, child) + _before_child_handlers = defaultdict(lambda: _register_new_before_child_handler) _before_child_handlers[ - numeric_expr.ExternalFunctionExpression] = _before_external_function + numeric_expr.ExternalFunctionExpression +] = _before_external_function for _type in nonpyomo_leaf_types: _before_child_handlers[_type] = _before_constant + class _FBBTVisitorLeafToRoot(StreamBasedExpressionVisitor): """ This walker propagates bounds from the variables to each node in the expression tree (all the way to the root node). """ + def __init__( self, bnds_dict, integer_tol=1e-4, feasibility_tol=1e-8, ignore_fixed=False ): @@ -1168,6 +1183,7 @@ def beforeChild(self, node, child, child_idx): def exitNode(self, node, data): _prop_bnds_leaf_to_root_map[node.__class__](self, node, *node.args) + # class _FBBTVisitorLeafToRoot(ExpressionValueVisitor): # """ # This walker propagates bounds from the variables to each node in From afd0b1bd055654b1601d5a2cd7a264ef1d11b994 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 28 Sep 2023 14:43:16 -0600 Subject: [PATCH 05/36] More black --- pyomo/gdp/plugins/bigm.py | 3 ++- pyomo/gdp/plugins/bigm_mixin.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index 0402a8c69fa..854faa68887 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -181,7 +181,8 @@ def _apply_to_impl(self, instance, **kwds): bnds_dict = ComponentMap() self._fbbt_visitor = _FBBTVisitorLeafToRoot( - bnds_dict, ignore_fixed=not self._config.assume_fixed_vars_permanent) + bnds_dict, ignore_fixed=not self._config.assume_fixed_vars_permanent + ) # filter out inactive targets and handle case where targets aren't # specified. diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index 45395e213db..3a74504abc7 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -12,7 +12,6 @@ from pyomo.gdp import GDP_Error from pyomo.common.collections import ComponentSet import pyomo.contrib.fbbt.interval as interval -#from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.core import Suffix From 1550f2e5d16e5f19c29d9469341e8753bfb47c4d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 28 Sep 2023 15:09:00 -0600 Subject: [PATCH 06/36] NFC: cleaning up a lot of docstrings and comments --- pyomo/contrib/fbbt/fbbt.py | 237 ++++++------------------------------- 1 file changed, 38 insertions(+), 199 deletions(-) diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index 36313597521..5ec8890ca6f 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -83,14 +83,10 @@ def _prop_bnds_leaf_to_root_ProductExpression(visitor, node, arg1, arg2): Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.ProductExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg1: First arg in product expression + arg2: Second arg in product expression """ bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg1] @@ -106,14 +102,9 @@ def _prop_bnds_leaf_to_root_SumExpression(visitor, node, *args): Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.SumExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + args: summands in SumExpression """ bnds_dict = visitor.bnds_dict bnds = (0, 0) @@ -127,14 +118,10 @@ def _prop_bnds_leaf_to_root_DivisionExpression(visitor, node, arg1, arg2): Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.DivisionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg1: dividend + arg2: divisor """ bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg1] @@ -149,14 +136,10 @@ def _prop_bnds_leaf_to_root_PowExpression(visitor, node, arg1, arg2): Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.PowExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg1: base + arg2: exponent """ bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg1] @@ -171,14 +154,9 @@ def _prop_bnds_leaf_to_root_NegationExpression(visitor, node, arg): Parameters ---------- - node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + visitor: _FBBTVisitorLeafToRoot + node: pyomo.core.expr.numeric_expr.NegationExpression + arg: NegationExpression arg """ bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] @@ -190,14 +168,9 @@ def _prop_bnds_leaf_to_root_exp(visitor, node, arg): Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] @@ -209,14 +182,9 @@ def _prop_bnds_leaf_to_root_log(visitor, node, arg): Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] @@ -228,14 +196,9 @@ def _prop_bnds_leaf_to_root_log10(visitor, node, arg): Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] @@ -247,14 +210,9 @@ def _prop_bnds_leaf_to_root_sin(visitor, node, arg): Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] @@ -266,14 +224,9 @@ def _prop_bnds_leaf_to_root_cos(visitor, node, arg): Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] @@ -285,14 +238,9 @@ def _prop_bnds_leaf_to_root_tan(visitor, node, arg): Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] @@ -304,14 +252,9 @@ def _prop_bnds_leaf_to_root_asin(visitor, node, arg): Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] @@ -325,14 +268,9 @@ def _prop_bnds_leaf_to_root_acos(visitor, node, arg): Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] @@ -346,14 +284,9 @@ def _prop_bnds_leaf_to_root_atan(visitor, node, arg): Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + """ bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] @@ -365,14 +298,9 @@ def _prop_bnds_leaf_to_root_sqrt(visitor, node, arg): Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ bnds_dict = visitor.bnds_dict lb1, ub1 = bnds_dict[arg] @@ -410,14 +338,9 @@ def _prop_bnds_leaf_to_root_UnaryFunctionExpression(visitor, node, arg): Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ _unary_leaf_to_root_map[node.getname()](visitor, node, arg) @@ -428,14 +351,9 @@ def _prop_bnds_leaf_to_root_GeneralExpression(visitor, node, expr): Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.base.expression._GeneralExpressionData - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + expr: GeneralExpression arg """ bnds_dict = visitor.bnds_dict if expr.__class__ in native_types: @@ -1184,85 +1102,6 @@ def exitNode(self, node, data): _prop_bnds_leaf_to_root_map[node.__class__](self, node, *node.args) -# class _FBBTVisitorLeafToRoot(ExpressionValueVisitor): -# """ -# This walker propagates bounds from the variables to each node in -# the expression tree (all the way to the root node). -# """ - -# def __init__( -# self, bnds_dict, integer_tol=1e-4, feasibility_tol=1e-8, ignore_fixed=False -# ): -# """ -# Parameters -# ---------- -# bnds_dict: ComponentMap -# integer_tol: float -# feasibility_tol: float -# If the bounds computed on the body of a constraint violate the bounds of the constraint by more than -# feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance -# is also used when performing certain interval arithmetic operations to ensure that none of the feasible -# region is removed due to floating point arithmetic and to prevent math domain errors (a larger value -# is more conservative). -# """ -# self.bnds_dict = bnds_dict -# self.integer_tol = integer_tol -# self.feasibility_tol = feasibility_tol -# self.ignore_fixed = ignore_fixed - -# def visit(self, node, values): -# if node.__class__ in _prop_bnds_leaf_to_root_map: -# _prop_bnds_leaf_to_root_map[node.__class__]( -# node, self.bnds_dict, self.feasibility_tol -# ) -# else: -# self.bnds_dict[node] = (-interval.inf, interval.inf) -# return None - -# def visiting_potential_leaf(self, node): -# if node.__class__ in nonpyomo_leaf_types: -# self.bnds_dict[node] = (node, node) -# return True, None - -# if node.is_variable_type(): -# if node in self.bnds_dict: -# return True, None -# if node.is_fixed() and not self.ignore_fixed: -# lb = value(node.value) -# ub = lb -# else: -# lb = value(node.lb) -# ub = value(node.ub) -# if lb is None: -# lb = -interval.inf -# if ub is None: -# ub = interval.inf -# if lb - self.feasibility_tol > ub: -# raise InfeasibleConstraintException( -# 'Variable has a lower bound that is larger than its upper bound: {0}'.format( -# str(node) -# ) -# ) -# self.bnds_dict[node] = (lb, ub) -# return True, None - -# if not node.is_potentially_variable(): -# # NPV nodes are effectively constant leaves. Evaluate it -# # and return the value. -# val = value(node) -# self.bnds_dict[node] = (val, val) -# return True, None - -# if node.__class__ is numeric_expr.ExternalFunctionExpression: -# # TODO: provide some mechanism for users to provide interval -# # arithmetic callback functions for general external -# # functions -# self.bnds_dict[node] = (-interval.inf, interval.inf) -# return True, None - -# return False, None - - class _FBBTVisitorRootToLeaf(ExpressionValueVisitor): """ This walker propagates bounds from the constraint back to the From cbedd1f1a306a075bc70945b24ef88cdcae04979 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 28 Sep 2023 15:16:14 -0600 Subject: [PATCH 07/36] Putting fbbt visitor in bigm mixin so that mbigm can use it too --- pyomo/gdp/plugins/bigm.py | 6 +----- pyomo/gdp/plugins/bigm_mixin.py | 9 ++++++++- pyomo/gdp/plugins/multiple_bigm.py | 1 + 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index 854faa68887..fffabf652e5 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -178,11 +178,7 @@ def _apply_to(self, instance, **kwds): def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) - - bnds_dict = ComponentMap() - self._fbbt_visitor = _FBBTVisitorLeafToRoot( - bnds_dict, ignore_fixed=not self._config.assume_fixed_vars_permanent - ) + self._set_up_fbbt_visitor() # filter out inactive targets and handle case where targets aren't # specified. diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index 3a74504abc7..fc64c5bd9db 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -10,7 +10,8 @@ # ___________________________________________________________________________ from pyomo.gdp import GDP_Error -from pyomo.common.collections import ComponentSet +from pyomo.common.collections import ComponentMap, ComponentSet +from pyomo.contrib.fbbt.fbbt import _FBBTVisitorLeafToRoot import pyomo.contrib.fbbt.interval as interval from pyomo.core import Suffix @@ -103,6 +104,12 @@ def _get_bigM_arg_list(self, bigm_args, block): block = block.parent_block() return arg_list + def _set_up_fbbt_visitor(self): + bnds_dict = ComponentMap() + self._fbbt_visitor = _FBBTVisitorLeafToRoot( + bnds_dict, ignore_fixed=not self._config.assume_fixed_vars_permanent + ) + def _process_M_value( self, m, diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 8f0592f204d..929f6072863 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -214,6 +214,7 @@ def _apply_to(self, instance, **kwds): def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) + self._set_up_fbbt_visitor() if ( self._config.only_mbigm_bound_constraints From 1e9723f55f084c711d318d8e3c6bc8057fbff1ff Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 28 Sep 2023 15:50:57 -0600 Subject: [PATCH 08/36] Removing unused import --- pyomo/gdp/plugins/bigm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index fffabf652e5..82ba55adfa0 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -20,7 +20,6 @@ from pyomo.contrib.cp.transform.logical_to_disjunctive_program import ( LogicalToDisjunctive, ) -from pyomo.contrib.fbbt.fbbt import _FBBTVisitorLeafToRoot from pyomo.core import ( Block, BooleanVar, From 86ee9507e64bba2a4d08f0dea0f3c312a396c622 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 28 Sep 2023 16:27:36 -0600 Subject: [PATCH 09/36] Moving the setup of fbbt visitor to init because gdpopt caught me assuming it was there from before config... --- pyomo/gdp/plugins/bigm.py | 5 ++++- pyomo/gdp/plugins/bigm_mixin.py | 9 +++------ pyomo/gdp/plugins/multiple_bigm.py | 4 +++- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index 82ba55adfa0..6502a5eab0e 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -161,6 +161,7 @@ class BigM_Transformation(GDP_to_MIP_Transformation, _BigM_MixIn): def __init__(self): super().__init__(logger) + self._set_up_fbbt_visitor() def _apply_to(self, instance, **kwds): self.used_args = ComponentMap() # If everything was sure to go well, @@ -174,10 +175,12 @@ def _apply_to(self, instance, **kwds): finally: self._restore_state() self.used_args.clear() + self._fbbt_visitor.ignore_fixed = True def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) - self._set_up_fbbt_visitor() + if self._config.assume_fixed_vars_permanent: + self._fbbt_visitor.ignore_fixed = False # filter out inactive targets and handle case where targets aren't # specified. diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index fc64c5bd9db..39242db248b 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -106,9 +106,9 @@ def _get_bigM_arg_list(self, bigm_args, block): def _set_up_fbbt_visitor(self): bnds_dict = ComponentMap() - self._fbbt_visitor = _FBBTVisitorLeafToRoot( - bnds_dict, ignore_fixed=not self._config.assume_fixed_vars_permanent - ) + # we assume the default config arg for 'assume_fixed_vars_permanent,` + # and we will change it during apply_to if we need to + self._fbbt_visitor = _FBBTVisitorLeafToRoot(bnds_dict, ignore_fixed=True) def _process_M_value( self, @@ -217,9 +217,6 @@ def _get_M_from_args(self, constraint, bigMargs, arg_list, lower, upper): return lower, upper def _estimate_M(self, expr, constraint): - # expr_lb, expr_ub = compute_bounds_on_expr( - # expr, ignore_fixed=not self._config.assume_fixed_vars_permanent - # ) self._fbbt_visitor.walk_expression(expr) expr_lb, expr_ub = self._fbbt_visitor.bnds_dict[expr] if expr_lb == -interval.inf or expr_ub == interval.inf: diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 929f6072863..edc511d089d 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -202,6 +202,7 @@ def __init__(self): super().__init__(logger) self.handlers[Suffix] = self._warn_for_active_suffix self._arg_list = {} + self._set_up_fbbt_visitor() def _apply_to(self, instance, **kwds): self.used_args = ComponentMap() @@ -214,7 +215,8 @@ def _apply_to(self, instance, **kwds): def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) - self._set_up_fbbt_visitor() + if self._config.assume_fixed_vars_permanent: + self._fbbt_visitor.ignore_fixed = False if ( self._config.only_mbigm_bound_constraints From c6cfd1c94d158e0ba69e1df0187fd0bd8bdc7ec1 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 4 Oct 2023 13:11:28 -0600 Subject: [PATCH 10/36] Rewriting mul for performance --- pyomo/contrib/fbbt/interval.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/fbbt/interval.py b/pyomo/contrib/fbbt/interval.py index aca6531c8df..9f784922d19 100644 --- a/pyomo/contrib/fbbt/interval.py +++ b/pyomo/contrib/fbbt/interval.py @@ -26,14 +26,24 @@ def sub(xl, xu, yl, yu): def mul(xl, xu, yl, yu): - options = [xl * yl, xl * yu, xu * yl, xu * yu] - if any(math.isnan(i) for i in options): - lb = -inf - ub = inf - else: - lb = min(options) - ub = max(options) + lb = inf + ub = -inf + for i in (xl * yl, xu * yu, xu * yl, xl * yu): + if i < lb: + lb = i + if i > ub: + ub = i + if i != i: # math.isnan(i) + return (-inf, inf) return lb, ub + # options = [xl * yl, xl * yu, xu * yl, xu * yu] + # if any(math.isnan(i) for i in options): + # lb = -inf + # ub = inf + # else: + # lb = min(options) + # ub = max(options) + # return lb, ub def inv(xl, xu, feasibility_tol): From b2520e2901fd7b9ea78db5a547f31e17c07ba247 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 4 Oct 2023 15:28:21 -0600 Subject: [PATCH 11/36] Wrapping the visitor and turning off the garbage collector --- pyomo/contrib/fbbt/fbbt.py | 73 ++++++++++++++++++++++++++---- pyomo/contrib/fbbt/interval.py | 11 +---- pyomo/gdp/plugins/bigm.py | 4 +- pyomo/gdp/plugins/bigm_mixin.py | 4 +- pyomo/gdp/plugins/multiple_bigm.py | 3 +- 5 files changed, 72 insertions(+), 23 deletions(-) diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index 5ec8890ca6f..9ac49b50ba4 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -29,12 +29,14 @@ import logging from pyomo.common.errors import InfeasibleConstraintException, PyomoException from pyomo.common.config import ( - ConfigBlock, + ConfigDict, ConfigValue, + document_kwargs_from_configdict, In, NonNegativeFloat, NonNegativeInt, ) +from pyomo.common.gc_manager import PauseGC from pyomo.common.numeric_types import native_types logger = logging.getLogger(__name__) @@ -89,12 +91,11 @@ def _prop_bnds_leaf_to_root_ProductExpression(visitor, node, arg1, arg2): arg2: Second arg in product expression """ bnds_dict = visitor.bnds_dict - lb1, ub1 = bnds_dict[arg1] - lb2, ub2 = bnds_dict[arg2] if arg1 is arg2: - bnds_dict[node] = interval.power(lb1, ub1, 2, 2, visitor.feasibility_tol) + bnds_dict[node] = interval.power(*bnds_dict[arg1], 2, 2, + visitor.feasibility_tol) else: - bnds_dict[node] = interval.mul(lb1, ub1, lb2, ub2) + bnds_dict[node] = interval.mul(*bnds_dict[arg1], *bnds_dict[arg2]) def _prop_bnds_leaf_to_root_SumExpression(visitor, node, *args): @@ -1061,6 +1062,62 @@ def _register_new_before_child_handler(visitor, child): _before_child_handlers[_type] = _before_constant +class FBBTVisitorLeafToRoot(object): + CONFIG = ConfigDict('fbbt_leaf_to_root') + # CONFIG.declare( + # 'bnds_dict', + # ConfigValue( + # domain=dict + # ) + # ) + CONFIG.declare( + 'integer_tol', + ConfigValue( + default=1e-4, + domain=float, + description="Integer tolerance" + ) + ) + CONFIG.declare( + 'feasibility_tol', + ConfigValue( + default=1e-8, + domain=float, + description="Constraint feasibility tolerance", + doc=""" + If the bounds computed on the body of a constraint violate the bounds of + the constraint by more than feasibility_tol, then the constraint is + considered infeasible and an exception is raised. This tolerance is also + used when performing certain interval arithmetic operations to ensure that + none of the feasible region is removed due to floating point arithmetic and + to prevent math domain errors (a larger value is more conservative). + """ + ) + ) + CONFIG.declare( + 'ignore_fixed', + ConfigValue( + default=False, + domain=bool, + description="Whether or not to treat fixed Vars as constants" + ) + ) + + @document_kwargs_from_configdict(CONFIG) + def __init__(self, bnds_dict, **kwds): + self.bnds_dict = bnds_dict + self.config = self.CONFIG(kwds) + print(kwds) + print(self.config.ignore_fixed) + + def walk_expression(self, expr): + with PauseGC(): + _FBBTVisitorLeafToRoot( + self.bnds_dict, integer_tol=self.config.integer_tol, + feasibility_tol=self.config.feasibility_tol, + ignore_fixed=self.config.ignore_fixed).walk_expression(expr) + + class _FBBTVisitorLeafToRoot(StreamBasedExpressionVisitor): """ This walker propagates bounds from the variables to each node in @@ -1252,7 +1309,7 @@ def _fbbt_con(con, config): ---------- con: pyomo.core.base.constraint.Constraint constraint on which to perform fbbt - config: ConfigBlock + config: ConfigDict see documentation for fbbt Returns @@ -1337,7 +1394,7 @@ def _fbbt_block(m, config): Parameters ---------- m: pyomo.core.base.block.Block or pyomo.core.base.PyomoModel.ConcreteModel - config: ConfigBlock + config: ConfigDict See the docs for fbbt Returns @@ -1468,7 +1525,7 @@ def fbbt( A ComponentMap mapping from variables a tuple containing the lower and upper bounds, respectively, computed from FBBT. """ - config = ConfigBlock() + config = ConfigDict() dsc_config = ConfigValue( default=deactivate_satisfied_constraints, domain=In({True, False}) ) diff --git a/pyomo/contrib/fbbt/interval.py b/pyomo/contrib/fbbt/interval.py index 9f784922d19..db036f50f01 100644 --- a/pyomo/contrib/fbbt/interval.py +++ b/pyomo/contrib/fbbt/interval.py @@ -36,14 +36,6 @@ def mul(xl, xu, yl, yu): if i != i: # math.isnan(i) return (-inf, inf) return lb, ub - # options = [xl * yl, xl * yu, xu * yl, xu * yu] - # if any(math.isnan(i) for i in options): - # lb = -inf - # ub = inf - # else: - # lb = min(options) - # ub = max(options) - # return lb, ub def inv(xl, xu, feasibility_tol): @@ -89,8 +81,7 @@ def inv(xl, xu, feasibility_tol): def div(xl, xu, yl, yu, feasibility_tol): - lb, ub = mul(xl, xu, *inv(yl, yu, feasibility_tol)) - return lb, ub + return mul(xl, xu, *inv(yl, yu, feasibility_tol)) def power(xl, xu, yl, yu, feasibility_tol): diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index 6502a5eab0e..be511ef04f3 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -175,12 +175,12 @@ def _apply_to(self, instance, **kwds): finally: self._restore_state() self.used_args.clear() - self._fbbt_visitor.ignore_fixed = True + self._fbbt_visitor.config.ignore_fixed = True def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) if self._config.assume_fixed_vars_permanent: - self._fbbt_visitor.ignore_fixed = False + self._fbbt_visitor.config.ignore_fixed = False # filter out inactive targets and handle case where targets aren't # specified. diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index 39242db248b..22bd109ef44 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -11,7 +11,7 @@ from pyomo.gdp import GDP_Error from pyomo.common.collections import ComponentMap, ComponentSet -from pyomo.contrib.fbbt.fbbt import _FBBTVisitorLeafToRoot +from pyomo.contrib.fbbt.fbbt import FBBTVisitorLeafToRoot import pyomo.contrib.fbbt.interval as interval from pyomo.core import Suffix @@ -108,7 +108,7 @@ def _set_up_fbbt_visitor(self): bnds_dict = ComponentMap() # we assume the default config arg for 'assume_fixed_vars_permanent,` # and we will change it during apply_to if we need to - self._fbbt_visitor = _FBBTVisitorLeafToRoot(bnds_dict, ignore_fixed=True) + self._fbbt_visitor = FBBTVisitorLeafToRoot(bnds_dict, ignore_fixed=True) def _process_M_value( self, diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index edc511d089d..42f4c2a6cd7 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -212,11 +212,12 @@ def _apply_to(self, instance, **kwds): self._restore_state() self.used_args.clear() self._arg_list.clear() + self._fbbt_visitor.config.ignore_fixed = True def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) if self._config.assume_fixed_vars_permanent: - self._fbbt_visitor.ignore_fixed = False + self._fbbt_visitor.config.ignore_fixed = False if ( self._config.only_mbigm_bound_constraints From c5b9c139ba942b07db4e37c4cb644fe94c9f3fe9 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 4 Oct 2023 16:03:50 -0600 Subject: [PATCH 12/36] Pausing GC for all of bigm transformations --- pyomo/contrib/fbbt/fbbt.py | 58 ------------------------------ pyomo/gdp/plugins/bigm.py | 16 +++++---- pyomo/gdp/plugins/bigm_mixin.py | 4 +-- pyomo/gdp/plugins/multiple_bigm.py | 18 +++++----- 4 files changed, 21 insertions(+), 75 deletions(-) diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index 9ac49b50ba4..a5656e825db 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -31,12 +31,10 @@ from pyomo.common.config import ( ConfigDict, ConfigValue, - document_kwargs_from_configdict, In, NonNegativeFloat, NonNegativeInt, ) -from pyomo.common.gc_manager import PauseGC from pyomo.common.numeric_types import native_types logger = logging.getLogger(__name__) @@ -1062,62 +1060,6 @@ def _register_new_before_child_handler(visitor, child): _before_child_handlers[_type] = _before_constant -class FBBTVisitorLeafToRoot(object): - CONFIG = ConfigDict('fbbt_leaf_to_root') - # CONFIG.declare( - # 'bnds_dict', - # ConfigValue( - # domain=dict - # ) - # ) - CONFIG.declare( - 'integer_tol', - ConfigValue( - default=1e-4, - domain=float, - description="Integer tolerance" - ) - ) - CONFIG.declare( - 'feasibility_tol', - ConfigValue( - default=1e-8, - domain=float, - description="Constraint feasibility tolerance", - doc=""" - If the bounds computed on the body of a constraint violate the bounds of - the constraint by more than feasibility_tol, then the constraint is - considered infeasible and an exception is raised. This tolerance is also - used when performing certain interval arithmetic operations to ensure that - none of the feasible region is removed due to floating point arithmetic and - to prevent math domain errors (a larger value is more conservative). - """ - ) - ) - CONFIG.declare( - 'ignore_fixed', - ConfigValue( - default=False, - domain=bool, - description="Whether or not to treat fixed Vars as constants" - ) - ) - - @document_kwargs_from_configdict(CONFIG) - def __init__(self, bnds_dict, **kwds): - self.bnds_dict = bnds_dict - self.config = self.CONFIG(kwds) - print(kwds) - print(self.config.ignore_fixed) - - def walk_expression(self, expr): - with PauseGC(): - _FBBTVisitorLeafToRoot( - self.bnds_dict, integer_tol=self.config.integer_tol, - feasibility_tol=self.config.feasibility_tol, - ignore_fixed=self.config.ignore_fixed).walk_expression(expr) - - class _FBBTVisitorLeafToRoot(StreamBasedExpressionVisitor): """ This walker propagates bounds from the variables to each node in diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index be511ef04f3..a73846c8aa5 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -15,6 +15,7 @@ from pyomo.common.collections import ComponentMap from pyomo.common.config import ConfigDict, ConfigValue +from pyomo.common.gc_manager import PauseGC from pyomo.common.modeling import unique_component_name from pyomo.common.deprecation import deprecated, deprecation_warning from pyomo.contrib.cp.transform.logical_to_disjunctive_program import ( @@ -170,17 +171,18 @@ def _apply_to(self, instance, **kwds): # as a key in bigMargs, I need the error # not to be when I try to put it into # this map! - try: - self._apply_to_impl(instance, **kwds) - finally: - self._restore_state() - self.used_args.clear() - self._fbbt_visitor.config.ignore_fixed = True + with PauseGC(): + try: + self._apply_to_impl(instance, **kwds) + finally: + self._restore_state() + self.used_args.clear() + self._fbbt_visitor.ignore_fixed = True def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) if self._config.assume_fixed_vars_permanent: - self._fbbt_visitor.config.ignore_fixed = False + self._fbbt_visitor.ignore_fixed = False # filter out inactive targets and handle case where targets aren't # specified. diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index 22bd109ef44..39242db248b 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -11,7 +11,7 @@ from pyomo.gdp import GDP_Error from pyomo.common.collections import ComponentMap, ComponentSet -from pyomo.contrib.fbbt.fbbt import FBBTVisitorLeafToRoot +from pyomo.contrib.fbbt.fbbt import _FBBTVisitorLeafToRoot import pyomo.contrib.fbbt.interval as interval from pyomo.core import Suffix @@ -108,7 +108,7 @@ def _set_up_fbbt_visitor(self): bnds_dict = ComponentMap() # we assume the default config arg for 'assume_fixed_vars_permanent,` # and we will change it during apply_to if we need to - self._fbbt_visitor = FBBTVisitorLeafToRoot(bnds_dict, ignore_fixed=True) + self._fbbt_visitor = _FBBTVisitorLeafToRoot(bnds_dict, ignore_fixed=True) def _process_M_value( self, diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 42f4c2a6cd7..5d76575d514 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -14,6 +14,7 @@ from pyomo.common.collections import ComponentMap from pyomo.common.config import ConfigDict, ConfigValue +from pyomo.common.gc_manager import PauseGC from pyomo.common.modeling import unique_component_name from pyomo.core import ( @@ -206,18 +207,19 @@ def __init__(self): def _apply_to(self, instance, **kwds): self.used_args = ComponentMap() - try: - self._apply_to_impl(instance, **kwds) - finally: - self._restore_state() - self.used_args.clear() - self._arg_list.clear() - self._fbbt_visitor.config.ignore_fixed = True + with PauseGC(): + try: + self._apply_to_impl(instance, **kwds) + finally: + self._restore_state() + self.used_args.clear() + self._arg_list.clear() + self._fbbt_visitor.ignore_fixed = True def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) if self._config.assume_fixed_vars_permanent: - self._fbbt_visitor.config.ignore_fixed = False + self._fbbt_visitor.ignore_fixed = False if ( self._config.only_mbigm_bound_constraints From 71b19c66b7584a4760721c095f7eef083169e51e Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 5 Oct 2023 09:56:25 -0600 Subject: [PATCH 13/36] Purging leaf to root handlers of unncessary local vars --- pyomo/contrib/fbbt/fbbt.py | 44 ++++++++++++-------------------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index a5656e825db..30879aea9f4 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -123,10 +123,8 @@ def _prop_bnds_leaf_to_root_DivisionExpression(visitor, node, arg1, arg2): arg2: divisor """ bnds_dict = visitor.bnds_dict - lb1, ub1 = bnds_dict[arg1] - lb2, ub2 = bnds_dict[arg2] bnds_dict[node] = interval.div( - lb1, ub1, lb2, ub2, feasibility_tol=visitor.feasibility_tol + *bnds_dict[arg1], *bnds_dict[arg2], feasibility_tol=visitor.feasibility_tol ) @@ -141,10 +139,8 @@ def _prop_bnds_leaf_to_root_PowExpression(visitor, node, arg1, arg2): arg2: exponent """ bnds_dict = visitor.bnds_dict - lb1, ub1 = bnds_dict[arg1] - lb2, ub2 = bnds_dict[arg2] bnds_dict[node] = interval.power( - lb1, ub1, lb2, ub2, feasibility_tol=visitor.feasibility_tol + *bnds_dict[arg1], *bnds_dict[arg2], feasibility_tol=visitor.feasibility_tol ) @@ -158,8 +154,7 @@ def _prop_bnds_leaf_to_root_NegationExpression(visitor, node, arg): arg: NegationExpression arg """ bnds_dict = visitor.bnds_dict - lb1, ub1 = bnds_dict[arg] - bnds_dict[node] = interval.sub(0, 0, lb1, ub1) + bnds_dict[node] = interval.sub(0, 0, *bnds_dict[arg]) def _prop_bnds_leaf_to_root_exp(visitor, node, arg): @@ -172,8 +167,7 @@ def _prop_bnds_leaf_to_root_exp(visitor, node, arg): arg: UnaryFunctionExpression arg """ bnds_dict = visitor.bnds_dict - lb1, ub1 = bnds_dict[arg] - bnds_dict[node] = interval.exp(lb1, ub1) + bnds_dict[node] = interval.exp(*bnds_dict[arg]) def _prop_bnds_leaf_to_root_log(visitor, node, arg): @@ -186,8 +180,7 @@ def _prop_bnds_leaf_to_root_log(visitor, node, arg): arg: UnaryFunctionExpression arg """ bnds_dict = visitor.bnds_dict - lb1, ub1 = bnds_dict[arg] - bnds_dict[node] = interval.log(lb1, ub1) + bnds_dict[node] = interval.log(*bnds_dict[arg]) def _prop_bnds_leaf_to_root_log10(visitor, node, arg): @@ -200,8 +193,7 @@ def _prop_bnds_leaf_to_root_log10(visitor, node, arg): arg: UnaryFunctionExpression arg """ bnds_dict = visitor.bnds_dict - lb1, ub1 = bnds_dict[arg] - bnds_dict[node] = interval.log10(lb1, ub1) + bnds_dict[node] = interval.log10(*bnds_dict[arg]) def _prop_bnds_leaf_to_root_sin(visitor, node, arg): @@ -214,8 +206,7 @@ def _prop_bnds_leaf_to_root_sin(visitor, node, arg): arg: UnaryFunctionExpression arg """ bnds_dict = visitor.bnds_dict - lb1, ub1 = bnds_dict[arg] - bnds_dict[node] = interval.sin(lb1, ub1) + bnds_dict[node] = interval.sin(*bnds_dict[arg]) def _prop_bnds_leaf_to_root_cos(visitor, node, arg): @@ -228,8 +219,7 @@ def _prop_bnds_leaf_to_root_cos(visitor, node, arg): arg: UnaryFunctionExpression arg """ bnds_dict = visitor.bnds_dict - lb1, ub1 = bnds_dict[arg] - bnds_dict[node] = interval.cos(lb1, ub1) + bnds_dict[node] = interval.cos(*bnds_dict[arg]) def _prop_bnds_leaf_to_root_tan(visitor, node, arg): @@ -242,8 +232,7 @@ def _prop_bnds_leaf_to_root_tan(visitor, node, arg): arg: UnaryFunctionExpression arg """ bnds_dict = visitor.bnds_dict - lb1, ub1 = bnds_dict[arg] - bnds_dict[node] = interval.tan(lb1, ub1) + bnds_dict[node] = interval.tan(*bnds_dict[arg]) def _prop_bnds_leaf_to_root_asin(visitor, node, arg): @@ -256,9 +245,8 @@ def _prop_bnds_leaf_to_root_asin(visitor, node, arg): arg: UnaryFunctionExpression arg """ bnds_dict = visitor.bnds_dict - lb1, ub1 = bnds_dict[arg] bnds_dict[node] = interval.asin( - lb1, ub1, -interval.inf, interval.inf, visitor.feasibility_tol + *bnds_dict[arg], -interval.inf, interval.inf, visitor.feasibility_tol ) @@ -272,9 +260,8 @@ def _prop_bnds_leaf_to_root_acos(visitor, node, arg): arg: UnaryFunctionExpression arg """ bnds_dict = visitor.bnds_dict - lb1, ub1 = bnds_dict[arg] bnds_dict[node] = interval.acos( - lb1, ub1, -interval.inf, interval.inf, visitor.feasibility_tol + *bnds_dict[arg], -interval.inf, interval.inf, visitor.feasibility_tol ) @@ -288,8 +275,7 @@ def _prop_bnds_leaf_to_root_atan(visitor, node, arg): """ bnds_dict = visitor.bnds_dict - lb1, ub1 = bnds_dict[arg] - bnds_dict[node] = interval.atan(lb1, ub1, -interval.inf, interval.inf) + bnds_dict[node] = interval.atan(*bnds_dict[arg], -interval.inf, interval.inf) def _prop_bnds_leaf_to_root_sqrt(visitor, node, arg): @@ -302,16 +288,14 @@ def _prop_bnds_leaf_to_root_sqrt(visitor, node, arg): arg: UnaryFunctionExpression arg """ bnds_dict = visitor.bnds_dict - lb1, ub1 = bnds_dict[arg] bnds_dict[node] = interval.power( - lb1, ub1, 0.5, 0.5, feasibility_tol=visitor.feasibility_tol + *bnds_dict[arg], 0.5, 0.5, feasibility_tol=visitor.feasibility_tol ) def _prop_bnds_leaf_to_root_abs(visitor, node, arg): bnds_dict = visitor.bnds_dict - lb1, ub1 = bnds_dict[arg] - bnds_dict[node] = interval.interval_abs(lb1, ub1) + bnds_dict[node] = interval.interval_abs(*bnds_dict[arg]) def _prop_no_bounds(visitor, node, *args): From 6a83a24f48725029acfaf8483bd1d09a5c0c9e5c Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 5 Oct 2023 13:18:53 -0600 Subject: [PATCH 14/36] Caching leaf bounds and other bounds separately --- pyomo/contrib/fbbt/fbbt.py | 62 ++++++++++++++++++++++++------ pyomo/gdp/plugins/bigm.py | 10 +++-- pyomo/gdp/plugins/bigm_mixin.py | 25 +++++++----- pyomo/gdp/plugins/multiple_bigm.py | 10 +++-- 4 files changed, 77 insertions(+), 30 deletions(-) diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index 30879aea9f4..05dc47460c4 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -339,11 +339,18 @@ def _prop_bnds_leaf_to_root_GeneralExpression(visitor, node, expr): expr: GeneralExpression arg """ bnds_dict = visitor.bnds_dict + if node in bnds_dict: + return + elif node in visitor.leaf_bnds_dict: + bnds_dict[node] = visitor.leaf_bnds_dict[node] + return + if expr.__class__ in native_types: expr_lb = expr_ub = expr else: expr_lb, expr_ub = bnds_dict[expr] bnds_dict[node] = (expr_lb, expr_ub) + visitor.leaf_bnds_dict[node] = (expr_lb, expr_ub) _prop_bnds_leaf_to_root_map = defaultdict(lambda: _prop_no_bounds) @@ -980,13 +987,22 @@ def _check_and_reset_bounds(var, lb, ub): def _before_constant(visitor, child): - visitor.bnds_dict[child] = (child, child) + if child in visitor.bnds_dict: + pass + elif child in visitor.leaf_bnds_dict: + visitor.bnds_dict[child] = visitor.leaf_bnds_dict[child] + else: + visitor.bnds_dict[child] = (child, child) + visitor.leaf_bnds_dict[child] = (child, child) return False, None def _before_var(visitor, child): if child in visitor.bnds_dict: return False, None + elif child in visitor.leaf_bnds_dict: + visitor.bnds_dict[child] = visitor.leaf_bnds_dict[child] + return False, None elif child.is_fixed() and not visitor.ignore_fixed: lb = value(child.value) ub = lb @@ -1003,12 +1019,18 @@ def _before_var(visitor, child): 'upper bound: {0}'.format(str(child)) ) visitor.bnds_dict[child] = (lb, ub) + visitor.leaf_bnds_dict[child] = (lb, ub) return False, None def _before_NPV(visitor, child): + if child in visitor.bnds_dict: + return False, None + if child in visitor.leaf_bnds_dict: + visitor.bnds_dict[child] = visitor.leaf_bnds_dict[child] val = value(child) visitor.bnds_dict[child] = (val, val) + visitor.leaf_bnds_dict[child] = (val, val) return False, None @@ -1050,13 +1072,13 @@ class _FBBTVisitorLeafToRoot(StreamBasedExpressionVisitor): the expression tree (all the way to the root node). """ - def __init__( - self, bnds_dict, integer_tol=1e-4, feasibility_tol=1e-8, ignore_fixed=False - ): + def __init__(self, leaf_bnds_dict=None, bnds_dict=None, integer_tol=1e-4, + feasibility_tol=1e-8, ignore_fixed=False ): """ Parameters ---------- - bnds_dict: ComponentMap + leaf_bnds_dict: ComponentMap, if you want to cache leaf-node bounds + bnds_dict: ComponentMap, if you want to cache non-leaf bounds integer_tol: float feasibility_tol: float If the bounds computed on the body of a constraint violate the bounds of @@ -1067,7 +1089,9 @@ def __init__( to prevent math domain errors (a larger value is more conservative). """ super().__init__() - self.bnds_dict = bnds_dict + self.bnds_dict = bnds_dict if bnds_dict is not None else ComponentMap() + self.leaf_bnds_dict = leaf_bnds_dict if leaf_bnds_dict is not None else \ + ComponentMap() self.integer_tol = integer_tol self.feasibility_tol = feasibility_tol self.ignore_fixed = ignore_fixed @@ -1085,6 +1109,13 @@ def exitNode(self, node, data): _prop_bnds_leaf_to_root_map[node.__class__](self, node, *node.args) +# class FBBTVisitorLeafToRoot(_FBBTVisitorLeafToRoot): +# def __init__(self, leaf_bnds_dict, bnds_dict=None, integer_tol=1e-4, +# feasibility_tol=1e-8, ignore_fixed=False): +# if bnds_dict is None: +# bnds_dict = {} + + class _FBBTVisitorRootToLeaf(ExpressionValueVisitor): """ This walker propagates bounds from the constraint back to the @@ -1252,7 +1283,8 @@ def _fbbt_con(con, config): ) # 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, feasibility_tol=config.feasibility_tol) + visitorA = _FBBTVisitorLeafToRoot(bnds_dict=bnds_dict, + feasibility_tol=config.feasibility_tol) visitorA.walk_expression(con.body) # Now we need to replace the bounds in bnds_dict for the root @@ -1489,23 +1521,29 @@ def fbbt( return new_var_bounds -def compute_bounds_on_expr(expr, ignore_fixed=False): +def compute_bounds_on_expr(expr, ignore_fixed=False, leaf_bnds_dict=None): """ - Compute bounds on an expression based on the bounds on the variables in the expression. + Compute bounds on an expression based on the bounds on the variables in + the expression. Parameters ---------- expr: pyomo.core.expr.numeric_expr.NumericExpression + ignore_fixed: bool, treats fixed Vars as constants if False, else treats + them as Vars + leaf_bnds_dict: ComponentMap, caches bounds for Vars, Params, and + Expressions, that could be helpful in future bound + computations on the same model. Returns ------- lb: float ub: float """ - bnds_dict = ComponentMap() - visitor = _FBBTVisitorLeafToRoot(bnds_dict, ignore_fixed=ignore_fixed) + visitor = _FBBTVisitorLeafToRoot(leaf_bnds_dict=leaf_bnds_dict, + ignore_fixed=ignore_fixed) visitor.walk_expression(expr) - lb, ub = bnds_dict[expr] + lb, ub = visitor.bnds_dict[expr] if lb == -interval.inf: lb = None if ub == interval.inf: diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index a73846c8aa5..d9f78a6caa1 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -162,7 +162,7 @@ class BigM_Transformation(GDP_to_MIP_Transformation, _BigM_MixIn): def __init__(self): super().__init__(logger) - self._set_up_fbbt_visitor() + #self._set_up_fbbt_visitor() def _apply_to(self, instance, **kwds): self.used_args = ComponentMap() # If everything was sure to go well, @@ -173,16 +173,18 @@ def _apply_to(self, instance, **kwds): # this map! with PauseGC(): try: + self._leaf_bnds_dict = ComponentMap() self._apply_to_impl(instance, **kwds) finally: self._restore_state() self.used_args.clear() - self._fbbt_visitor.ignore_fixed = True + self._leaf_bnds_dict = ComponentMap() + #self._fbbt_visitor.ignore_fixed = True def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) - if self._config.assume_fixed_vars_permanent: - self._fbbt_visitor.ignore_fixed = False + #if self._config.assume_fixed_vars_permanent: + #self._fbbt_visitor.ignore_fixed = False # filter out inactive targets and handle case where targets aren't # specified. diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index 39242db248b..ca840f1aeee 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -11,8 +11,8 @@ from pyomo.gdp import GDP_Error from pyomo.common.collections import ComponentMap, ComponentSet -from pyomo.contrib.fbbt.fbbt import _FBBTVisitorLeafToRoot -import pyomo.contrib.fbbt.interval as interval +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr#_FBBTVisitorLeafToRoot +#import pyomo.contrib.fbbt.interval as interval from pyomo.core import Suffix @@ -104,11 +104,11 @@ def _get_bigM_arg_list(self, bigm_args, block): block = block.parent_block() return arg_list - def _set_up_fbbt_visitor(self): - bnds_dict = ComponentMap() - # we assume the default config arg for 'assume_fixed_vars_permanent,` - # and we will change it during apply_to if we need to - self._fbbt_visitor = _FBBTVisitorLeafToRoot(bnds_dict, ignore_fixed=True) + # def _set_up_fbbt_visitor(self): + # bnds_dict = ComponentMap() + # # we assume the default config arg for 'assume_fixed_vars_permanent,` + # # and we will change it during apply_to if we need to + # self._fbbt_visitor = _FBBTVisitorLeafToRoot(bnds_dict, ignore_fixed=True) def _process_M_value( self, @@ -217,9 +217,14 @@ def _get_M_from_args(self, constraint, bigMargs, arg_list, lower, upper): return lower, upper def _estimate_M(self, expr, constraint): - self._fbbt_visitor.walk_expression(expr) - expr_lb, expr_ub = self._fbbt_visitor.bnds_dict[expr] - if expr_lb == -interval.inf or expr_ub == interval.inf: + expr_lb, expr_ub = compute_bounds_on_expr( + expr, ignore_fixed=not + self._config.assume_fixed_vars_permanent, + leaf_bnds_dict=self._leaf_bnds_dict) + # self._fbbt_visitor.walk_expression(expr) + # expr_lb, expr_ub = self._fbbt_visitor.bnds_dict[expr] + #if expr_lb == -interval.inf or expr_ub == interval.inf: + if expr_lb is None or expr_ub is None: raise GDP_Error( "Cannot estimate M for unbounded " "expressions.\n\t(found while processing " diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 5d76575d514..afc362117cb 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -203,23 +203,25 @@ def __init__(self): super().__init__(logger) self.handlers[Suffix] = self._warn_for_active_suffix self._arg_list = {} - self._set_up_fbbt_visitor() + #self._set_up_fbbt_visitor() def _apply_to(self, instance, **kwds): self.used_args = ComponentMap() with PauseGC(): try: + self._leaf_bnds_dict = ComponentMap() self._apply_to_impl(instance, **kwds) finally: self._restore_state() self.used_args.clear() self._arg_list.clear() - self._fbbt_visitor.ignore_fixed = True + self._leaf_bnds_dict = ComponentMap() + #self._fbbt_visitor.ignore_fixed = True def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) - if self._config.assume_fixed_vars_permanent: - self._fbbt_visitor.ignore_fixed = False + # if self._config.assume_fixed_vars_permanent: + # self._fbbt_visitor.ignore_fixed = False if ( self._config.only_mbigm_bound_constraints From 8fdc42b0c4860b5f5b1f92ddb28c063e5242d714 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 5 Oct 2023 13:42:26 -0600 Subject: [PATCH 15/36] Having walk_expression manage the caching situation, going back to the bigm transformations only building on visitor, and caching the leaf bounds --- pyomo/contrib/fbbt/fbbt.py | 42 ++++++++++++++++++------------ pyomo/gdp/plugins/bigm.py | 8 +++--- pyomo/gdp/plugins/bigm_mixin.py | 25 +++++++----------- pyomo/gdp/plugins/multiple_bigm.py | 8 +++--- 4 files changed, 44 insertions(+), 39 deletions(-) diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index 05dc47460c4..46acf833f23 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -1072,8 +1072,8 @@ class _FBBTVisitorLeafToRoot(StreamBasedExpressionVisitor): the expression tree (all the way to the root node). """ - def __init__(self, leaf_bnds_dict=None, bnds_dict=None, integer_tol=1e-4, - feasibility_tol=1e-8, ignore_fixed=False ): + def __init__(self, integer_tol=1e-4, feasibility_tol=1e-8, + ignore_fixed=False ): """ Parameters ---------- @@ -1089,9 +1089,9 @@ def __init__(self, leaf_bnds_dict=None, bnds_dict=None, integer_tol=1e-4, to prevent math domain errors (a larger value is more conservative). """ super().__init__() - self.bnds_dict = bnds_dict if bnds_dict is not None else ComponentMap() - self.leaf_bnds_dict = leaf_bnds_dict if leaf_bnds_dict is not None else \ - ComponentMap() + # self.bnds_dict = bnds_dict if bnds_dict is not None else ComponentMap() + # self.leaf_bnds_dict = leaf_bnds_dict if leaf_bnds_dict is not None else \ + # ComponentMap() self.integer_tol = integer_tol self.feasibility_tol = feasibility_tol self.ignore_fixed = ignore_fixed @@ -1108,6 +1108,20 @@ def beforeChild(self, node, child, child_idx): def exitNode(self, node, data): _prop_bnds_leaf_to_root_map[node.__class__](self, node, *node.args) + def walk_expression(self, expr, bnds_dict=None, leaf_bnds_dict=None): + try: + self.bnds_dict = bnds_dict if bnds_dict is not None else ComponentMap() + self.leaf_bnds_dict = leaf_bnds_dict if leaf_bnds_dict is not None else \ + ComponentMap() + super().walk_expression(expr) + result = self.bnds_dict[expr] + finally: + if bnds_dict is None: + self.bnds_dict.clear() + if leaf_bnds_dict is None: + self.leaf_bnds_dict.clear() + return result + # class FBBTVisitorLeafToRoot(_FBBTVisitorLeafToRoot): # def __init__(self, leaf_bnds_dict, bnds_dict=None, integer_tol=1e-4, @@ -1283,9 +1297,8 @@ def _fbbt_con(con, config): ) # 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=bnds_dict, - feasibility_tol=config.feasibility_tol) - visitorA.walk_expression(con.body) + visitorA = _FBBTVisitorLeafToRoot(feasibility_tol=config.feasibility_tol) + visitorA.walk_expression(con.body, bnds_dict=bnds_dict) # Now we need to replace the bounds in bnds_dict for the root # node with the bounds on the constraint (if those bounds are @@ -1521,7 +1534,7 @@ def fbbt( return new_var_bounds -def compute_bounds_on_expr(expr, ignore_fixed=False, leaf_bnds_dict=None): +def compute_bounds_on_expr(expr, ignore_fixed=False): """ Compute bounds on an expression based on the bounds on the variables in the expression. @@ -1531,19 +1544,16 @@ def compute_bounds_on_expr(expr, ignore_fixed=False, leaf_bnds_dict=None): expr: pyomo.core.expr.numeric_expr.NumericExpression ignore_fixed: bool, treats fixed Vars as constants if False, else treats them as Vars - leaf_bnds_dict: ComponentMap, caches bounds for Vars, Params, and - Expressions, that could be helpful in future bound - computations on the same model. Returns ------- lb: float ub: float """ - visitor = _FBBTVisitorLeafToRoot(leaf_bnds_dict=leaf_bnds_dict, - ignore_fixed=ignore_fixed) - visitor.walk_expression(expr) - lb, ub = visitor.bnds_dict[expr] + bnds_dict = ComponentMap() + visitor = _FBBTVisitorLeafToRoot(ignore_fixed=ignore_fixed) + visitor.walk_expression(expr, bnds_dict=bnds_dict) + lb, ub = bnds_dict[expr] if lb == -interval.inf: lb = None if ub == interval.inf: diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index d9f78a6caa1..7fd683c9f47 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -162,7 +162,7 @@ class BigM_Transformation(GDP_to_MIP_Transformation, _BigM_MixIn): def __init__(self): super().__init__(logger) - #self._set_up_fbbt_visitor() + self._set_up_fbbt_visitor() def _apply_to(self, instance, **kwds): self.used_args = ComponentMap() # If everything was sure to go well, @@ -179,12 +179,12 @@ def _apply_to(self, instance, **kwds): self._restore_state() self.used_args.clear() self._leaf_bnds_dict = ComponentMap() - #self._fbbt_visitor.ignore_fixed = True + self._fbbt_visitor.ignore_fixed = True def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) - #if self._config.assume_fixed_vars_permanent: - #self._fbbt_visitor.ignore_fixed = False + if self._config.assume_fixed_vars_permanent: + self._fbbt_visitor.ignore_fixed = False # filter out inactive targets and handle case where targets aren't # specified. diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index ca840f1aeee..5a306832b09 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -11,8 +11,8 @@ from pyomo.gdp import GDP_Error from pyomo.common.collections import ComponentMap, ComponentSet -from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr#_FBBTVisitorLeafToRoot -#import pyomo.contrib.fbbt.interval as interval +from pyomo.contrib.fbbt.fbbt import _FBBTVisitorLeafToRoot +import pyomo.contrib.fbbt.interval as interval from pyomo.core import Suffix @@ -104,11 +104,11 @@ def _get_bigM_arg_list(self, bigm_args, block): block = block.parent_block() return arg_list - # def _set_up_fbbt_visitor(self): - # bnds_dict = ComponentMap() - # # we assume the default config arg for 'assume_fixed_vars_permanent,` - # # and we will change it during apply_to if we need to - # self._fbbt_visitor = _FBBTVisitorLeafToRoot(bnds_dict, ignore_fixed=True) + def _set_up_fbbt_visitor(self): + #bnds_dict = ComponentMap() + # we assume the default config arg for 'assume_fixed_vars_permanent,` + # and we will change it during apply_to if we need to + self._fbbt_visitor = _FBBTVisitorLeafToRoot(ignore_fixed=True) def _process_M_value( self, @@ -217,14 +217,9 @@ def _get_M_from_args(self, constraint, bigMargs, arg_list, lower, upper): return lower, upper def _estimate_M(self, expr, constraint): - expr_lb, expr_ub = compute_bounds_on_expr( - expr, ignore_fixed=not - self._config.assume_fixed_vars_permanent, - leaf_bnds_dict=self._leaf_bnds_dict) - # self._fbbt_visitor.walk_expression(expr) - # expr_lb, expr_ub = self._fbbt_visitor.bnds_dict[expr] - #if expr_lb == -interval.inf or expr_ub == interval.inf: - if expr_lb is None or expr_ub is None: + expr_lb, expr_ub = self._fbbt_visitor.walk_expression( + expr, leaf_bnds_dict=self._leaf_bnds_dict) + if expr_lb == -interval.inf or expr_ub == interval.inf: raise GDP_Error( "Cannot estimate M for unbounded " "expressions.\n\t(found while processing " diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index afc362117cb..1e23121602b 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -203,7 +203,7 @@ def __init__(self): super().__init__(logger) self.handlers[Suffix] = self._warn_for_active_suffix self._arg_list = {} - #self._set_up_fbbt_visitor() + self._set_up_fbbt_visitor() def _apply_to(self, instance, **kwds): self.used_args = ComponentMap() @@ -216,12 +216,12 @@ def _apply_to(self, instance, **kwds): self.used_args.clear() self._arg_list.clear() self._leaf_bnds_dict = ComponentMap() - #self._fbbt_visitor.ignore_fixed = True + self._fbbt_visitor.ignore_fixed = True def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) - # if self._config.assume_fixed_vars_permanent: - # self._fbbt_visitor.ignore_fixed = False + if self._config.assume_fixed_vars_permanent: + self._fbbt_visitor.ignore_fixed = False if ( self._config.only_mbigm_bound_constraints From 2ae56fb4f1ab4b25c6dea237d74e594933cbded6 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 6 Oct 2023 11:00:53 -0600 Subject: [PATCH 16/36] fixing a typo in before NPV visitor --- pyomo/contrib/fbbt/fbbt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index 46acf833f23..7f192c4a0b9 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -1028,6 +1028,7 @@ def _before_NPV(visitor, child): return False, None if child in visitor.leaf_bnds_dict: visitor.bnds_dict[child] = visitor.leaf_bnds_dict[child] + return False, None val = value(child) visitor.bnds_dict[child] = (val, val) visitor.leaf_bnds_dict[child] = (val, val) From 94de29a0c35145b9544b3310b47a63125c9a7ab4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 9 Oct 2023 10:11:50 -0600 Subject: [PATCH 17/36] Adding new expression bounds walker, and it passes a test --- .../contrib/fbbt/expression_bounds_walker.py | 243 ++++++++++++++++++ pyomo/contrib/fbbt/fbbt.py | 7 +- pyomo/contrib/fbbt/tests/test_fbbt.py | 1 + 3 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 pyomo/contrib/fbbt/expression_bounds_walker.py diff --git a/pyomo/contrib/fbbt/expression_bounds_walker.py b/pyomo/contrib/fbbt/expression_bounds_walker.py new file mode 100644 index 00000000000..a6373108d51 --- /dev/null +++ b/pyomo/contrib/fbbt/expression_bounds_walker.py @@ -0,0 +1,243 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from collections import defaultdict +from pyomo.common.collections import ComponentMap +from pyomo.contrib.fbbt.interval import ( + add, acos, asin, atan, cos, div, exp, interval_abs, log, + log10, mul, power, sin, sub, tan, +) +from pyomo.core.base.expression import _GeneralExpressionData, ScalarExpression +from pyomo.core.expr.numeric_expr import ( + NegationExpression, + ProductExpression, + DivisionExpression, + PowExpression, + AbsExpression, + UnaryFunctionExpression, + MonomialTermExpression, + LinearExpression, + SumExpression, + ExternalFunctionExpression, +) +from pyomo.core.expr.numvalue import native_numeric_types, native_types, value +from pyomo.core.expr.visitor import StreamBasedExpressionVisitor + +inf = float('inf') + + +def _before_external_function(visitor, child): + # [ESJ 10/6/23]: If external functions ever implement callbacks to help with + # this then this should use them + return False, (-inf, inf) + + +def _before_var(visitor, child): + leaf_bounds = visitor.leaf_bounds + if child in leaf_bounds: + pass + elif child.is_fixed() and visitor.use_fixed_var_values_as_bounds: + val = child.value + if val is None: + raise ValueError( + "Var '%s' is fixed to None. This value cannot be used to " + "calculate bounds." % child.name) + leaf_bounds[child] = (child.value, child.value) + else: + lb = value(child.lb) + ub = value(child.ub) + if lb is None: + lb = -inf + if ub is None: + ub = inf + leaf_bounds[child] = (lb, ub) + return False, leaf_bounds[child] + + +def _before_named_expression(visitor, child): + leaf_bounds = visitor.leaf_bounds + if child in leaf_bounds: + return False, leaf_bounds[child] + else: + return True, None + + +def _before_param(visitor, child): + return False, (child.value, child.value) + + +def _before_constant(visitor, child): + return False, (child, child) + + +def _before_other(visitor, child): + return True, None + + +def _register_new_before_child_handler(visitor, child): + handlers = _before_child_handlers + child_type = child.__class__ + if child_type in native_numeric_types: + handlers[child_type] = _before_constant + elif child_type in native_types: + pass + # TODO: catch this, it's bad. + elif not child.is_expression_type(): + if child.is_potentially_variable(): + handlers[child_type] = _before_var + else: + handlers[child_type] = _before_param + elif issubclass(child_type, _GeneralExpressionData): + handlers[child_type] = _before_named_expression + else: + handlers[child_type] = _before_other + return handlers[child_type](visitor, child) + + +_before_child_handlers = defaultdict(lambda: _register_new_before_child_handler) +_before_child_handlers[ExternalFunctionExpression] = _before_external_function + + +def _handle_ProductExpression(visitor, node, arg1, arg2): + return mul(*arg1, *arg2) + + +def _handle_SumExpression(visitor, node, *args): + bnds = (0, 0) + for arg in args: + bnds = add(*bnds, *arg) + return bnds + + +def _handle_DivisionExpression(visitor, node, arg1, arg2): + return div(*arg1, *arg2, feasibility_tol=visitor.feasibility_tol) + + +def _handle_PowExpression(visitor, node, arg1, arg2): + return power(*arg1, *arg2, feasibility_tol=visitor.feasibility_tol) + + +def _handle_NegationExpression(visitor, node, arg): + return sub(0, 0, *arg) + + +def _handle_exp(visitor, node, arg): + return exp(*arg) + + +def _handle_log(visitor, node, arg): + return log(*arg) + + +def _handle_log10(visitor, node, arg): + return log10(*arg) + + +def _handle_sin(visitor, node, arg): + return sin(*arg) + + +def _handle_cos(visitor, node, arg): + return cos(*arg) + + +def _handle_tan(visitor, node, arg): + return tan(*arg) + + +def _handle_asin(visitor, node, arg): + return asin(*arg) + + +def _handle_acos(visitor, node, arg): + return acos(*arg) + + +def _handle_atan(visitor, node, arg): + return atan(*arg) + + +def _handle_sqrt(visitor, node, arg): + return power(*arg, 0.5, 0.5, feasibility_tol=visitor.feasibility_tol) + + +def _handle_abs(visitor, node, arg): + return interval_abs(*arg) + + +def _handle_no_bounds(visitor, node, *args): + return (-inf, inf) + + +def _handle_UnaryFunctionExpression(visitor, node, arg): + return _unary_function_dispatcher[node.getname()](visitor, node, arg) + + +def _handle_named_expression(visitor, node, arg): + visitor.leaf_bounds[node] = arg + return arg + + +_unary_function_dispatcher = { + 'exp': _handle_exp, + 'log': _handle_log, + 'log10': _handle_log10, + 'sin': _handle_sin, + 'cos': _handle_cos, + 'tan': _handle_tan, + 'asin': _handle_asin, + 'acos': _handle_acos, + 'atan': _handle_atan, + 'sqrt': _handle_sqrt, + 'abs': _handle_abs, +} + +_operator_dispatcher = defaultdict( + lambda: _handle_no_bounds, { + ProductExpression: _handle_ProductExpression, + DivisionExpression: _handle_DivisionExpression, + PowExpression: _handle_PowExpression, + SumExpression: _handle_SumExpression, + MonomialTermExpression: _handle_ProductExpression, + NegationExpression: _handle_NegationExpression, + UnaryFunctionExpression: _handle_UnaryFunctionExpression, + LinearExpression: _handle_SumExpression, + _GeneralExpressionData: _handle_named_expression, + ScalarExpression: _handle_named_expression, + } +) + +class ExpressionBoundsVisitor(StreamBasedExpressionVisitor): + """ + Walker to calculate bounds on an expression, from leaf to root, with + caching of terminal node bounds (Vars and Expressions) + """ + def __init__(self, leaf_bounds=None, feasibility_tol=1e-8, + use_fixed_var_values_as_bounds=False): + super().__init__() + self.leaf_bounds = leaf_bounds if leaf_bounds is not None \ + else ComponentMap() + self.feasibility_tol = feasibility_tol + self.use_fixed_var_values_as_bounds = use_fixed_var_values_as_bounds + + def initializeWalker(self, expr): + print(expr) + print(self.beforeChild(None, expr, 0)) + walk, result = self.beforeChild(None, expr, 0) + if not walk: + return False, result + return True, expr + + def beforeChild(self, node, child, child_idx): + return _before_child_handlers[child.__class__](self, child) + + def exitNode(self, node, data): + return _operator_dispatcher[node.__class__](self, node, *data) diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index 7f192c4a0b9..4fbad47c427 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -11,6 +11,7 @@ from collections import defaultdict from pyomo.common.collections import ComponentMap, ComponentSet +from pyomo.contrib.fbbt.expression_bounds_walker import ExpressionBoundsVisitor import pyomo.core.expr.numeric_expr as numeric_expr from pyomo.core.expr.visitor import ( ExpressionValueVisitor, @@ -1551,10 +1552,8 @@ def compute_bounds_on_expr(expr, ignore_fixed=False): lb: float ub: float """ - bnds_dict = ComponentMap() - visitor = _FBBTVisitorLeafToRoot(ignore_fixed=ignore_fixed) - visitor.walk_expression(expr, bnds_dict=bnds_dict) - lb, ub = bnds_dict[expr] + lb, ub = ExpressionBoundsVisitor( + use_fixed_var_values_as_bounds=not ignore_fixed).walk_expression(expr) if lb == -interval.inf: lb = None if ub == interval.inf: diff --git a/pyomo/contrib/fbbt/tests/test_fbbt.py b/pyomo/contrib/fbbt/tests/test_fbbt.py index 5e8d656eeab..7fa17bfbb9a 100644 --- a/pyomo/contrib/fbbt/tests/test_fbbt.py +++ b/pyomo/contrib/fbbt/tests/test_fbbt.py @@ -1110,6 +1110,7 @@ def test_compute_expr_bounds(self): m.y = pyo.Var(bounds=(-1, 1)) e = m.x + m.y lb, ub = compute_bounds_on_expr(e) + print(lb, ub) self.assertAlmostEqual(lb, -2, 14) self.assertAlmostEqual(ub, 2, 14) From f45d417e1c1644ecad2a01848ef7ca9f6ac5a39e Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 9 Oct 2023 14:09:01 -0600 Subject: [PATCH 18/36] Moving bigm transformations onto new expression bounds visitor --- pyomo/gdp/plugins/bigm.py | 6 +++--- pyomo/gdp/plugins/bigm_mixin.py | 10 +++++----- pyomo/gdp/plugins/multiple_bigm.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index 8ca4efd4be8..cbc03bafb18 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -162,7 +162,7 @@ class BigM_Transformation(GDP_to_MIP_Transformation, _BigM_MixIn): def __init__(self): super().__init__(logger) - self._set_up_fbbt_visitor() + self._set_up_expr_bound_visitor() def _apply_to(self, instance, **kwds): self.used_args = ComponentMap() # If everything was sure to go well, @@ -179,12 +179,12 @@ def _apply_to(self, instance, **kwds): self._restore_state() self.used_args.clear() self._leaf_bnds_dict = ComponentMap() - self._fbbt_visitor.ignore_fixed = True + self._expr_bound_visitor.use_fixed_var_values_as_bounds = False def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) if self._config.assume_fixed_vars_permanent: - self._fbbt_visitor.ignore_fixed = False + self._expr_bound_visitor.use_fixed_var_values_as_bounds = True # filter out inactive targets and handle case where targets aren't # specified. diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index 5a306832b09..5209dad0860 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -11,7 +11,7 @@ from pyomo.gdp import GDP_Error from pyomo.common.collections import ComponentMap, ComponentSet -from pyomo.contrib.fbbt.fbbt import _FBBTVisitorLeafToRoot +from pyomo.contrib.fbbt.expression_bounds_walker import ExpressionBoundsVisitor import pyomo.contrib.fbbt.interval as interval from pyomo.core import Suffix @@ -104,11 +104,12 @@ def _get_bigM_arg_list(self, bigm_args, block): block = block.parent_block() return arg_list - def _set_up_fbbt_visitor(self): + def _set_up_expr_bound_visitor(self): #bnds_dict = ComponentMap() # we assume the default config arg for 'assume_fixed_vars_permanent,` # and we will change it during apply_to if we need to - self._fbbt_visitor = _FBBTVisitorLeafToRoot(ignore_fixed=True) + self._expr_bound_visitor = ExpressionBoundsVisitor( + use_fixed_var_values_as_bounds=False) def _process_M_value( self, @@ -217,8 +218,7 @@ def _get_M_from_args(self, constraint, bigMargs, arg_list, lower, upper): return lower, upper def _estimate_M(self, expr, constraint): - expr_lb, expr_ub = self._fbbt_visitor.walk_expression( - expr, leaf_bnds_dict=self._leaf_bnds_dict) + expr_lb, expr_ub = self._expr_bound_visitor.walk_expression(expr) if expr_lb == -interval.inf or expr_ub == interval.inf: raise GDP_Error( "Cannot estimate M for unbounded " diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 7641ddd4e83..ca6a01cee52 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -203,7 +203,7 @@ def __init__(self): super().__init__(logger) self.handlers[Suffix] = self._warn_for_active_suffix self._arg_list = {} - self._set_up_fbbt_visitor() + self._set_up_expr_bound_visitor() def _apply_to(self, instance, **kwds): self.used_args = ComponentMap() @@ -216,12 +216,12 @@ def _apply_to(self, instance, **kwds): self.used_args.clear() self._arg_list.clear() self._leaf_bnds_dict = ComponentMap() - self._fbbt_visitor.ignore_fixed = True + self._expr_bound_visitor.use_fixed_var_values_as_bounds = False def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) if self._config.assume_fixed_vars_permanent: - self._fbbt_visitor.ignore_fixed = False + self._bound_visitor.use_fixed_var_values_as_bounds = True if ( self._config.only_mbigm_bound_constraints From 091ab3223495227c6cc6ef76c623371cb8066403 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 9 Oct 2023 14:15:41 -0600 Subject: [PATCH 19/36] Removing some debugging --- pyomo/contrib/fbbt/expression_bounds_walker.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyomo/contrib/fbbt/expression_bounds_walker.py b/pyomo/contrib/fbbt/expression_bounds_walker.py index a6373108d51..fb55a779017 100644 --- a/pyomo/contrib/fbbt/expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/expression_bounds_walker.py @@ -229,8 +229,6 @@ def __init__(self, leaf_bounds=None, feasibility_tol=1e-8, self.use_fixed_var_values_as_bounds = use_fixed_var_values_as_bounds def initializeWalker(self, expr): - print(expr) - print(self.beforeChild(None, expr, 0)) walk, result = self.beforeChild(None, expr, 0) if not walk: return False, result From 3a4399d69b621f007ba0fc0e5cffdbfb55f2fd5b Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 19 Oct 2023 13:17:54 -0600 Subject: [PATCH 20/36] Making inverse trig calls match the old walker, I think --- pyomo/contrib/fbbt/expression_bounds_walker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/fbbt/expression_bounds_walker.py b/pyomo/contrib/fbbt/expression_bounds_walker.py index fb55a779017..7c84d4fc171 100644 --- a/pyomo/contrib/fbbt/expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/expression_bounds_walker.py @@ -154,15 +154,15 @@ def _handle_tan(visitor, node, arg): def _handle_asin(visitor, node, arg): - return asin(*arg) + return asin(*arg, -inf, inf, visitor.feasibility_tol) def _handle_acos(visitor, node, arg): - return acos(*arg) + return acos(*arg, -inf, inf, visitor.feasibility_tol) def _handle_atan(visitor, node, arg): - return atan(*arg) + return atan(*arg, -inf, inf) def _handle_sqrt(visitor, node, arg): From 393fe7fe99c544ed75ed89668b9e2b99291d258b Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 20 Oct 2023 12:51:05 -0600 Subject: [PATCH 21/36] Fixing a bug with absolute value --- pyomo/contrib/fbbt/expression_bounds_walker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/fbbt/expression_bounds_walker.py b/pyomo/contrib/fbbt/expression_bounds_walker.py index 7c84d4fc171..cd6fe73fd14 100644 --- a/pyomo/contrib/fbbt/expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/expression_bounds_walker.py @@ -169,7 +169,7 @@ def _handle_sqrt(visitor, node, arg): return power(*arg, 0.5, 0.5, feasibility_tol=visitor.feasibility_tol) -def _handle_abs(visitor, node, arg): +def _handle_AbsExpression(visitor, node, arg): return interval_abs(*arg) @@ -197,7 +197,6 @@ def _handle_named_expression(visitor, node, arg): 'acos': _handle_acos, 'atan': _handle_atan, 'sqrt': _handle_sqrt, - 'abs': _handle_abs, } _operator_dispatcher = defaultdict( @@ -205,6 +204,7 @@ def _handle_named_expression(visitor, node, arg): ProductExpression: _handle_ProductExpression, DivisionExpression: _handle_DivisionExpression, PowExpression: _handle_PowExpression, + AbsExpression: _handle_AbsExpression, SumExpression: _handle_SumExpression, MonomialTermExpression: _handle_ProductExpression, NegationExpression: _handle_NegationExpression, From 3bb68f17f18e13f381e94cda74ebf4edafefd10d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 20 Oct 2023 12:51:18 -0600 Subject: [PATCH 22/36] Adding some comments --- pyomo/contrib/fbbt/interval.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/fbbt/interval.py b/pyomo/contrib/fbbt/interval.py index db036f50f01..61a89817fc0 100644 --- a/pyomo/contrib/fbbt/interval.py +++ b/pyomo/contrib/fbbt/interval.py @@ -147,7 +147,7 @@ def power(xl, xu, yl, yu, feasibility_tol): else: lb = xl**y ub = xu**y - else: + else: # xu is positive if y < 0: if y % 2 == 0: lb = min(xl**y, xu**y) @@ -155,8 +155,9 @@ def power(xl, xu, yl, yu, feasibility_tol): else: lb = -inf ub = inf - else: + else: # exponent is nonnegative if y % 2 == 0: + # xl is negative and xu is positive, so lb is 0 lb = 0 ub = max(xl**y, xu**y) else: @@ -321,7 +322,7 @@ def _inverse_power2(zl, zu, xl, xu, feasiblity_tol): def interval_abs(xl, xu): abs_xl = abs(xl) abs_xu = abs(xu) - if xl <= 0 <= xu: + if xl <= 0 and 0 <= xu: res_lb = 0 res_ub = max(abs_xl, abs_xu) else: From 062bae5f7f31fc2c377e6fc67d360c832b4f302d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 20 Oct 2023 12:52:24 -0600 Subject: [PATCH 23/36] Blackify --- .../contrib/fbbt/expression_bounds_walker.py | 40 ++++++++++++++----- pyomo/contrib/fbbt/fbbt.py | 18 +++++---- pyomo/contrib/fbbt/interval.py | 6 +-- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/pyomo/contrib/fbbt/expression_bounds_walker.py b/pyomo/contrib/fbbt/expression_bounds_walker.py index cd6fe73fd14..476800807fe 100644 --- a/pyomo/contrib/fbbt/expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/expression_bounds_walker.py @@ -12,8 +12,21 @@ from collections import defaultdict from pyomo.common.collections import ComponentMap from pyomo.contrib.fbbt.interval import ( - add, acos, asin, atan, cos, div, exp, interval_abs, log, - log10, mul, power, sin, sub, tan, + add, + acos, + asin, + atan, + cos, + div, + exp, + interval_abs, + log, + log10, + mul, + power, + sin, + sub, + tan, ) from pyomo.core.base.expression import _GeneralExpressionData, ScalarExpression from pyomo.core.expr.numeric_expr import ( @@ -27,7 +40,7 @@ LinearExpression, SumExpression, ExternalFunctionExpression, -) +) from pyomo.core.expr.numvalue import native_numeric_types, native_types, value from pyomo.core.expr.visitor import StreamBasedExpressionVisitor @@ -49,7 +62,8 @@ def _before_var(visitor, child): if val is None: raise ValueError( "Var '%s' is fixed to None. This value cannot be used to " - "calculate bounds." % child.name) + "calculate bounds." % child.name + ) leaf_bounds[child] = (child.value, child.value) else: lb = value(child.lb) @@ -200,7 +214,8 @@ def _handle_named_expression(visitor, node, arg): } _operator_dispatcher = defaultdict( - lambda: _handle_no_bounds, { + lambda: _handle_no_bounds, + { ProductExpression: _handle_ProductExpression, DivisionExpression: _handle_DivisionExpression, PowExpression: _handle_PowExpression, @@ -212,19 +227,24 @@ def _handle_named_expression(visitor, node, arg): LinearExpression: _handle_SumExpression, _GeneralExpressionData: _handle_named_expression, ScalarExpression: _handle_named_expression, - } + }, ) + class ExpressionBoundsVisitor(StreamBasedExpressionVisitor): """ Walker to calculate bounds on an expression, from leaf to root, with caching of terminal node bounds (Vars and Expressions) """ - def __init__(self, leaf_bounds=None, feasibility_tol=1e-8, - use_fixed_var_values_as_bounds=False): + + def __init__( + self, + leaf_bounds=None, + feasibility_tol=1e-8, + use_fixed_var_values_as_bounds=False, + ): super().__init__() - self.leaf_bounds = leaf_bounds if leaf_bounds is not None \ - else ComponentMap() + self.leaf_bounds = leaf_bounds if leaf_bounds is not None else ComponentMap() self.feasibility_tol = feasibility_tol self.use_fixed_var_values_as_bounds = use_fixed_var_values_as_bounds diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index 4fbad47c427..0fd6da8ac50 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -91,8 +91,9 @@ def _prop_bnds_leaf_to_root_ProductExpression(visitor, node, arg1, arg2): """ bnds_dict = visitor.bnds_dict if arg1 is arg2: - bnds_dict[node] = interval.power(*bnds_dict[arg1], 2, 2, - visitor.feasibility_tol) + bnds_dict[node] = interval.power( + *bnds_dict[arg1], 2, 2, visitor.feasibility_tol + ) else: bnds_dict[node] = interval.mul(*bnds_dict[arg1], *bnds_dict[arg2]) @@ -1074,8 +1075,7 @@ class _FBBTVisitorLeafToRoot(StreamBasedExpressionVisitor): the expression tree (all the way to the root node). """ - def __init__(self, integer_tol=1e-4, feasibility_tol=1e-8, - ignore_fixed=False ): + def __init__(self, integer_tol=1e-4, feasibility_tol=1e-8, ignore_fixed=False): """ Parameters ---------- @@ -1113,8 +1113,9 @@ def exitNode(self, node, data): def walk_expression(self, expr, bnds_dict=None, leaf_bnds_dict=None): try: self.bnds_dict = bnds_dict if bnds_dict is not None else ComponentMap() - self.leaf_bnds_dict = leaf_bnds_dict if leaf_bnds_dict is not None else \ - ComponentMap() + self.leaf_bnds_dict = ( + leaf_bnds_dict if leaf_bnds_dict is not None else ComponentMap() + ) super().walk_expression(expr) result = self.bnds_dict[expr] finally: @@ -1130,7 +1131,7 @@ def walk_expression(self, expr, bnds_dict=None, leaf_bnds_dict=None): # feasibility_tol=1e-8, ignore_fixed=False): # if bnds_dict is None: # bnds_dict = {} - + class _FBBTVisitorRootToLeaf(ExpressionValueVisitor): """ @@ -1553,7 +1554,8 @@ def compute_bounds_on_expr(expr, ignore_fixed=False): ub: float """ lb, ub = ExpressionBoundsVisitor( - use_fixed_var_values_as_bounds=not ignore_fixed).walk_expression(expr) + use_fixed_var_values_as_bounds=not ignore_fixed + ).walk_expression(expr) if lb == -interval.inf: lb = None if ub == interval.inf: diff --git a/pyomo/contrib/fbbt/interval.py b/pyomo/contrib/fbbt/interval.py index 61a89817fc0..fd86af4c106 100644 --- a/pyomo/contrib/fbbt/interval.py +++ b/pyomo/contrib/fbbt/interval.py @@ -33,7 +33,7 @@ def mul(xl, xu, yl, yu): lb = i if i > ub: ub = i - if i != i: # math.isnan(i) + if i != i: # math.isnan(i) return (-inf, inf) return lb, ub @@ -147,7 +147,7 @@ def power(xl, xu, yl, yu, feasibility_tol): else: lb = xl**y ub = xu**y - else: # xu is positive + else: # xu is positive if y < 0: if y % 2 == 0: lb = min(xl**y, xu**y) @@ -155,7 +155,7 @@ def power(xl, xu, yl, yu, feasibility_tol): else: lb = -inf ub = inf - else: # exponent is nonnegative + else: # exponent is nonnegative if y % 2 == 0: # xl is negative and xu is positive, so lb is 0 lb = 0 From 126d3d8537b7f93293cbc11ecc94822a19b8fd7f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 27 Oct 2023 12:46:46 -0600 Subject: [PATCH 24/36] Moving onto BeforeChildDispatcher and ExitNodeDispatcher base classes --- .../contrib/fbbt/expression_bounds_walker.py | 146 +++++++++--------- 1 file changed, 72 insertions(+), 74 deletions(-) diff --git a/pyomo/contrib/fbbt/expression_bounds_walker.py b/pyomo/contrib/fbbt/expression_bounds_walker.py index 476800807fe..ec50385784e 100644 --- a/pyomo/contrib/fbbt/expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/expression_bounds_walker.py @@ -28,7 +28,7 @@ sub, tan, ) -from pyomo.core.base.expression import _GeneralExpressionData, ScalarExpression +from pyomo.core.base.expression import Expression from pyomo.core.expr.numeric_expr import ( NegationExpression, ProductExpression, @@ -43,83 +43,86 @@ ) from pyomo.core.expr.numvalue import native_numeric_types, native_types, value from pyomo.core.expr.visitor import StreamBasedExpressionVisitor +from pyomo.repn.util import BeforeChildDispatcher, ExitNodeDispatcher inf = float('inf') - -def _before_external_function(visitor, child): - # [ESJ 10/6/23]: If external functions ever implement callbacks to help with - # this then this should use them - return False, (-inf, inf) - - -def _before_var(visitor, child): - leaf_bounds = visitor.leaf_bounds - if child in leaf_bounds: - pass - elif child.is_fixed() and visitor.use_fixed_var_values_as_bounds: - val = child.value - if val is None: - raise ValueError( - "Var '%s' is fixed to None. This value cannot be used to " - "calculate bounds." % child.name - ) - leaf_bounds[child] = (child.value, child.value) - else: - lb = value(child.lb) - ub = value(child.ub) - if lb is None: - lb = -inf - if ub is None: - ub = inf - leaf_bounds[child] = (lb, ub) - return False, leaf_bounds[child] - - -def _before_named_expression(visitor, child): - leaf_bounds = visitor.leaf_bounds - if child in leaf_bounds: +class ExpressionBoundsBeforeChildDispatcher(BeforeChildDispatcher): + __slots__ = () + + def __init__(self): + self[ExternalFunctionExpression] = self._before_external_function + + @staticmethod + def _before_external_function(visitor, child): + # [ESJ 10/6/23]: If external functions ever implement callbacks to help with + # this then this should use them + return False, (-inf, inf) + + @staticmethod + def _before_var(visitor, child): + leaf_bounds = visitor.leaf_bounds + if child in leaf_bounds: + pass + elif child.is_fixed() and visitor.use_fixed_var_values_as_bounds: + val = child.value + if val is None: + raise ValueError( + "Var '%s' is fixed to None. This value cannot be used to " + "calculate bounds." % child.name + ) + leaf_bounds[child] = (child.value, child.value) + else: + lb = value(child.lb) + ub = value(child.ub) + if lb is None: + lb = -inf + if ub is None: + ub = inf + leaf_bounds[child] = (lb, ub) return False, leaf_bounds[child] - else: - return True, None - - -def _before_param(visitor, child): - return False, (child.value, child.value) - -def _before_constant(visitor, child): - return False, (child, child) + @staticmethod + def _before_named_expression(visitor, child): + leaf_bounds = visitor.leaf_bounds + if child in leaf_bounds: + return False, leaf_bounds[child] + else: + return True, None + @staticmethod + def _before_param(visitor, child): + return False, (child.value, child.value) -def _before_other(visitor, child): - return True, None + @staticmethod + def _before_native(visitor, child): + return False, (child, child) + @staticmethod + def _before_string(visitor, child): + raise ValueError( + f"Cannot compute bounds on expression containing {child!r} " + "of type {type(child)}, which is not a valid numeric type") -def _register_new_before_child_handler(visitor, child): - handlers = _before_child_handlers - child_type = child.__class__ - if child_type in native_numeric_types: - handlers[child_type] = _before_constant - elif child_type in native_types: - pass - # TODO: catch this, it's bad. - elif not child.is_expression_type(): - if child.is_potentially_variable(): - handlers[child_type] = _before_var - else: - handlers[child_type] = _before_param - elif issubclass(child_type, _GeneralExpressionData): - handlers[child_type] = _before_named_expression - else: - handlers[child_type] = _before_other - return handlers[child_type](visitor, child) + @staticmethod + def _before_invalid(visitor, child): + raise ValueError( + f"Cannot compute bounds on expression containing {child!r} " + "of type {type(child)}, which is not a valid numeric type") + @staticmethod + def _before_complex(visitor, child): + raise ValueError( + f"Cannot compute bounds on expression containing " + "complex numbers. Encountered when processing {child!r}") -_before_child_handlers = defaultdict(lambda: _register_new_before_child_handler) -_before_child_handlers[ExternalFunctionExpression] = _before_external_function + @staticmethod + def _before_npv(visitor, child): + return False, (value(child), value(child)) +_before_child_handlers = ExpressionBoundsBeforeChildDispatcher() + def _handle_ProductExpression(visitor, node, arg1, arg2): return mul(*arg1, *arg2) @@ -187,10 +190,6 @@ def _handle_AbsExpression(visitor, node, arg): return interval_abs(*arg) -def _handle_no_bounds(visitor, node, *args): - return (-inf, inf) - - def _handle_UnaryFunctionExpression(visitor, node, arg): return _unary_function_dispatcher[node.getname()](visitor, node, arg) @@ -213,8 +212,8 @@ def _handle_named_expression(visitor, node, arg): 'sqrt': _handle_sqrt, } -_operator_dispatcher = defaultdict( - lambda: _handle_no_bounds, + +_operator_dispatcher = ExitNodeDispatcher( { ProductExpression: _handle_ProductExpression, DivisionExpression: _handle_DivisionExpression, @@ -225,9 +224,8 @@ def _handle_named_expression(visitor, node, arg): NegationExpression: _handle_NegationExpression, UnaryFunctionExpression: _handle_UnaryFunctionExpression, LinearExpression: _handle_SumExpression, - _GeneralExpressionData: _handle_named_expression, - ScalarExpression: _handle_named_expression, - }, + Expression: _handle_named_expression, + } ) From b4b2c5b069088de3f7f876e0c8225c9351c08e7d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 27 Oct 2023 13:49:18 -0600 Subject: [PATCH 25/36] Testing error messages for expression bounds walker, handling inverse trig with bounds on the range --- .../contrib/fbbt/expression_bounds_walker.py | 22 +- .../tests/test_expression_bounds_walker.py | 295 ++++++++++++++++++ 2 files changed, 308 insertions(+), 9 deletions(-) create mode 100644 pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py diff --git a/pyomo/contrib/fbbt/expression_bounds_walker.py b/pyomo/contrib/fbbt/expression_bounds_walker.py index ec50385784e..3665f5bace7 100644 --- a/pyomo/contrib/fbbt/expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/expression_bounds_walker.py @@ -10,6 +10,7 @@ # ___________________________________________________________________________ from collections import defaultdict +from math import pi from pyomo.common.collections import ComponentMap from pyomo.contrib.fbbt.interval import ( add, @@ -101,20 +102,23 @@ def _before_native(visitor, child): @staticmethod def _before_string(visitor, child): raise ValueError( - f"Cannot compute bounds on expression containing {child!r} " - "of type {type(child)}, which is not a valid numeric type") + f"{child!r} ({type(child)}) is not a valid numeric type. " + f"Cannot compute bounds on expression." + ) @staticmethod def _before_invalid(visitor, child): raise ValueError( - f"Cannot compute bounds on expression containing {child!r} " - "of type {type(child)}, which is not a valid numeric type") + f"{child!r} ({type(child)}) is not a valid numeric type. " + f"Cannot compute bounds on expression." + ) @staticmethod def _before_complex(visitor, child): raise ValueError( - f"Cannot compute bounds on expression containing " - "complex numbers. Encountered when processing {child!r}") + f"Cannot compute bounds on expressions containing " + f"complex numbers. Encountered when processing {child}" + ) @staticmethod def _before_npv(visitor, child): @@ -171,15 +175,15 @@ def _handle_tan(visitor, node, arg): def _handle_asin(visitor, node, arg): - return asin(*arg, -inf, inf, visitor.feasibility_tol) + return asin(*arg, -pi/2, pi/2, visitor.feasibility_tol) def _handle_acos(visitor, node, arg): - return acos(*arg, -inf, inf, visitor.feasibility_tol) + return acos(*arg, 0, pi, visitor.feasibility_tol) def _handle_atan(visitor, node, arg): - return atan(*arg, -inf, inf) + return atan(*arg, -pi/2, pi/2) def _handle_sqrt(visitor, node, arg): diff --git a/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py b/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py new file mode 100644 index 00000000000..adc0754d83e --- /dev/null +++ b/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py @@ -0,0 +1,295 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import math +from pyomo.environ import exp, log, log10, sin, cos, tan, asin, acos, atan, sqrt +import pyomo.common.unittest as unittest +from pyomo.contrib.fbbt.expression_bounds_walker import ExpressionBoundsVisitor +from pyomo.core import Any, ConcreteModel, Expression, Param, Var + + +class TestExpressionBoundsWalker(unittest.TestCase): + def make_model(self): + m = ConcreteModel() + m.x = Var(bounds=(-2, 4)) + m.y = Var(bounds=(3, 5)) + m.z = Var(bounds=(0.5, 0.75)) + return m + + def test_sum_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.x + m.y) + self.assertEqual(lb, 1) + self.assertEqual(ub, 9) + + self.assertEqual(len(visitor.leaf_bounds), 2) + self.assertIn(m.x, visitor.leaf_bounds) + self.assertIn(m.y, visitor.leaf_bounds) + self.assertEqual(visitor.leaf_bounds[m.x], (-2, 4)) + self.assertEqual(visitor.leaf_bounds[m.y], (3, 5)) + + def test_fixed_var(self): + m = self.make_model() + m.x.fix(3) + + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.x + m.y) + self.assertEqual(lb, 1) + self.assertEqual(ub, 9) + + self.assertEqual(len(visitor.leaf_bounds), 2) + self.assertIn(m.x, visitor.leaf_bounds) + self.assertIn(m.y, visitor.leaf_bounds) + self.assertEqual(visitor.leaf_bounds[m.x], (-2, 4)) + self.assertEqual(visitor.leaf_bounds[m.y], (3, 5)) + + def test_fixed_var_value_used_for_bounds(self): + m = self.make_model() + m.x.fix(3) + + visitor = ExpressionBoundsVisitor(use_fixed_var_values_as_bounds=True) + lb, ub = visitor.walk_expression(m.x + m.y) + self.assertEqual(lb, 6) + self.assertEqual(ub, 8) + + self.assertEqual(len(visitor.leaf_bounds), 2) + self.assertIn(m.x, visitor.leaf_bounds) + self.assertIn(m.y, visitor.leaf_bounds) + self.assertEqual(visitor.leaf_bounds[m.x], (3, 3)) + self.assertEqual(visitor.leaf_bounds[m.y], (3, 5)) + + def test_product_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.x * m.y) + self.assertEqual(lb, -10) + self.assertEqual(ub, 20) + + def test_division_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.x / m.y) + self.assertAlmostEqual(lb, -2 / 3) + self.assertAlmostEqual(ub, 4 / 3) + + def test_power_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.y**m.x) + self.assertEqual(lb, 5 ** (-2)) + self.assertEqual(ub, 5**4) + + def test_negation_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(-(m.y + 3 * m.x)) + self.assertEqual(lb, -17) + self.assertEqual(ub, 3) + + def test_exp_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(exp(m.y)) + self.assertAlmostEqual(lb, math.e**3) + self.assertAlmostEqual(ub, math.e**5) + + def test_log_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(log(m.y)) + self.assertAlmostEqual(lb, log(3)) + self.assertAlmostEqual(ub, log(5)) + + def test_log10_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(log10(m.y)) + self.assertAlmostEqual(lb, log10(3)) + self.assertAlmostEqual(ub, log10(5)) + + def test_sin_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(sin(m.y)) + self.assertAlmostEqual(lb, -1) # reaches -1 at 3*pi/2 \approx 4.712 + self.assertAlmostEqual(ub, sin(3)) # it's positive here + + def test_cos_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(cos(m.y)) + self.assertAlmostEqual(lb, -1) # reaches -1 at pi + self.assertAlmostEqual(ub, cos(5)) # it's positive here + + def test_tan_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(tan(m.y)) + self.assertEqual(lb, -float('inf')) + self.assertEqual(ub, float('inf')) + + def test_asin_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(asin(m.z)) + self.assertAlmostEqual(lb, asin(0.5)) + self.assertAlmostEqual(ub, asin(0.75)) + + def test_acos_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(acos(m.z)) + self.assertAlmostEqual(lb, acos(0.75)) + self.assertAlmostEqual(ub, acos(0.5)) + + def test_atan_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(atan(m.z)) + self.assertAlmostEqual(lb, atan(0.5)) + self.assertAlmostEqual(ub, atan(0.75)) + + def test_sqrt_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(sqrt(m.y)) + self.assertAlmostEqual(lb, sqrt(3)) + self.assertAlmostEqual(ub, sqrt(5)) + + def test_abs_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(abs(m.x)) + self.assertEqual(lb, 0) + self.assertEqual(ub, 4) + + def test_leaf_bounds_cached(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.x - m.y) + self.assertEqual(lb, -7) + self.assertEqual(ub, 1) + + self.assertIn(m.x, visitor.leaf_bounds) + self.assertEqual(visitor.leaf_bounds[m.x], m.x.bounds) + self.assertIn(m.y, visitor.leaf_bounds) + self.assertEqual(visitor.leaf_bounds[m.y], m.y.bounds) + + # This should exercise the code that uses the cache. + lb, ub = visitor.walk_expression(m.x**2 + 3) + self.assertEqual(lb, 3) + self.assertEqual(ub, 19) + + def test_var_fixed_to_None(self): + m = self.make_model() + m.x.fix(None) + + visitor = ExpressionBoundsVisitor(use_fixed_var_values_as_bounds=True) + with self.assertRaisesRegex( + ValueError, + "Var 'x' is fixed to None. This value cannot be " + "used to calculate bounds.", + ): + lb, ub = visitor.walk_expression(m.x - m.y) + + def test_var_with_no_lb(self): + m = self.make_model() + m.x.setlb(None) + + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.x - m.y) + self.assertEqual(lb, -float('inf')) + self.assertEqual(ub, 1) + + def test_var_with_no_ub(self): + m = self.make_model() + m.y.setub(None) + + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.x - m.y) + self.assertEqual(lb, -float('inf')) + self.assertEqual(ub, 1) + + def test_param(self): + m = self.make_model() + m.p = Param(initialize=6) + + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.p**m.y) + self.assertEqual(lb, 6**3) + self.assertEqual(ub, 6**5) + + def test_mutable_param(self): + m = self.make_model() + m.p = Param(initialize=6, mutable=True) + + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.p**m.y) + self.assertEqual(lb, 6**3) + self.assertEqual(ub, 6**5) + + def test_named_expression(self): + m = self.make_model() + m.e = Expression(expr=sqrt(m.x**2 + m.y**2)) + visitor = ExpressionBoundsVisitor() + + lb, ub = visitor.walk_expression(m.e + 4) + self.assertEqual(lb, 7) + self.assertAlmostEqual(ub, sqrt(41) + 4) + + self.assertIn(m.e, visitor.leaf_bounds) + self.assertEqual(visitor.leaf_bounds[m.e][0], 3) + self.assertAlmostEqual(visitor.leaf_bounds[m.e][1], sqrt(41)) + + # exercise the using of the cached bounds + lb, ub = visitor.walk_expression(m.e) + self.assertEqual(lb, 3) + self.assertAlmostEqual(ub, sqrt(41)) + + def test_npv_expression(self): + m = self.make_model() + m.p = Param(initialize=4, mutable=True) + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(1/m.p) + self.assertEqual(lb, 0.25) + self.assertEqual(ub, 0.25) + + def test_invalid_numeric_type(self): + m = self.make_model() + m.p = Param(initialize=True, domain=Any) + visitor = ExpressionBoundsVisitor() + with self.assertRaisesRegex( + ValueError, + r"True \(\) is not a valid numeric type. " + r"Cannot compute bounds on expression."): + lb, ub = visitor.walk_expression(m.p + m.y) + + def test_invalid_string(self): + m = self.make_model() + m.p = Param(initialize='True', domain=Any) + visitor = ExpressionBoundsVisitor() + with self.assertRaisesRegex( + ValueError, + r"'True' \(\) is not a valid numeric type. " + r"Cannot compute bounds on expression."): + lb, ub = visitor.walk_expression(m.p + m.y) + + def test_invalid_complex(self): + m = self.make_model() + m.p = Param(initialize=complex(4, 5), domain=Any) + visitor = ExpressionBoundsVisitor() + with self.assertRaisesRegex( + ValueError, + r"Cannot compute bounds on expressions containing " + r"complex numbers. Encountered when processing \(4\+5j\)" + ): + lb, ub = visitor.walk_expression(m.p + m.y) From e6e70a380742b21f03fd97c709f5c67448e4cf38 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 27 Oct 2023 13:50:08 -0600 Subject: [PATCH 26/36] Running black --- .../contrib/fbbt/expression_bounds_walker.py | 8 ++++--- .../tests/test_expression_bounds_walker.py | 22 ++++++++++--------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/fbbt/expression_bounds_walker.py b/pyomo/contrib/fbbt/expression_bounds_walker.py index 3665f5bace7..ef4a398debe 100644 --- a/pyomo/contrib/fbbt/expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/expression_bounds_walker.py @@ -48,6 +48,7 @@ inf = float('inf') + class ExpressionBoundsBeforeChildDispatcher(BeforeChildDispatcher): __slots__ = () @@ -124,9 +125,10 @@ def _before_complex(visitor, child): def _before_npv(visitor, child): return False, (value(child), value(child)) + _before_child_handlers = ExpressionBoundsBeforeChildDispatcher() - + def _handle_ProductExpression(visitor, node, arg1, arg2): return mul(*arg1, *arg2) @@ -175,7 +177,7 @@ def _handle_tan(visitor, node, arg): def _handle_asin(visitor, node, arg): - return asin(*arg, -pi/2, pi/2, visitor.feasibility_tol) + return asin(*arg, -pi / 2, pi / 2, visitor.feasibility_tol) def _handle_acos(visitor, node, arg): @@ -183,7 +185,7 @@ def _handle_acos(visitor, node, arg): def _handle_atan(visitor, node, arg): - return atan(*arg, -pi/2, pi/2) + return atan(*arg, -pi / 2, pi / 2) def _handle_sqrt(visitor, node, arg): diff --git a/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py b/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py index adc0754d83e..9f991d5849a 100644 --- a/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py @@ -259,7 +259,7 @@ def test_npv_expression(self): m = self.make_model() m.p = Param(initialize=4, mutable=True) visitor = ExpressionBoundsVisitor() - lb, ub = visitor.walk_expression(1/m.p) + lb, ub = visitor.walk_expression(1 / m.p) self.assertEqual(lb, 0.25) self.assertEqual(ub, 0.25) @@ -268,9 +268,10 @@ def test_invalid_numeric_type(self): m.p = Param(initialize=True, domain=Any) visitor = ExpressionBoundsVisitor() with self.assertRaisesRegex( - ValueError, - r"True \(\) is not a valid numeric type. " - r"Cannot compute bounds on expression."): + ValueError, + r"True \(\) is not a valid numeric type. " + r"Cannot compute bounds on expression.", + ): lb, ub = visitor.walk_expression(m.p + m.y) def test_invalid_string(self): @@ -278,9 +279,10 @@ def test_invalid_string(self): m.p = Param(initialize='True', domain=Any) visitor = ExpressionBoundsVisitor() with self.assertRaisesRegex( - ValueError, - r"'True' \(\) is not a valid numeric type. " - r"Cannot compute bounds on expression."): + ValueError, + r"'True' \(\) is not a valid numeric type. " + r"Cannot compute bounds on expression.", + ): lb, ub = visitor.walk_expression(m.p + m.y) def test_invalid_complex(self): @@ -288,8 +290,8 @@ def test_invalid_complex(self): m.p = Param(initialize=complex(4, 5), domain=Any) visitor = ExpressionBoundsVisitor() with self.assertRaisesRegex( - ValueError, - r"Cannot compute bounds on expressions containing " - r"complex numbers. Encountered when processing \(4\+5j\)" + ValueError, + r"Cannot compute bounds on expressions containing " + r"complex numbers. Encountered when processing \(4\+5j\)", ): lb, ub = visitor.walk_expression(m.p + m.y) From 6590c4e6b40ba3e5307aa78516d24f6a047fc6d4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 27 Oct 2023 13:55:34 -0600 Subject: [PATCH 27/36] Whoops, more black --- pyomo/gdp/plugins/bigm_mixin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index 5209dad0860..6e8eca172d4 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -105,11 +105,12 @@ def _get_bigM_arg_list(self, bigm_args, block): return arg_list def _set_up_expr_bound_visitor(self): - #bnds_dict = ComponentMap() + # bnds_dict = ComponentMap() # we assume the default config arg for 'assume_fixed_vars_permanent,` # and we will change it during apply_to if we need to self._expr_bound_visitor = ExpressionBoundsVisitor( - use_fixed_var_values_as_bounds=False) + use_fixed_var_values_as_bounds=False + ) def _process_M_value( self, From b4503bb545b73d8c01442926a61a0679d2b8cec2 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 27 Oct 2023 14:19:04 -0600 Subject: [PATCH 28/36] Removing a lot of unnecessary changes in FBBT leaf-to-root walker --- pyomo/contrib/fbbt/fbbt.py | 80 ++++++++------------------- pyomo/contrib/fbbt/tests/test_fbbt.py | 1 - 2 files changed, 22 insertions(+), 59 deletions(-) diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index 0fd6da8ac50..b78d69547dc 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -274,7 +274,6 @@ def _prop_bnds_leaf_to_root_atan(visitor, node, arg): ---------- visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - """ bnds_dict = visitor.bnds_dict bnds_dict[node] = interval.atan(*bnds_dict[arg], -interval.inf, interval.inf) @@ -304,18 +303,21 @@ def _prop_no_bounds(visitor, node, *args): visitor.bnds_dict[node] = (-interval.inf, interval.inf) -_unary_leaf_to_root_map = defaultdict(lambda: _prop_no_bounds) -_unary_leaf_to_root_map['exp'] = _prop_bnds_leaf_to_root_exp -_unary_leaf_to_root_map['log'] = _prop_bnds_leaf_to_root_log -_unary_leaf_to_root_map['log10'] = _prop_bnds_leaf_to_root_log10 -_unary_leaf_to_root_map['sin'] = _prop_bnds_leaf_to_root_sin -_unary_leaf_to_root_map['cos'] = _prop_bnds_leaf_to_root_cos -_unary_leaf_to_root_map['tan'] = _prop_bnds_leaf_to_root_tan -_unary_leaf_to_root_map['asin'] = _prop_bnds_leaf_to_root_asin -_unary_leaf_to_root_map['acos'] = _prop_bnds_leaf_to_root_acos -_unary_leaf_to_root_map['atan'] = _prop_bnds_leaf_to_root_atan -_unary_leaf_to_root_map['sqrt'] = _prop_bnds_leaf_to_root_sqrt -_unary_leaf_to_root_map['abs'] = _prop_bnds_leaf_to_root_abs +_unary_leaf_to_root_map = defaultdict( + lambda: _prop_no_bounds, + { + 'exp': _prop_bnds_leaf_to_root_exp, + 'log': _prop_bnds_leaf_to_root_log, + 'log10': _prop_bnds_leaf_to_root_log10, + 'sin': _prop_bnds_leaf_to_root_sin, + 'cos': _prop_bnds_leaf_to_root_cos, + 'tan': _prop_bnds_leaf_to_root_tan, + 'asin': _prop_bnds_leaf_to_root_asin, + 'acos': _prop_bnds_leaf_to_root_acos, + 'atan': _prop_bnds_leaf_to_root_atan, + 'sqrt': _prop_bnds_leaf_to_root_sqrt, + 'abs': _prop_bnds_leaf_to_root_abs, + }) def _prop_bnds_leaf_to_root_UnaryFunctionExpression(visitor, node, arg): @@ -343,16 +345,12 @@ def _prop_bnds_leaf_to_root_GeneralExpression(visitor, node, expr): bnds_dict = visitor.bnds_dict if node in bnds_dict: return - elif node in visitor.leaf_bnds_dict: - bnds_dict[node] = visitor.leaf_bnds_dict[node] - return if expr.__class__ in native_types: expr_lb = expr_ub = expr else: expr_lb, expr_ub = bnds_dict[expr] bnds_dict[node] = (expr_lb, expr_ub) - visitor.leaf_bnds_dict[node] = (expr_lb, expr_ub) _prop_bnds_leaf_to_root_map = defaultdict(lambda: _prop_no_bounds) @@ -991,20 +989,14 @@ def _check_and_reset_bounds(var, lb, ub): def _before_constant(visitor, child): if child in visitor.bnds_dict: pass - elif child in visitor.leaf_bnds_dict: - visitor.bnds_dict[child] = visitor.leaf_bnds_dict[child] else: visitor.bnds_dict[child] = (child, child) - visitor.leaf_bnds_dict[child] = (child, child) return False, None def _before_var(visitor, child): if child in visitor.bnds_dict: return False, None - elif child in visitor.leaf_bnds_dict: - visitor.bnds_dict[child] = visitor.leaf_bnds_dict[child] - return False, None elif child.is_fixed() and not visitor.ignore_fixed: lb = value(child.value) ub = lb @@ -1021,19 +1013,14 @@ def _before_var(visitor, child): 'upper bound: {0}'.format(str(child)) ) visitor.bnds_dict[child] = (lb, ub) - visitor.leaf_bnds_dict[child] = (lb, ub) return False, None def _before_NPV(visitor, child): if child in visitor.bnds_dict: return False, None - if child in visitor.leaf_bnds_dict: - visitor.bnds_dict[child] = visitor.leaf_bnds_dict[child] - return False, None val = value(child) visitor.bnds_dict[child] = (val, val) - visitor.leaf_bnds_dict[child] = (val, val) return False, None @@ -1075,12 +1062,12 @@ class _FBBTVisitorLeafToRoot(StreamBasedExpressionVisitor): the expression tree (all the way to the root node). """ - def __init__(self, integer_tol=1e-4, feasibility_tol=1e-8, ignore_fixed=False): + def __init__(self, bnds_dict, integer_tol=1e-4, feasibility_tol=1e-8, + ignore_fixed=False): """ Parameters ---------- - leaf_bnds_dict: ComponentMap, if you want to cache leaf-node bounds - bnds_dict: ComponentMap, if you want to cache non-leaf bounds + bnds_dict: ComponentMap integer_tol: float feasibility_tol: float If the bounds computed on the body of a constraint violate the bounds of @@ -1091,9 +1078,7 @@ def __init__(self, integer_tol=1e-4, feasibility_tol=1e-8, ignore_fixed=False): to prevent math domain errors (a larger value is more conservative). """ super().__init__() - # self.bnds_dict = bnds_dict if bnds_dict is not None else ComponentMap() - # self.leaf_bnds_dict = leaf_bnds_dict if leaf_bnds_dict is not None else \ - # ComponentMap() + self.bnds_dict = bnds_dict self.integer_tol = integer_tol self.feasibility_tol = feasibility_tol self.ignore_fixed = ignore_fixed @@ -1110,28 +1095,6 @@ def beforeChild(self, node, child, child_idx): def exitNode(self, node, data): _prop_bnds_leaf_to_root_map[node.__class__](self, node, *node.args) - def walk_expression(self, expr, bnds_dict=None, leaf_bnds_dict=None): - try: - self.bnds_dict = bnds_dict if bnds_dict is not None else ComponentMap() - self.leaf_bnds_dict = ( - leaf_bnds_dict if leaf_bnds_dict is not None else ComponentMap() - ) - super().walk_expression(expr) - result = self.bnds_dict[expr] - finally: - if bnds_dict is None: - self.bnds_dict.clear() - if leaf_bnds_dict is None: - self.leaf_bnds_dict.clear() - return result - - -# class FBBTVisitorLeafToRoot(_FBBTVisitorLeafToRoot): -# def __init__(self, leaf_bnds_dict, bnds_dict=None, integer_tol=1e-4, -# feasibility_tol=1e-8, ignore_fixed=False): -# if bnds_dict is None: -# bnds_dict = {} - class _FBBTVisitorRootToLeaf(ExpressionValueVisitor): """ @@ -1300,8 +1263,9 @@ def _fbbt_con(con, config): ) # 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(feasibility_tol=config.feasibility_tol) - visitorA.walk_expression(con.body, bnds_dict=bnds_dict) + visitorA = _FBBTVisitorLeafToRoot(bnds_dict=bnds_dict, + feasibility_tol=config.feasibility_tol) + visitorA.walk_expression(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 diff --git a/pyomo/contrib/fbbt/tests/test_fbbt.py b/pyomo/contrib/fbbt/tests/test_fbbt.py index 7fa17bfbb9a..5e8d656eeab 100644 --- a/pyomo/contrib/fbbt/tests/test_fbbt.py +++ b/pyomo/contrib/fbbt/tests/test_fbbt.py @@ -1110,7 +1110,6 @@ def test_compute_expr_bounds(self): m.y = pyo.Var(bounds=(-1, 1)) e = m.x + m.y lb, ub = compute_bounds_on_expr(e) - print(lb, ub) self.assertAlmostEqual(lb, -2, 14) self.assertAlmostEqual(ub, 2, 14) From 417dec0d2ae1b23f620bc2d50b65b5b4528ed892 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 27 Oct 2023 14:19:15 -0600 Subject: [PATCH 29/36] Removing unused import --- pyomo/contrib/fbbt/expression_bounds_walker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/fbbt/expression_bounds_walker.py b/pyomo/contrib/fbbt/expression_bounds_walker.py index ef4a398debe..b16016aa630 100644 --- a/pyomo/contrib/fbbt/expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/expression_bounds_walker.py @@ -9,7 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from collections import defaultdict from math import pi from pyomo.common.collections import ComponentMap from pyomo.contrib.fbbt.interval import ( From d77405618ca7c7a7a02ff514b6c4914164212726 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 27 Oct 2023 14:19:33 -0600 Subject: [PATCH 30/36] NFC: black --- pyomo/contrib/fbbt/fbbt.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index b78d69547dc..f6dd04d1c15 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -317,7 +317,8 @@ def _prop_no_bounds(visitor, node, *args): 'atan': _prop_bnds_leaf_to_root_atan, 'sqrt': _prop_bnds_leaf_to_root_sqrt, 'abs': _prop_bnds_leaf_to_root_abs, - }) + }, +) def _prop_bnds_leaf_to_root_UnaryFunctionExpression(visitor, node, arg): @@ -1062,8 +1063,9 @@ class _FBBTVisitorLeafToRoot(StreamBasedExpressionVisitor): the expression tree (all the way to the root node). """ - def __init__(self, bnds_dict, integer_tol=1e-4, feasibility_tol=1e-8, - ignore_fixed=False): + def __init__( + self, bnds_dict, integer_tol=1e-4, feasibility_tol=1e-8, ignore_fixed=False + ): """ Parameters ---------- @@ -1263,8 +1265,9 @@ def _fbbt_con(con, config): ) # 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=bnds_dict, - feasibility_tol=config.feasibility_tol) + visitorA = _FBBTVisitorLeafToRoot( + bnds_dict=bnds_dict, feasibility_tol=config.feasibility_tol + ) visitorA.walk_expression(con.body) # Now we need to replace the bounds in bnds_dict for the root From 00723ab926aa6491264f7601adf53c664dfbded0 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 27 Oct 2023 14:30:43 -0600 Subject: [PATCH 31/36] Building the leaf-to-root handler defaultdict all at once --- pyomo/contrib/fbbt/fbbt.py | 49 +++++++++++++------------------------- 1 file changed, 16 insertions(+), 33 deletions(-) diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index f6dd04d1c15..b4d11829ca7 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -354,39 +354,22 @@ def _prop_bnds_leaf_to_root_GeneralExpression(visitor, node, expr): bnds_dict[node] = (expr_lb, expr_ub) -_prop_bnds_leaf_to_root_map = defaultdict(lambda: _prop_no_bounds) -_prop_bnds_leaf_to_root_map[ - numeric_expr.ProductExpression -] = _prop_bnds_leaf_to_root_ProductExpression -_prop_bnds_leaf_to_root_map[ - numeric_expr.DivisionExpression -] = _prop_bnds_leaf_to_root_DivisionExpression -_prop_bnds_leaf_to_root_map[ - numeric_expr.PowExpression -] = _prop_bnds_leaf_to_root_PowExpression -_prop_bnds_leaf_to_root_map[ - numeric_expr.SumExpression -] = _prop_bnds_leaf_to_root_SumExpression -_prop_bnds_leaf_to_root_map[ - numeric_expr.MonomialTermExpression -] = _prop_bnds_leaf_to_root_ProductExpression -_prop_bnds_leaf_to_root_map[ - numeric_expr.NegationExpression -] = _prop_bnds_leaf_to_root_NegationExpression -_prop_bnds_leaf_to_root_map[ - numeric_expr.UnaryFunctionExpression -] = _prop_bnds_leaf_to_root_UnaryFunctionExpression -_prop_bnds_leaf_to_root_map[ - numeric_expr.LinearExpression -] = _prop_bnds_leaf_to_root_SumExpression -_prop_bnds_leaf_to_root_map[numeric_expr.AbsExpression] = _prop_bnds_leaf_to_root_abs - -_prop_bnds_leaf_to_root_map[ - _GeneralExpressionData -] = _prop_bnds_leaf_to_root_GeneralExpression -_prop_bnds_leaf_to_root_map[ - ScalarExpression -] = _prop_bnds_leaf_to_root_GeneralExpression +_prop_bnds_leaf_to_root_map = defaultdict( + lambda: _prop_no_bounds, + { + numeric_expr.ProductExpression: _prop_bnds_leaf_to_root_ProductExpression, + numeric_expr.DivisionExpression: _prop_bnds_leaf_to_root_DivisionExpression, + numeric_expr.PowExpression: _prop_bnds_leaf_to_root_PowExpression, + numeric_expr.SumExpression: _prop_bnds_leaf_to_root_SumExpression, + numeric_expr.MonomialTermExpression: _prop_bnds_leaf_to_root_ProductExpression, + numeric_expr.NegationExpression: _prop_bnds_leaf_to_root_NegationExpression, + numeric_expr.UnaryFunctionExpression: _prop_bnds_leaf_to_root_UnaryFunctionExpression, + numeric_expr.LinearExpression: _prop_bnds_leaf_to_root_SumExpression, + numeric_expr.AbsExpression: _prop_bnds_leaf_to_root_abs, + _GeneralExpressionData: _prop_bnds_leaf_to_root_GeneralExpression, + ScalarExpression: _prop_bnds_leaf_to_root_GeneralExpression, + }, +) def _prop_bnds_root_to_leaf_ProductExpression(node, bnds_dict, feasibility_tol): From ca6c4803686cdd3e7dce56055088c66432650c62 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 27 Oct 2023 14:35:23 -0600 Subject: [PATCH 32/36] NFC: formatting --- pyomo/contrib/fbbt/fbbt.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index b4d11829ca7..dbdd992b9c8 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -1248,9 +1248,7 @@ def _fbbt_con(con, config): ) # 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=bnds_dict, feasibility_tol=config.feasibility_tol - ) + visitorA = _FBBTVisitorLeafToRoot(bnds_dict, feasibility_tol=config.feasibility_tol) visitorA.walk_expression(con.body) # Now we need to replace the bounds in bnds_dict for the root From b67f73aa703fd53785a347363933b6ed743a756a Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 27 Oct 2023 15:00:16 -0600 Subject: [PATCH 33/36] Removing unused things in BigM transformations, restoring state on the bounds visitor --- pyomo/gdp/plugins/bigm.py | 3 +-- pyomo/gdp/plugins/bigm_mixin.py | 3 +-- pyomo/gdp/plugins/multiple_bigm.py | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index cbc03bafb18..b960b5087ea 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -173,12 +173,11 @@ def _apply_to(self, instance, **kwds): # this map! with PauseGC(): try: - self._leaf_bnds_dict = ComponentMap() self._apply_to_impl(instance, **kwds) finally: self._restore_state() self.used_args.clear() - self._leaf_bnds_dict = ComponentMap() + self._expr_bound_visitor.leaf_bounds.clear() self._expr_bound_visitor.use_fixed_var_values_as_bounds = False def _apply_to_impl(self, instance, **kwds): diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index 6e8eca172d4..a4df641c8c6 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from pyomo.gdp import GDP_Error -from pyomo.common.collections import ComponentMap, ComponentSet +from pyomo.common.collections import ComponentSet from pyomo.contrib.fbbt.expression_bounds_walker import ExpressionBoundsVisitor import pyomo.contrib.fbbt.interval as interval from pyomo.core import Suffix @@ -105,7 +105,6 @@ def _get_bigM_arg_list(self, bigm_args, block): return arg_list def _set_up_expr_bound_visitor(self): - # bnds_dict = ComponentMap() # we assume the default config arg for 'assume_fixed_vars_permanent,` # and we will change it during apply_to if we need to self._expr_bound_visitor = ExpressionBoundsVisitor( diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index ca6a01cee52..18f159c7ca2 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -209,13 +209,12 @@ def _apply_to(self, instance, **kwds): self.used_args = ComponentMap() with PauseGC(): try: - self._leaf_bnds_dict = ComponentMap() self._apply_to_impl(instance, **kwds) finally: self._restore_state() self.used_args.clear() self._arg_list.clear() - self._leaf_bnds_dict = ComponentMap() + self._expr_bound_visitor.leaf_bounds.clear() self._expr_bound_visitor.use_fixed_var_values_as_bounds = False def _apply_to_impl(self, instance, **kwds): From f00520c977d7279254a2bc971454335cb1718490 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 27 Oct 2023 16:28:06 -0600 Subject: [PATCH 34/36] Fixing a bug where products didn't check if they should be squares --- pyomo/contrib/fbbt/expression_bounds_walker.py | 2 ++ pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/pyomo/contrib/fbbt/expression_bounds_walker.py b/pyomo/contrib/fbbt/expression_bounds_walker.py index b16016aa630..2d2f2701848 100644 --- a/pyomo/contrib/fbbt/expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/expression_bounds_walker.py @@ -129,6 +129,8 @@ def _before_npv(visitor, child): def _handle_ProductExpression(visitor, node, arg1, arg2): + if arg1 is arg2: + return power(*arg1, 2, 2, feasibility_tol=visitor.feasibility_tol) return mul(*arg1, *arg2) diff --git a/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py b/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py index 9f991d5849a..c51230155a7 100644 --- a/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py @@ -88,6 +88,14 @@ def test_power_bounds(self): self.assertEqual(lb, 5 ** (-2)) self.assertEqual(ub, 5**4) + def test_sums_of_squares_bounds(self): + m = ConcreteModel() + m.x = Var([1, 2], bounds=(-2, 6)) + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.x[1] * m.x[1] + m.x[2] * m.x[2]) + self.assertEqual(lb, 0) + self.assertEqual(ub, 72) + def test_negation_bounds(self): m = self.make_model() visitor = ExpressionBoundsVisitor() From db0cb67696342e6aee56c2eac5264bc99cfd19aa Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 27 Oct 2023 16:35:09 -0600 Subject: [PATCH 35/36] NFC: Adding a doc string with a word to the wise --- pyomo/contrib/fbbt/expression_bounds_walker.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pyomo/contrib/fbbt/expression_bounds_walker.py b/pyomo/contrib/fbbt/expression_bounds_walker.py index 2d2f2701848..35cc33522ba 100644 --- a/pyomo/contrib/fbbt/expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/expression_bounds_walker.py @@ -240,6 +240,22 @@ class ExpressionBoundsVisitor(StreamBasedExpressionVisitor): """ Walker to calculate bounds on an expression, from leaf to root, with caching of terminal node bounds (Vars and Expressions) + + NOTE: If anything changes on the model (e.g., Var bounds, fixing, mutable + Param values, etc), then you need to either create a new instance of this + walker, or clear self.leaf_bounds! + + Parameters + ---------- + leaf_bounds: ComponentMap in which to cache bounds at leaves of the expression + tree + feasibility_tol: float, feasibility tolerance for interval arithmetic + calculations + use_fixed_var_values_as_bounds: bool, whether or not to use the values of + fixed Vars as the upper and lower bounds for those Vars or to instead + ignore fixed status and use the bounds. Set to 'True' if you do not + anticipate the fixed status of Variables to change for the duration that + the computed bounds should be valid. """ def __init__( From 6dfe1917772df0a51cd060ff486d770ad1b9e298 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 3 Nov 2023 16:47:42 -0600 Subject: [PATCH 36/36] Removing a lot of unnecessary calls to value --- pyomo/contrib/fbbt/expression_bounds_walker.py | 7 ++++--- pyomo/contrib/fbbt/fbbt.py | 12 ++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/fbbt/expression_bounds_walker.py b/pyomo/contrib/fbbt/expression_bounds_walker.py index 35cc33522ba..426d30f0ee6 100644 --- a/pyomo/contrib/fbbt/expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/expression_bounds_walker.py @@ -74,8 +74,8 @@ def _before_var(visitor, child): ) leaf_bounds[child] = (child.value, child.value) else: - lb = value(child.lb) - ub = value(child.ub) + lb = child.lb + ub = child.ub if lb is None: lb = -inf if ub is None: @@ -122,7 +122,8 @@ def _before_complex(visitor, child): @staticmethod def _before_npv(visitor, child): - return False, (value(child), value(child)) + val = value(child) + return False, (val, val) _before_child_handlers = ExpressionBoundsBeforeChildDispatcher() diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index dbdd992b9c8..db33c27dd96 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -957,8 +957,8 @@ def _check_and_reset_bounds(var, lb, ub): """ This function ensures that lb is not less than var.lb and that ub is not greater than var.ub. """ - orig_lb = value(var.lb) - orig_ub = value(var.ub) + orig_lb = var.lb + orig_ub = var.ub if orig_lb is None: orig_lb = -interval.inf if orig_ub is None: @@ -985,8 +985,8 @@ def _before_var(visitor, child): lb = value(child.value) ub = lb else: - lb = value(child.lb) - ub = value(child.ub) + lb = child.lb + ub = child.ub if lb is None: lb = -interval.inf if ub is None: @@ -1339,11 +1339,11 @@ def _fbbt_block(m, config): if v.lb is None: var_lbs[v] = -interval.inf else: - var_lbs[v] = value(v.lb) + var_lbs[v] = v.lb if v.ub is None: var_ubs[v] = interval.inf else: - var_ubs[v] = value(v.ub) + var_ubs[v] = v.ub var_to_con_map[v].append(c) n_cons += 1