Source code for pyomo.core.plugins.transform.nonnegative_transform
# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2024
# 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.
# ___________________________________________________________________________
import pyomo.core.expr as EXPR
from pyomo.core import (
nonpyomo_leaf_types,
TransformationFactory,
IntegerSet,
Integers,
PositiveIntegers,
NonPositiveIntegers,
NegativeIntegers,
NonNegativeIntegers,
Reals,
PositiveReals,
NonNegativeReals,
NegativeReals,
NonPositiveReals,
PercentFraction,
RealSet,
Var,
Set,
value,
Binary,
Constraint,
Objective,
)
from pyomo.core.base.misc import create_name
from pyomo.core.plugins.transform.util import partial
from pyomo.core.plugins.transform.hierarchy import IsomorphicTransformation
from pyomo.core.plugins.transform.util import collectAbstractComponents
import logging
logger = logging.getLogger('pyomo.core')
[docs]
class VarmapVisitor(EXPR.ExpressionReplacementVisitor):
def visiting_potential_leaf(self, node):
if node.__class__ in nonpyomo_leaf_types:
return True, node
#
# Clone leaf nodes in the expression tree
#
if node.is_variable_type():
if node.local_name in self.varmap:
return True, self.varmap[node.local_name]
else:
return True, node
if isinstance(node, EXPR.LinearExpression):
with EXPR.nonlinear_expression() as expr:
for c, v in zip(node.linear_coefs, node.linear_vars):
if hasattr(v, 'local_name'):
expr += c * self.varmap.get(v.local_name)
else:
expr += c * v
return True, expr
return False, None
def _walk_expr(expr, varMap):
"""
Walks an expression tree, making the replacements defined in varMap
"""
visitor = VarmapVisitor(varMap)
return visitor.dfs_postorder_stack(expr)
[docs]
@TransformationFactory.register(
"core.nonnegative_vars",
doc="Create an equivalent model in which all variables lie in the nonnegative orthant.",
)
class NonNegativeTransformation(IsomorphicTransformation):
"""
Creates a new, equivalent model by forcing all variables to lie in
the nonnegative orthant by introducing auxiliary variables.
"""
[docs]
def __init__(self, **kwds):
kwds["name"] = kwds.pop("name", "vars")
super(NonNegativeTransformation, self).__init__(**kwds)
self.realSets = (
Reals,
PositiveReals,
NonNegativeReals,
NegativeReals,
NonPositiveReals,
PercentFraction,
RealSet,
)
# Intentionally leave out Binary, Boolean, BinarySet, and BooleanSet;
# we check for those explicitly
self.discreteSets = (
IntegerSet,
Integers,
PositiveIntegers,
NonPositiveIntegers,
NegativeIntegers,
NonNegativeIntegers,
)
def _create_using(self, model, **kwds):
"""
Force all variables to lie in the nonnegative orthant.
Required arguments:
model The model to transform.
Optional keyword arguments:
pos_suffix The suffix applied to the 'positive' component of
converted variables. Default is '_plus'.
neg_suffix The suffix applied to the 'positive' component of
converted variables. Default is '_minus'.
"""
#
# Optional naming schemes
#
pos_suffix = kwds.pop("pos_suffix", "_plus")
neg_suffix = kwds.pop("neg_suffix", "_minus")
#
# We first perform an abstract problem transformation. Then, if model
# data is available, we instantiate the new model. If not, we construct
# a mapping that can later be used to populate the new model.
#
nonneg = model.clone()
components = collectAbstractComponents(nonneg)
# Map from variable base names to a {index, rule} map
constraint_rules = {}
# Map from variable base names to a rule defining the domains for that
# variable
domain_rules = {}
# Map from variable base names to its set of indices
var_indices = {}
# Map from fully qualified variable names to replacement expressions.
# For now, it is actually a map from a variable name to a closure that
# must later be evaluated with a model containing the replacement
# variables.
var_map = {}
#
# Get the constraints that enforce the bounds and domains of each
# variable
#
for var_name in components["Var"]:
var = nonneg.__getattribute__(var_name)
# Individual bounds and domains
orig_bounds = {}
orig_domain = {}
# New indices
indices = set()
# Map from constraint names to a constraint rule.
constraints = {}
# Map from variable indices to a domain
domains = {}
for ndx in var:
# Fully qualified variable name
vname = create_name(str(var_name), ndx)
# We convert each index to a string to avoid difficult issues
# regarding appending a suffix to tuples.
#
# If the index is None, this casts the index to a string,
# which doesn't match up with how Pyomo treats None indices
# internally. Replace with "" to be consistent.
if ndx is None:
v_ndx = ""
else:
v_ndx = str(ndx)
# Get the variable bounds
lb = value(var[ndx].lb)
ub = value(var[ndx].ub)
orig_bounds[ndx] = (lb, ub)
# Get the variable domain
if var[ndx].domain is not None:
orig_domain[ndx] = var[ndx].domain
else:
orig_domain[ndx] = var.domain
# Determine the replacement expression. Either a new single
# variable with the same attributes, or a sum of two new
# variables.
#
# If both the bounds and domain allow for negative values,
# replace the variable with the sum of nonnegative ones.
bounds_neg = (
orig_bounds[ndx] == (None, None)
or orig_bounds[ndx][0] is None
or orig_bounds[ndx][0] < 0
)
domain_neg = (
orig_domain[ndx] is None
or orig_domain[ndx].bounds()[0] is None
or orig_domain[ndx].bounds()[0] < 0
)
if bounds_neg and domain_neg:
# Make two new variables.
posVarSuffix = "%s%s" % (v_ndx, pos_suffix)
negVarSuffix = "%s%s" % (v_ndx, neg_suffix)
new_indices = (posVarSuffix, negVarSuffix)
# Replace the original variable with a sum expression
expr_dict = {posVarSuffix: 1, negVarSuffix: -1}
else:
# Add the new index. Lie if is 'None', since Pyomo treats
# 'None' specially as a key.
#
# More lies: don't let a blank index exist. Replace it with
# '_'. I don't actually have a justification for this other
# than that allowing "" as a key will eventually almost
# certainly lead to a strange bug.
if v_ndx is None:
t_ndx = "None"
elif v_ndx == "":
t_ndx = "_"
else:
t_ndx = v_ndx
new_indices = (t_ndx,)
# Replace the original variable with a sum expression
expr_dict = {t_ndx: 1}
# Add the new indices
for x in new_indices:
indices.add(x)
# Replace the original variable with an expression
var_map[vname] = partial(self.sumRule, var_name, expr_dict)
# Enforce bounds as constraints
if orig_bounds[ndx] != (None, None):
cname = "%s_%s" % (vname, "bounds")
tmp = orig_bounds[ndx]
constraints[cname] = partial(
self.boundsConstraintRule, tmp[0], tmp[1], var_name, expr_dict
)
# Enforce the bounds of the domain as constraints
if orig_domain[ndx] != None:
cname = "%s_%s" % (vname, "domain_bounds")
tmp = orig_domain[ndx].bounds()
constraints[cname] = partial(
self.boundsConstraintRule, tmp[0], tmp[1], var_name, expr_dict
)
# Domain will either be NonNegativeReals, NonNegativeIntegers,
# or Binary. We consider Binary because some solvers may
# optimize over binary variables.
if var[ndx].is_continuous():
for x in new_indices:
domains[x] = NonNegativeReals
elif var[ndx].is_binary():
for x in new_indices:
domains[x] = Binary
elif var[ndx].is_integer():
for x in new_indices:
domains[x] = NonNegativeIntegers
else:
logger.warning(
"Warning: domain '%s' not recognized, "
"defaulting to 'NonNegativeReals'" % (var.domain,)
)
for x in new_indices:
domains[x] = NonNegativeReals
constraint_rules[var_name] = constraints
domain_rules[var_name] = partial(self.exprMapRule, domains)
var_indices[var_name] = indices
# Remove all existing variables.
toRemove = []
for attr_name, attr in nonneg.__dict__.items():
if isinstance(attr, Var):
toRemove.append(attr_name)
for attr_name in toRemove:
nonneg.__delattr__(attr_name)
# Add the sets defining the variables, then the variables
for k, v in var_indices.items():
sname = "%s_indices" % k
nonneg.__setattr__(sname, Set(initialize=v))
nonneg.__setattr__(
k,
Var(
nonneg.__getattribute__(sname),
domain=domain_rules[k],
bounds=(0, None),
),
)
# Construct the model to get the variables and their indices
# recognized in the model
##nonneg = nonneg.create()
# Safe to evaluate the modifiedVars mapping
for var in var_map:
var_map[var] = var_map[var](nonneg)
# Map from constraint base names to maps from indices to expressions
constraintExprs = {}
#
# Convert all modified variables in all constraints in the original
# problem
#
for conName in components["Constraint"]:
con = nonneg.__getattribute__(conName)
# Map from constraint indices to a corrected expression
exprMap = {}
for ndx, cdata in con._data.items():
lower = _walk_expr(cdata.lower, var_map)
body = _walk_expr(cdata.body, var_map)
upper = _walk_expr(cdata.upper, var_map)
# Lie if ndx is None. Pyomo treats 'None' indices specially.
if ndx is None:
ndx = "None"
# Cast indices to strings, otherwise tuples ruin everything
exprMap[str(ndx)] = (lower, body, upper)
# Add to list of expression maps
constraintExprs[conName] = exprMap
# Map from constraint base names to maps from indices to expressions
objectiveExprs = {}
#
# Convert all modified variables in all objectives in the original
# problem
#
for objName in components["Objective"]:
obj = nonneg.__getattribute__(objName)
# Map from objective indices to a corrected expression
exprMap = {}
for ndx, odata in obj._data.items():
exprMap[ndx] = _walk_expr(odata.expr, var_map)
# Add to list of expression maps
objectiveExprs[objName] = exprMap
# Make the modified original constraints
for conName, ruleMap in constraintExprs.items():
# Make the set of indices
sname = conName + "_indices"
_set = Set(initialize=ruleMap.keys())
nonneg.__setattr__(sname, _set)
_set.construct()
# Define the constraint
_con = Constraint(
nonneg.__getattribute__(sname), rule=partial(self.exprMapRule, ruleMap)
)
nonneg.__setattr__(conName, _con)
_con.construct()
# Make the bounds constraints
for varName, ruleMap in constraint_rules.items():
conName = varName + "_constraints"
# Make the set of indices
sname = conName + "_indices"
_set = Set(initialize=ruleMap.keys())
nonneg.__setattr__(sname, _set)
_set.construct()
# Define the constraint
_con = Constraint(
nonneg.__getattribute__(sname),
rule=partial(self.delayedExprMapRule, ruleMap),
)
nonneg.__setattr__(conName, _con)
_con.construct()
# Make the objectives
for objName, ruleMap in objectiveExprs.items():
# Make the set of indices
sname = objName + "_indices"
_set = Set(initialize=ruleMap.keys())
nonneg.__setattr__(sname, _set)
_set.construct()
# Define the constraint
_obj = Objective(
nonneg.__getattribute__(sname), rule=partial(self.exprMapRule, ruleMap)
)
nonneg.__setattr__(objName, _obj)
_obj.construct()
return nonneg
[docs]
@staticmethod
def boundsConstraintRule(lb, ub, attr, vars, model):
"""
Produces 'lb < x^+ - x^- < ub' style constraints. Designed to
be made a closer through functools.partial, across lb, ub, attr,
and vars. vars is a {varname: coefficient} dictionary. attr is the
base variable name; that is, X[1] would be referenced by
model.__getattribute__('X')[1]
and so attr='X', and 1 is a key of vars.
"""
return (
lb,
sum(c * model.__getattribute__(attr)[v] for (v, c) in vars.items()),
ub,
)
@staticmethod
def noConstraint(*args):
return None
[docs]
@staticmethod
def sumRule(attr, vars, model):
"""
Returns a sum expression.
"""
return sum(c * model.__getattribute__(attr)[v] for (v, c) in vars.items())
[docs]
@staticmethod
def exprMapRule(ruleMap, model, ndx=None):
"""Rule intended to return expressions from a lookup table"""
return ruleMap[ndx]
[docs]
@staticmethod
def delayedExprMapRule(ruleMap, model, ndx=None):
"""
Rule intended to return expressions from a lookup table. Each entry
in the lookup table is a functor that needs to be evaluated before
returning.
"""
return ruleMap[ndx](model)