# ____________________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2026 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.
# ____________________________________________________________________________________
"""
Between Steps (P-Split) reformulation for GDPs from [KMT21]_.
"""
from pyomo.common.config import (
ConfigBlock,
ConfigValue,
document_kwargs_from_configdict,
)
from pyomo.common.modeling import unique_component_name
from pyomo.core import (
Block,
Constraint,
Var,
SortComponents,
Transformation,
TransformationFactory,
TraversalStrategy,
NonNegativeIntegers,
value,
ConcreteModel,
Objective,
ComponentMap,
BooleanVar,
LogicalConstraint,
Connector,
Expression,
Suffix,
Param,
Set,
SetOf,
RangeSet,
Reference,
Binary,
LogicalConstraintList,
maximize,
)
from pyomo.core.base.external import ExternalFunction
from pyomo.network import Port
from pyomo.common.collections import ComponentSet
from pyomo.repn import generate_standard_repn
import pyomo.core.expr as EXPR
from pyomo.opt import SolverFactory
from pyomo.util.vars_from_expressions import get_vars_from_components
from pyomo.gdp import Disjunct, Disjunction, GDP_Error
from pyomo.gdp.util import (
is_child_of,
_to_dict,
verify_successful_solve,
NORMAL,
clone_without_expression_components,
_warn_for_active_disjunct,
get_gdp_tree,
)
from pyomo.core.util import target_list
from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr
from weakref import ref as weakref_ref
from math import floor
import logging
logger = logging.getLogger('pyomo.gdp.partition_disjuncts')
def _generate_additively_separable_repn(nonlinear_part):
if nonlinear_part.__class__ is not EXPR.SumExpression:
# This isn't separable, so we just have the one expression
return {
'nonlinear_vars': [
tuple(v for v in EXPR.identify_variables(nonlinear_part))
],
'nonlinear_exprs': [nonlinear_part],
}
# else, it was a SumExpression, and we will break it into the summands,
# recording which variables are there.
nonlinear_decomp = {'nonlinear_vars': [], 'nonlinear_exprs': []}
for summand in nonlinear_part.args:
nonlinear_decomp['nonlinear_exprs'].append(summand)
nonlinear_decomp['nonlinear_vars'].append(
tuple(v for v in EXPR.identify_variables(summand))
)
return nonlinear_decomp
[docs]
def arbitrary_partition(disjunction, P):
"""Returns a valid partition into P sets of the variables that appear in
algebraic additively separable constraints in the Disjuncts in
'disjunction'. Note that this method may return an invalid partition
if the constraints are not additively separable!
Arguments:
----------
disjunction : DisjunctionData
A Disjunction object for which the variable partition will be
created.
P : int
the number of partitions
"""
# collect variables
v_set = ComponentSet()
for disj in disjunction.disjuncts:
v_set.update(
get_vars_from_components(disj, Constraint, descend_into=Block, active=True)
)
# assign them to partitions
partitions = [ComponentSet() for i in range(P)]
for i, v in enumerate(v_set):
partitions[i % P].add(v)
return partitions
[docs]
def compute_optimal_bounds(expr, global_constraints, opt):
"""Returns a tuple (LB, UB) where LB and UB are the results of minimizing
and maximizing expr over the variable bounds and the constraints on the
global_constraints block. Note that if expr is nonlinear, even if one of
the min and max problems is convex, the other won't be!
Arguments:
----------
expr : ExpressionBase
The subexpression whose bounds we will return
global_constraints : BlockData
A Block which contains the global Constraints and Vars of the
original model
opt : SolverBase
A configured Solver object to use to minimize and maximize expr
over the set defined by global_constraints. Note that if expr
is nonlinear, opt will need to be capable of optimizing
nonconvex problems.
"""
if opt is None:
raise GDP_Error(
"No solver was specified to optimize the "
"subproblems for computing expression bounds! "
"Please specify a configured solver in the "
"'compute_bounds_solver' argument if using "
"'compute_optimal_bounds.'"
)
# add temporary objective and calculate bounds
obj = Objective(expr=expr)
global_constraints.add_component(
unique_component_name(global_constraints, "tmp_obj"), obj
)
# Solve first minimizing, to get a lower bound
results = opt.solve(global_constraints)
if verify_successful_solve(results) is not NORMAL:
logger.warning(
"Problem to find lower bound for expression %s"
"did not solve normally.\n\n%s" % (expr, results)
)
LB = None
else:
# This has some risks, if you're using a solver the gives a lower bound,
# getting that would be better. But this is why this is a callback.
LB = value(obj.expr)
# Now solve maximizing, to get an upper bound
obj.sense = maximize
results = opt.solve(global_constraints)
if verify_successful_solve(results) is not NORMAL:
logger.warning(
"Problem to find upper bound for expression %s"
"did not solve normally.\n\n%s" % (expr, results)
)
UB = None
else:
UB = value(obj.expr)
# clean up
global_constraints.del_component(obj)
del obj
return (LB, UB)
[docs]
def compute_fbbt_bounds(expr, global_constraints, opt):
"""
Calls fbbt on expr and returns the lower and upper bounds on the expression
based on the bounds of the Vars that appear in the expression. Ignores
the global_constraints and opt arguments.
"""
return compute_bounds_on_expr(expr)