From d864d2fc882c4c7104c542075aff49ec7ff924f4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 00:32:46 -0600 Subject: [PATCH 1/5] Remove repeated code --- pyomo/core/base/constraint.py | 45 +++++++++++++---------------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index bc9a32f5404..8c7921b060f 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -236,6 +236,21 @@ def to_bounded_expression(self): return 0 if expr.__class__ is EqualityExpression else None, lhs - rhs, 0 return None, None, None + def _evaluate_bound(self, bound, is_lb): + if bound is None: + return None + if bound.__class__ not in native_numeric_types: + bound = float(value(bound)) + # Note that "bound != bound" catches float('nan') + if bound in _nonfinite_values or bound != bound: + if bound == (-_inf if is_lb else _inf): + return None + raise ValueError( + f"Constraint '{self.name}' created with an invalid non-finite " + f"{'lower' if is_lb else 'upper'} bound ({bound})." + ) + return bound + @property def body(self): """Access the body of a constraint expression.""" @@ -291,38 +306,12 @@ def upper(self): @property def lb(self): """Access the value of the lower bound of a constraint expression.""" - bound = self.to_bounded_expression()[0] - if bound is None: - return None - if bound.__class__ not in native_numeric_types: - bound = float(value(bound)) - # Note that "bound != bound" catches float('nan') - if bound in _nonfinite_values or bound != bound: - if bound == -_inf: - return None - raise ValueError( - f"Constraint '{self.name}' created with an invalid non-finite " - f"lower bound ({bound})." - ) - return bound + return self._evaluate_bound(self.to_bounded_expression()[0], True) @property def ub(self): """Access the value of the upper bound of a constraint expression.""" - bound = self.to_bounded_expression()[2] - if bound is None: - return None - if bound.__class__ not in native_numeric_types: - bound = float(value(bound)) - # Note that "bound != bound" catches float('nan') - if bound in _nonfinite_values or bound != bound: - if bound == _inf: - return None - raise ValueError( - f"Constraint '{self.name}' created with an invalid non-finite " - f"upper bound ({bound})." - ) - return bound + return self._evaluate_bound(self.to_bounded_expression()[2], False) @property def equality(self): From 58da384ff9853cc2f70ef1e1f6e6ca2b02365762 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 00:33:19 -0600 Subject: [PATCH 2/5] Add option to Constraint.to_bounded_expression() to evaluate the bounds --- pyomo/core/base/constraint.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 8c7921b060f..f0e020bcfd0 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -179,7 +179,7 @@ def __call__(self, exception=True): body = value(self.body, exception=exception) return body - def to_bounded_expression(self): + def to_bounded_expression(self, evaluate_bounds=False): """Convert this constraint to a tuple of 3 expressions (lb, body, ub) This method "standardizes" the expression into a 3-tuple of @@ -195,6 +195,13 @@ def to_bounded_expression(self): extension, the result) can change after fixing / unfixing :py:class:`Var` objects. + Parameters + ---------- + evaluate_bounds: bool + + If True, then the lower and upper bounds will be evaluated + to a finite numeric constant or None. + Raises ------ @@ -226,15 +233,21 @@ def to_bounded_expression(self): "variable upper bound. Cannot normalize the " "constraint or send it to a solver." ) - return ans - elif expr is not None: + elif expr is None: + ans = None, None, None + else: lhs, rhs = expr.args if rhs.__class__ in native_types or not rhs.is_potentially_variable(): - return rhs if expr.__class__ is EqualityExpression else None, lhs, rhs - if lhs.__class__ in native_types or not lhs.is_potentially_variable(): - return lhs, rhs, lhs if expr.__class__ is EqualityExpression else None - return 0 if expr.__class__ is EqualityExpression else None, lhs - rhs, 0 - return None, None, None + ans = rhs if expr.__class__ is EqualityExpression else None, lhs, rhs + elif lhs.__class__ in native_types or not lhs.is_potentially_variable(): + ans = lhs, rhs, lhs if expr.__class__ is EqualityExpression else None + else: + ans = 0 if expr.__class__ is EqualityExpression else None, lhs - rhs, 0 + + if evaluate_bounds: + lb, body, ub = ans + return self._evaluate_bound(lb, True), body, self._evaluate_bound(ub, False) + return ans def _evaluate_bound(self, bound, is_lb): if bound is None: From d29d3db3acd6a56918a7136fe14cc8d4e33534ee Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 00:36:04 -0600 Subject: [PATCH 3/5] Update writers to use to_bounded_expression; recover performance loss from #3293 --- pyomo/repn/plugins/baron_writer.py | 49 ++++++++++++++++-------------- pyomo/repn/plugins/gams_writer.py | 15 ++++----- pyomo/repn/plugins/lp_writer.py | 10 +++--- pyomo/repn/plugins/nl_writer.py | 10 +++--- 4 files changed, 45 insertions(+), 39 deletions(-) diff --git a/pyomo/repn/plugins/baron_writer.py b/pyomo/repn/plugins/baron_writer.py index ab673b0c1c3..861735dc973 100644 --- a/pyomo/repn/plugins/baron_writer.py +++ b/pyomo/repn/plugins/baron_writer.py @@ -256,9 +256,9 @@ def _skip_trivial(constraint_data): suffix_gen = ( lambda b: pyomo.core.base.suffix.active_export_suffix_generator(b) ) - r_o_eqns = [] - c_eqns = [] - l_eqns = [] + r_o_eqns = {} + c_eqns = {} + l_eqns = {} branching_priorities_suffixes = [] for block in all_blocks_list: for name, suffix in suffix_gen(block): @@ -266,13 +266,14 @@ def _skip_trivial(constraint_data): branching_priorities_suffixes.append(suffix) elif name == 'constraint_types': for constraint_data, constraint_type in suffix.items(): + info = constraint_data.to_bounded_expression(True) if not _skip_trivial(constraint_data): if constraint_type.lower() == 'relaxationonly': - r_o_eqns.append(constraint_data) + r_o_eqns[constraint_data] = info elif constraint_type.lower() == 'convex': - c_eqns.append(constraint_data) + c_eqns[constraint_data] = info elif constraint_type.lower() == 'local': - l_eqns.append(constraint_data) + l_eqns[constraint_data] = info else: raise ValueError( "A suffix '%s' contained an invalid value: %s\n" @@ -294,7 +295,10 @@ def _skip_trivial(constraint_data): % (name, _location) ) - non_standard_eqns = r_o_eqns + c_eqns + l_eqns + non_standard_eqns = set() + non_standard_eqns.update(r_o_eqns) + non_standard_eqns.update(c_eqns) + non_standard_eqns.update(l_eqns) # # EQUATIONS @@ -304,7 +308,7 @@ def _skip_trivial(constraint_data): n_roeqns = len(r_o_eqns) n_ceqns = len(c_eqns) n_leqns = len(l_eqns) - eqns = [] + eqns = {} # Alias the constraints by declaration order since Baron does not # include the constraint names in the solution file. It is important @@ -321,14 +325,15 @@ def _skip_trivial(constraint_data): for constraint_data in block.component_data_objects( Constraint, active=True, sort=sorter, descend_into=False ): - if (not constraint_data.has_lb()) and (not constraint_data.has_ub()): + lb, body, ub = constraint_data.to_bounded_expression(True) + if lb is None and ub is None: assert not constraint_data.equality continue # non-binding, so skip if (not _skip_trivial(constraint_data)) and ( constraint_data not in non_standard_eqns ): - eqns.append(constraint_data) + eqns[constraint_data] = lb, body, ub con_symbol = symbol_map.createSymbol(constraint_data, c_labeler) assert not con_symbol.startswith('.') @@ -407,12 +412,12 @@ def mutable_param_gen(b): # Equation Definition output_file.write('c_e_FIX_ONE_VAR_CONST__: ONE_VAR_CONST__ == 1;\n') - for constraint_data in itertools.chain(eqns, r_o_eqns, c_eqns, l_eqns): + for constraint_data, (lb, body, ub) in itertools.chain( + eqns.items(), r_o_eqns.items(), c_eqns.items(), l_eqns.items() + ): variables = OrderedSet() # print(symbol_map.byObject.keys()) - eqn_body = expression_to_string( - constraint_data.body, variables, smap=symbol_map - ) + eqn_body = expression_to_string(body, variables, smap=symbol_map) # print(symbol_map.byObject.keys()) referenced_variable_ids.update(variables) @@ -439,22 +444,22 @@ def mutable_param_gen(b): # Equality constraint if constraint_data.equality: eqn_lhs = '' - eqn_rhs = ' == ' + ftoa(constraint_data.upper) + eqn_rhs = ' == ' + ftoa(ub) # Greater than constraint - elif not constraint_data.has_ub(): - eqn_rhs = ' >= ' + ftoa(constraint_data.lower) + elif ub is None: + eqn_rhs = ' >= ' + ftoa(lb) eqn_lhs = '' # Less than constraint - elif not constraint_data.has_lb(): - eqn_rhs = ' <= ' + ftoa(constraint_data.upper) + elif lb is None: + eqn_rhs = ' <= ' + ftoa(ub) eqn_lhs = '' # Double-sided constraint - elif constraint_data.has_lb() and constraint_data.has_ub(): - eqn_lhs = ftoa(constraint_data.lower) + ' <= ' - eqn_rhs = ' <= ' + ftoa(constraint_data.upper) + elif lb is not None and ub is not None: + eqn_lhs = ftoa(lb) + ' <= ' + eqn_rhs = ' <= ' + ftoa(ub) eqn_string = eqn_lhs + eqn_body + eqn_rhs + ';\n' output_file.write(eqn_string) diff --git a/pyomo/repn/plugins/gams_writer.py b/pyomo/repn/plugins/gams_writer.py index a0f407d7952..f0a9eb7afef 100644 --- a/pyomo/repn/plugins/gams_writer.py +++ b/pyomo/repn/plugins/gams_writer.py @@ -619,11 +619,12 @@ def _write_model( # encountered will be added to the var_list due to the labeler # defined above. for con in model.component_data_objects(Constraint, active=True, sort=sort): - if not con.has_lb() and not con.has_ub(): + lb, body, ub = con.to_bounded_expression(True) + if lb is None and ub is None: assert not con.equality continue # non-binding, so skip - con_body = as_numeric(con.body) + con_body = as_numeric(body) if skip_trivial_constraints and con_body.is_fixed(): continue if linear: @@ -642,20 +643,20 @@ def _write_model( constraint_names.append('%s' % cName) ConstraintIO.write( '%s.. %s =e= %s ;\n' - % (constraint_names[-1], con_body_str, ftoa(con.upper, False)) + % (constraint_names[-1], con_body_str, ftoa(ub, False)) ) else: - if con.has_lb(): + if lb is not None: constraint_names.append('%s_lo' % cName) ConstraintIO.write( '%s.. %s =l= %s ;\n' - % (constraint_names[-1], ftoa(con.lower, False), con_body_str) + % (constraint_names[-1], ftoa(lb, False), con_body_str) ) - if con.has_ub(): + if ub is not None: constraint_names.append('%s_hi' % cName) ConstraintIO.write( '%s.. %s =l= %s ;\n' - % (constraint_names[-1], con_body_str, ftoa(con.upper, False)) + % (constraint_names[-1], con_body_str, ftoa(ub, False)) ) obj = list(model.component_data_objects(Objective, active=True, sort=sort)) diff --git a/pyomo/repn/plugins/lp_writer.py b/pyomo/repn/plugins/lp_writer.py index 814f79a4eb9..2fbdae3571d 100644 --- a/pyomo/repn/plugins/lp_writer.py +++ b/pyomo/repn/plugins/lp_writer.py @@ -408,10 +408,10 @@ def write(self, model): if with_debug_timing and con.parent_component() is not last_parent: timer.toc('Constraint %s', last_parent, level=logging.DEBUG) last_parent = con.parent_component() - # Note: Constraint.lb/ub guarantee a return value that is - # either a (finite) native_numeric_type, or None - lb = con.lb - ub = con.ub + # Note: Constraint.to_bounded_expression(evaluate_bounds=True) + # guarantee a return value that is either a (finite) + # native_numeric_type, or None + lb, body, ub = con.to_bounded_expression(True) if lb is None and ub is None: # Note: you *cannot* output trivial (unbounded) @@ -419,7 +419,7 @@ def write(self, model): # slack variable if skip_trivial_constraints is False, # but that seems rather silly. continue - repn = constraint_visitor.walk_expression(con.body) + repn = constraint_visitor.walk_expression(body) if repn.nonlinear is not None: raise ValueError( f"Model constraint ({con.name}) contains nonlinear terms that " diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 8fc82d21d30..ca7786ce167 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -723,14 +723,14 @@ def write(self, model): timer.toc('Constraint %s', last_parent, level=logging.DEBUG) last_parent = con.parent_component() scale = scaling_factor(con) - expr_info = visitor.walk_expression((con.body, con, 0, scale)) + # Note: Constraint.to_bounded_expression(evaluate_bounds=True) + # guarantee a return value that is either a (finite) + # native_numeric_type, or None + lb, body, ub = con.to_bounded_expression(True) + expr_info = visitor.walk_expression((body, con, 0, scale)) if expr_info.named_exprs: self._record_named_expression_usage(expr_info.named_exprs, con, 0) - # Note: Constraint.lb/ub guarantee a return value that is - # either a (finite) native_numeric_type, or None - lb = con.lb - ub = con.ub if lb is None and ub is None: # and self.config.skip_trivial_constraints: continue if scale != 1: From 9f59794537d147e1d7aaa91d3e15e851be5035e6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 01:04:53 -0600 Subject: [PATCH 4/5] Fix kernel incompatibility --- pyomo/core/kernel/constraint.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pyomo/core/kernel/constraint.py b/pyomo/core/kernel/constraint.py index fe8eb8b2c1f..ed877e8af92 100644 --- a/pyomo/core/kernel/constraint.py +++ b/pyomo/core/kernel/constraint.py @@ -160,6 +160,17 @@ def has_ub(self): ub = self.ub return (ub is not None) and (value(ub) != float('inf')) + def to_bounded_expression(self, evaluate_bounds=False): + if evaluate_bounds: + lb = self.lb + if lb == -float('inf'): + lb = None + ub = self.ub + if ub == float('inf'): + ub = None + return lb, self.body, ub + return self.lower, self.body, self.upper + class _MutableBoundsConstraintMixin(object): """ @@ -177,9 +188,6 @@ class _MutableBoundsConstraintMixin(object): # Define some of the IConstraint abstract methods # - def to_bounded_expression(self): - return self.lower, self.body, self.upper - @property def lower(self): """The expression for the lower bound of the constraint""" From 3f6bc13fdb6e9f1f772ca6401b59ab4e08be180d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 08:22:45 -0600 Subject: [PATCH 5/5] Add to_bounded_expression() to LinearMatrixConstraint --- pyomo/repn/beta/matrix.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyomo/repn/beta/matrix.py b/pyomo/repn/beta/matrix.py index 0201c46eb18..992e1810fec 100644 --- a/pyomo/repn/beta/matrix.py +++ b/pyomo/repn/beta/matrix.py @@ -587,6 +587,11 @@ def constant(self): # Abstract Interface (ConstraintData) # + def to_bounded_expression(self, evaluate_bounds=False): + """Access this constraint as a single expression.""" + # Note that the bounds are always going to be floats... + return self.lower, self.body, self.upper + @property def body(self): """Access the body of a constraint expression."""