# coding=utf-8
##############################################################################
# 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".
##############################################################################
"""
Utility functions for generating envelopes for convex nonlinear functions
"""
from __future__ import division
from .var import ub, lb, is_fixed_by_bounds, \
tighten_var_bound, tighten_block_bound
from pyomo.environ import Var, NonNegativeReals, Constraint, RangeSet, Set, Binary
from functools import partial
from .mccormick import squish_concat
__author__ = "Qi Chen <qichen@andrew.cmu.edu>"
[docs]def try_eval(f, x):
try:
return f(x)
except ValueError:
return None
except ZeroDivisionError:
return None
def _setup_convex(b, nsegs, indx):
if not hasattr(b, 'sets'):
if indx is None:
b.sets = Set()
else:
b.sets = Set(dimen=len(indx))
if not hasattr(b, 'overest'):
if indx is not None:
b.overest = Constraint(b.sets)
else:
b.overest = Constraint()
if not hasattr(b, 'segs'):
b.segs = RangeSet(nsegs)
b.segs_m1 = RangeSet(nsegs - 1, within=b.segs)
else:
if nsegs != len(b.segs):
raise ValueError('We do not currently support different segment counts within the same set of McCormick relaxations.')
if not hasattr(b, 'x_defn'):
if indx is not None:
b.x_defn = Constraint(b.sets, ['lb', 'ub'])
else:
b.x_defn = Constraint(['lb', 'ub'])
if not hasattr(b, 'delta'):
if indx is not None:
b.delta = Var(b.sets, b.segs, domain=NonNegativeReals, initialize=0)
else:
b.delta = Var(b.segs, domain=NonNegativeReals, initialize=0)
if not hasattr(b, 'delta_defn'):
if indx is not None:
b.delta_defn = Constraint(b.sets, b.segs, ['lb', 'ub'])
else:
b.delta_defn = Constraint(b.segs, ['lb', 'ub'])
if not hasattr(b, 'omega'):
if indx is not None:
b.omega = Var(b.sets, b.segs_m1, domain=Binary)
else:
b.omega = Var(b.segs_m1, domain=Binary)
if not hasattr(b, 'omega_exist'):
if indx is not None:
b.omega_exist = Constraint(b.sets)
else:
b.omega_exist = Constraint()
if not hasattr(b, 'w'):
# define bilinear w = y * x
if indx is not None:
b.w = Var(b.sets, domain=NonNegativeReals)
else:
b.w = Var(domain=NonNegativeReals)
if not hasattr(b, 'underest'):
if indx is not None:
b.underest = Constraint(b.sets, ['lb', 'ub'])
else:
b.underest = Constraint(['lb', 'ub'])
if not hasattr(b, 'w_defn'):
if indx is not None:
b.w_defn = Constraint(b.sets, ['lb1', 'ub1', 'lb2', 'ub2'])
else:
b.w_defn = Constraint(['lb1', 'ub1', 'lb2', 'ub2'])
[docs]def add_convex_relaxation(b, z, x, f_expr, df_expr, nsegs, indx, exists,
block_bounds=(None, None), bound_contract=None):
"""Constructs a linear relaxation to bound a convex equality function
Args:
b (Block): PyOMO block in which to generate variables and constraints
z (Expression): PyOMO expression for the convex function output
x (Expression): PyOMO expression for the convex function input
f_expr (function): convex function
df_expr (function): function giving first derivative of convex function
with respect to x
exists (_VarData): Variable corresponding to existence of unit
block_bounds (dict, optional): dictionary describing disjunctive
bounds present for variables associated with current block
Returns:
None
"""
lbb = partial(lb, block_bounds=block_bounds) # lower block bound
ubb = partial(ub, block_bounds=block_bounds) # upper block bound
# Define constants
f_lb = try_eval(f_expr, lb(x)) # z value at lb of x
f_lbb = try_eval(f_expr, lbb(x)) # z value at lbb of x
f_ub = try_eval(f_expr, ub(x)) # z value at ub of x
f_ubb = try_eval(f_expr, ubb(x)) # z value at ubb of x
df_lb = try_eval(df_expr, lb(x)) # dz/dx value at lb of x
df_lbb = try_eval(df_expr, lbb(x)) # dz/dx value at lbb of x
df_ub = try_eval(df_expr, ub(x)) # dz/dx value at ub of x
df_ubb = try_eval(df_expr, ubb(x)) # dz/dx at ubb of x
# Tighten bounds on z
if bound_contract == 'monotonic_increase':
tighten_var_bound(z, (f_lb, f_ub))
tighten_block_bound(z, (f_lbb, f_ubb), block_bounds)
elif bound_contract == 'monotonic_decrease':
tighten_var_bound(z, (f_ub, f_lb))
tighten_block_bound(z, (f_ubb, f_lbb), block_bounds)
else:
# TODO do nothing for now, but could also solve optimization problem
# to determine bounds
pass
_setup_convex(b, nsegs, indx)
y = exists # Alias equipment existence binary as 'y'
if not is_fixed_by_bounds(x, block_bounds=block_bounds):
a = (ubb(x) - lbb(x)) / nsegs # segment length
b.x_defn.add(squish_concat(indx, 'lb'), expr=x >= lb(x) + sum(b.delta[squish_concat(indx, s)] for s in b.segs) + (lbb(x) - lb(x)) * y)
b.x_defn.add(squish_concat(indx, 'ub'), expr=x <= ub(x) + sum(b.delta[squish_concat(indx, s)] for s in b.segs) + (lbb(x) - ub(x)) * y)
b.overest.add(indx, expr=z <= f_lbb + sum((f_expr(lbb(x) + a * s) - f_expr(lbb(x) + a * (s - 1))) / a * b.delta[squish_concat(indx, s)] for s in b.segs) + (ub(z) - f_lbb) * (1 - y))
for s in b.segs:
b.delta[squish_concat(indx, s)].setub(a)
if s < nsegs:
b.delta_defn.add(squish_concat(indx, s, 'lb'), expr=a * b.omega[squish_concat(indx, s)] <= b.delta[squish_concat(indx, s)])
if s > 1:
b.delta_defn.add(squish_concat(indx, s, 'ub'), expr=b.delta[squish_concat(indx, s)] <= a * b.omega[squish_concat(indx, s - 1)])
else:
b.delta_defn.add(squish_concat(indx, s, 'ub'), expr=b.delta[squish_concat(indx, s)] <= a * y)
if nsegs > 1:
b.omega_exist.add(indx, expr=b.omega[squish_concat(indx, 1)] <= y)
tighten_var_bound(b.w[indx], (lb(x), ub(x)))
b.underest.add(squish_concat(indx, 'lb'), expr=z >= df_lb * (x - (lb(x) + (lbb(x) - lb(x)) * y)) + (df_lbb - df_lb) * (b.w - lbb(x) * y) + f_lb + (f_lbb - f_lb) * y)
b.underest.add(squish_concat(indx, 'ub'), expr=z >= df_ub * (x - (ub(x) + (ubb(x) - ub(x)) * y)) + (df_ubb - df_ub) * (b.w - ubb(x) * y) + f_ub + (f_ubb - f_ub) * y)
# Add inequalities for overestimators at the lb and ub of x,
# and Glover envelope for w = y * x
b.w_defn.add(squish_concat(indx, 'lb1'), expr=b.w >= y * lbb(x))
b.w_defn.add(squish_concat(indx, 'ub1'), expr=b.w <= y * ubb(x))
b.w_defn.add(squish_concat(indx, 'lb2'), expr=b.w <= x - (1 - y) * lb(x))
b.w_defn.add(squish_concat(indx, 'ub2'), expr=b.w >= x - (1 - y) * ub(x))
else:
# x is effectively fixed due to its bounds. Impose z = f(x).
b.underest.add(squish_concat(indx, 'lb'), expr=z >= f_lbb + (lb(z) - f_lbb) * (1 - y))
b.overest.add(indx, expr=z <= f_ubb + (ub(z) - f_ubb) * (1 - y))
b.x_defn.add(squish_concat(indx, 'lb'), expr=x >= lb(x) + (lbb(x) - lb(x)) * y)
b.x_defn.add(squish_concat(indx, 'ub'), expr=x <= ub(x) + (ubb(x) - ub(x)) * y)
# TODO: use z.fix(f_lb) instead?