Source code for idaes.core.util.var

##############################################################################
# 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)