##############################################################################
# Institute for the Design of Advanced Energy Systems Process Systems
# Engineering Framework (IDAES PSE Framework) Copyright (c) 2018, by the
# software owners: The Regents of the University of California, through
# Lawrence Berkeley National Laboratory, National Technology & Engineering
# Solutions of Sandia, LLC, Carnegie Mellon University, West Virginia
# University Research Corporation, et al. All rights reserved.
#
# Please see the files COPYRIGHT.txt and LICENSE.txt for full copyright and
# license information, respectively. Both files are also available online
# at the URL "https://github.com/IDAES/idaes".
##############################################################################
"""
This module provides utility functions and classes related to Pyomo variables
"""
from __future__ import division
from six.moves import range
from six import iteritems
import pyomo.environ as pe
__author__ = "Qi Chen <qichen@andrew.cmu.edu>"
[docs]class SliceVar(object):
"""
This class provides a way to pass sliced variables
into other utility functions
"""
def __init__(self, var, slices):
# raise DeprecationWarning('The SliceVar class is deprecated.')
self.var = var
self.slices = slices
# Slices is a dictionary with the format {index: value}
# So {2: 5, 4: 6} will mean that a call to the SliceVar x with
# x[1, 3] will return var[1, 5, 3, 6]
def __getitem__(self, key):
if key is None:
key = tuple()
for pos, value in sorted(iteritems(self.slices)):
key = key[:pos - 1] + (value,) + key[pos - 1:]
if len(key) == 0:
return self.var[None]
else:
return self.var[key]
def __getattr__(self, attr):
getattr(self.var, attr)
@property
def _index(self):
raise NotImplementedError('I need to overwrite the return value of this to provide the slices of the index set that apply to this SliceVar')
[docs]class Wrapper(object):
"""
This class provides a wrapper around a Pyomo Component so that a Block does
not try to attach and construct it.
TODO: this might be a good place for a weakref implementation
"""
def __init__(self, obj):
self._obj = obj
def __getattr__(self, attr):
return getattr(self._obj, attr)
def __eq__(self, other):
"""
Override == operator to test for equlity of the wrapped object instead of the
wrapper.
"""
return self._obj == other
[docs]def wrap_var(obj):
"""
Provides a Wrapper around the obj if it is not a constant value; otherwise,
returns the constant value.
"""
if pe.is_constant(obj):
return pe.value(obj)
else:
return Wrapper(obj)
[docs]def unwrap(obj):
"""
Unwraps the wrapper, if one exists.
"""
if isinstance(obj, Wrapper):
return obj._obj
else:
return obj
[docs]def none_if_empty(tup):
"""Returns None if passed an empty tuple
This is helpful since a SimpleVar is actually an IndexedVar with
a single index of None rather than the more intuitive empty tuple.
"""
if tup is ():
return None
else:
return tup
[docs]def lb(expr, block_bounds=(None, None)):
"""Returns the lower bound of the expression or variable """
return _get_bound(expr, 'lb', block_bounds)
[docs]def ub(expr, block_bounds=(None, None)):
"""Returns the upper bound of the expression or variable """
return _get_bound(expr, 'ub', block_bounds)
def _get_bound(expr, bound_type, block_bounds=(None, None)):
"""Returns the bound of the expression or variable """
block_LB, block_UB = block_bounds
# --- TODO: This is a hack to enable support for floats or ints
from pyomo.core.base.numvalue import is_constant as const, value
if const(expr):
return value(expr)
# ---
# if expr.is_constant():
# return expr.value
from pyomo.core.base.var import _GeneralVarData
if isinstance(expr, _GeneralVarData):
# Returns the tightest variable bound. If the block bound is None,
# returns the variable bound. If the variable bound is None, returns
# the block bound. If both are None, then it returns None.
if bound_type == 'lb':
block_LB_value = block_LB.get(expr, None) \
if block_LB is not None else None
if block_LB_value is None:
return expr.lb
elif expr.lb is None:
return block_LB_value
else:
return max(block_LB_value, expr.lb)
elif bound_type == 'ub':
block_UB_value = block_UB.get(expr, None) \
if block_UB is not None else None
if block_UB_value is None:
return expr.ub
elif expr.ub is None:
return block_UB_value
else:
return min(block_UB_value, expr.ub)
else:
raise ValueError('Unknown bound type: {}'.format(bound_type))
from pyomo.core.expr.current import MonomialTermExpression, \
SumExpression, ProductExpression, ReciprocalExpression
if isinstance(expr, MonomialTermExpression):
# args are a constant (0) times something else (1)
return expr.arg(0) * _get_bound(
expr.arg(1),
bound_type if expr.arg(0) >= 0 else _invert_bound(bound_type),
block_bounds)
if isinstance(expr, SumExpression):
return sum(
_get_bound(arg, bound_type, block_bounds) for arg in expr.args)
if isinstance(expr, ProductExpression):
# This cannot do products of multiple variable bounds because you
# would need to keep track of when to invert the bound_type.
# It will fail if it finds products within products.
# This can probably be fixed in the future.
one_var = True
for arg in expr.args:
if isinstance(arg, ReciprocalExpression):
one_var = False
break
if isinstance(arg, ProductExpression):
one_var = False
break
if const(expr) and value(expr) < 0:
# invert the bound type when there is a negative const coef
bound_type = _invert_bound(bound_type)
if one_var:
return prod(
_get_bound(arg, bound_type, block_bounds) for arg in expr.args)
# else: (We don't support the expression)
# else: (We don't recognize the expression)
raise NotImplementedError(
'Cannot determine {} for unrecognized expression {}'
.format(bound_type, expr))
def _invert_bound(bound_type):
"""Inverts the given bound. 'lb' --> 'ub' and vice versa.
Raises:
ValueError: if unrecognized bound type
"""
if bound_type == 'lb':
return 'ub'
elif bound_type == 'ub':
return 'lb'
else:
raise ValueError('Unknown bound type: {}'.format(bound_type))
[docs]def is_fixed_by_bounds(expr, block_bounds=(None, None), tol=1E-14):
if abs(ub(expr, block_bounds) - lb(expr, block_bounds)) <= tol:
return True
else:
return False
[docs]def tighten_var_bound(var, newbound):
"""Tightens the variable bounds for one variable
This function does not blindly apply the bounds passed in. Rather, it
evaluates whether the new proposed bounds are better than the existing
variable bounds.
Args:
var (_VarData): single Pyomo variable object (not indexed like Var)
newbound (tuple): a tuple of (new lower bound, new upper bound)
Returns:
None
"""
lb, ub = var.bounds
newlb, newub = newbound
if lb is None or (newlb is not None and newlb > lb):
var.setlb(newlb)
if ub is None or (newub is not None and newub < ub):
var.setub(newub)
[docs]def tighten_block_bound(var, newbound, block_bounds):
newlb, newub = newbound
blkLB, blkUB = block_bounds
if blkLB.get(var) is None or \
(newlb is not None and newlb > blkLB.get(var)):
blkLB.set_value(var, newlb)
if blkUB.get(var) is None or \
(newlb is not None and newub < blkUB.get(var)):
blkUB.set_value(var, newub)
[docs]def min_lb(expr1, expr2):
return min(
lb(expr1) * lb(expr2),
lb(expr1) * ub(expr2),
ub(expr1) * lb(expr2),
ub(expr1) * ub(expr2))
[docs]def max_ub(expr1, expr2):
return max(
lb(expr1) * lb(expr2),
lb(expr1) * ub(expr2),
ub(expr1) * lb(expr2),
ub(expr1) * ub(expr2))
[docs]def min_lbb(expr1, expr2, block_bounds):
return min(
lb(expr1, block_bounds) * lb(expr2, block_bounds),
lb(expr1, block_bounds) * ub(expr2, block_bounds),
ub(expr1, block_bounds) * lb(expr2, block_bounds),
ub(expr1, block_bounds) * ub(expr2, block_bounds))
[docs]def max_ubb(expr1, expr2, block_bounds):
return max(
lb(expr1, block_bounds) * lb(expr2, block_bounds),
lb(expr1, block_bounds) * ub(expr2, block_bounds),
ub(expr1, block_bounds) * lb(expr2, block_bounds),
ub(expr1, block_bounds) * ub(expr2, block_bounds))
[docs]def tighten_mc_var(vardata, x_expr, y_expr, block_bounds):
tighten_var_bound(
vardata, (min_lb(x_expr, y_expr), max_ub(x_expr, y_expr)))
if block_bounds:
tighten_block_bound(vardata,
(min_lbb(x_expr, y_expr, block_bounds),
max_ubb(x_expr, y_expr, block_bounds)),
block_bounds)