From f1a97393102e2e461be944a02f829eae16206ad8 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Dec 2025 12:08:39 -0700 Subject: [PATCH 01/21] Move name_repr, index_repr into common.formatting --- pyomo/common/formatting.py | 82 ++++++++++++++++++++++++++++++ pyomo/core/base/component.py | 14 +++-- pyomo/core/base/component_namer.py | 80 ++++++++--------------------- pyomo/core/base/componentuid.py | 10 ++-- 4 files changed, 118 insertions(+), 68 deletions(-) diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index 710116a98fe..703b089d7ea 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -102,6 +102,88 @@ def tostr(value, quote_str=False): } +# Literals are used in parsing string names (and indicate tuples, +# indexing, and token separators) +name_literals = '()[],.' +# Special characters are additional characters that if they appear in +# the string force us to quote the string. This includes the obvious +# things like single and double quote characters, but also backslash +# (indicates that the string contains escaped - possibly unicode - +# characters), and the colon (used as a token separator in the old +# ComponentUID "v1" format). +name_special_chars = name_literals + '\'":\\' + +re_number = re.compile( + r'(?:[-+]?(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][-+]?[0-9]+)?|-?inf|nan)' +) +re_name_special_char = re.compile(r'[' + re.escape(name_special_chars) + ']') + + +def name_repr(x, unknown_handler=str): + """Generate a "friendly" string representation of a Pyomo name. + + Convert ``x`` into a "user-friendly" string. Numbers are converted + to strings. Strings are left unquoted, *unless* the string contains + a "special" character that is used in parsing Pyomom component names + (currently any of ``{name_special_chars}``) or the string could be + interpreted as a number. + + """ + if x.__class__ is not str: + _repr = _repr_map.get(x.__class__, None) + if _repr is None: + if not isinstance(x, str): + return unknown_handler(x) + else: + return _repr(x) + # Special handling for strings: only quote the string if it contains + # "special" characters or looks like a number + quoted = repr(x) + if quoted[1] == '|': + return quoted + unquoted = quoted[1:-1] + if re_name_special_char.search(unquoted): + return quoted + if re_number.fullmatch(unquoted): + return quoted + return unquoted + + +def tuple_repr(x, unknown_handler=str): + return ( + '(' + + ','.join(name_repr(_, unknown_handler) for _ in x) + + (',)' if len(x) == 1 else ')') + ) + + +def index_repr(idx, unknown_handler=str): + """Return a string representation of an index. + + This will nominally return the :func:`name_repr` for the elements of + ``idx``. For 1-tuples, the parens are omittted unless the tuple is + empty. + + Note that the brackets (``[]``) normally associated with Pyomo + indexes are *not* included in the resulting string. + + """ + if idx.__class__ is tuple and len(idx) > 1: + return ",".join(name_repr(i, unknown_handler) for i in idx) + return name_repr(idx, unknown_handler) + + +_repr_map = { + slice: lambda x: '*', + Ellipsis.__class__: lambda x: '**', + int: repr, + float: repr, + str: repr, + # Note: the function is unbound at this point; extract with __func__ + tuple: tuple_repr, +} + + def tabular_writer(ostream, prefix, data, header, row_generator): """Output data in tabular form diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 8bbdc4d571b..3022ee74078 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -26,11 +26,15 @@ relocated_module_attribute, ) from pyomo.common.factory import Factory -from pyomo.common.formatting import tabular_writer, StreamIndenter +from pyomo.common.formatting import ( + tabular_writer, + StreamIndenter, + name_repr, + index_repr, +) from pyomo.common.modeling import NOTSET from pyomo.common.sorting import sorted_robust from pyomo.core.pyomoobject import PyomoObject -from pyomo.core.base.component_namer import name_repr, index_repr from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.initializer import PartialInitializer @@ -69,7 +73,7 @@ def name(component, index=NOTSET, fully_qualified=False, relative_to=None): raise KeyError( "Index %s is not valid for component %s" % (index, component.name) ) - return base + index_repr(index) + return base + '[' + index_repr(index) + ']' @deprecated(msg="The cname() function has been renamed to name()", version='5.6.9') @@ -902,7 +906,7 @@ def getname(self, fully_qualified=False, name_buffer=None, relative_to=None): # Iterate through the dictionary and generate all names in # the buffer for idx, obj in c.items(): - name_buffer[id(obj)] = base + index_repr(idx) + name_buffer[id(obj)] = base + '[' + index_repr(idx) + ']' if id(self) in name_buffer: # Return the name if it is in the buffer return name_buffer[id(self)] @@ -911,7 +915,7 @@ def getname(self, fully_qualified=False, name_buffer=None, relative_to=None): # No buffer, we can do what we are going to do all the time after we # deprecate the buffer. # - return base + index_repr(self.index()) + return base + '[' + index_repr(self.index()) + ']' # raise RuntimeError( "Fatal error: cannot find the component data in " diff --git a/pyomo/core/base/component_namer.py b/pyomo/core/base/component_namer.py index 8168f01288c..6c758218aa4 100644 --- a/pyomo/core/base/component_namer.py +++ b/pyomo/core/base/component_namer.py @@ -9,65 +9,29 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import re - -# Literals are used in parsing string names (and indicate tuples, -# indexing, and token separators) -literals = '()[],.' -# Special characters are additional characters that if they appear in -# the string force us to quote the string. This includes the obvious -# things like single and double quote characters, but also backslash -# (indicates that the string contains escaped - possibly unicode - -# characters), and the colon (used as a token separator in the old -# ComponentUID "v1" format). -special_chars = literals + '\'":\\' - -re_number = re.compile( - r'(?:[-+]?(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][-+]?[0-9]+)?|-?inf|nan)' -) -re_special_char = re.compile(r'[' + re.escape(special_chars) + ']') - - -def name_repr(x, unknown_handler=str): - if not isinstance(x, str): - return _repr_map.get(x.__class__, unknown_handler)(x) - else: - x = repr(x) - if x[1] == '|': - return x - unquoted = x[1:-1] - if re_special_char.search(unquoted): - return x - if re_number.fullmatch(unquoted): - return x - return unquoted - - -def tuple_repr(x, unknown_handler=str): - return ( - '(' - + ','.join(name_repr(_, unknown_handler) for _ in x) - + (',)' if len(x) == 1 else ')') +from pyomo.common.deprecation import deprecated, relocated_module_attribute +from pyomo.common.formatting import index_repr as _index_repr + +for attr, new_attr in ( + ('literals', 'name_literals'), + ('special_chars', 'name_special_chars'), + ('re_number', 're_number'), + ('re_special_char', 're_name_special_char'), + ('name_repr', 'name_repr'), + ('tuple_repr', 'tuple_repr'), +): + relocated_module_attribute( + attr, + 'pyomo.common.formatting.' + new_attr, + version='6.10.0.dev0', + f_globals=globals(), ) +@deprecated( + "index_repr has moved to pyomom.common.formatting.index_repr. " + "Note that the return value has also changed.", + version='6.10.0.dev0', +) def index_repr(idx, unknown_handler=str): - """ - Return a string representation of an index. - """ - if idx.__class__ is tuple and len(idx) > 1: - idx_str = ",".join(name_repr(i, unknown_handler) for i in idx) - else: - idx_str = name_repr(idx, unknown_handler) - return "[" + idx_str + "]" - - -_repr_map = { - slice: lambda x: '*', - Ellipsis.__class__: lambda x: '**', - int: repr, - float: repr, - str: repr, - # Note: the function is unbound at this point; extract with __func__ - tuple: tuple_repr, -} + return '[' + _index_repr(idx, unknown_handler) + ']' diff --git a/pyomo/core/base/componentuid.py b/pyomo/core/base/componentuid.py index ba33647506f..0420133ca12 100644 --- a/pyomo/core/base/componentuid.py +++ b/pyomo/core/base/componentuid.py @@ -16,9 +16,9 @@ from pyomo.common.collections import ComponentMap from pyomo.common.dependencies import pickle from pyomo.common.deprecation import deprecated -from pyomo.core.base.component_namer import ( - literals, - special_chars, +from pyomo.common.formatting import ( + name_literals as literals, + name_special_chars, name_repr as __name_repr, index_repr as __index_repr, re_number as _re_number, @@ -40,7 +40,7 @@ def _name_repr(x): def _index_repr(x): - return __index_repr(x, _pickle) + return '[' + __index_repr(x, _pickle) + ']' def _context_err(_type): @@ -683,7 +683,7 @@ def t_NUMBER(t): # number of "non-special" characters. This regex matches numbers as # well as more traditional string names, so it is important that it is # declared *after* t_NUMBER. -@ply.lex.TOKEN(r'[a-zA-Z_0-9][^' + re.escape(special_chars) + r']*') +@ply.lex.TOKEN(r'[a-zA-Z_0-9][^' + re.escape(name_special_chars) + r']*') def t_WORD(t): t.value = t.value.strip() return t From 4bdb3e75018c7f03d2d53235654f84ddee98a183 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Dec 2025 12:09:05 -0700 Subject: [PATCH 02/21] Minor simplification to tabular_writer (slight performance improvement) --- pyomo/common/formatting.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index 703b089d7ea..6a52bca3c54 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -195,7 +195,7 @@ def tabular_writer(ostream, prefix, data, header, row_generator): prefix each generated line with this string data: iterable an iterable object that returns (key, value) pairs - (e.g., from iteritems()) defining each row in the table + (e.g., from items()) defining each row in the table header: List[str] list of column headers row_generator: function @@ -241,22 +241,21 @@ def tabular_writer(ostream, prefix, data, header, row_generator): if not _rows[_key]: _minWidth = 4 elif not _width: - _width = [0] * len(_rows[_key][0]) - for _row in _rows[_key]: - for col, x in enumerate(_row): - _width[col] = max(_width[col], len(x), col and _minWidth) + _width = [len(x) for x in _rows[_key][0]] + else: + for _row in _rows[_key]: + for col, x in enumerate(_row): + _width[col] = max(_width[col], len(x)) + + if _minWidth: + for i in range(1, len(_width)): + _width[i] = max(_minWidth, _width[i]) # NB: left-justify header entries if header: # Note: do not right-pad the last header with unnecessary spaces - tmp = _width[-1] - _width[-1] = 0 - ostream.write( - prefix - + " : ".join("%%-%ds" % _width[i] % x for i, x in enumerate(header)) - + "\n" - ) - _width[-1] = tmp + line_fmt = " : ".join(f"%-{w}s" for w in _width[:-1]) + " : %s\n" + ostream.write(prefix + (line_fmt % header)) # If there is no data, we are done... if not _rows: @@ -264,18 +263,17 @@ def tabular_writer(ostream, prefix, data, header, row_generator): # right-justify data, except for the last column if there are spaces # in the data (probably an expression or vector) - _width = ["%" + str(i) + "s" for i in _width] - + _width = [f"%{w}s" for w in _width] if any(' ' in r[-1] for x in _rows.values() if x is not None for r in x): _width[-1] = '%s' + line_fmt = " : ".join(_width) + "\n" + for _key in sorted_robust(_rows): _rowSet = _rows[_key] if not _rowSet: - _rowSet = [[_key] + [None] * (len(_width) - 1)] + _rowSet = [(_key,) + (None,) * (len(_width) - 1)] for _data in _rowSet: - ostream.write( - prefix + " : ".join(_width[i] % x for i, x in enumerate(_data)) + "\n" - ) + ostream.write(prefix + (line_fmt % _data)) class StreamIndenter: From ad6e6ead76ac6d236cec57cafdc4afa2e31cf4ba Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 5 Dec 2025 13:31:28 -0700 Subject: [PATCH 03/21] Convert Param.sparse_{keys,values,items} to return generators Leverage implementation from IndexedComponent.keys --- pyomo/core/base/param.py | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index 02ba103cae3..8b8944603c2 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -26,6 +26,7 @@ from pyomo.core.expr.expr_common import _type_check_exception_arg from pyomo.core.expr.numvalue import NumericValue from pyomo.core.base.component import ComponentData, ModelComponentFactory +from pyomo.core.base.enums import SortComponents from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.indexed_component import ( IndexedComponent, @@ -432,17 +433,42 @@ def get_units(self): # only loop over the defined data. # - def sparse_keys(self): + def sparse_keys(self, sort=SortComponents.UNSORTED): """Return a list of keys in the defined parameters""" - return list(self._data.keys()) + try: + # Temporarily remove the default value so that len(self) == + # len(self._dict). This will cause the base class + # implementation of keys() to only return values from + # self._data: + tmp = self._default_val + self._default_val = Param.NoValue + return self.keys(sort) + finally: + self._default_val = tmp - def sparse_values(self): + def sparse_values(self, sort=SortComponents.UNSORTED): """Return a list of the defined param data objects""" - return list(self._data.values()) + # Implementing things this way for consistency with items() (and + # so that any changes in the base class implementation are + # picked up here, too): + try: + tmp = self._default_val + self._default_val = Param.NoValue + return self.values(sort) + finally: + self._default_val = tmp - def sparse_items(self): + def sparse_items(self, sort=SortComponents.UNSORTED): """Return a list (index,data) tuples for defined parameters""" - return list(self._data.items()) + # The base class implements special handling for references. + # Instead of reimplementing that here, we will follow the + # pattern used for sparse_keys (and get len() to "lie") + try: + tmp = self._default_val + self._default_val = Param.NoValue + return self.items(sort) + finally: + self._default_val = tmp def sparse_iterkeys(self): """Return an iterator for the keys in the defined parameters""" From c249ad32054aa22defc6e43afe6541f24d266d40 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 5 Dec 2025 13:34:02 -0700 Subject: [PATCH 04/21] Deprecate Param.sparse_iter* --- pyomo/core/base/param.py | 25 ++++++++++++++++++++----- pyomo/core/tests/unit/test_param.py | 2 +- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index 8b8944603c2..d8cc6f990db 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -17,7 +17,7 @@ from weakref import ref as weakref_ref from pyomo.common.autoslots import AutoSlots -from pyomo.common.deprecation import deprecation_warning, RenamedClass +from pyomo.common.deprecation import deprecated, deprecation_warning, RenamedClass from pyomo.common.log import is_debug_set from pyomo.common.modeling import NOTSET from pyomo.common.numeric_types import native_types, value as expr_value @@ -470,17 +470,32 @@ def sparse_items(self, sort=SortComponents.UNSORTED): finally: self._default_val = tmp + @deprecated( + "The sparse_iterkeys method is deprecated. Use sparse_keys()", + # This should have been deprecated when we dropped Python 2.7 + version='6.10.0.dev0', + ) def sparse_iterkeys(self): """Return an iterator for the keys in the defined parameters""" - return self._data.keys() + return self.sparse_keys() + @deprecated( + "The sparse_itervalues method is deprecated. Use sparse_values()", + # This should have been deprecated when we dropped Python 2.7 + version='6.10.0.dev0', + ) def sparse_itervalues(self): """Return an iterator for the defined param data objects""" - return self._data.values() + return self.sparse_values() + @deprecated( + "The sparse_iteritems method is deprecated. Use sparse_items()", + # This should have been deprecated when we dropped Python 2.7 + version='6.10.0.dev0', + ) def sparse_iteritems(self): """Return an iterator of (index,data) tuples for defined parameters""" - return self._data.items() + return self.sparse_items() def extract_values(self): """ @@ -959,7 +974,7 @@ def _pprint(self): ] if self._units is not None: headers.append(('Units', str(self._units))) - return (headers, self.sparse_iteritems(), ("Value",), dataGen) + return (headers, self.sparse_items(), ("Value",), dataGen) class ScalarParam(ParamData, Param): diff --git a/pyomo/core/tests/unit/test_param.py b/pyomo/core/tests/unit/test_param.py index b335ce33777..eadc0f118e0 100644 --- a/pyomo/core/tests/unit/test_param.py +++ b/pyomo/core/tests/unit/test_param.py @@ -174,7 +174,7 @@ def test_setitem_index_error(self): raise def test_setitem_preexisting(self): - keys = self.instance.A.sparse_keys() + keys = list(self.instance.A.sparse_keys()) if not keys or None in keys: return From 7aa6e40a18841a146339d15308244d6b387e2496 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 5 Dec 2025 13:34:39 -0700 Subject: [PATCH 05/21] Simplify implementation of Param.extract_values --- pyomo/core/base/param.py | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index d8cc6f990db..055e3ef2553 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -512,13 +512,7 @@ def extract_values(self): # 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()} + return {key: param_data() for key, param_data in self.items()} else: # # The parameter is not mutable, so iteritems() can be @@ -541,22 +535,13 @@ def extract_values_sparse(self): # 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()} + return {key: param_data() for key, param_data in self.sparse_items()} else: # - # The parameter is not mutable, so sparse_iteritems() can be + # The parameter is not mutable, so sparse_items() can be # converted into a dictionary containing parameter values. # - return dict(self.sparse_iteritems()) + return dict(self.sparse_items()) def store_values(self, new_values, check=True): """ From 25ec90722c36045dfb029abe1206ca8f341060df Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 5 Dec 2025 13:35:10 -0700 Subject: [PATCH 06/21] Update Param testing --- pyomo/core/tests/unit/test_param.py | 76 +++++++++++++---------------- 1 file changed, 33 insertions(+), 43 deletions(-) diff --git a/pyomo/core/tests/unit/test_param.py b/pyomo/core/tests/unit/test_param.py index eadc0f118e0..3fd62dd5eb8 100644 --- a/pyomo/core/tests/unit/test_param.py +++ b/pyomo/core/tests/unit/test_param.py @@ -356,73 +356,63 @@ def test_items(self): raise def test_iterkeys(self): - test = self.instance.A.iterkeys() + with LoggingIntercept() as LOG: + test = self.instance.A.iterkeys() + self.assertIn("The iterkeys method is deprecated.", LOG.getvalue()) self.assertEqual(sorted(test), sorted(self.instance.A.keys())) def test_itervalues(self): - expectException = False - # len(self.sparse_data) < len(self.data) and \ - # not self.instance.A._default_val is None and \ - # not self.instance.A.mutable - try: - test = self.instance.A.values() - test = zip(self.instance.A.keys(), test) - if self.instance.A._default_val is NoValue: - self.validateDict(self.sparse_data.items(), test) - else: - self.validateDict(self.data.items(), test) - self.assertFalse(expectException) - except ValueError: - if not expectException: - raise + with LoggingIntercept() as LOG: + test = self.instance.A.itervalues() + self.assertIn("The itervalues method is deprecated.", LOG.getvalue()) + self.assertEqual(list(self.instance.A.values()), list(test)) def test_iteritems(self): - expectException = False - # len(self.sparse_data) < len(self.data) and \ - # not self.instance.A._default_val is None and \ - # not self.instance.A.mutable - try: - test = self.instance.A.items() - if self.instance.A._default_val is NoValue: - self.validateDict(self.sparse_data.items(), test) - else: - self.validateDict(self.data.items(), test) - self.assertFalse(expectException) - except ValueError: - if not expectException: - raise + with LoggingIntercept() as LOG: + test = self.instance.A.iteritems() + self.assertIn("The iteritems method is deprecated.", LOG.getvalue()) + self.assertEqual(list(self.instance.A.items()), list(test)) def test_sparse_keys(self): test = self.instance.A.sparse_keys() - self.assertEqual(type(test), list) self.assertEqual(sorted(test), sorted(self.sparse_data.keys())) def test_sparse_values(self): - # self.instance.pprint() test = self.instance.A.sparse_values() - self.assertEqual(type(test), list) - # print test - # print self.sparse_data.items() test = zip(self.instance.A.keys(), test) self.validateDict(self.sparse_data.items(), test) def test_sparse_items(self): test = self.instance.A.sparse_items() - self.assertEqual(type(test), list) self.validateDict(self.sparse_data.items(), test) def test_sparse_iterkeys(self): - test = self.instance.A.sparse_iterkeys() - self.assertEqual(sorted(test), sorted(self.sparse_data.keys())) + with LoggingIntercept() as LOG: + test = self.instance.A.sparse_iterkeys() + self.assertIn("The sparse_iterkeys method is deprecated.", LOG.getvalue()) + self.assertEqual(list(test), list(self.instance.A.sparse_keys())) def test_sparse_itervalues(self): - test = self.instance.A.sparse_itervalues() - test = zip(self.instance.A.keys(), test) - self.validateDict(self.sparse_data.items(), test) + with LoggingIntercept() as LOG: + test = self.instance.A.sparse_itervalues() + self.assertIn("The sparse_itervalues method is deprecated.", LOG.getvalue()) + self.assertEqual(list(test), list(self.instance.A.sparse_values())) def test_sparse_iteritems(self): - test = self.instance.A.sparse_iteritems() - self.validateDict(self.sparse_data.items(), test) + with LoggingIntercept() as LOG: + test = self.instance.A.sparse_iteritems() + self.assertIn("The sparse_iteritems method is deprecated.", LOG.getvalue()) + self.assertEqual(list(test), list(self.instance.A.sparse_items())) + + def test_extract_values(self): + if self.instance.A._default_val is NoValue: + ref = self.sparse_data + else: + ref = self.data + self.assertEqual(ref, self.instance.A.extract_values()) + + def test_extract_values_sparse(self): + self.assertEqual(self.sparse_data, self.instance.A.extract_values_sparse()) def test_len(self): # """Check the use of len""" From fd0ea00e59d34bc5e89ddd2074e9b033674b973b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 5 Dec 2025 13:36:30 -0700 Subject: [PATCH 07/21] Improve expected efficiency of IndexedCOmponent.keys when sorting sparse components --- pyomo/core/base/indexed_component.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index 8bbcb6fdf55..9d3479f9641 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -511,9 +511,15 @@ def keys(self, sort=SortComponents.UNSORTED, ordered=NOTSET): """ % (self.name,) ) + return iter(self._data) + elif SortComponents.SORTED_INDICES in sort: + # We are sorting the indices (and this is a sparse + # IndexedComponent): we might as well just sort the sparse + # _data keys instead of iterating over the whole index. + return iter(sorted_robust(self._data)) else: # - # Test each element of a sparse data with an ordered + # Test each element of a sparse _data with an ordered # index set in order. This is potentially *slow*: if # the component is in fact very sparse, we could be # iterating over a huge (dense) index in order to sort a @@ -591,7 +597,8 @@ def items(self, sort=SortComponents.UNSORTED, ordered=NOTSET): return self._data.items(sort) except TypeError: pass - return ((s, self[s]) for s in self.keys(sort)) + _getitem = self.__getitem__ + return ((s, _getitem(s)) for s in self.keys(sort)) @deprecated('The iterkeys method is deprecated. Use dict.keys().', version='6.0') def iterkeys(self): From 8564bfd09114d0cf50cd1bdfbd23750f52a2ba94 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 5 Dec 2025 13:40:07 -0700 Subject: [PATCH 08/21] Minor tabular_writer clarification / efficiency fix --- pyomo/common/formatting.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index 6a52bca3c54..686738c51d3 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -232,8 +232,11 @@ def tabular_writer(ostream, prefix, data, header, row_generator): _rows[_key] = None continue + # Include the key for only the first line in a rowset, and only + # if we printed out a header (if there is no header, then then + # key is not included) _rows[_key] = [ - ((tostr("" if i else _key),) if header else ()) + (("" if i else tostr(_key),) if header else ()) + tuple(tostr(x) for x in _r) for i, _r in enumerate(_rowSet) ] From f6f970cac9ab4b3a686e922a3c33fb901c344ce0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 5 Dec 2025 13:50:00 -0700 Subject: [PATCH 09/21] Remove sorting from tabular_writer --- pyomo/common/formatting.py | 2 +- pyomo/common/tests/test_formatting.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index 686738c51d3..6d42e4c1168 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -271,7 +271,7 @@ def tabular_writer(ostream, prefix, data, header, row_generator): _width[-1] = '%s' line_fmt = " : ".join(_width) + "\n" - for _key in sorted_robust(_rows): + for _key in _rows: _rowSet = _rows[_key] if not _rowSet: _rowSet = [(_key,) + (None,) * (len(_width) - 1)] diff --git a/pyomo/common/tests/test_formatting.py b/pyomo/common/tests/test_formatting.py index 0798a80589c..38fe939f295 100644 --- a/pyomo/common/tests/test_formatting.py +++ b/pyomo/common/tests/test_formatting.py @@ -96,8 +96,8 @@ def test_no_header(self): data = {(2,): (["a", 1], 1), (1, 3): ({1: 'a', 2: '2'}, '2')} tabular_writer(os, "", data.items(), [], lambda k, v: v) ref = u""" -{1: 'a', 2: '2'} : 2 ['a', 1] : 1 +{1: 'a', 2: '2'} : 2 """ self.assertEqual(ref.strip(), os.getvalue().strip()) From 1d606b59227f9659058dd0535fa98c9f8647fb3b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 5 Dec 2025 13:50:00 -0700 Subject: [PATCH 10/21] Remove sorting from tabular_writer --- pyomo/common/formatting.py | 2 +- pyomo/common/tests/test_formatting.py | 2 +- pyomo/core/base/component.py | 25 ++++++++++++++++++------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index 686738c51d3..6d42e4c1168 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -271,7 +271,7 @@ def tabular_writer(ostream, prefix, data, header, row_generator): _width[-1] = '%s' line_fmt = " : ".join(_width) + "\n" - for _key in sorted_robust(_rows): + for _key in _rows: _rowSet = _rows[_key] if not _rowSet: _rowSet = [(_key,) + (None,) * (len(_width) - 1)] diff --git a/pyomo/common/tests/test_formatting.py b/pyomo/common/tests/test_formatting.py index 0798a80589c..38fe939f295 100644 --- a/pyomo/common/tests/test_formatting.py +++ b/pyomo/common/tests/test_formatting.py @@ -96,8 +96,8 @@ def test_no_header(self): data = {(2,): (["a", 1], 1), (1, 3): ({1: 'a', 2: '2'}, '2')} tabular_writer(os, "", data.items(), [], lambda k, v: v) ref = u""" -{1: 'a', 2: '2'} : 2 ['a', 1] : 1 +{1: 'a', 2: '2'} : 2 """ self.assertEqual(ref.strip(), os.getvalue().strip()) diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 3022ee74078..55d0ab8b63c 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -399,23 +399,34 @@ def _pprint_base_impl( return if type(_fcn) is tuple: + # Exception to the standard formatter case: with two + # callbacks, we will use the first to generate the normal + # table, then call the second callback for each data. + # Currently only used by Complimentarity (which should be + # refactored to remove the need for this edge case) _fcn, _fcn2 = _fcn else: _fcn2 = None if _header is not None: + # This is a standard component, where all the component + # information is printed in a single table if _fcn2 is not None: - _data_dict = dict(_data) - _data = _data_dict.items() + _data = list(_data) tabular_writer(ostream, '', _data, _header, _fcn) if _fcn2 is not None: - for _key in sorted_robust(_data_dict): - _fcn2(ostream, _key, _data_dict[_key]) + for _key, _val in _data: + _fcn2(ostream, _key, _val) elif _fcn is not None: - _data_dict = dict(_data) - for _key in sorted_robust(_data_dict): - _fcn(ostream, _key, _data_dict[_key]) + # This is a non-standard component where we will not + # generate a table at all, and instead defer all formatting + # / printing to the callback. This is primarily used by + # BLocks (and block-like things) + for _key, _val in _data: + _fcn(ostream, _key, _val) elif _data is not None: + # Catch all for everything else: assume that _pprint() + # returned a formatted string. ostream.write(_data) From 9d9e0a8a37145068df5e1a2e44ace5a6830cdf4c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 9 Dec 2025 16:53:45 -0700 Subject: [PATCH 11/21] Update tests to reflect the new default ordering in pprint() --- pyomo/core/tests/unit/test_block.py | 4 ++-- pyomo/core/tests/unit/test_set.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index 0a3823d6f01..3b1045a47c8 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -2683,10 +2683,10 @@ def test_pprint(self): a[1] : Active=True 2 Block Declarations c : Size=2, Index=a1_IDX, Active=True - a[1].c[4] : Active=True - 0 Declarations: a[1].c[5] : Active=True 0 Declarations: + a[1].c[4] : Active=True + 0 Declarations: d : Size=1, Index=None, Active=True 0 Declarations: diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 5e445c27c73..dc885012221 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -6308,13 +6308,13 @@ def c_rule(m, a, b, c): None : None : Any : 3 : {((1, 2, 3), 4, 3), ((1, 2, 2), 4, 3), ((1, 3, 3), 4, 3)} x : Size=3, Index=CHOICES Key : Lower : Value : Upper : Fixed : Stale : Domain - ((1, 2, 2), 4, 3) : None : None : None : False : True : Reals ((1, 2, 3), 4, 3) : None : None : None : False : True : Reals + ((1, 2, 2), 4, 3) : None : None : None : False : True : Reals ((1, 3, 3), 4, 3) : None : None : None : False : True : Reals c : Size=3, Index=CHOICES, Active=True Key : Lower : Body : Upper : Active - ((1, 2, 2), 4, 3) : 0.0 : x[(1,2,2),4,3] : 0.0 : True ((1, 2, 3), 4, 3) : 0.0 : x[(1,2,3),4,3] : 0.0 : True + ((1, 2, 2), 4, 3) : 0.0 : x[(1,2,2),4,3] : 0.0 : True ((1, 3, 3), 4, 3) : 0.0 : x[(1,3,3),4,3] : 0.0 : True """.strip() self.assertEqual(output.getvalue().strip(), ref) @@ -6343,13 +6343,13 @@ def c_rule(m, a1, a2, a3, b, c): None : 5 : Any : 3 : {(1, 2, 3, 4, 3), (1, 2, 2, 4, 3), (1, 3, 3, 4, 3)} x : Size=3, Index=CHOICES Key : Lower : Value : Upper : Fixed : Stale : Domain - (1, 2, 2, 4, 3) : None : None : None : False : True : Reals (1, 2, 3, 4, 3) : None : None : None : False : True : Reals + (1, 2, 2, 4, 3) : None : None : None : False : True : Reals (1, 3, 3, 4, 3) : None : None : None : False : True : Reals c : Size=3, Index=CHOICES, Active=True Key : Lower : Body : Upper : Active - (1, 2, 2, 4, 3) : 0.0 : x[1,2,2,4,3] : 0.0 : True (1, 2, 3, 4, 3) : 0.0 : x[1,2,3,4,3] : 0.0 : True + (1, 2, 2, 4, 3) : 0.0 : x[1,2,2,4,3] : 0.0 : True (1, 3, 3, 4, 3) : 0.0 : x[1,3,3,4,3] : 0.0 : True """.strip() self.assertEqual(output.getvalue().strip(), ref) From 00a6cf17d76d49405e3c88f73f5b299c6022cd65 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 9 Dec 2025 22:42:55 -0700 Subject: [PATCH 12/21] Fix extract_values for scalar params --- pyomo/core/base/param.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index 055e3ef2553..8bac8d08bdc 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -507,17 +507,20 @@ def extract_values(self): the contents of a parameter. """ 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. - # + # The parameter is mutable so parameter data are ParamData + # types. We need to evaluate the ParamData back to POD + # (numeric) data when creating the result. return {key: param_data() for key, param_data in self.items()} + elif not self.is_indexed(): + # The scalar could be defined (in which case items() will + # return the ScalarParam, OR it could be defined by a + # default value (in which case items() will return the + # actual numeric value. to cover both cases we will use + # value(): + return {key: expr_value(param_data) for key, param_data in self.items()} else: - # # The parameter is not mutable, so iteritems() can be # converted into a dictionary containing parameter values. - # return dict(self.items()) def extract_values_sparse(self): @@ -529,18 +532,20 @@ def extract_values_sparse(self): repeated __getitem__ calls are too expensive to extract the contents of a parameter. """ - 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. + if self._mutable or not self.is_indexed(): + # The parameter is mutable so parameter data are ParamData + # types. We need to evaluate the ParamData back to POD + # (numeric) data when creating the result. # + # Note that if this is a scalar, sparse_items will return + # the ScalarParam only if it is explicitly defined (in which + # case it will still be evaluatable by calling it). + # ScalarParams whose value comes from the default are not + # returned by sparse_items() return {key: param_data() for key, param_data in self.sparse_items()} else: - # # The parameter is not mutable, so sparse_items() can be # converted into a dictionary containing parameter values. - # return dict(self.sparse_items()) def store_values(self, new_values, check=True): From 71fc882cf41b056b9c37733d1ae2f632795ce5bc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 9 Dec 2025 22:43:23 -0700 Subject: [PATCH 13/21] Expand testing for immutable scalar params --- pyomo/core/tests/unit/test_param.py | 154 +++++++++++++++++++++------- 1 file changed, 119 insertions(+), 35 deletions(-) diff --git a/pyomo/core/tests/unit/test_param.py b/pyomo/core/tests/unit/test_param.py index 3fd62dd5eb8..e60372c57e3 100644 --- a/pyomo/core/tests/unit/test_param.py +++ b/pyomo/core/tests/unit/test_param.py @@ -409,10 +409,16 @@ def test_extract_values(self): ref = self.sparse_data else: ref = self.data - self.assertEqual(ref, self.instance.A.extract_values()) + vals = self.instance.A.extract_values() + self.assertEqual(ref, vals) + for k, v in vals.items(): + self.assertIsInstance(v, (float, int)) def test_extract_values_sparse(self): - self.assertEqual(self.sparse_data, self.instance.A.extract_values_sparse()) + vals = self.instance.A.extract_values_sparse() + self.assertEqual(self.sparse_data, vals) + for k, v in vals.items(): + self.assertIsInstance(v, (float, int)) def test_len(self): # """Check the use of len""" @@ -839,17 +845,42 @@ def setUp(self, **kwds): def test_value_scalar(self): # """Check the value of the parameter""" if self.data.get(None, NoValue) is NoValue: - self.assertRaises(ValueError, value, self.instance.A) - self.assertRaises(TypeError, float, self.instance.A) - self.assertRaises(TypeError, int, self.instance.A) + if self.instance.A.mutable: + msg = "The Param value is currently set to an invalid value." + with self.assertRaisesRegex(ValueError, msg): + value(self.instance.A) + else: + msg = "The Param value is undefined and no default value is specified" + with self.assertRaisesRegex(ValueError, msg): + value(self.instance.A) else: val = self.data[None] tmp = value(self.instance.A) self.assertEqual(type(tmp), type(val)) self.assertEqual(tmp, val) + self.assertIsInstance(val, (int, float)) - self.assertRaises(TypeError, float, self.instance.A) - self.assertRaises(TypeError, int, self.instance.A) + def test_cast_to_number(self): + if self.instance.A.mutable: + msg = r"Implicit conversion of Pyomo numeric value \(A\) to " + with self.assertRaisesRegex(TypeError, msg + 'float'): + float(self.instance.A) + with self.assertRaisesRegex(TypeError, msg + 'int'): + int(self.instance.A) + elif self.data.get(None, NoValue) is NoValue: + msg = "The Param value is undefined and no default value is specified" + with self.assertRaisesRegex(ValueError, msg): + float(self.instance.A) + with self.assertRaisesRegex(ValueError, msg): + int(self.instance.A) + else: + val = self.data[None] + tmp = float(self.instance.A) + self.assertEqual(tmp, val) + self.assertIs(type(tmp), float) + tmp = int(self.instance.A) + self.assertEqual(tmp, int(val)) + self.assertIs(type(tmp), int) def test_call(self): # """Check the use of the __call__ method""" @@ -857,47 +888,48 @@ def test_call(self): self.sparse_data.get(None, 0) is NoValue or self.data.get(None, NoValue) is NoValue ): # not self.sparse_data: - self.assertRaisesRegex( - ValueError, - ".*currently set to an invalid value", - self.instance.A.__call__, - ) + if self.instance.A.mutable: + msg = "The Param value is currently set to an invalid value." + else: + msg = "The Param value is undefined and no default value is specified" + with self.assertRaisesRegex(ValueError, msg): + self.instance.A(), else: self.assertEqual(self.instance.A(), self.data[None]) def test_get_valueattr(self): self.assertEqual(self.instance.A._value, self.sparse_data.get(None, NoValue)) if self.data.get(None, 0) is NoValue: # not self.sparse_data: - try: - value(self.instance.A) - self.fail("Expected value error") - except ValueError: - pass + if self.instance.A.mutable: + msg = "The Param value is currently set to an invalid value." + else: + msg = "The Param value is undefined and no default value is specified" + with self.assertRaisesRegex(ValueError, msg): + self.instance.A.value else: self.assertEqual(self.instance.A.value, self.data[None]) def test_set_valueattr(self): - self.instance.A.value = 4.3 - self.assertEqual(self.instance.A.value, 4.3) - self.assertEqual(self.instance.A(), 4.3) - - def test_get_value(self): - if ( - self.sparse_data.get(None, 0) is NoValue - or self.data.get(None, NoValue) is NoValue - ): # not self.sparse_data: - try: - value(self.instance.A) - self.fail("Expected value error") - except ValueError: - pass + if self.instance.A.mutable: + self.instance.A.value = 4.3 + self.assertEqual(self.instance.A.value, 4.3) + self.assertEqual(self.instance.A(), 4.3) else: - self.assertEqual(self.instance.A.value, self.data[None]) + with self.assertRaisesRegex( + TypeError, "Attempting to set the value of the immutable parameter A" + ): + self.instance.A.value = 4.3 def test_set_value(self): - self.instance.A = 4.3 - self.assertEqual(self.instance.A.value, 4.3) - self.assertEqual(self.instance.A(), 4.3) + if self.instance.A.mutable: + self.instance.A = 4.3 + self.assertEqual(self.instance.A.value, 4.3) + self.assertEqual(self.instance.A(), 4.3) + else: + with self.assertRaisesRegex( + TypeError, "Attempting to set the value of the immutable parameter A" + ): + self.instance.A = 4.3 def test_is_indexed(self): self.assertFalse(self.instance.A.is_indexed()) @@ -906,6 +938,58 @@ def test_dim(self): # """Check the use of dim""" self.assertEqual(self.instance.A.dim(), 0) + def test_extract_values(self): + if self.instance.A._default_val is NoValue: + ref = self.sparse_data + else: + ref = self.data + vals = self.instance.A.extract_values() + self.assertEqual(ref, vals) + for k, v in vals.items(): + self.assertIsInstance(v, (float, int)) + + def test_extract_values_sparse(self): + vals = self.instance.A.extract_values_sparse() + self.assertEqual(self.sparse_data, vals) + for k, v in vals.items(): + self.assertIsInstance(v, (float, int)) + + +class ScalarParam_immutable_noDefault(ScalarTester, unittest.TestCase): + def setUp(self, **kwds): + # + # Sparse single-index Param, no default + # + self.model = AbstractModel() + ScalarTester.setUp(self, mutable=False, **kwds) + + self.sparse_data = {} + self.data = {None: NoValue} + + +class ScalarParam_immutable_init(ScalarTester, unittest.TestCase): + def setUp(self, **kwds): + # + # Sparse single-index Param, no default + # + self.model = AbstractModel() + ScalarTester.setUp(self, mutable=False, initialize=1.3, **kwds) + + self.sparse_data = {None: 1.3} + self.data = {None: 1.3} + + +class ScalarParam_immutable_floatDefault(ScalarTester, unittest.TestCase): + def setUp(self, **kwds): + # + # Sparse single-index Param, no default + # + self.model = AbstractModel() + ScalarTester.setUp(self, mutable=False, default=1.3, **kwds) + + self.sparse_data = {} + self.data = {None: 1.3} + class ScalarParam_mutable_noDefault(ScalarTester, unittest.TestCase): def setUp(self, **kwds): From 9f99c3fad77dff4617fea3cdc631c7603f20bcf4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 14 Dec 2025 15:29:12 -0700 Subject: [PATCH 14/21] Resolve bugs with keys/values/items for scalar Set objects --- pyomo/core/base/set.py | 73 +++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 8c858f96ba8..c3ee26063dc 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -39,6 +39,7 @@ is_constant, ) from pyomo.core.base.disable_methods import disable_methods +from pyomo.core.base.enums import SortComponents from pyomo.core.base.initializer import ( CountedCallInitializer, IndexedCallInitializer, @@ -1608,23 +1609,6 @@ class _FiniteSetData(metaclass=RenamedClass): __renamed__version__ = '6.7.2' -class _ScalarOrderedSetMixin: - # This mixin is required because scalar ordered sets implement - # __getitem__() as an alias of at() - __slots__ = () - - def values(self): - """Return an iterator of the component data objects in the dictionary""" - if list(self.keys()): - yield self - - def items(self): - """Return an iterator of (index,data) tuples from the dictionary""" - _keys = list(self.keys()) - if _keys: - yield _keys[0], self - - class _OrderedSetMixin: __slots__ = () _valid_getitem_keys = {None, (None,), Ellipsis} @@ -2022,6 +2006,29 @@ class _SortedSetData(metaclass=RenamedClass): _SETDATA_API = ('set_value', 'add', 'remove', 'discard', 'clear', 'update', 'pop') +class _ScalarSetMixin: + # This mixin is required because scalar Sets implement __len__(), + # which raises an exception for non-finite sets. Further finite + # scalar sets also implement __getitem__() as an alias of at(), and + # IndexedComponent.items() / IndexedComponent.values() relies on + # __getitem__() + __slots__ = () + + def keys(self, sort=SortComponents.UNSORTED): + # Scalar sets are always defined (we don't support Skip): + return iter(UnindexedComponent_set) + + def values(self, sort=SortComponents.UNSORTED): + """Return an iterator of the component data objects in the dictionary""" + # Scalar sets are always defined (we don't support Skip): + yield self + + def items(self, sort=SortComponents.UNSORTED): + """Return an iterator of (index,data) tuples from the dictionary""" + # Scalar sets are always defined (we don't support Skip): + yield UnindexedComponent_index, self + + @ModelComponentFactory.register("Set data that is used to define a model instance.") class Set(IndexedComponent): """A component used to index other Pyomo components. @@ -2505,7 +2512,7 @@ def __getitem__(self, index) -> SetData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore -class FiniteScalarSet(FiniteSetData, Set): +class FiniteScalarSet(_ScalarSetMixin, FiniteSetData, Set): def __init__(self, **kwds): FiniteSetData.__init__(self, component=self) Set.__init__(self, **kwds) @@ -2517,7 +2524,7 @@ class FiniteSimpleSet(metaclass=RenamedClass): __renamed__version__ = '6.0' -class OrderedScalarSet(_ScalarOrderedSetMixin, InsertionOrderSetData, Set): +class OrderedScalarSet(_ScalarSetMixin, InsertionOrderSetData, Set): def __init__(self, **kwds): # In case someone inherits from us, we will provide a rational # default for the "ordered" flag @@ -2532,7 +2539,7 @@ class OrderedSimpleSet(metaclass=RenamedClass): __renamed__version__ = '6.0' -class SortedScalarSet(_ScalarOrderedSetMixin, SortedSetData, Set): +class SortedScalarSet(_ScalarSetMixin, SortedSetData, Set): def __init__(self, **kwds): # In case someone inherits from us, we will provide a rational # default for the "ordered" flag @@ -2695,7 +2702,7 @@ class UnorderedSetOf(metaclass=RenamedClass): __renamed__version__ = '6.2' -class OrderedSetOf(_ScalarOrderedSetMixin, _OrderedSetMixin, FiniteSetOf): +class OrderedSetOf(_OrderedSetMixin, FiniteSetOf): def at(self, index): i = self._to_0_based_index(index) try: @@ -3337,7 +3344,7 @@ class InfiniteSimpleRangeSet(metaclass=RenamedClass): __renamed__version__ = '6.0' -class FiniteScalarRangeSet(_ScalarOrderedSetMixin, FiniteRangeSetData, RangeSet): +class FiniteScalarRangeSet(FiniteRangeSetData, RangeSet): def __init__(self, *args, **kwds): FiniteRangeSetData.__init__(self, component=self) RangeSet.__init__(self, *args, **kwds) @@ -3377,7 +3384,7 @@ class AbstractFiniteSimpleRangeSet(metaclass=RenamedClass): ############################################################################ -class SetOperator(SetData, Set): +class SetOperator(_ScalarSetMixin, SetData, Set): __slots__ = ('_sets',) def __init__(self, *args, **kwds): @@ -3609,7 +3616,7 @@ def __len__(self): return len(set0) + sum(1 for s in set1 if s not in set0) -class SetUnion_OrderedSet(_ScalarOrderedSetMixin, _OrderedSetMixin, SetUnion_FiniteSet): +class SetUnion_OrderedSet(_OrderedSetMixin, SetUnion_FiniteSet): __slots__ = tuple() def at(self, index): @@ -3749,9 +3756,7 @@ def __len__(self): return sum(1 for _ in self) -class SetIntersection_OrderedSet( - _ScalarOrderedSetMixin, _OrderedSetMixin, SetIntersection_FiniteSet -): +class SetIntersection_OrderedSet(_OrderedSetMixin, SetIntersection_FiniteSet): __slots__ = tuple() def at(self, index): @@ -3843,9 +3848,7 @@ def __len__(self): return sum(1 for _ in self) -class SetDifference_OrderedSet( - _ScalarOrderedSetMixin, _OrderedSetMixin, SetDifference_FiniteSet -): +class SetDifference_OrderedSet(_OrderedSetMixin, SetDifference_FiniteSet): __slots__ = tuple() def at(self, index): @@ -3954,7 +3957,7 @@ def __len__(self): class SetSymmetricDifference_OrderedSet( - _ScalarOrderedSetMixin, _OrderedSetMixin, SetSymmetricDifference_FiniteSet + _OrderedSetMixin, SetSymmetricDifference_FiniteSet ): __slots__ = tuple() @@ -4236,9 +4239,7 @@ def __len__(self): return ans -class SetProduct_OrderedSet( - _ScalarOrderedSetMixin, _OrderedSetMixin, SetProduct_FiniteSet -): +class SetProduct_OrderedSet(_OrderedSetMixin, SetProduct_FiniteSet): __slots__ = tuple() def at(self, index): @@ -4287,7 +4288,7 @@ def ord(self, item): ############################################################################ -class _AnySet(SetData, Set): +class _AnySet(_ScalarSetMixin, SetData, Set): def __init__(self, **kwds): SetData.__init__(self, component=self) # There is a chicken-and-egg game here: the SetInitializer uses @@ -4343,7 +4344,7 @@ def get(self, val, default=None): return super(_AnyWithNoneSet, self).get(val, default) -class _EmptySet(_FiniteSetMixin, SetData, Set): +class _EmptySet(_FiniteSetMixin, _ScalarSetMixin, SetData, Set): def __init__(self, **kwds): SetData.__init__(self, component=self) Set.__init__(self, **kwds) From ce0a6c12104cec805c1666c47a32a4353aabaee9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 14 Dec 2025 21:08:32 -0700 Subject: [PATCH 15/21] Add the option to control sorting in pprint() --- pyomo/contrib/cp/sequence_var.py | 2 +- pyomo/core/base/block.py | 30 ++++--- pyomo/core/base/boolean_var.py | 2 +- pyomo/core/base/component.py | 22 +++-- pyomo/core/base/connector.py | 4 +- pyomo/core/base/constraint.py | 2 +- pyomo/core/base/expression.py | 2 +- pyomo/core/base/external.py | 4 +- pyomo/core/base/indexed_component.py | 2 +- pyomo/core/base/logical_constraint.py | 2 +- pyomo/core/base/objective.py | 2 +- pyomo/core/base/param.py | 2 +- pyomo/core/base/set.py | 6 +- pyomo/core/base/suffix.py | 10 ++- pyomo/core/base/var.py | 2 +- pyomo/core/tests/unit/test_block.py | 114 +++++++++++++++++++++++++- pyomo/core/tests/unit/test_set.py | 8 +- pyomo/gdp/disjunct.py | 2 +- pyomo/mpec/complementarity.py | 6 +- pyomo/network/arc.py | 2 +- pyomo/network/port.py | 4 +- 21 files changed, 182 insertions(+), 48 deletions(-) diff --git a/pyomo/contrib/cp/sequence_var.py b/pyomo/contrib/cp/sequence_var.py index 57f555d36bf..46181e4607e 100644 --- a/pyomo/contrib/cp/sequence_var.py +++ b/pyomo/contrib/cp/sequence_var.py @@ -134,7 +134,7 @@ def _pprint(self): ] return ( headers, - self._data.items(), + self.items, ("IntervalVars",), lambda k, v: ['[' + ', '.join(iv.name for iv in v.interval_vars) + ']'], ) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 97b2dee721b..6028d3272db 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -1851,7 +1851,7 @@ def is_constructed(self): return False return True - def _pprint_blockdata_components(self, ostream): + def _pprint_blockdata_components(self, ostream, sort): # # We hard-code the order of the core Pyomo modeling # components, to ensure that the output follows the logical order @@ -1877,7 +1877,10 @@ def _pprint_blockdata_components(self, ostream): indented_ostream = StreamIndenter(ostream, self._PPRINT_INDENT) for item in items: - keys = sorted(self.component_map(item)) + if SortComponents.ALPHABETICAL in sort: + keys = sorted(self.component_map(item)) + else: + keys = list(self.component_map(item)) if not keys: continue # @@ -1885,7 +1888,7 @@ def _pprint_blockdata_components(self, ostream): # ostream.write("%d %s Declarations\n" % (len(keys), item.__name__)) for key in keys: - self.component(key).pprint(ostream=indented_ostream) + self.component(key).pprint(ostream=indented_ostream, sort=sort) ostream.write("\n") # # Model Order @@ -2252,25 +2255,26 @@ def construct(self, data=None): _BlockConstruction.data.pop(id(self), None) timer.report() - def _pprint_callback(self, ostream, idx, data): + def _pprint_callback(self, ostream, sort, idx, data): if not self.is_indexed(): - data._pprint_blockdata_components(ostream) + data._pprint_blockdata_components(ostream, sort) else: ostream.write("%s : Active=%s\n" % (data.name, data.active)) ostream = StreamIndenter(ostream, self._PPRINT_INDENT) - data._pprint_blockdata_components(ostream) + data._pprint_blockdata_components(ostream, sort) def _pprint(self): - _attrs = [ - ("Size", len(self)), - ("Index", self._index_set if self.is_indexed() else None), - ('Active', self.active), - ] # HACK: suppress the top-level block header (for historical reasons) if self.parent_block() is None and not self.is_indexed(): - return None, self._data.items(), None, self._pprint_callback + _attrs = None else: - return _attrs, self._data.items(), None, self._pprint_callback + _attrs = [ + ("Size", len(self)), + ("Index", self._index_set if self.is_indexed() else None), + ('Active', self.active), + ] + + return _attrs, self.items, None, self._pprint_callback def display(self, filename=None, ostream=None, prefix=""): """ diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index 46536596832..a99f3681b1d 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -457,7 +457,7 @@ def _pprint(self): ("Size", len(self)), ("Index", self._index_set if self.is_indexed() else None), ], - self._data.items(), + self.items, ("Value", "Fixed", "Stale"), lambda k, v: [v.value, v.fixed, v.stale], ) diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 55d0ab8b63c..ea03f01f8f5 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -35,6 +35,7 @@ from pyomo.common.modeling import NOTSET from pyomo.common.sorting import sorted_robust from pyomo.core.pyomoobject import PyomoObject +from pyomo.core.base.enums import SortComponents from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.initializer import PartialInitializer @@ -45,6 +46,7 @@ ) _ref_types = {type(None), weakref_ref} +DEFAULT_PPRINT_SORT = SortComponents.ALPHABETICAL | SortComponents.SORTED_INDICES class ModelComponentFactoryClass(Factory): @@ -278,7 +280,7 @@ def __deepcopy_field__(self, value, memo, slot_name): def cname(self, *args, **kwds): return self.getname(*args, **kwds) - def pprint(self, ostream=None, verbose=False, prefix=""): + def pprint(self, ostream=None, verbose=False, prefix="", sort=NOTSET): """Print component information Note that this method is generally only reachable through @@ -296,12 +298,13 @@ def pprint(self, ostream=None, verbose=False, prefix=""): _name = comp.local_name else: # restrict output to only this data object - _data = iter(((self.index(), self),)) + _data = lambda _sort: iter(((self.index(), self),)) _name = "{Member of %s}" % (comp.local_name,) self._pprint_base_impl( ostream, verbose, prefix, + sort, _name, comp.doc, comp.is_constructed(), @@ -352,6 +355,7 @@ def _pprint_base_impl( ostream, verbose, prefix, + sort, _name, _doc, _constructed, @@ -365,6 +369,10 @@ def _pprint_base_impl( if prefix: ostream = StreamIndenter(ostream, prefix) + if sort is NOTSET: + sort = DEFAULT_PPRINT_SORT + sort = SortComponents(sort) + # FIXME: HACK for backwards compatibility with suppressing the # header for the top block if not _attr and self.parent_block() is None: @@ -408,6 +416,9 @@ def _pprint_base_impl( else: _fcn2 = None + if hasattr(_data, '__call__'): + _data = _data(sort) + if _header is not None: # This is a standard component, where all the component # information is printed in a single table @@ -416,14 +427,14 @@ def _pprint_base_impl( tabular_writer(ostream, '', _data, _header, _fcn) if _fcn2 is not None: for _key, _val in _data: - _fcn2(ostream, _key, _val) + _fcn2(ostream, sort, _key, _val) elif _fcn is not None: # This is a non-standard component where we will not # generate a table at all, and instead defer all formatting # / printing to the callback. This is primarily used by # BLocks (and block-like things) for _key, _val in _data: - _fcn(ostream, _key, _val) + _fcn(ostream, sort, _key, _val) elif _data is not None: # Catch all for everything else: assume that _pprint() # returned a formatted string. @@ -531,12 +542,13 @@ def valid_model_component(self): """Return True if this can be used as a model component.""" return True - def pprint(self, ostream=None, verbose=False, prefix=""): + def pprint(self, ostream=None, verbose=False, prefix="", sort=NOTSET): """Print component information""" self._pprint_base_impl( ostream, verbose, prefix, + sort, self.local_name, self.doc, self.is_constructed(), diff --git a/pyomo/core/base/connector.py b/pyomo/core/base/connector.py index 26b87569e42..e3ec3a3a4e1 100644 --- a/pyomo/core/base/connector.py +++ b/pyomo/core/base/connector.py @@ -202,7 +202,7 @@ def _initialize_members(self, initSet): for key, val in items.items(): tmp.add(val, key) - def _pprint(self, ostream=None, verbose=False): + def _pprint(self): """Print component information.""" def _line_generator(k, v): @@ -222,7 +222,7 @@ def _line_generator(k, v): ("Size", len(self)), ("Index", self._index_set if self.is_indexed() else None), ], - self._data.items(), + self.items, ("Name", "Size", "Variable"), _line_generator, ) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 970c393425b..8fe9781c500 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -766,7 +766,7 @@ def _pprint(self): ("Index", self._index_set if self.is_indexed() else None), ("Active", self.active), ], - self.items(), + self.items, ("Lower", "Body", "Upper", "Active"), lambda k, v: [ "-Inf" if v.lower is None else v.lower, diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index e3766e0ec45..28e51b069ce 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -303,7 +303,7 @@ def _pprint(self): ('Size', len(self)), ('Index', None if (not self.is_indexed()) else self._index_set), ], - self.items(), + self.items, ("Expression",), lambda k, v: ["Undefined" if v.expr is None else v.expr], ) diff --git a/pyomo/core/base/external.py b/pyomo/core/base/external.py index a968e886219..a9485d10864 100644 --- a/pyomo/core/base/external.py +++ b/pyomo/core/base/external.py @@ -460,7 +460,7 @@ def _pprint(self): ), ), ], - (), + None, None, None, ) @@ -614,7 +614,7 @@ def _pprint(self): ), ), ], - (), + None, None, None, ) diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index 9d3479f9641..2c8d1f2ce4e 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -1142,7 +1142,7 @@ def _pprint(self): ("Size", len(self)), ("Index", self._index_set if self.is_indexed() else None), ], - self._data.items(), + self.items, ("Object",), lambda k, v: [type(v)], ) diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index fdb9870bc85..066ee19559b 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -309,7 +309,7 @@ def _pprint(self): ("Index", self._index_set if self.is_indexed() else None), ("Active", self.active), ], - self.items(), + self.items, ("Body", "Active"), lambda k, v: [v.body, v.active], ) diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index 5123a9207f9..9d80b692e18 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -393,7 +393,7 @@ def _pprint(self): ("Index", self._index_set if self.is_indexed() else None), ("Active", self.active), ], - self._data.items(), + self.items, ("Active", "Sense", "Expression"), lambda k, v: [v.active, v.sense, v.expr], ) diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index 8bac8d08bdc..fe158673028 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -964,7 +964,7 @@ def _pprint(self): ] if self._units is not None: headers.append(('Units', str(self._units))) - return (headers, self.sparse_items(), ("Value",), dataGen) + return (headers, self.sparse_items, ("Value",), dataGen) class ScalarParam(ParamData, Param): diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index c3ee26063dc..3879594a288 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -2490,7 +2490,7 @@ def _pprint(self): ("Index", self._index_set if self.is_indexed() else None), ("Ordered", _ordered), ], - self._data.items(), + self.items, ("Dimen", "Domain", "Size", "Members"), lambda k, v: [ Set._pprint_dimen(v), @@ -2659,7 +2659,7 @@ def _pprint(self): """ return ( [("Dimen", self.dimen), ("Size", len(self)), ("Bounds", self.bounds())], - {None: self}.items(), + [(UnindexedComponent_index, self)], ("Ordered", "Members"), lambda k, v: [v.isordered(), str(v._ref)], ) @@ -3320,7 +3320,7 @@ def _pprint(self): ("Size", len(self) if self.isfinite() else 'Inf'), ("Bounds", self.bounds()), ], - {None: self}.items(), + [(UnindexedComponent_index, self)], ("Finite", "Members"), lambda k, v: [ v.isfinite(), # isinstance(v, _FiniteSetMixin), diff --git a/pyomo/core/base/suffix.py b/pyomo/core/base/suffix.py index 0ce55c67057..1dd8797c394 100644 --- a/pyomo/core/base/suffix.py +++ b/pyomo/core/base/suffix.py @@ -10,6 +10,7 @@ # ___________________________________________________________________________ import logging +import operator from pyomo.common.collections import ComponentMap from pyomo.common.config import In @@ -22,6 +23,7 @@ from pyomo.core.base.block import BlockData from pyomo.core.base.component import ActiveComponent, ModelComponentFactory from pyomo.core.base.disable_methods import disable_methods +from pyomo.core.base.enums import SortComponents from pyomo.core.base.initializer import Initializer logger = logging.getLogger('pyomo.core') @@ -386,12 +388,18 @@ def get_direction(self): return self.direction def _pprint(self): + def _data(sort): + data = ((str(k), v) for k, v in self._dict.values()) + if SortComponents.SORTED_INDICES in sort: + data = sorted(data, key=operator.itemgetter(0)) + return data + return ( [ ('Direction', str(self._direction.name)), ('Datatype', getattr(self._datatype, 'name', 'None')), ], - ((str(k), v) for k, v in self._dict.values()), + _data, ("Value",), lambda k, v: [v], ) diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index 6b9b5fb4151..0830c10db2d 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -844,7 +844,7 @@ def _pprint(self): headers.append(('Units', str(self._units))) return ( headers, - self._data.items(), + self.items, ("Lower", "Value", "Upper", "Fixed", "Stale", "Domain"), lambda k, v: [ value(v.lb), diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index 3b1045a47c8..0c5c439863d 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -2683,10 +2683,10 @@ def test_pprint(self): a[1] : Active=True 2 Block Declarations c : Size=2, Index=a1_IDX, Active=True - a[1].c[5] : Active=True - 0 Declarations: a[1].c[4] : Active=True 0 Declarations: + a[1].c[5] : Active=True + 0 Declarations: d : Size=1, Index=None, Active=True 0 Declarations: @@ -2713,6 +2713,116 @@ def test_pprint(self): """ self.assertEqual(ref, buf.getvalue()) + def test_pprint_sorting(self): + m = ConcreteModel() + m.I = Set(ordered=False, initialize=[3, 'a', 1]) + m.y = Var(m.I) + m.x = Var([3, 2, 1]) + + OUT = StringIO() + m.pprint(ostream=OUT, sort=False) + self.assertEqual( + """1 Set Declarations + I : Size=1, Index=None, Ordered=False + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {%s, %s, %s} + +2 Var Declarations + y : Size=3, Index=I + Key : Lower : Value : Upper : Fixed : Stale : Domain + %s : None : None : None : False : True : Reals + %s : None : None : None : False : True : Reals + %s : None : None : None : False : True : Reals + x : Size=3, Index={3, 2, 1} + Key : Lower : Value : Upper : Fixed : Stale : Domain + 3 : None : None : None : False : True : Reals + 2 : None : None : None : False : True : Reals + 1 : None : None : None : False : True : Reals + +3 Declarations: I y x +""" + % (tuple(repr(_) for _ in m.I.ordered_iter()) + tuple(m.I)), + OUT.getvalue(), + ) + + OUT = StringIO() + m.pprint(ostream=OUT, sort=SortComponents.ALPHABETICAL) + self.assertEqual( + """1 Set Declarations + I : Size=1, Index=None, Ordered=False + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {%s, %s, %s} + +2 Var Declarations + x : Size=3, Index={3, 2, 1} + Key : Lower : Value : Upper : Fixed : Stale : Domain + 3 : None : None : None : False : True : Reals + 2 : None : None : None : False : True : Reals + 1 : None : None : None : False : True : Reals + y : Size=3, Index=I + Key : Lower : Value : Upper : Fixed : Stale : Domain + %s : None : None : None : False : True : Reals + %s : None : None : None : False : True : Reals + %s : None : None : None : False : True : Reals + +3 Declarations: I y x +""" + % (tuple(repr(_) for _ in m.I.ordered_iter()) + tuple(m.I)), + OUT.getvalue(), + ) + + OUT = StringIO() + m.pprint(ostream=OUT, sort=SortComponents.ORDERED_INDICES) + self.assertEqual( + """1 Set Declarations + I : Size=1, Index=None, Ordered=False + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {%s, %s, %s} + +2 Var Declarations + y : Size=3, Index=I + Key : Lower : Value : Upper : Fixed : Stale : Domain + 1 : None : None : None : False : True : Reals + 3 : None : None : None : False : True : Reals + a : None : None : None : False : True : Reals + x : Size=3, Index={3, 2, 1} + Key : Lower : Value : Upper : Fixed : Stale : Domain + 3 : None : None : None : False : True : Reals + 2 : None : None : None : False : True : Reals + 1 : None : None : None : False : True : Reals + +3 Declarations: I y x +""" + % tuple(repr(_) for _ in m.I.ordered_iter()), + OUT.getvalue(), + ) + + OUT = StringIO() + m.pprint(ostream=OUT, sort=True) + self.assertEqual( + """1 Set Declarations + I : Size=1, Index=None, Ordered=False + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {%s, %s, %s} + +2 Var Declarations + x : Size=3, Index={3, 2, 1} + Key : Lower : Value : Upper : Fixed : Stale : Domain + 1 : None : None : None : False : True : Reals + 2 : None : None : None : False : True : Reals + 3 : None : None : None : False : True : Reals + y : Size=3, Index=I + Key : Lower : Value : Upper : Fixed : Stale : Domain + 1 : None : None : None : False : True : Reals + 3 : None : None : None : False : True : Reals + a : None : None : None : False : True : Reals + +3 Declarations: I y x +""" + % tuple(repr(_) for _ in m.I.ordered_iter()), + OUT.getvalue(), + ) + @unittest.skipIf(not 'glpk' in solvers, "glpk solver is not available") def test_solve1(self): model = Block(concrete=True) diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index dc885012221..5e445c27c73 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -6308,13 +6308,13 @@ def c_rule(m, a, b, c): None : None : Any : 3 : {((1, 2, 3), 4, 3), ((1, 2, 2), 4, 3), ((1, 3, 3), 4, 3)} x : Size=3, Index=CHOICES Key : Lower : Value : Upper : Fixed : Stale : Domain - ((1, 2, 3), 4, 3) : None : None : None : False : True : Reals ((1, 2, 2), 4, 3) : None : None : None : False : True : Reals + ((1, 2, 3), 4, 3) : None : None : None : False : True : Reals ((1, 3, 3), 4, 3) : None : None : None : False : True : Reals c : Size=3, Index=CHOICES, Active=True Key : Lower : Body : Upper : Active - ((1, 2, 3), 4, 3) : 0.0 : x[(1,2,3),4,3] : 0.0 : True ((1, 2, 2), 4, 3) : 0.0 : x[(1,2,2),4,3] : 0.0 : True + ((1, 2, 3), 4, 3) : 0.0 : x[(1,2,3),4,3] : 0.0 : True ((1, 3, 3), 4, 3) : 0.0 : x[(1,3,3),4,3] : 0.0 : True """.strip() self.assertEqual(output.getvalue().strip(), ref) @@ -6343,13 +6343,13 @@ def c_rule(m, a1, a2, a3, b, c): None : 5 : Any : 3 : {(1, 2, 3, 4, 3), (1, 2, 2, 4, 3), (1, 3, 3, 4, 3)} x : Size=3, Index=CHOICES Key : Lower : Value : Upper : Fixed : Stale : Domain - (1, 2, 3, 4, 3) : None : None : None : False : True : Reals (1, 2, 2, 4, 3) : None : None : None : False : True : Reals + (1, 2, 3, 4, 3) : None : None : None : False : True : Reals (1, 3, 3, 4, 3) : None : None : None : False : True : Reals c : Size=3, Index=CHOICES, Active=True Key : Lower : Body : Upper : Active - (1, 2, 3, 4, 3) : 0.0 : x[1,2,3,4,3] : 0.0 : True (1, 2, 2, 4, 3) : 0.0 : x[1,2,2,4,3] : 0.0 : True + (1, 2, 3, 4, 3) : 0.0 : x[1,2,3,4,3] : 0.0 : True (1, 3, 3, 4, 3) : 0.0 : x[1,3,3,4,3] : 0.0 : True """.strip() self.assertEqual(output.getvalue().strip(), ref) diff --git a/pyomo/gdp/disjunct.py b/pyomo/gdp/disjunct.py index fd2f0c5afd6..d20cbd449fc 100644 --- a/pyomo/gdp/disjunct.py +++ b/pyomo/gdp/disjunct.py @@ -765,7 +765,7 @@ def _pprint(self): ("Index", self._index_set if self.is_indexed() else None), ("Active", self.active), ], - self.items(), + self.items, ("Disjuncts", "Active", "XOR"), lambda k, v: [[x.name for x in v.disjuncts], v.active, v.xor], ) diff --git a/pyomo/mpec/complementarity.py b/pyomo/mpec/complementarity.py index be1779f37e0..c3d0a4b613c 100644 --- a/pyomo/mpec/complementarity.py +++ b/pyomo/mpec/complementarity.py @@ -287,9 +287,9 @@ def _pprint(self): # Book). _transformed = not issubclass(self.ctype, Complementarity) - def _conditional_block_printer(ostream, idx, data): + def _conditional_block_printer(ostream, sort, idx, data): if _transformed or len(data.component_map()): - self._pprint_callback(ostream, idx, data) + self._pprint_callback(ostream, sort, idx, data) return ( [ @@ -297,7 +297,7 @@ def _conditional_block_printer(ostream, idx, data): ("Index", self._index_set if self.is_indexed() else None), ("Active", self.active), ], - self._data.items(), + self.items, ("Arg0", "Arg1", "Active"), (_table_data, _conditional_block_printer), ) diff --git a/pyomo/network/arc.py b/pyomo/network/arc.py index 5bea3f75704..560c59f7c4d 100644 --- a/pyomo/network/arc.py +++ b/pyomo/network/arc.py @@ -368,7 +368,7 @@ def _pprint(self): ("Index", self._index_set if self.is_indexed() else None), ("Active", self.active), ], - self.items(), + self.items, ("Ports", "Directed", "Active"), lambda k, v: [ "(%s, %s)" % v.ports if v.ports is not None else None, diff --git a/pyomo/network/port.py b/pyomo/network/port.py index 882e0bf911b..10dcd7b94e1 100644 --- a/pyomo/network/port.py +++ b/pyomo/network/port.py @@ -415,7 +415,7 @@ def _add_from_container(self, port, items): else: port.add(val) - def _pprint(self, ostream=None, verbose=False): + def _pprint(self): """Print component information.""" def _line_generator(k, v): @@ -433,7 +433,7 @@ def _line_generator(k, v): ("Size", len(self)), ("Index", self._index_set if self.is_indexed() else None), ], - self._data.items(), + self.items, ("Name", "Size", "Variable"), _line_generator, ) From 7f8687c5b4b5b85283002b26d4eb7f38312fb217 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 14 Dec 2025 21:08:48 -0700 Subject: [PATCH 16/21] Make IndexedCOmponent .items() and .values() more robust --- pyomo/core/base/indexed_component.py | 37 +++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index 2c8d1f2ce4e..e97e9755d6f 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -562,7 +562,23 @@ def values(self, sort=SortComponents.UNSORTED, ordered=NOTSET): return self._data.values(sort) except TypeError: pass - return map(self.__getitem__, self.keys(sort)) + # We would like to look things up directly in _data (as that is + # fast, since we know that keys() will return valid entries). + # However, some components (notably Param with a default) have + # valid keys that do not have an entry in _data. If we just + # rely on getitem, then we will hit issues for abstract + # components. So we will use a custom getter that tries _data + # first (both for efficiency and to correctly handle + # AbstractScalar components), and then falls back on getitem. + _getdata = self._data.__getitem__ + + def getter(s): + try: + return _getdata(s) + except KeyError: + return self[s] + + return map(getter, self.keys(sort)) def items(self, sort=SortComponents.UNSORTED, ordered=NOTSET): """Return an iterator of (index,data) component data tuples @@ -597,8 +613,23 @@ def items(self, sort=SortComponents.UNSORTED, ordered=NOTSET): return self._data.items(sort) except TypeError: pass - _getitem = self.__getitem__ - return ((s, _getitem(s)) for s in self.keys(sort)) + # We would like to look things up directly in _data (as that is + # fast, since we know that keys() will return valid entries). + # However, some components (notably Param with a default) have + # valid keys that do not have an entry in _data. If we just + # rely on getitem, then we will hit issues in pprint for + # abstract components. So we will use a custom getter that + # tries _data first (both for efficiency and to correctly handle + # AbstractScalar components), and then falls back on getitem. + _getdata = self._data.__getitem__ + + def getter(s): + try: + return s, _getdata(s) + except KeyError: + return s, self[s] + + return map(getter, self.keys(sort)) @deprecated('The iterkeys method is deprecated. Use dict.keys().', version='6.0') def iterkeys(self): From 31a05bc7f158dbc2ae9555aaed39770ebb74bcee Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 14 Dec 2025 21:09:47 -0700 Subject: [PATCH 17/21] NFC: fix comment --- pyomo/core/base/param.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index fe158673028..764e37cdb33 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -425,7 +425,7 @@ def mutable(self): return self._mutable def get_units(self): - """Return the units for this ParamData""" + """Return the units for this Param""" return self._units # From cb77a4f991cab3153a0f1ac9240c1223300adffa Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 14 Dec 2025 21:24:03 -0700 Subject: [PATCH 18/21] Revert "Move name_repr, index_repr into common.formatting" This reverts commit f1a97393102e2e461be944a02f829eae16206ad8. --- pyomo/common/formatting.py | 82 ------------------------------ pyomo/core/base/component.py | 14 ++--- pyomo/core/base/component_namer.py | 80 +++++++++++++++++++++-------- pyomo/core/base/componentuid.py | 10 ++-- 4 files changed, 68 insertions(+), 118 deletions(-) diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index 6d42e4c1168..da57dbf0c0d 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -102,88 +102,6 @@ def tostr(value, quote_str=False): } -# Literals are used in parsing string names (and indicate tuples, -# indexing, and token separators) -name_literals = '()[],.' -# Special characters are additional characters that if they appear in -# the string force us to quote the string. This includes the obvious -# things like single and double quote characters, but also backslash -# (indicates that the string contains escaped - possibly unicode - -# characters), and the colon (used as a token separator in the old -# ComponentUID "v1" format). -name_special_chars = name_literals + '\'":\\' - -re_number = re.compile( - r'(?:[-+]?(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][-+]?[0-9]+)?|-?inf|nan)' -) -re_name_special_char = re.compile(r'[' + re.escape(name_special_chars) + ']') - - -def name_repr(x, unknown_handler=str): - """Generate a "friendly" string representation of a Pyomo name. - - Convert ``x`` into a "user-friendly" string. Numbers are converted - to strings. Strings are left unquoted, *unless* the string contains - a "special" character that is used in parsing Pyomom component names - (currently any of ``{name_special_chars}``) or the string could be - interpreted as a number. - - """ - if x.__class__ is not str: - _repr = _repr_map.get(x.__class__, None) - if _repr is None: - if not isinstance(x, str): - return unknown_handler(x) - else: - return _repr(x) - # Special handling for strings: only quote the string if it contains - # "special" characters or looks like a number - quoted = repr(x) - if quoted[1] == '|': - return quoted - unquoted = quoted[1:-1] - if re_name_special_char.search(unquoted): - return quoted - if re_number.fullmatch(unquoted): - return quoted - return unquoted - - -def tuple_repr(x, unknown_handler=str): - return ( - '(' - + ','.join(name_repr(_, unknown_handler) for _ in x) - + (',)' if len(x) == 1 else ')') - ) - - -def index_repr(idx, unknown_handler=str): - """Return a string representation of an index. - - This will nominally return the :func:`name_repr` for the elements of - ``idx``. For 1-tuples, the parens are omittted unless the tuple is - empty. - - Note that the brackets (``[]``) normally associated with Pyomo - indexes are *not* included in the resulting string. - - """ - if idx.__class__ is tuple and len(idx) > 1: - return ",".join(name_repr(i, unknown_handler) for i in idx) - return name_repr(idx, unknown_handler) - - -_repr_map = { - slice: lambda x: '*', - Ellipsis.__class__: lambda x: '**', - int: repr, - float: repr, - str: repr, - # Note: the function is unbound at this point; extract with __func__ - tuple: tuple_repr, -} - - def tabular_writer(ostream, prefix, data, header, row_generator): """Output data in tabular form diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index ea03f01f8f5..21a4813cb4b 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -26,15 +26,11 @@ relocated_module_attribute, ) from pyomo.common.factory import Factory -from pyomo.common.formatting import ( - tabular_writer, - StreamIndenter, - name_repr, - index_repr, -) +from pyomo.common.formatting import tabular_writer, StreamIndenter from pyomo.common.modeling import NOTSET from pyomo.common.sorting import sorted_robust from pyomo.core.pyomoobject import PyomoObject +from pyomo.core.base.component_namer import name_repr, index_repr from pyomo.core.base.enums import SortComponents from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.initializer import PartialInitializer @@ -75,7 +71,7 @@ def name(component, index=NOTSET, fully_qualified=False, relative_to=None): raise KeyError( "Index %s is not valid for component %s" % (index, component.name) ) - return base + '[' + index_repr(index) + ']' + return base + index_repr(index) @deprecated(msg="The cname() function has been renamed to name()", version='5.6.9') @@ -929,7 +925,7 @@ def getname(self, fully_qualified=False, name_buffer=None, relative_to=None): # Iterate through the dictionary and generate all names in # the buffer for idx, obj in c.items(): - name_buffer[id(obj)] = base + '[' + index_repr(idx) + ']' + name_buffer[id(obj)] = base + index_repr(idx) if id(self) in name_buffer: # Return the name if it is in the buffer return name_buffer[id(self)] @@ -938,7 +934,7 @@ def getname(self, fully_qualified=False, name_buffer=None, relative_to=None): # No buffer, we can do what we are going to do all the time after we # deprecate the buffer. # - return base + '[' + index_repr(self.index()) + ']' + return base + index_repr(self.index()) # raise RuntimeError( "Fatal error: cannot find the component data in " diff --git a/pyomo/core/base/component_namer.py b/pyomo/core/base/component_namer.py index 6c758218aa4..8168f01288c 100644 --- a/pyomo/core/base/component_namer.py +++ b/pyomo/core/base/component_namer.py @@ -9,29 +9,65 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.common.deprecation import deprecated, relocated_module_attribute -from pyomo.common.formatting import index_repr as _index_repr - -for attr, new_attr in ( - ('literals', 'name_literals'), - ('special_chars', 'name_special_chars'), - ('re_number', 're_number'), - ('re_special_char', 're_name_special_char'), - ('name_repr', 'name_repr'), - ('tuple_repr', 'tuple_repr'), -): - relocated_module_attribute( - attr, - 'pyomo.common.formatting.' + new_attr, - version='6.10.0.dev0', - f_globals=globals(), - ) +import re +# Literals are used in parsing string names (and indicate tuples, +# indexing, and token separators) +literals = '()[],.' +# Special characters are additional characters that if they appear in +# the string force us to quote the string. This includes the obvious +# things like single and double quote characters, but also backslash +# (indicates that the string contains escaped - possibly unicode - +# characters), and the colon (used as a token separator in the old +# ComponentUID "v1" format). +special_chars = literals + '\'":\\' -@deprecated( - "index_repr has moved to pyomom.common.formatting.index_repr. " - "Note that the return value has also changed.", - version='6.10.0.dev0', +re_number = re.compile( + r'(?:[-+]?(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][-+]?[0-9]+)?|-?inf|nan)' ) +re_special_char = re.compile(r'[' + re.escape(special_chars) + ']') + + +def name_repr(x, unknown_handler=str): + if not isinstance(x, str): + return _repr_map.get(x.__class__, unknown_handler)(x) + else: + x = repr(x) + if x[1] == '|': + return x + unquoted = x[1:-1] + if re_special_char.search(unquoted): + return x + if re_number.fullmatch(unquoted): + return x + return unquoted + + +def tuple_repr(x, unknown_handler=str): + return ( + '(' + + ','.join(name_repr(_, unknown_handler) for _ in x) + + (',)' if len(x) == 1 else ')') + ) + + def index_repr(idx, unknown_handler=str): - return '[' + _index_repr(idx, unknown_handler) + ']' + """ + Return a string representation of an index. + """ + if idx.__class__ is tuple and len(idx) > 1: + idx_str = ",".join(name_repr(i, unknown_handler) for i in idx) + else: + idx_str = name_repr(idx, unknown_handler) + return "[" + idx_str + "]" + + +_repr_map = { + slice: lambda x: '*', + Ellipsis.__class__: lambda x: '**', + int: repr, + float: repr, + str: repr, + # Note: the function is unbound at this point; extract with __func__ + tuple: tuple_repr, +} diff --git a/pyomo/core/base/componentuid.py b/pyomo/core/base/componentuid.py index 0420133ca12..ba33647506f 100644 --- a/pyomo/core/base/componentuid.py +++ b/pyomo/core/base/componentuid.py @@ -16,9 +16,9 @@ from pyomo.common.collections import ComponentMap from pyomo.common.dependencies import pickle from pyomo.common.deprecation import deprecated -from pyomo.common.formatting import ( - name_literals as literals, - name_special_chars, +from pyomo.core.base.component_namer import ( + literals, + special_chars, name_repr as __name_repr, index_repr as __index_repr, re_number as _re_number, @@ -40,7 +40,7 @@ def _name_repr(x): def _index_repr(x): - return '[' + __index_repr(x, _pickle) + ']' + return __index_repr(x, _pickle) def _context_err(_type): @@ -683,7 +683,7 @@ def t_NUMBER(t): # number of "non-special" characters. This regex matches numbers as # well as more traditional string names, so it is important that it is # declared *after* t_NUMBER. -@ply.lex.TOKEN(r'[a-zA-Z_0-9][^' + re.escape(name_special_chars) + r']*') +@ply.lex.TOKEN(r'[a-zA-Z_0-9][^' + re.escape(special_chars) + r']*') def t_WORD(t): t.value = t.value.strip() return t From 57ef4526228e581c7a2a60ed0b57e52d14f0239c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 14 Dec 2025 21:25:40 -0700 Subject: [PATCH 19/21] Revert "Minor simplification to tabular_writer (slight performance improvement)" This reverts commit 4bdb3e75018c7f03d2d53235654f84ddee98a183. --- pyomo/common/formatting.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index da57dbf0c0d..0583993fd3a 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -113,7 +113,7 @@ def tabular_writer(ostream, prefix, data, header, row_generator): prefix each generated line with this string data: iterable an iterable object that returns (key, value) pairs - (e.g., from items()) defining each row in the table + (e.g., from iteritems()) defining each row in the table header: List[str] list of column headers row_generator: function @@ -162,21 +162,22 @@ def tabular_writer(ostream, prefix, data, header, row_generator): if not _rows[_key]: _minWidth = 4 elif not _width: - _width = [len(x) for x in _rows[_key][0]] - else: - for _row in _rows[_key]: - for col, x in enumerate(_row): - _width[col] = max(_width[col], len(x)) - - if _minWidth: - for i in range(1, len(_width)): - _width[i] = max(_minWidth, _width[i]) + _width = [0] * len(_rows[_key][0]) + for _row in _rows[_key]: + for col, x in enumerate(_row): + _width[col] = max(_width[col], len(x), col and _minWidth) # NB: left-justify header entries if header: # Note: do not right-pad the last header with unnecessary spaces - line_fmt = " : ".join(f"%-{w}s" for w in _width[:-1]) + " : %s\n" - ostream.write(prefix + (line_fmt % header)) + tmp = _width[-1] + _width[-1] = 0 + ostream.write( + prefix + + " : ".join("%%-%ds" % _width[i] % x for i, x in enumerate(header)) + + "\n" + ) + _width[-1] = tmp # If there is no data, we are done... if not _rows: @@ -184,17 +185,18 @@ def tabular_writer(ostream, prefix, data, header, row_generator): # right-justify data, except for the last column if there are spaces # in the data (probably an expression or vector) - _width = [f"%{w}s" for w in _width] + _width = ["%" + str(i) + "s" for i in _width] + if any(' ' in r[-1] for x in _rows.values() if x is not None for r in x): _width[-1] = '%s' - line_fmt = " : ".join(_width) + "\n" - for _key in _rows: _rowSet = _rows[_key] if not _rowSet: - _rowSet = [(_key,) + (None,) * (len(_width) - 1)] + _rowSet = [[_key] + [None] * (len(_width) - 1)] for _data in _rowSet: - ostream.write(prefix + (line_fmt % _data)) + ostream.write( + prefix + " : ".join(_width[i] % x for i, x in enumerate(_data)) + "\n" + ) class StreamIndenter: From f2513358f663be45cc67869db72c24a88a5ab42a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 22 Dec 2025 10:10:50 -0700 Subject: [PATCH 20/21] Update SOSConstraint to use standard _pprint() infrastructure --- doc/OnlineDocs/src/kernel/examples.txt | 52 +++++++++++--------------- pyomo/core/base/sos.py | 36 ++++++++---------- 2 files changed, 37 insertions(+), 51 deletions(-) diff --git a/doc/OnlineDocs/src/kernel/examples.txt b/doc/OnlineDocs/src/kernel/examples.txt index 8ba072d28b1..7d5d2b34820 100644 --- a/doc/OnlineDocs/src/kernel/examples.txt +++ b/doc/OnlineDocs/src/kernel/examples.txt @@ -76,29 +76,22 @@ 3 : -5.0 : vl[3] - v : 5.0 : True 3 SOSConstraint Declarations - sd : Size=2 Index= OrderedScalarSet - 1 - Type=1 - Weight : Variable - 1 : vd[1] - 2 : vd[2] - 2 - Type=1 - Weight : Variable - 1 : vl[1] - 2 : vl[2] - 3 : vl[3] - sos1 : Size=1 - Type=1 - Weight : Variable - 1 : vl[1] - 2 : vl[2] - 3 : vl[3] - sos2 : Size=1 - Type=2 - Weight : Variable - 1 : vd[1] - 2 : vd[2] + sd : Size=2, Index={1, 2} + Key : Type : Weight : Variable + 1 : 1 : 1 : vd[1] + : : 2 : vd[2] + 2 : 1 : 1 : vl[1] + : : 2 : vl[2] + : : 3 : vl[3] + sos1 : Size=1, Index=None + Key : Type : Weight : Variable + None : 1 : 1 : vl[1] + : : 2 : vl[2] + : : 3 : vl[3] + sos2 : Size=1, Index=None + Key : Type : Weight : Variable + None : 2 : 1 : vd[1] + : : 2 : vd[2] 2 Block Declarations b : Size=1, Index=None, Active=True @@ -120,13 +113,12 @@ 3 : 1.0 : pw.SOS2_y[0] + pw.SOS2_y[1] + pw.SOS2_y[2] + pw.SOS2_y[3] : 1.0 : True 1 SOSConstraint Declarations - SOS2_sosconstraint : Size=1 - Type=2 - Weight : Variable - 1 : pw.SOS2_y[0] - 2 : pw.SOS2_y[1] - 3 : pw.SOS2_y[2] - 4 : pw.SOS2_y[3] + SOS2_sosconstraint : Size=1, Index=None + Key : Type : Weight : Variable + None : 2 : 1 : pw.SOS2_y[0] + : : 2 : pw.SOS2_y[1] + : : 3 : pw.SOS2_y[2] + : : 4 : pw.SOS2_y[3] 3 Declarations: SOS2_y SOS2_constraint SOS2_sosconstraint diff --git a/pyomo/core/base/sos.py b/pyomo/core/base/sos.py index 46581982468..2a9108f68b5 100644 --- a/pyomo/core/base/sos.py +++ b/pyomo/core/base/sos.py @@ -531,27 +531,21 @@ def add(self, index, variables, weights=None): else: soscondata.set_items(variables, weights) - # NOTE: the prefix option is ignored - def pprint(self, ostream=None, verbose=False, prefix=""): - """TODO""" - if ostream is None: - ostream = sys.stdout - ostream.write(" " + self.local_name + " : ") - if not self.doc is None: - ostream.write(self.doc + '\n') - ostream.write(" ") - ostream.write("\tSize=" + str(len(self._data.keys())) + ' ') - if self.is_indexed(): - ostream.write("\tIndex= " + self._index_set.name + '\n') - else: - ostream.write("\n") - for val in self._data: - if not val is None: - ostream.write("\t" + str(val) + '\n') - ostream.write("\t\tType=" + str(self._data[val].level) + '\n') - ostream.write("\t\tWeight : Variable\n") - for var, weight in self._data[val].get_items(): - ostream.write("\t\t" + str(weight) + ' : ' + var.name + '\n') + def _pprint(self): + """Print component information.""" + headers = [ + ("Size", len(self)), + ("Index", self._index_set if self.is_indexed() else None), + ] + return ( + headers, + self.items, + ("Type", "Weight", "Variable"), + lambda k, v: ( + ("" if i else v.level, w, var) + for i, (var, w) in enumerate(v.get_items()) + ), + ) class ScalarSOSConstraint(SOSConstraint, SOSConstraintData): From e3d6e82f2d974b3c38e8a1a6bdd16bda16d67084 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 22 Dec 2025 10:11:22 -0700 Subject: [PATCH 21/21] Remove unneeded Suffix.pprint() overload --- pyomo/core/base/suffix.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyomo/core/base/suffix.py b/pyomo/core/base/suffix.py index 1dd8797c394..43d942863a5 100644 --- a/pyomo/core/base/suffix.py +++ b/pyomo/core/base/suffix.py @@ -410,9 +410,6 @@ def _data(sort): # complications with __setstate__ # - def pprint(self, *args, **kwds): - return ActiveComponent.pprint(self, *args, **kwds) - def __str__(self): return ActiveComponent.__str__(self)