Skip to content

Commit

Permalink
Merge pull request Pyomo#3343 from jsiirola/writer-constraint-perf
Browse files Browse the repository at this point in the history
Resolve writer performance degradation
  • Loading branch information
jsiirola authored Aug 13, 2024
2 parents 404fd6d + 3f6bc13 commit a777d7a
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 78 deletions.
74 changes: 38 additions & 36 deletions pyomo/core/base/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
------
Expand Down Expand Up @@ -226,15 +233,36 @@ 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:
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):
Expand Down Expand Up @@ -291,38 +319,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):
Expand Down
14 changes: 11 additions & 3 deletions pyomo/core/kernel/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand All @@ -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"""
Expand Down
5 changes: 5 additions & 0 deletions pyomo/repn/beta/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
49 changes: 27 additions & 22 deletions pyomo/repn/plugins/baron_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,23 +256,24 @@ 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):
if name in {'branching_priorities', 'priority'}:
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"
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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('.')
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand Down
15 changes: 8 additions & 7 deletions pyomo/repn/plugins/gams_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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))
Expand Down
10 changes: 5 additions & 5 deletions pyomo/repn/plugins/lp_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,18 +408,18 @@ 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)
# constraints in LP format. I suppose we could add a
# 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 "
Expand Down
10 changes: 5 additions & 5 deletions pyomo/repn/plugins/nl_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit a777d7a

Please sign in to comment.