Source code for pyomo.core.base.param

#  ___________________________________________________________________________
#
#  Pyomo: Python Optimization Modeling Objects
#  Copyright (c) 2008-2022
#  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.
#  ___________________________________________________________________________

__all__ = ['Param']

import sys
import types
import logging
from weakref import ref as weakref_ref
from pyomo.common.pyomo_typing import overload

from pyomo.common.autoslots import AutoSlots
from pyomo.common.deprecation import deprecation_warning, RenamedClass
from pyomo.common.log import is_debug_set
from pyomo.common.modeling import NOTSET
from pyomo.common.timing import ConstructionTimer

from pyomo.core.base.component import ComponentData, ModelComponentFactory
from pyomo.core.base.global_set import UnindexedComponent_index
from pyomo.core.base.indexed_component import (
    IndexedComponent, UnindexedComponent_set, IndexedComponent_NDArrayMixin
)
from pyomo.core.base.initializer import Initializer
from pyomo.core.base.misc import apply_indexed_rule, apply_parameterized_indexed_rule
from pyomo.core.base.numvalue import (
    NumericValue, native_types, value as expr_value
)
from pyomo.core.base.set import Reals, _AnySet
from pyomo.core.base.units_container import units
from pyomo.core.expr.current import GetItemExpression

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

def _raise_modifying_immutable_error(obj, index):
    if obj.is_indexed():
        name = "%s[%s]" % (obj.name, index)
    else:
        name = obj.name
    raise TypeError(
        "Attempting to set the value of the immutable parameter "
        "%s after the parameter has been constructed.  If you intend "
        "to change the value of this parameter dynamically, please "
        "declare the parameter as mutable [i.e., Param(mutable=True)]"
        % (name,))


class _ImplicitAny(_AnySet):
    """An Any that issues a deprecation warning for non-Real values.

    This is a helper class to implement the deprecation warnings for the
    change of Param's implicit domain from Any to Reals.

    """
    __slots__ = ('_owner',)
    __autoslot_mappers__ = {'_owner': AutoSlots.weakref_mapper}

    def __new__(cls, **kwargs):
        # Strip off owner / kwargs before calling base __new__
        return super().__new__(cls)

    def __init__(self, owner, **kwargs):
        self._owner = weakref_ref(owner)
        super().__init__(**kwargs)
        self._component = weakref_ref(self)
        self.construct()
        # Because this is a "global set", we need to define the _bounds
        # and _interval fields
        object.__setattr__(self, '_parent', None)
        self._bounds = (None, None)
        self._interval = (None, None, None)

    def __contains__(self, val):
        if val not in Reals:
            if self._owner is None or self._owner() is None:
                name = 'Unknown'
            else:
                name = self._owner().name
            deprecation_warning(
                f"Param '{name}' declared with an implicit domain of 'Any'. "
                "The default domain for Param objects is 'Any'.  However, "
                "we will be changing that default to 'Reals' in the "
                "future.  If you really intend the domain of this Param"
                "to be 'Any', you can suppress this warning by explicitly "
                "specifying 'within=Any' to the Param constructor.",
                version='5.6.9', remove_in='6.0')
        return True

    # This should "mock up" a global set, so the "name" should always be
    # the local name (without block scope)
    def getname(self, fully_qualified=False, name_buffer=None, relative_to=None):
        return super().getname(False, name_buffer, relative_to)

    # The parent tracks the parent of the owner.  We can't set it
    # directly here because the owner has not been assigned to a block
    # when we create the _ImplicitAny
    @property
    def _parent(self):
        if self._owner is None or self._owner() is None:
            return None
        return self._owner()._parent
    # This is not settable.  However the base classes assume that it is,
    # so we need to define the setter and just ignore the incoming value
    @_parent.setter
    def _parent(self, val):
        pass


class _ParamData(ComponentData, NumericValue):
    """
    This class defines the data for a mutable parameter.

    Constructor Arguments:
        owner       The Param object that owns this data.
        value       The value of this parameter.

    Public Class Attributes:
        value       The numeric value of this variable.
    """

    __slots__ = ('_value',)

    def __init__(self, component):
        #
        # The following is equivalent to calling
        # the base ComponentData constructor.
        #
        self._component = weakref_ref(component)
        self._index = NOTSET
        #
        # The following is equivalent to calling the
        # base NumericValue constructor.
        #
        self._value = Param.NoValue

    # Note: because NONE of the slots on this class need to be edited,
    # we don't need to implement a specialized __setstate__ method.

    def clear(self):
        """Clear the data in this component"""
        self._value = Param.NoValue

    # FIXME: ComponentData need to have pointers to their index to make
    # operations like validation efficient.  As it stands now, if
    # set_value is called without specifying an index, this call
    # involves a linear scan of the _data dict.
    def set_value(self, value, idx=NOTSET):
        #
        # If this param has units, then we need to check the incoming
        # value and see if it is "units compatible".  We only need to
        # check here in set_value, because all united Params are
        # required to be mutable.
        #
        _comp = self.parent_component()
        if type(value) in native_types:
            # TODO: warn/error: check if this Param has units: assigning
            # a dimensionless value to a united param should be an error
            pass
        elif _comp._units is not None:
            _src_magnitude = expr_value(value)
            _src_units = units.get_units(value)
            value = units.convert_value(
                num_value=_src_magnitude, from_units=_src_units,
                to_units=_comp._units)

        old_value, self._value = self._value, value
        try:
            _comp._validate_value(idx, value, data=self)
        except:
            self._value = old_value
            raise

    def __call__(self, exception=True):
        """
        Return the value of this object.
        """
        if self._value is Param.NoValue:
            if exception:
                raise ValueError(
                    "Error evaluating Param value (%s):\n\tThe Param value is "
                    "currently set to an invalid value.  This is\n\ttypically "
                    "from a scalar Param or mutable Indexed Param without\n"
                    "\tan initial or default value."
                    % ( self.name, ))
            else:
                return None
        return self._value

    @property
    def value(self):
        """Return the value for this variable."""
        return self()
    @value.setter
    def value(self, val):
        """Set the value for this variable."""
        self.set_value(val)

    def get_units(self):
        """Return the units for this ParamData"""
        return self.parent_component()._units

    def is_fixed(self):
        """
        Returns True because this value is fixed.
        """
        return True

    def is_constant(self):
        """
        Returns False because this is not a constant in an expression.
        """
        return False

    def is_parameter_type(self):
        """
        Returns True because this is a parameter object.
        """
        return True

    def _compute_polynomial_degree(self, result):
        """
        Returns 0 because this object can never reference variables.
        """
        return 0


[docs]@ModelComponentFactory.register("Parameter data that is used to define a model instance.") class Param(IndexedComponent, IndexedComponent_NDArrayMixin): """ A parameter value, which may be defined over an index. Constructor Arguments: domain A set that defines the type of values that each parameter must be. within A set that defines the type of values that each parameter must be. validate A rule for validating this parameter w.r.t. data that exists in the model default A scalar, rule, or dictionary that defines default values for this parameter initialize A dictionary or rule for setting up this parameter with existing model data unit: pyomo unit expression An expression containing the units for the parameter mutable: `boolean` Flag indicating if the value of the parameter may change between calls to a solver. Defaults to `False` name Name for this component. doc Text describing this component. """ DefaultMutable = False
[docs] class NoValue(object): """A dummy type that is pickle-safe that we can use as the default value for Params to indicate that no valid value is present.""" pass
def __new__(cls, *args, **kwds): if cls != Param: return super(Param, cls).__new__(cls) if not args or (args[0] is UnindexedComponent_set and len(args) == 1): return super(Param, cls).__new__(ScalarParam) else: return super(Param, cls).__new__(IndexedParam) @overload def __init__(self, *indexes, rule=NOTSET, initialize=NOTSET, domain=None, within=None, validate=None, mutable=False, default=NoValue, initialize_as_dense=False, units=None, name=None, doc=None): ... def __init__(self, *args, **kwd): _init = self._pop_from_kwargs( 'Param', kwd, ('rule', 'initialize'), NOTSET) self.domain = self._pop_from_kwargs('Param', kwd, ('domain', 'within')) self._validate = kwd.pop('validate', None ) self._mutable = kwd.pop('mutable', Param.DefaultMutable ) self._default_val = kwd.pop('default', Param.NoValue ) self._dense_initialize = kwd.pop('initialize_as_dense', False) self._units = kwd.pop('units', None) if self._units is not None: self._units = units.get_units(self._units) self._mutable = True kwd.setdefault('ctype', Param) IndexedComponent.__init__(self, *args, **kwd) if self.domain is None: self.domain = _ImplicitAny(owner=self, name='Any') # After IndexedComponent.__init__ so we can call is_indexed(). self._rule = Initializer(_init, treat_sequences_as_mappings=self.is_indexed(), arg_not_specified=NOTSET) def __len__(self): """ Return the number of component data objects stored by this component. If a default value is specified, then the length equals the number of items in the component index. """ if self._default_val is Param.NoValue: return len(self._data) return len(self._index_set) def __contains__(self, idx): """ Return true if the index is in the dictionary. If the default value is specified, then all members of the component index are valid. """ if self._default_val is Param.NoValue: return idx in self._data return idx in self._index_set # We do not need to override keys(), as the __len__ override will # cause the base class keys() to correctly correctly handle default # values #def keys(self, ordered=False): @property def mutable(self): return self._mutable
[docs] def get_units(self): """Return the units for this ParamData""" return self._units
# # These are "sparse equivalent" access / iteration methods that # only loop over the defined data. #
[docs] def sparse_keys(self): """Return a list of keys in the defined parameters""" return list(self._data.keys())
[docs] def sparse_values(self): """Return a list of the defined param data objects""" return list(self._data.values())
[docs] def sparse_items(self): """Return a list (index,data) tuples for defined parameters""" return list(self._data.items())
[docs] def sparse_iterkeys(self): """Return an iterator for the keys in the defined parameters""" return self._data.keys()
[docs] def sparse_itervalues(self): """Return an iterator for the defined param data objects""" return self._data.values()
[docs] def sparse_iteritems(self): """Return an iterator of (index,data) tuples for defined parameters""" return self._data.items()
[docs] def extract_values(self): """ A utility to extract all index-value pairs defined for this parameter, returned as a dictionary. This method is useful in contexts where key iteration and 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. # 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() } else: # # The parameter is not mutable, so iteritems() can be # converted into a dictionary containing parameter values. # return dict( self.items() )
[docs] def extract_values_sparse(self): """ A utility to extract all index-value pairs defined with non-default values, returned as a dictionary. This method is useful in contexts where key iteration and 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. # 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() } else: # # The parameter is not mutable, so sparse_iteritems() can be # converted into a dictionary containing parameter values. # return dict( self.sparse_iteritems() )
[docs] def store_values(self, new_values, check=True): """ A utility to update a Param with a dictionary or scalar. If check=True, then both the index and value are checked through the __getitem__ method. Using check=False should only be used by developers! """ if not self._mutable: _raise_modifying_immutable_error(self, '*') # _srcType = type(new_values) _isDict = _srcType is dict or ( \ hasattr(_srcType, '__getitem__') and not isinstance(new_values, NumericValue) ) # if check: if _isDict: for index, new_value in new_values.items(): self[index] = new_value else: for index in self._index_set: self[index] = new_values return # # The argument check is False, so we bypass almost all of the # Param logic for ensuring data integrity. # if self.is_indexed(): if _isDict: # It is possible that the Param is sparse and that the # index is not already in the _data dict. As these # cases are rare, we will recover from the exception # instead of incurring the penalty of checking. for index, new_value in new_values.items(): if index not in self._data: self._data[index] = _ParamData(self) self._data[index]._value = new_value else: # For scalars, we will choose an approach based on # how "dense" the Param is if not self._data: # empty for index in self._index_set: p = self._data[index] = _ParamData(self) p._value = new_values elif len(self._data) == len(self._index_set): for index in self._index_set: self._data[index]._value = new_values else: for index in self._index_set: if index not in self._data: self._data[index] = _ParamData(self) self._data[index]._value = new_values else: # # Initialize a scalar # if _isDict: if None not in new_values: raise RuntimeError( "Cannot store value for scalar Param %s:\n\tNo value " "with index None in the new values dict." % (self.name,)) new_values = new_values[None] # scalars have to be handled differently self[None] = new_values
[docs] def set_default(self, val): """ Perform error checks and then set the default value for this parameter. NOTE: this test will not validate the value of function return values. """ if self._constructed \ and val is not Param.NoValue \ and type(val) in native_types \ and val not in self.domain: raise ValueError( "Default value (%s) is not valid for Param %s domain %s" % (str(val), self.name, self.domain.name)) self._default_val = val
[docs] def default(self): """ Return the value of the parameter default. Possible values: Param.NoValue No default value is provided. Numeric A constant value that is the default value for all undefined parameters. Function f(model, i) returns the value for the default value for parameter i """ return self._default_val
def _getitem_when_not_present(self, index): """ Returns the default component data value """ # # Local values # val = self._default_val if val is Param.NoValue: # We should allow the creation of mutable params without # a default value, as long as *solving* a model without # reasonable values produces an informative error. if self._mutable: # Note: _ParamData defaults to Param.NoValue if self.is_indexed(): ans = self._data[index] = _ParamData(self) else: ans = self._data[index] = self ans._index = index return ans if self.is_indexed(): idx_str = '%s[%s]' % (self.name, index,) else: idx_str = '%s' % (self.name,) raise ValueError( "Error retrieving immutable Param value (%s):\n\tThe Param " "value is undefined and no default value is specified." % ( idx_str,) ) _default_type = type(val) _check_value_domain = True if _default_type in native_types: # # The set_default() method validates the domain of native types, so # we can skip the check on the value domain. # _check_value_domain = False elif _default_type is types.FunctionType: val = apply_indexed_rule(self, val, self.parent_block(), index) elif hasattr(val, '__getitem__') and ( not isinstance(val, NumericValue) or val.is_indexed() ): # Things that look like Dictionaries should be allowable. This # includes other IndexedComponent objects. val = val[index] else: # this is something simple like a non-indexed component pass # # If the user wants to validate values, we need to validate the # default value as well. For Mutable Params, this is easy: # _setitem_impl will inject the value into _data and # then call validate. # if self._mutable: return self._setitem_when_not_present(index, val) # # For immutable params, we never inject the default into the data # dictionary. This will break validation, as the validation rule is # allowed to assume the data is already present (actually, it will # die on infinite recursion, as Param.__getitem__() will re-call # _getitem_when_not_present). # # So, we will do something very inefficient: if we are # validating, we will inject the value into the dictionary, # call validate, and remove it. # if _check_value_domain or self._validate: try: self._data[index] = val self._validate_value(index, val, _check_value_domain) finally: del self._data[index] return val def _setitem_impl(self, index, obj, value): """The __setitem__ method performs significant validation around the input indices, particularly when the index value is new. In various contexts, we don't need to incur this overhead (e.g. during initialization). The _setitem_impl assumes the input value is in the set native_types """ # # We need to ensure that users don't override the value for immutable # parameters. # if self._constructed and not self._mutable: _raise_modifying_immutable_error(self, index) # # Params should contain *values*. Note that if we just call # value(), then that forces the value to be a numeric value. # Notably, we allow Params with domain==Any to hold strings, tuples, # etc. The following lets us use NumericValues to initialize # Params, but is optimized to check for "known" native types to # bypass a potentially expensive isinstance()==False call. # if value.__class__ not in native_types: if isinstance(value, NumericValue): value = value() # # Set the value depending on the type of param value. # if self._mutable: obj.set_value(value, index) return obj else: old_value, self._data[index] = self._data[index], value # Because we do not have a _ParamData, we cannot rely on the # validation that occurs in _ParamData.set_value() try: self._validate_value(index, value) return value except: self._data[index] = old_value raise def _setitem_when_not_present(self, index, value, _check_domain=True): # # We need to ensure that users don't override the value for immutable # parameters. # if self._constructed and not self._mutable: _raise_modifying_immutable_error(self, index) # # Params should contain *values*. Note that if we just call # value(), then that forces the value to be a numeric value. # Notably, we allow Params with domain==Any to hold strings, tuples, # etc. The following lets us use NumericValues to initialize # Params, but is optimized to check for "known" native types to # bypass a potentially expensive isinstance()==False call. # if value.__class__ not in native_types: if isinstance(value, NumericValue): value = value() # # Set the value depending on the type of param value. # try: if index is None and not self.is_indexed(): self._data[None] = self self.set_value(value, index) self._index = UnindexedComponent_index return self elif self._mutable: obj = self._data[index] = _ParamData(self) obj.set_value(value, index) obj._index = index return obj else: self._data[index] = value # Because we do not have a _ParamData, we cannot rely on the # validation that occurs in _ParamData.set_value() self._validate_value(index, value, _check_domain) return value except: del self._data[index] raise def _validate_value(self, index, value, validate_domain=True, data=None): """ Validate a given input/value pair. """ # # Check if the value is valid within the current domain # if validate_domain and not value in self.domain: if index is NOTSET: index = data.index() raise ValueError( "Invalid parameter value: %s[%s] = '%s', value type=%s.\n" "\tValue not in parameter domain %s" % (self.name, index, value, type(value), self.domain.name)) if self._validate: if index is NOTSET: index = data.index() valid = apply_parameterized_indexed_rule( self, self._validate, self.parent_block(), value, index ) if not valid: raise ValueError( "Invalid parameter value: %s[%s] = '%s', value type=%s.\n" "\tValue failed parameter validation rule" % ( self.name, index, value, type(value) ) )
[docs] def construct(self, data=None): """ Initialize this component. A parameter is constructed using the initial data or the data loaded from an external source. We first set all the values based on self._rule, and then allow the data dictionary to overwrite anything. Note that we allow an undefined Param value to be constructed. We throw an exception if a user tries to use an uninitialized Param. """ if self._constructed: return timer = ConstructionTimer(self) if is_debug_set(logger): #pragma:nocover logger.debug("Constructing Param, name=%s, from data=%s" % ( self.name, str(data) )) try: # # If the default value is a simple type, we check it versus # the domain. # val = self._default_val if val is not Param.NoValue \ and type(val) in native_types \ and val not in self.domain: raise ValueError( "Default value (%s) is not valid for Param %s domain %s" % (str(val), self.name, self.domain.name)) # # Flag that we are in the "during construction" phase # self._constructed = None # # Step #1: initialize data from rule value # self._construct_from_rule_using_setitem() # # Step #2: allow any user-specified (external) data to override # the initialization # if data is not None: try: data_items = data.items() except AttributeError: raise ValueError( "Attempting to initialize parameter=%s with data=%s.\n" "\tData type is not a mapping type, and a Mapping is " "expected." % (self.name, str(data)) ) else: data_items = iter(()) try: for key, val in data_items: self._setitem_when_not_present( self._validate_index(key), val) except: msg = sys.exc_info()[1] raise RuntimeError( "Failed to set value for param=%s, index=%s, value=%s.\n" "\tsource error message=%s" % (self.name, str(key), str(val), str(msg)) ) # # Flag that things are fully constructed now (and changing an # immutable Param is now an exception). # self._constructed = True # populate all other indices with default data # (avoids calling _set_contains on self._index_set at runtime) if self._dense_initialize: self.to_dense_data() finally: timer.report()
def _pprint(self): """ Return data that will be printed for this component. """ if self._default_val is Param.NoValue: default = "None" # for backwards compatibility in reporting elif type(self._default_val) is types.FunctionType: default = "(function)" else: default = str(self._default_val) if self._mutable or not self.is_indexed(): dataGen = lambda k, v: [ v._value, ] else: dataGen = lambda k, v: [ v, ] headers = [ ("Size", len(self)), ("Index", self._index_set if self.is_indexed() else None), ("Domain", self.domain.name), ("Default", default), ("Mutable", self._mutable), ] if self._units is not None: headers.append(('Units', str(self._units))) return ( headers, self.sparse_iteritems(), ("Value",), dataGen, )
class ScalarParam(_ParamData, Param): def __init__(self, *args, **kwds): Param.__init__(self, *args, **kwds) _ParamData.__init__(self, component=self) self._index = UnindexedComponent_index # # Since this class derives from Component and Component.__getstate__ # just packs up the entire __dict__ into the state dict, there s # nothng special that we need to do here. We will just defer to the # super() get/set state. Since all of our get/set state methods # rely on super() to traverse the MRO, this will automatically pick # up both the Component and Data base classes. # def __call__(self, exception=True): """ Return the value of this parameter. """ if self._constructed: if not self._data: if self._mutable: # This will trigger populating the _data dict and setting # the _default, if applicable self[None] else: # Immutable Param defaults never get added to the # _data dict return self[None] return super(ScalarParam, self).__call__(exception=exception) if exception: raise ValueError( "Evaluating the numeric value of parameter '%s' before\n\t" "the Param has been constructed (there is currently no " "value to return)." % (self.name,) ) def set_value(self, value, index=NOTSET): if index is NOTSET: index = None if self._constructed and not self._mutable: _raise_modifying_immutable_error(self, index) if not self._data: self._data[index] = self super(ScalarParam, self).set_value(value, index) def is_constant(self): """Determine if this ScalarParam is constant (and can be eliminated) Returns False if either unconstructed or mutable, as it must be kept in expressions (as it either doesn't have a value yet or the value can change later. """ return self._constructed and not self._mutable class SimpleParam(metaclass=RenamedClass): __renamed__new_class__ = ScalarParam __renamed__version__ = '6.0' class IndexedParam(Param): def __call__(self, exception=True): """Compute the value of the parameter""" if exception: raise TypeError('Cannot compute the value of an indexed Param (%s)' % (self.name,) ) # Because IndexedParam can use a non-standard data store (i.e., the # values in the _data dict may not be ComponentData objects), we # need to override the normal scheme for pre-allocating # ComponentData objects during deepcopy. def _create_objects_for_deepcopy(self, memo, component_list): if self.mutable: # Normal indexed object; leverage base implementation return super()._create_objects_for_deepcopy(memo, component_list) # This is immutable; only add the container (not the _data) to # the component_list. _new = self.__class__.__new__(self.__class__) _ans = memo.setdefault(id(self), _new) if _ans is _new: component_list.append(self) return _ans # Because CP supports indirection [the ability to index objects by # another (inter) Var] for certain types (including Var), we will # catch the normal RuntimeError and return a (variable) # GetItemExpression. # # FIXME: We should integrate this logic into the base implementation # of `__getitem__()`, including the recognition / differentiation # between potentially variable GetItemExpression objects and # "constant" GetItemExpression objects. That will need to wait for # the expression rework [JDS; Nov 22]. def __getitem__(self, args): try: return super().__getitem__(args) except: tmp = args if args.__class__ is tuple else (args,) if any(hasattr(arg, 'is_potentially_variable') and arg.is_potentially_variable() for arg in tmp ): return GetItemExpression((self,) + tmp) raise