Source code for pyomo.core.base.suffix

#  ___________________________________________________________________________
#
#  Pyomo: Python Optimization Modeling Objects
#  Copyright (c) 2008-2024
#  National Technology and Engineering Solutions of Sandia, LLC
#  Under the terms of Contract DE-NA0003525 with National Technology and
#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
#  rights in this software.
#  This software is distributed under the 3-clause BSD License.
#  ___________________________________________________________________________

import logging

from pyomo.common.collections import ComponentMap
from pyomo.common.config import In
from pyomo.common.deprecation import deprecated
from pyomo.common.enums import IntEnum
from pyomo.common.log import is_debug_set
from pyomo.common.modeling import NOTSET
from pyomo.common.pyomo_typing import overload
from pyomo.common.timing import ConstructionTimer
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.initializer import Initializer

logger = logging.getLogger('pyomo.core')

_SUFFIX_API = (
    ('__contains__', 'test membership in'),
    ('__iter__', 'iterate over'),
    '__getitem__',
    '__setitem__',
    'set_value',
    'set_all_values',
    'clear_value',
    'clear_all_values',
    'update_values',
)

# A list of convenient suffix generators, including:
#   - active_export_suffix_generator
#       **(used by problem writers)
#   - export_suffix_generator
#   - active_import_suffix_generator
#       **(used by OptSolver and PyomoModel._load_solution)
#   - import_suffix_generator
#   - active_local_suffix_generator
#   - local_suffix_generator
#   - active_suffix_generator
#   - suffix_generator


[docs] def suffix_generator(a_block, datatype=NOTSET, direction=NOTSET, active=None): _iter = a_block.component_map(Suffix, active=active).items() if direction is not NOTSET: direction = _SuffixDirectionDomain(direction) if not direction: _iter = filter(lambda item: item[1].direction == direction, _iter) else: _iter = filter(lambda item: item[1].direction & direction, _iter) if datatype is not NOTSET: _iter = filter(lambda item: item[1].datatype == datatype, _iter) return _iter
[docs] def active_export_suffix_generator(a_block, datatype=NOTSET): return suffix_generator(a_block, datatype, SuffixDirection.EXPORT, True)
[docs] def export_suffix_generator(a_block, datatype=NOTSET): return suffix_generator(a_block, datatype, SuffixDirection.EXPORT)
[docs] def active_import_suffix_generator(a_block, datatype=NOTSET): return suffix_generator(a_block, datatype, SuffixDirection.IMPORT, True)
[docs] def import_suffix_generator(a_block, datatype=NOTSET): return suffix_generator(a_block, datatype, SuffixDirection.IMPORT)
[docs] def active_local_suffix_generator(a_block, datatype=NOTSET): return suffix_generator(a_block, datatype, SuffixDirection.LOCAL, True)
[docs] def local_suffix_generator(a_block, datatype=NOTSET): return suffix_generator(a_block, datatype, SuffixDirection.LOCAL)
[docs] def active_suffix_generator(a_block, datatype=NOTSET): return suffix_generator(a_block, datatype, active=True)
[docs] class SuffixDataType(IntEnum): """Suffix data types AMPL only supports two data types for Suffixes: int and float. The numeric values here are specific to the NL file format and should not be changed without checking/updating the NL writer. """ INT = 0 FLOAT = 4
[docs] class SuffixDirection(IntEnum): """Suffix data flow definition. This identifies if the specific Suffix is to be sent to the solver, read from the solver output, both, or neither: - LOCAL: Suffix is local to Pyomo and should not be sent to or read from the solver. - EXPORT: Suffix should be sent to the solver as supplemental model information. - IMPORT: Suffix values will be returned from the solver and should be read from the solver output. - IMPORT_EXPORT: The Suffix is both an EXPORT and IMPORT suffix. """ LOCAL = 0 EXPORT = 1 IMPORT = 2 IMPORT_EXPORT = 3
_SuffixDataTypeDomain = In(SuffixDataType) _SuffixDirectionDomain = In(SuffixDirection)
[docs] @ModelComponentFactory.register("Declare a container for extraneous model data") class Suffix(ComponentMap, ActiveComponent): """A model suffix, representing extraneous model data""" """ Constructor Arguments: direction The direction of information flow for this suffix. By default, this is LOCAL, indicating that no suffix data is exported or imported. datatype A variable type associated with all values of this suffix. """ # # The following local (class) aliases are provided for convenience # and backwards compatibility with The Book, 3rd ed # # Suffix Directions: # - neither sent to solver or received from solver LOCAL = SuffixDirection.LOCAL # - sent to solver or other external location EXPORT = SuffixDirection.EXPORT # - obtained from solver or other external source IMPORT = SuffixDirection.IMPORT # - both import and export IMPORT_EXPORT = SuffixDirection.IMPORT_EXPORT FLOAT = SuffixDataType.FLOAT INT = SuffixDataType.INT def __new__(cls, *args, **kwargs): if cls is not Suffix: return super().__new__(cls) return super().__new__(AbstractSuffix) def __setstate__(self, state): super().__setstate__(state) # As the concrete class *is* the "Suffix" base class, the normal # implementation of deepcopy (through get/setstate) will create # the new Suffix, and __new__ will map it to AbstractSuffix. We # need to map constructed Suffixes back to Suffix: if self._constructed and self.__class__ is AbstractSuffix: self.__class__ = Suffix @overload def __init__( self, *, direction=LOCAL, datatype=FLOAT, initialize=None, rule=None, name=None, doc=None, ): ...
[docs] def __init__(self, **kwargs): # Suffix type information self._direction = None self._datatype = None self._rule = None # The suffix direction (note the setter performs error checking) self.direction = kwargs.pop('direction', Suffix.LOCAL) # The suffix datatype (note the setter performs error checking) self.datatype = kwargs.pop('datatype', Suffix.FLOAT) # The suffix construction rule # TODO: deprecate the use of 'rule' self._rule = Initializer( self._pop_from_kwargs('Suffix', kwargs, ('rule', 'initialize'), None), treat_sequences_as_mappings=False, allow_generators=True, ) # Initialize base classes kwargs.setdefault('ctype', Suffix) ActiveComponent.__init__(self, **kwargs) ComponentMap.__init__(self) if self._rule is None: self.construct()
[docs] def construct(self, data=None): """ Constructs this component, applying rule if it exists. """ if is_debug_set(logger): logger.debug(f"Constructing %s '%s'", self.__class__.__name__, self.name) if self._constructed is True: return timer = ConstructionTimer(self) self._constructed = True if self._rule is not None: rule = self._rule if rule.contains_indices(): # The rule contains explicit indices (e.g., is a dict). # Iterate over the indices, expand them, and store the # result block = self.parent_block() for index in rule.indices(): self.set_value(index, rule(block, index), expand=True) else: self.update_values(rule(self.parent_block(), None), expand=True) timer.report()
@property def datatype(self): """Return the suffix datatype.""" return self._datatype @datatype.setter def datatype(self, datatype): """Set the suffix datatype.""" if datatype is not None: datatype = _SuffixDataTypeDomain(datatype) self._datatype = datatype @property def direction(self): """Return the suffix direction.""" return self._direction @direction.setter def direction(self, direction): """Set the suffix direction.""" self._direction = _SuffixDirectionDomain(direction)
[docs] def export_enabled(self): """ Returns True when this suffix is enabled for export to solvers. """ return bool(self._direction & Suffix.EXPORT)
[docs] def import_enabled(self): """ Returns True when this suffix is enabled for import from solutions. """ return bool(self._direction & Suffix.IMPORT)
[docs] def update_values(self, data, expand=True): """ Updates the suffix data given a list of component,value tuples. Provides an improvement in efficiency over calling set_value on every component. """ if expand: try: items = data.items() except AttributeError: items = data for component, value in items: self.set_value(component, value, expand=expand) else: # As implemented by MutableMapping self.update(data)
[docs] def set_value(self, component, value, expand=True): """ Sets the value of this suffix on the specified component. When expand is True (default), array components are handled by storing a reference and value for each index, with no reference being stored for the array component itself. When expand is False (this is the case for __setitem__), this behavior is disabled and a reference to the array component itself is kept. """ if expand and component.is_indexed(): for component_ in component.values(): self[component_] = value else: self[component] = value
[docs] def set_all_values(self, value): """ Sets the value of this suffix on all components. """ for ndx in self: self[ndx] = value
[docs] def clear_value(self, component, expand=True): """ Clears suffix information for a component. """ if expand and component.is_indexed(): for component_ in component.values(): self.pop(component_, None) else: self.pop(component, None)
[docs] def clear_all_values(self): """ Clears all suffix data. """ self.clear()
[docs] @deprecated( 'Suffix.set_datatype is replaced with the Suffix.datatype property', version='6.7.1', ) def set_datatype(self, datatype): """ Set the suffix datatype. """ self.datatype = datatype
[docs] @deprecated( 'Suffix.get_datatype is replaced with the Suffix.datatype property', version='6.7.1', ) def get_datatype(self): """ Return the suffix datatype. """ return self.datatype
[docs] @deprecated( 'Suffix.set_direction is replaced with the Suffix.direction property', version='6.7.1', ) def set_direction(self, direction): """ Set the suffix direction. """ self.direction = direction
[docs] @deprecated( 'Suffix.get_direction is replaced with the Suffix.direction property', version='6.7.1', ) def get_direction(self): """ Return the suffix direction. """ return self.direction
def _pprint(self): return ( [ ('Direction', str(self._direction.name)), ('Datatype', getattr(self._datatype, 'name', 'None')), ], ((str(k), v) for k, v in self._dict.values()), ("Value",), lambda k, v: [v], ) # # Override a few methods to make sure the ActiveComponent versions are # called. We can't just switch the inheritance order due to # complications with __setstate__ #
[docs] def pprint(self, *args, **kwds): return ActiveComponent.pprint(self, *args, **kwds)
def __str__(self): return ActiveComponent.__str__(self)
[docs] @disable_methods(_SUFFIX_API) class AbstractSuffix(Suffix): pass
[docs] class SuffixFinder(object):
[docs] def __init__(self, name, default=None, context=None): """This provides an efficient utility for finding suffix values on a (hierarchical) Pyomo model. Parameters ---------- name: str Name of Suffix to search for. default: Default value to return from `.find()` if no matching Suffix is found. context: BlockData The root of the Block hierarchy to use when searching for Suffix components. Suffixes outside this hierarchy will not be interrogated and components that are queried (with :py:meth:`find(component_data)` will return the default value. """ self.name = name self.default = default self.all_suffixes = [] self._context = context self._suffixes_by_block = ComponentMap() self._suffixes_by_block[self._context] = [] if context is not None: s = context.component(name) if s is not None and s.ctype is Suffix and s.active: self._suffixes_by_block[context].append(s) self.all_suffixes.append(s)
[docs] def find(self, component_data): """Find suffix value for a given component data object in model tree Suffixes are searched by traversing the model hierarchy in three passes: 1. Search for a Suffix matching the specific component_data, starting at the `root` and descending down the tree to the component_data. Return the first match found. 2. Search for a Suffix matching the component_data's container, starting at the `root` and descending down the tree to the component_data. Return the first match found. 3. Search for a Suffix with key `None`, starting from the component_data and working up the tree to the `root`. Return the first match found. 4. Return the default value Parameters ---------- component_data: ComponentDataBase Component or component data object to find suffix value for. Returns ------- The value for Suffix associated with component data if found, else None. """ # Walk parent tree and search for suffixes if isinstance(component_data, BlockData): _block = component_data else: _block = component_data.parent_block() try: suffixes = self._get_suffix_list(_block) except AttributeError: # Component was outside the context (eventually parent # becomes None and parent.parent_block() raises an # AttributeError): we will return the default value return self.default # Pass 1: look for the component_data, working root to leaf for s in suffixes: if component_data in s: return s[component_data] # Pass 2: look for the component container, working root to leaf parent_comp = component_data.parent_component() if parent_comp is not component_data: for s in suffixes: if parent_comp in s: return s[parent_comp] # Pass 3: look for None, working leaf to root for s in reversed(suffixes): if None in s: return s[None] return self.default
def _get_suffix_list(self, parent): if parent in self._suffixes_by_block: return self._suffixes_by_block[parent] suffixes = list(self._get_suffix_list(parent.parent_block())) self._suffixes_by_block[parent] = suffixes s = parent.component(self.name) if s is not None and s.ctype is Suffix and s.active: suffixes.append(s) self.all_suffixes.append(s) return suffixes