Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d7ffc8b
Define InvalidConstraintError for unallowed constraint expressions
jsiirola Sep 17, 2025
6dde46f
Remove check_duplicates from linear template compiler
jsiirola Sep 17, 2025
fd28560
When compiling templates, call to_string in everything that is not a …
jsiirola Sep 17, 2025
5b55d91
Catch (and test) bug where TemplateVarRecorder ignored the sorter
jsiirola Sep 17, 2025
1c55286
Avoid awkward call to base class by just reimplementing the method
jsiirola Sep 17, 2025
db497ef
Use attempt_import to break circular dependencies
jsiirola Sep 19, 2025
2b5669c
Start tests for repn/linear_template
jsiirola Sep 19, 2025
2c7d772
bugfix: converting LinearTemplateRepn to str
jsiirola Sep 19, 2025
90a0d22
Improve specificity of raised exceptions
jsiirola Sep 19, 2025
9f65886
Fix filter for 0 multipliers/coefs
jsiirola Sep 21, 2025
52a78ac
Correctly handle empty subexpressions (e.g., from 0 multipliers)
jsiirola Sep 21, 2025
bb4dd68
Explicitly disallow nonlinear expressions in the LinearTemplateRepn
jsiirola Sep 22, 2025
e343628
Use SetOf when iterating over non-Set indexing sets
jsiirola Nov 7, 2025
21a07cf
Rework extract_values to use defaultdict for Params with default values
jsiirola Nov 7, 2025
cdeaa7c
SetOf.dimen updates:
jsiirola Nov 7, 2025
fad7cb7
Ensure we only attempt to templatize generators
jsiirola Nov 7, 2025
e2d6081
Expand summations that do not involve IndexTemplates
jsiirola Nov 7, 2025
55b1252
Improve error reporting
jsiirola Nov 7, 2025
585a7b4
Rework linear_template to enable better unit testing
jsiirola Nov 7, 2025
b6bf6b4
Resolve handling of fixed variables in bounds and skipped constraints
jsiirola Nov 7, 2025
72b4eea
Don't unnecessarily expand sparse Params
jsiirola Nov 8, 2025
d3a3a24
Improve error
jsiirola Nov 8, 2025
6353b49
Verify absence of bounds for objectives; update comment
jsiirola Nov 8, 2025
3b2da5d
Add interface to get var_list for TemplateVarRecorder
jsiirola Nov 8, 2025
edbc4e1
Expand linear_template testing
jsiirola Nov 8, 2025
f95ff38
Merge branch 'main' into linear-template-testing
jsiirola Nov 10, 2025
c7c5d6f
SetOf: revert change to the dimen of empty iterables
jsiirola Nov 10, 2025
ff1f45c
Update tests to reflect use of SetOf; remove deprecation warnings
jsiirola Nov 10, 2025
7e6fb2b
Revert "SetOf: revert change to the dimen of empty iterables"
jsiirola Nov 10, 2025
c2b2b92
Track change that SetOf([]).dimen is now None
jsiirola Nov 10, 2025
1a55b4c
SetOf({}).dimen should return UnknownSetDimen
jsiirola Nov 10, 2025
f214fec
Merge branch 'main' into linear-template-testing
jsiirola Nov 10, 2025
7641291
Avoid slicing over UnknownSetDimen referenecs
jsiirola Nov 11, 2025
ed79074
Explicit sum() should not prevent templatizing constraint
jsiirola Nov 11, 2025
f2290a1
Additional validation before creating TemplateSumExpression objects
jsiirola Nov 11, 2025
19f0fe3
Remove (unused) 'remove_fixed_vars' option
jsiirola Nov 11, 2025
1dcf6ff
Remove duplicated code
jsiirola Nov 11, 2025
3d8e302
NFC: fix typo
jsiirola Nov 11, 2025
17d0fcf
Merge branch 'main' into linear-template-testing
jsiirola Nov 14, 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
21 changes: 18 additions & 3 deletions pyomo/common/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,25 @@ def __str__(self):


class InfeasibleConstraintException(PyomoException):
"""Exception raised by Pyomo transformations or solver interfaces
to indicate that an infeasible constraint has been identified
(e.g. in the course of range reduction).

"""
Exception class used by Pyomo transformations to indicate
that an infeasible constraint has been identified (e.g. in
the course of range reduction).


class InvalidConstraintError(PyomoException, ValueError):
"""Exception raised for constraints that cannot be represented or emitted.

Pyomo will raise this exception when:

- Creating a constraint with a trivial (Boolean) expression.
- Creating a constraint from an incorrectly structured tuple.
- Compiling a ranged constraint (``lb <= body <= ub``) where
either ``lb`` or ``ub`` are variable expressions.
- Compiling a constraint that cannot be expressed / written in the
target format or interface.

"""


Expand Down
2 changes: 2 additions & 0 deletions pyomo/common/tests/test_timing.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def test_report_timing(self):
(0(\.\d+)?) seconds to construct Var x; 2 indices total
(0(\.\d+)?) seconds to construct Var y; 0 indices total
(0(\.\d+)?) seconds to construct Suffix Suffix
(0(\.\d+)?) seconds to construct SetOf 'y._data'
(0(\.\d+)?) seconds to apply Transformation RelaxIntegerVars \(in-place\)
""".strip()

Expand Down Expand Up @@ -147,6 +148,7 @@ def test_report_timing_context_manager(self):
(0(\.\d+)?) seconds to construct Var x; 2 indices total
(0(\.\d+)?) seconds to construct Var y; 0 indices total
(0(\.\d+)?) seconds to construct Suffix Suffix
(0(\.\d+)?) seconds to construct SetOf 'y._data'
(0(\.\d+)?) seconds to apply Transformation RelaxIntegerVars \(in-place\)
""".strip()

Expand Down
4 changes: 2 additions & 2 deletions pyomo/contrib/incidence_analysis/tests/test_scc_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def test_dynamic_backward_disc_with_initial_conditions(self):
# At t1, "input var" height[t0] is fixed, so
# it is not included here.
self.assertEqual(len(inputs), len(other_var_set))
for var in block.input_vars[:]:
for var in block.input_vars.values():
self.assertIn(var, other_var_set)
self.assertTrue(var.fixed)

Expand Down Expand Up @@ -216,7 +216,7 @@ def test_dynamic_backward_disc_without_initial_conditions(self):
# At t1, "input var" height[t0] is fixed, so
# it is not included here.
self.assertEqual(len(inputs), len(other_var_set))
for var in block.input_vars[:]:
for var in block.input_vars.values():
self.assertIn(var, other_var_set)
self.assertTrue(var.fixed)

Expand Down
18 changes: 11 additions & 7 deletions pyomo/core/base/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
from typing import Union, Type

from pyomo.common.deprecation import RenamedClass, deprecated
from pyomo.common.errors import DeveloperError, TemplateExpressionError
from pyomo.common.errors import (
DeveloperError,
InvalidConstraintError,
TemplateExpressionError,
)
from pyomo.common.formatting import tabular_writer
from pyomo.common.log import is_debug_set
from pyomo.common.modeling import NOTSET
Expand Down Expand Up @@ -219,7 +223,7 @@ def to_bounded_expression(self, evaluate_bounds=False):
and lb.is_potentially_variable()
and not lb.is_fixed()
):
raise ValueError(
raise InvalidConstraintError(
f"Constraint '{self.name}' is a Ranged Inequality with a "
"variable lower bound. Cannot normalize the "
"constraint or send it to a solver."
Expand All @@ -229,7 +233,7 @@ def to_bounded_expression(self, evaluate_bounds=False):
and ub.is_potentially_variable()
and not ub.is_fixed()
):
raise ValueError(
raise InvalidConstraintError(
f"Constraint '{self.name}' is a Ranged Inequality with a "
"variable upper bound. Cannot normalize the "
"constraint or send it to a solver."
Expand Down Expand Up @@ -414,7 +418,7 @@ def set_value(self, expr):
or isinstance(arg, NumericValue)
):
continue
raise ValueError(
raise InvalidConstraintError(
"Constraint '%s' does not have a proper value. "
"Constraint expressions expressed as tuples must "
"contain native numeric types or Pyomo NumericValue "
Expand All @@ -426,7 +430,7 @@ def set_value(self, expr):
# Form equality expression
#
if expr[0] is None or expr[1] is None:
raise ValueError(
raise InvalidConstraintError(
"Constraint '%s' does not have a proper value. "
"Equality Constraints expressed as 2-tuples "
"cannot contain None [received %s]" % (self.name, expr)
Expand All @@ -445,7 +449,7 @@ def set_value(self, expr):
self._expr = RangedExpression(expr, False)
return
else:
raise ValueError(
raise InvalidConstraintError(
"Constraint '%s' does not have a proper value. "
"Found a tuple of length %d. Expecting a tuple of "
"length 2 or 3:\n"
Expand All @@ -464,7 +468,7 @@ def set_value(self, expr):
raise ValueError(_rule_returned_none_error % (self.name,))

elif expr.__class__ is bool:
raise ValueError(
raise InvalidConstraintError(
"Invalid constraint expression. The constraint "
"expression resolved to a trivial Boolean (%s) "
"instead of a Pyomo object. Please modify your "
Expand Down
15 changes: 9 additions & 6 deletions pyomo/core/base/indexed_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,13 +462,16 @@ def keys(self, sort=SortComponents.UNSORTED, ordered=NOTSET):
# of the underlying Set, there should be no warning if the
# user iterates over the set when the _data dict is empty.
#
if (
SortComponents.SORTED_INDICES in sort
or SortComponents.ORDERED_INDICES in sort
):
return iter(sorted_robust(self._data))
# We will leverage SetOf here so we will cleanly pick up the
# iter overrides when we are templatizing
#
tmp_set = BASE.set.FiniteSetOf(self._data, name=self.name + "._data")
if SortComponents.SORTED_INDICES in sort:
return tmp_set.sorted_iter()
elif SortComponents.ORDERED_INDICES in sort:
return tmp_set.ordered_iter()
else:
return self._data.__iter__()
return iter(tmp_set)

if SortComponents.SORTED_INDICES in sort:
ans = self._index_set.sorted_iter()
Expand Down
52 changes: 33 additions & 19 deletions pyomo/core/base/param.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# ___________________________________________________________________________

from __future__ import annotations
import collections
import sys
import types
import logging
Expand Down Expand Up @@ -465,25 +466,33 @@ def extract_values(self):
repeated __getitem__ calls are too expensive to extract
the contents of a parameter.
"""
if not self.is_indexed():
#
# The parameter is a scalar, so we need to create a temporary
# dictionary using the value for this parameter.
#
return {None: self()}
if self._mutable:
#
# The parameter is mutable, parameter data are ParamData types.
# Thus, we need to create a temporary dictionary that contains the
# values from the ParamData objects.
#
return {key: param_value() for key, param_value in self.items()}
elif not self.is_indexed():
#
# The parameter is a scalar, so we need to create a temporary
# dictionary using the value for this parameter.
#
return {None: self()}
ans = {key: param_value() for key, param_value in self.items()}
else:
#
# The parameter is not mutable, so iteritems() can be
# converted into a dictionary containing parameter values.
#
return dict(self.items())
ans = dict(self.items())

# We need to fill-in the "missing" values with the declared default
#
# TBD [11/2025]: should we declare __missing__ so we can still
# validate the index for any missing values?
if self._default_val is not Param.NoValue and not self._index_set.isfinite():
ans = collections.defaultdict(lambda: self._default_val, ans)
return ans

def extract_values_sparse(self):
"""
Expand All @@ -494,28 +503,33 @@ def extract_values_sparse(self):
repeated __getitem__ calls are too expensive to extract
the contents of a parameter.
"""
if not self.is_indexed():
#
# The parameter is a scalar, so we need to create a temporary
# dictionary using the value for this parameter.
#
return {None: self()}
if self._mutable:
#
# The parameter is mutable, parameter data are ParamData types.
# Thus, we need to create a temporary dictionary that contains the
# values from the ParamData objects.
#
ans = {}
for key, param_value in self.sparse_iteritems():
ans[key] = param_value()
return ans
elif not self.is_indexed():
#
# The parameter is a scalar, so we need to create a temporary
# dictionary using the value for this parameter.
#
return {None: self()}
ans = {key: param_value() for key, param_value in self.sparse_iteritems()}
else:
#
# The parameter is not mutable, so sparse_iteritems() can be
# converted into a dictionary containing parameter values.
#
return dict(self.sparse_iteritems())
ans = dict(self.sparse_iteritems())

# We need to fill-in the "missing" values with the declared default
#
# TBD [11/2025]: should we declare __missing__ so we can still
# validate the index for any missing values?
if self._default_val is not Param.NoValue:
ans = collections.defaultdict(lambda: self._default_val, ans)
return ans

def store_values(self, new_values, check=True):
"""
Expand Down
8 changes: 5 additions & 3 deletions pyomo/core/base/set.py
Original file line number Diff line number Diff line change
Expand Up @@ -2627,15 +2627,17 @@ def construct(self, data=None):
def dimen(self):
if isinstance(self._ref, SetData):
return self._ref.dimen
_iter = iter(self)
_iter = iter(self._ref)
try:
x = next(_iter)
if type(x) is tuple:
ans = len(x)
else:
ans = 1
except:
return 0
except StopIteration:
# The referenced object is empty, so we can't infer / verify
# the dimensionality.
return UnknownSetDimen
for x in _iter:
_this = len(x) if type(x) is tuple else 1
if _this != ans:
Expand Down
Loading
Loading