# ___________________________________________________________________________
#
# 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.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 Any, GlobalSetBase, Reals
from pyomo.core.base.units_container import units
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(Any.__class__):
"""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.
"""
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()
def __getstate__(self):
state = super().__getstate__()
state['_owner'] = None if self._owner is None else self._owner()
return state
def __setstate__(self, state):
_owner = state.pop('_owner')
super().__setstate__(state)
self._owner = None if _owner is None else weakref_ref(_owner)
def __deepcopy__(self, memo):
# Note: we need to start super() at GlobalSetBase to actually
# copy this object
return super(GlobalSetBase, self).__deepcopy__(memo)
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
def __getstate__(self):
"""
This method must be defined because this class uses slots.
"""
state = super(_ParamData, self).__getstate__()
for i in _ParamData.__slots__:
state[i] = getattr(self, i)
return state
# 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 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):
_id = id(self)
if _id not in memo:
component_list.append(self)
memo[_id] = self.__class__.__new__(self.__class__)
if self.mutable:
for obj in self._data.values():
_id = id(obj)
if _id in memo:
continue
component_list.append(obj)
memo[id(obj)] = obj.__class__.__new__(obj.__class__)