Skip to content
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
84de166
Move ScalarObjective to use AbstractScalar pattern
jsiirola Jun 9, 2025
dbd2fa0
Rework/simplify constraint & var bound evaluation
jsiirola Jun 9, 2025
a944c8b
Support templatization of scalar constraint/objective components
jsiirola Jun 9, 2025
72450b9
Disable __call__ on AbstractScalarConstraint components
jsiirola Jun 9, 2025
c59ef84
Catch exceptions when attempting to evaluate a unary function with an…
jsiirola Jun 9, 2025
9c12dbe
Promote inv2str to util as val2str
jsiirola Jun 9, 2025
049909a
Promote FIXED processing to the main linear/quadratic walkers
jsiirola Jun 9, 2025
aade470
Route checks for 0 and 1 mult coefficients through constant_flag and …
jsiirola Jun 9, 2025
3194125
overhaul, simplify, and combine parameterized linear and quadratic re…
jsiirola Jun 9, 2025
c2d028e
NFC: minor cleanup, improve comments
jsiirola Jun 9, 2025
e78870b
Silence NumPy deprecation warning
jsiirola Jun 9, 2025
52dfe87
Update linear template repn visitor
jsiirola Jun 9, 2025
df72496
Update TemplateVarRecorder to not accept var_order; correctly initial…
jsiirola Jun 9, 2025
54c88a2
Fix logging namespace in pyomo.repn.util
jsiirola Jun 9, 2025
82586ca
Catch additional errors applying operations to InvalidNumber
jsiirola Jun 9, 2025
ffca68a
Update tests to reflect changes in repn compilers
jsiirola Jun 9, 2025
a18e936
Update BigM test: int argument is now preserved
jsiirola Jun 9, 2025
6d0ca6b
Test standard form compiler for both regular and templated expressions
jsiirola Jun 9, 2025
104a8cb
Provide special-case handling for LINEAR**2
jsiirola Jun 14, 2025
874fbee
Improve use of mutable_expression context manager
jsiirola Jun 15, 2025
6045edc
Merge branch 'main' into repn-refactor
jsiirola Jun 16, 2025
6ffce1e
Merge branch 'main' into repn-refactor
jsiirola Jun 18, 2025
190d09b
NFC: apply black
jsiirola Jun 18, 2025
cac10c7
Fix copy/paste typo
jsiirola Jun 18, 2025
0bcbaa6
NFC: fix typo
jsiirola Jun 18, 2025
8eb7a10
Fix detection of TemplateRepn
jsiirola Jun 26, 2025
5d2923c
Merge branch 'main' into repn-refactor
jsiirola Jun 29, 2025
44c20d8
Allow creating parameterized vectors from iterators
jsiirola Jun 30, 2025
ef8ea2d
Catch edge case: trivial constraints with non-numeric linear constants
jsiirola Jun 30, 2025
0e4ff92
Standardize construction of CSC matricies (empty and nonempty)
jsiirola Jun 30, 2025
7ef0db4
NFC: fix typo
jsiirola Jun 30, 2025
a2ea1c3
Merge branch 'main' into repn-refactor
jsiirola Jun 30, 2025
3cadc0a
Explicitly check for InvalidNumber in linear,quadratic walkers
jsiirola Jul 1, 2025
d212669
Improve robustness of QuadraticRepn to str
jsiirola Jul 1, 2025
627e0b5
Improve linear,quadratic repn coverage
jsiirola Jul 1, 2025
41fc0d5
Apply black
mrmundt Jul 2, 2025
065448d
Merge branch 'main' into repn-refactor
jsiirola Jul 30, 2025
0bc65e1
Make Templatized objective and constraint classes more consistent
jsiirola Aug 4, 2025
4e2d87a
NFC: add/update comments
jsiirola Aug 4, 2025
67c9ef3
Merge branch 'main' into repn-refactor
jsiirola Aug 4, 2025
f8562a0
Apply black - newlines are the worst
mrmundt Aug 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyomo/contrib/pyros/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
from pyomo.core.util import prod
from pyomo.opt import SolverFactory
import pyomo.repn.ampl as pyomo_ampl_repn
from pyomo.repn.parameterized_quadratic import ParameterizedQuadraticRepnVisitor
from pyomo.repn.parameterized import ParameterizedQuadraticRepnVisitor
import pyomo.repn.plugins.nl_writer as pyomo_nl_writer
from pyomo.repn.util import OrderedVarRecorder
from pyomo.util.vars_from_expressions import get_vars_from_components
Expand Down
84 changes: 56 additions & 28 deletions pyomo/core/base/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@
TEMPLATIZE_CONSTRAINTS = False

_inf = float('inf')
_nonfinite_values = {_inf, -_inf}
_ninf = -_inf
_nonfinite_values = {_inf, _ninf}
_known_relational_expression_types = {
EqualityExpression,
InequalityExpression,
Expand Down Expand Up @@ -246,21 +247,31 @@ def to_bounded_expression(self, evaluate_bounds=False):

if evaluate_bounds:
lb, body, ub = ans
return self._evaluate_bound(lb, True), body, self._evaluate_bound(ub, False)
return self._evaluate_bound(lb, _ninf), body, self._evaluate_bound(ub, _inf)
return ans

def _evaluate_bound(self, bound, is_lb):
def _evaluate_bound(self, bound, unbounded):
if bound is None:
return None
if bound.__class__ not in native_numeric_types:
bound = float(value(bound))
bound = value(bound)
if bound.__class__ not in native_numeric_types:
# Starting in numpy 1.25, casting 1-element ndarray to
# float is deprecated. We still want to support
# that... but without enforcing a hard numpy dependence
for cls in bound.__class__.__mro__:
if cls.__name__ == 'ndarray' and cls.__module__ == 'numpy':
if len(bound) == 1:
bound = bound[0]
break
bound = float(bound)
# Note that "bound != bound" catches float('nan')
if bound in _nonfinite_values or bound != bound:
if bound == (-_inf if is_lb else _inf):
if bound == unbounded:
return None
raise ValueError(
f"Constraint '{self.name}' created with an invalid non-finite "
f"{'lower' if is_lb else 'upper'} bound ({bound})."
f"{'upper' if unbounded==_inf else 'lower'} bound ({bound})."
)
return bound

Expand Down Expand Up @@ -333,12 +344,12 @@ def upper(self):
@property
def lb(self):
"""float : the value of the lower bound of a constraint expression."""
return self._evaluate_bound(self.to_bounded_expression()[0], True)
return self._evaluate_bound(self.to_bounded_expression()[0], _ninf)

@property
def ub(self):
"""float : the value of the upper bound of a constraint expression."""
return self._evaluate_bound(self.to_bounded_expression()[2], False)
return self._evaluate_bound(self.to_bounded_expression()[2], _inf)

@property
def equality(self):
Expand Down Expand Up @@ -527,20 +538,9 @@ class _GeneralConstraintData(metaclass=RenamedClass):
__renamed__version__ = '6.7.2'


class TemplateConstraintData(ConstraintData):
class TemplateDataMixin(object):
__slots__ = ()

def __init__(self, template_info, component, index):
# These lines represent in-lining of the
# following constructors:
# - ConstraintData,
# - ActiveComponentData
# - ComponentData
self._component = component
self._active = True
self._index = index
self._expr = template_info

@property
def expr(self):
# Note that it is faster to just generate the expression from
Expand All @@ -552,17 +552,34 @@ def template_expr(self):
return self._expr

def set_value(self, expr):
self.__class__ = ConstraintData
self.__class__ = self.__class__.__mro__[
self.__class__.__mro__.index(TemplateDataMixin) + 1
]
Comment thread
jsiirola marked this conversation as resolved.
return self.set_value(expr)

def to_bounded_expression(self):
def to_bounded_expression(self, evaluate_bounds=False):
tmp, self._expr = self._expr, self._expr[0]
try:
return super().to_bounded_expression()
return super().to_bounded_expression(evaluate_bounds)
finally:
self._expr = tmp


class TemplateConstraintData(TemplateDataMixin, ConstraintData):
__slots__ = ()

def __init__(self, template_info, component, index):
# These lines represent in-lining of the
# following constructors:
# - ConstraintData,
# - ActiveComponentData
# - ComponentData
self._component = component
self._active = True
self._index = index
self._expr = template_info


@ModelComponentFactory.register("General constraint expressions.")
class Constraint(ActiveIndexedComponent):
"""
Expand Down Expand Up @@ -695,11 +712,17 @@ def construct(self, data=None):
if TEMPLATIZE_CONSTRAINTS:
try:
template_info = templatize_constraint(self)
comp = weakref_ref(self)
self._data = {
idx: TemplateConstraintData(template_info, comp, idx)
for idx in self.index_set()
}
if self.is_indexed():
comp = weakref_ref(self)
self._data = {
idx: TemplateConstraintData(template_info, comp, idx)
for idx in self.index_set()
}
else:
assert self.__class__ is ScalarConstraint
self.__class__ = TemplateScalarConstraint
self._expr = template_info
self._data = {None: self}
return
except TemplateExpressionError:
pass
Expand Down Expand Up @@ -926,6 +949,7 @@ class SimpleConstraint(metaclass=RenamedClass):

@disable_methods(
{
'__call__',
'add',
'set_value',
'to_bounded_expression',
Expand All @@ -947,6 +971,10 @@ class AbstractSimpleConstraint(metaclass=RenamedClass):
__renamed__version__ = '6.0'


class TemplateScalarConstraint(TemplateDataMixin, ScalarConstraint):
pass


class IndexedConstraint(Constraint):
#
# Leaving this method for backward compatibility reasons
Expand Down
150 changes: 72 additions & 78 deletions pyomo/core/base/objective.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from pyomo.core.expr.numvalue import value
from pyomo.core.expr.template_expr import templatize_rule
from pyomo.core.base.component import ActiveComponentData, ModelComponentFactory
from pyomo.core.base.disable_methods import disable_methods
from pyomo.core.base.global_set import UnindexedComponent_index
from pyomo.core.base.indexed_component import (
ActiveIndexedComponent,
Expand Down Expand Up @@ -165,7 +166,27 @@ class _GeneralObjectiveData(metaclass=RenamedClass):
__renamed__version__ = '6.7.2'


class TemplateObjectiveData(ObjectiveData):
class TemplateDataMixin(object):
__slots__ = ()

@property
def args(self):
# Note that it is faster to just generate the expression from
# scratch than it is to clone it and replace the IndexTemplate objects
self.set_value(self.parent_component().rule(self.parent_block(), self.index()))
return self._args_

def template_expr(self):
return self._args_

def set_value(self, expr):
self.__class__ = self.__class__.__mro__[
self.__class__.__mro__.index(TemplateDataMixin) + 1
]
Comment thread
jsiirola marked this conversation as resolved.
return self.set_value(expr)


class TemplateObjectiveData(TemplateDataMixin, ObjectiveData):
__slots__ = ()

def __init__(self, template_info, component, index, sense):
Expand Down Expand Up @@ -249,11 +270,11 @@ class Objective(ActiveIndexedComponent):

def __new__(cls, *args, **kwds):
if cls != Objective:
return super(Objective, cls).__new__(cls)
return super().__new__(cls)
if not args or (args[0] is UnindexedComponent_set and len(args) == 1):
return ScalarObjective.__new__(ScalarObjective)
return super().__new__(AbstractScalarObjective)
Comment thread
emma58 marked this conversation as resolved.
else:
return IndexedObjective.__new__(IndexedObjective)
return super().__new__(IndexedObjective)

@overload
def __init__(
Expand Down Expand Up @@ -320,13 +341,23 @@ def construct(self, data=None):
if TEMPLATIZE_OBJECTIVES:
try:
template_info = templatize_rule(block, rule, self.index_set())
comp = weakref_ref(self)
self._data = {
idx: TemplateObjectiveData(
template_info, comp, idx, self._init_sense(block, index)
)
for idx in self.index_set()
}
if self.is_indexed():
comp = weakref_ref(self)
self._data = {
idx: TemplateObjectiveData(
template_info,
comp,
idx,
self._init_sense(block, index),
)
for idx in self.index_set()
}
else:
assert self.__class__ is ScalarObjective
self.__class__ = TemplateScalarObjective
self._expr = template_info
self._data = {None: self}
self.set_sense(self._init_sense(self, self.index()))
return
except TemplateExpressionError:
pass
Expand Down Expand Up @@ -428,50 +459,27 @@ def __init__(self, *args, **kwd):
Objective.__init__(self, *args, **kwd)
self._index = UnindexedComponent_index

#
# Override abstract interface methods to first check for
# construction
#

def __call__(self, exception=NOTSET):
exception = _type_check_exception_arg(self, exception)
if self._constructed:
if len(self._data) == 0:
raise ValueError(
"Evaluating the expression of ScalarObjective "
"'%s' before the Objective has been assigned "
"a sense or expression (there is currently "
"no value to return)." % (self.name)
)
return super().__call__(exception)
raise ValueError(
"Evaluating the expression of objective '%s' "
"before the Objective has been constructed (there "
"is currently no value to return)." % (self.name)
)

@property
def expr(self):
"""Access the expression of this objective."""
if self._constructed:
if len(self._data) == 0:
raise ValueError(
"Accessing the expression of ScalarObjective "
"'%s' before the Objective has been assigned "
"a sense or expression (there is currently "
"no value to return)." % (self.name)
)
return ObjectiveData.expr.fget(self)
raise ValueError(
"Accessing the expression of objective '%s' "
"before the Objective has been constructed (there "
"is currently no value to return)." % (self.name)
)
if not self._data:
raise ValueError(
"Evaluating the expression of ScalarObjective "
"'%s' before the Objective has been assigned "
"a sense or expression (there is currently "
"no value to return)." % (self.name)
)
return super().__call__(exception)

@expr.setter
def expr(self, expr):
"""Set the expression of this objective."""
self.set_value(expr)
#
# Singleton objectives are strange in that we want them to be
# both be constructed but have len() == 0 when not initialized with
# anything (at least according to the unit tests that are
# currently in place). So during initialization only, we will
# treat them as "indexed" objects where things like
# Objective.Skip are managed. But after that they will behave
# like ObjectiveData objects where set_value does not handle
# Objective.Skip but expects a valid expression or None
#

@property
def sense(self):
Expand All @@ -496,43 +504,20 @@ def sense(self, sense):
"""Set the sense (direction) of this objective."""
self.set_sense(sense)

#
# Singleton objectives are strange in that we want them to be
# both be constructed but have len() == 0 when not initialized with
# anything (at least according to the unit tests that are
# currently in place). So during initialization only, we will
# treat them as "indexed" objects where things like
# Objective.Skip are managed. But after that they will behave
# like ObjectiveData objects where set_value does not handle
# Objective.Skip but expects a valid expression or None
#

def clear(self):
self._data = {}

def set_value(self, expr):
"""Set the expression of this objective."""
if not self._constructed:
raise ValueError(
"Setting the value of objective '%s' "
"before the Objective has been constructed (there "
"is currently no object to set)." % (self.name)
)
if not self._data:
self._data[None] = self
return super().set_value(expr)

def set_sense(self, sense):
"""Set the sense (direction) of this objective."""
if self._constructed:
if len(self._data) == 0:
self._data[None] = self
return ObjectiveData.set_sense(self, sense)
raise ValueError(
"Setting the sense of objective '%s' "
"before the Objective has been constructed (there "
"is currently no object to set)." % (self.name)
)
if len(self._data) == 0:
self._data[None] = self
return ObjectiveData.set_sense(self, sense)

#
# Leaving this method for backward compatibility reasons.
Expand All @@ -554,6 +539,15 @@ class SimpleObjective(metaclass=RenamedClass):
__renamed__version__ = '6.0'


@disable_methods({'__call__', 'add', 'expr', 'sense', 'set_sense', 'set_value'})
class AbstractScalarObjective(ScalarObjective):
pass


class TemplateScalarObjective(TemplateDataMixin, ScalarObjective):
pass


class IndexedObjective(Objective):
#
# Leaving this method for backward compatibility reasons
Expand Down
Loading
Loading