diff --git a/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp b/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp index 708cfd9e073..ca865d429e2 100644 --- a/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp +++ b/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp @@ -205,7 +205,7 @@ void process_fbbt_constraints(FBBTModel *model, PyomoExprTypes &expr_types, py::handle con_body; for (py::handle c : cons) { - lower_body_upper = c.attr("normalize_constraint")(); + lower_body_upper = c.attr("to_bounded_expression")(); con_lb = lower_body_upper[0]; con_body = lower_body_upper[1]; con_ub = lower_body_upper[2]; diff --git a/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp b/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp index 996bb34f564..f33060ee523 100644 --- a/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp +++ b/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp @@ -289,7 +289,7 @@ void process_lp_constraints(py::list cons, py::object writer) { py::object nonlinear_expr; PyomoExprTypes expr_types = PyomoExprTypes(); for (py::handle c : cons) { - lower_body_upper = c.attr("normalize_constraint")(); + lower_body_upper = c.attr("to_bounded_expression")(); cname = getSymbol(c, labeler); repn = generate_standard_repn( lower_body_upper[1], "compute_values"_a = false, "quadratic"_a = true); diff --git a/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp b/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp index 477bdd87aee..854262496ea 100644 --- a/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp +++ b/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp @@ -527,7 +527,7 @@ void process_nl_constraints(NLWriter *nl_writer, PyomoExprTypes &expr_types, py::handle repn_nonlinear_expr; for (py::handle c : cons) { - lower_body_upper = c.attr("normalize_constraint")(); + lower_body_upper = c.attr("to_bounded_expression")(); repn = generate_standard_repn( lower_body_upper[1], "compute_values"_a = false, "quadratic"_a = false); _const = appsi_expr_from_pyomo_expr(repn.attr("constant"), var_map, diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index defaea99dff..5a9d1da5af1 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -174,12 +174,35 @@ def __init__(self, expr=None, component=None): def __call__(self, exception=True): """Compute the value of the body of this constraint.""" - body = self.normalize_constraint()[1] + body = self.to_bounded_expression()[1] if body.__class__ not in native_numeric_types: body = value(self.body, exception=exception) return body - def normalize_constraint(self): + def to_bounded_expression(self): + """Convert this constraint to a tuple of 3 expressions (lb, body, ub) + + This method "standardizes" the expression into a 3-tuple of + expressions: (`lower_bound`, `body`, `upper_bound`). Upon + conversion, `lower_bound` and `upper_bound` are guaranteed to be + `None`, numeric constants, or fixed (not necessarily constant) + expressions. + + Note + ---- + As this method operates on the *current state* of the + expression, the any required expression manipulations (and by + extension, the result) can change after fixing / unfixing + :py:class:`Var` objects. + + Raises + ------ + + ValueError: Raised if the expression cannot be mapped to this + form (i.e., :py:class:`RangedExpression` constraints with + variable lower of upper bounds. + + """ expr = self._expr if expr.__class__ is RangedExpression: lb, body, ub = ans = expr.args @@ -217,8 +240,12 @@ def normalize_constraint(self): def body(self): """Access the body of a constraint expression.""" try: - ans = self.normalize_constraint()[1] + ans = self.to_bounded_expression()[1] except ValueError: + # It is possible that the expression is not currently valid + # (i.e., a ranged expression with a non-fixed bound). We + # will catch that exception here and - if this actually *is* + # a RangedExpression - return the body. if self._expr.__class__ is RangedExpression: _, ans, _ = self._expr.args else: @@ -229,14 +256,14 @@ def body(self): # # [JDS 6/2024: it would be nice to remove this behavior, # although possibly unnecessary, as people should use - # normalize_constraint() instead] + # to_bounded_expression() instead] return as_numeric(ans) return ans @property def lower(self): """Access the lower bound of a constraint expression.""" - ans = self.normalize_constraint()[0] + ans = self.to_bounded_expression()[0] if ans.__class__ in native_types and ans is not None: # Historically, constraint.lower was guaranteed to return a type # derived from Pyomo NumericValue (or None). Replicate that @@ -250,7 +277,7 @@ def lower(self): @property def upper(self): """Access the upper bound of a constraint expression.""" - ans = self.normalize_constraint()[2] + ans = self.to_bounded_expression()[2] if ans.__class__ in native_types and ans is not None: # Historically, constraint.upper was guaranteed to return a type # derived from Pyomo NumericValue (or None). Replicate that @@ -264,7 +291,7 @@ def upper(self): @property def lb(self): """Access the value of the lower bound of a constraint expression.""" - bound = self.normalize_constraint()[0] + bound = self.to_bounded_expression()[0] if bound is None: return None if bound.__class__ not in native_numeric_types: @@ -282,7 +309,7 @@ def lb(self): @property def ub(self): """Access the value of the upper bound of a constraint expression.""" - bound = self.normalize_constraint()[2] + bound = self.to_bounded_expression()[2] if bound is None: return None if bound.__class__ not in native_numeric_types: @@ -824,7 +851,7 @@ class SimpleConstraint(metaclass=RenamedClass): { 'add', 'set_value', - 'normalize_constraint', + 'to_bounded_expression', 'body', 'lower', 'upper', diff --git a/pyomo/core/kernel/constraint.py b/pyomo/core/kernel/constraint.py index 6b8c4c619f5..fe8eb8b2c1f 100644 --- a/pyomo/core/kernel/constraint.py +++ b/pyomo/core/kernel/constraint.py @@ -177,7 +177,7 @@ class _MutableBoundsConstraintMixin(object): # Define some of the IConstraint abstract methods # - def normalize_constraint(self): + def to_bounded_expression(self): return self.lower, self.body, self.upper @property diff --git a/pyomo/gdp/plugins/bilinear.py b/pyomo/gdp/plugins/bilinear.py index 70b6e83b52f..bc91836ea9c 100644 --- a/pyomo/gdp/plugins/bilinear.py +++ b/pyomo/gdp/plugins/bilinear.py @@ -77,7 +77,7 @@ def _transformBlock(self, block, instance): for component in block.component_data_objects( Constraint, active=True, descend_into=False ): - lb, body, ub = component.normalize_constraint() + lb, body, ub = component.to_bounded_expression() expr = self._transformExpression(body, instance) instance.bilinear_data_.c_body[id(component)] = body component.set_value((lb, expr, ub)) diff --git a/pyomo/gdp/plugins/cuttingplane.py b/pyomo/gdp/plugins/cuttingplane.py index a757f23c826..4cef098eba9 100644 --- a/pyomo/gdp/plugins/cuttingplane.py +++ b/pyomo/gdp/plugins/cuttingplane.py @@ -400,7 +400,7 @@ def back_off_constraint_with_calculated_cut_violation( val = value(transBlock_rHull.infeasibility_objective) - TOL if val <= 0: logger.info("\tBacking off cut by %s" % val) - lb, body, ub = cut.normalize_constraint() + lb, body, ub = cut.to_bounded_expression() cut.set_value((lb, body + abs(val), ub)) # else there is nothing to do: restore the objective transBlock_rHull.del_component(transBlock_rHull.infeasibility_objective) @@ -425,7 +425,7 @@ def back_off_constraint_by_fixed_tolerance( this callback TOL: An absolute tolerance to be added to make cut more conservative. """ - lb, body, ub = cut.normalize_constraint() + lb, body, ub = cut.to_bounded_expression() cut.set_value((lb, body + TOL, ub)) diff --git a/pyomo/solvers/plugins/solvers/persistent_solver.py b/pyomo/solvers/plugins/solvers/persistent_solver.py index ef96bfa339f..ef883fe5496 100644 --- a/pyomo/solvers/plugins/solvers/persistent_solver.py +++ b/pyomo/solvers/plugins/solvers/persistent_solver.py @@ -262,7 +262,7 @@ def _add_and_collect_column_data(self, var, obj_coef, constraints, coefficients) coeff_list = list() constr_list = list() for val, c in zip(coefficients, constraints): - lb, body, ub = c.normalize_constraint() + lb, body, ub = c.to_bounded_expression() body += val * var c.set_value((lb, body, ub)) self._vars_referenced_by_con[c].add(var)