# ____________________________________________________________________________________
#
# 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.
# ____________________________________________________________________________________
import logging
from collections import defaultdict
from pyomo.common.dependencies import numpy as np, numpy_available
from pyomo.common.autoslots import AutoSlots
import pyomo.common.config as cfg
from pyomo.common import deprecated
from pyomo.common.collections import ComponentMap, ComponentSet, DefaultComponentMap
from pyomo.common.modeling import unique_component_name
import pyomo.core.expr as EXPR
from pyomo.core.base import TransformationFactory, SortComponents
from pyomo.repn.quadratic import QuadraticRepnVisitor
from pyomo.repn.util import OrderedVarRecorder
from pyomo.core import (
Block,
Constraint,
ConstraintList,
Set,
Suffix,
Var,
Expression,
Reals,
NonNegativeReals,
value,
NonNegativeIntegers,
ConcreteModel,
Objective,
Reference,
)
from pyomo.gdp import Disjunct, Disjunction, GDP_Error
from pyomo.gdp.plugins.gdp_to_mip_transformation import GDP_to_MIP_Transformation
from pyomo.gdp.util import clone_without_expression_components
from pyomo.gdp.plugins.multiple_bigm import Solver
from pyomo.core.util import target_list
from pyomo.core.expr.visitor import (
IdentifyVariableVisitor,
StreamBasedExpressionVisitor,
)
from pyomo.repn.linear import LinearRepnVisitor
from pyomo.repn.util import VarRecorder
from pyomo.opt.results.solver import TerminationCondition
from pyomo.opt.solver import SolverStatus
from weakref import ref as weakref_ref
import math
import itertools
logger = logging.getLogger('pyomo.gdp.hull')
class _HullTransformationData(AutoSlots.Mixin):
__slots__ = (
'disaggregated_var_map',
'original_var_map',
'bigm_constraint_map',
'disaggregation_constraint_map',
'well_defined_points_map',
'exact_quadratic_aux_var_map',
)
def __init__(self):
self.disaggregated_var_map = DefaultComponentMap(ComponentMap)
self.original_var_map = ComponentMap()
self.bigm_constraint_map = DefaultComponentMap(ComponentMap)
self.disaggregation_constraint_map = DefaultComponentMap(ComponentMap)
self.well_defined_points_map = {}
self.exact_quadratic_aux_var_map = ComponentMap()
Block.register_private_data_initializer(_HullTransformationData)
@TransformationFactory.register(
'gdp.chull',
doc="[DEPRECATED] please use 'gdp.hull' to get the Hull transformation.",
)
@deprecated(
"The 'gdp.chull' name is deprecated. "
"Please use the more apt 'gdp.hull' instead.",
logger='pyomo.gdp',
version="5.7",
)
class _Deprecated_Name_Hull(Hull_Reformulation):
def __init__(self):
super(_Deprecated_Name_Hull, self).__init__()
# Walk the expression and, whenever encountering an expression that
# could fail to be well-defined, create a constraint that keeps it
# well-defined. This is done at the Pyomo level to make this capability
# more generic instead of needing to specially set up the options for
# each solver (plus, some solvers get rather buggy when used for this
# task).
class _WellDefinedConstraintGenerator(StreamBasedExpressionVisitor):
def __init__(self, cons_list, **kwds):
self.cons_list = cons_list
super().__init__(**kwds)
# Whenever we exit a node (entering would also be fine) check if it
# has a restricted domain, and if it does add a corresponding
# constraint
def exitNode(self, node, data):
if node.__class__ in _expr_handlers:
for con in _expr_handlers[node.__class__](node):
# note: con should never be a boolean True here, such
# cases should have been filtered out during the handler
# call
self.cons_list.add(con)
# Epsilon for handling function domains with strict inequalities. This
# is a heuristic so it's not important for this to be tight.
EPS_HEURISTIC = 1e-4
def _handlePowExpression(node):
base, exp = node.args
# if base is not variable, nothing for us to do
if base.__class__ in EXPR.native_types or not base.is_potentially_variable():
return ()
# If exp is a NPV nonnegative integer, there are no restrictions on
# base. If exp is a NPV negative integer, base should not be
# zero. If exp is a NPV nonnegative fraction, base should not be
# negative. Otherwise, base should be strictly positive (as we can't
# be sure that exp could not be negative or fractional).
# Note: this is problematic for LP, but I don't want to potentially
# invoke a MIP solve here, so replace "x is nonzero" with "x is >=
# eps". It's a heuristic so this is not critical.
if exp.__class__ in EXPR.native_types or not exp.is_potentially_variable():
val = value(exp)
if round(val) == val:
if val >= 0:
return ()
else:
# return base != 0
return (base >= EPS_HEURISTIC,)
elif val >= 0:
return (base >= 0,)
return (base >= EPS_HEURISTIC,)
def _handleDivisionExpression(node):
# No division by zero. Dividing by an NPV is always allowed.
arg = node.args[1]
if arg.__class__ in EXPR.native_types or not arg.is_potentially_variable():
return ()
# Same LP vs MIP problem as before
return (arg >= EPS_HEURISTIC,)
def _handleUnaryFunctionExpression(node):
arg = node.args[0]
if (
node.name in _unary_functions_unrestricted
or arg.__class__ in EXPR.native_types
or not arg.is_potentially_variable()
):
return ()
elif node.name in _unary_function_handlers:
return _unary_function_handlers[node.name](arg)
else:
raise GDP_Error(
"Hull transformation base point heuristic: no domain "
f"information available for unfamiliar unary function {node.name}"
)
# Unary function handlers
def _handle_log(arg):
return (arg >= EPS_HEURISTIC,)
def _handle_log10(arg):
return (arg >= EPS_HEURISTIC,)
def _handle_sqrt(arg):
return (arg >= 0,)
def _handle_asin(arg):
return (arg >= -1, arg <= 1)
def _handle_acos(arg):
return (arg >= -1, arg <= 1)
def _handle_tan(arg):
# It can't be exactly pi/2 plus a multiple of pi. Rather difficult
# to enforce, so make a conservative effort by instead keeping it in
# (-pi/2, pi/2).
return (arg >= -(math.pi / 2) + EPS_HEURISTIC, arg <= (math.pi / 2) - EPS_HEURISTIC)
def _handle_acosh(arg):
return (arg >= 1,)
def _handle_atanh(arg):
return (arg >= -1 + EPS_HEURISTIC, arg <= 1 - EPS_HEURISTIC)
# All expression types that can potentially be
# ill-defined:
_expr_handlers = {
# You are on your own here
# EXPR.ExternalFunctionExpression,
EXPR.PowExpression: _handlePowExpression,
EXPR.DivisionExpression: _handleDivisionExpression,
EXPR.UnaryFunctionExpression: _handleUnaryFunctionExpression,
}
# All unary functions that can potentially be
# ill-defined:
_unary_function_handlers = {
'log': _handle_log,
'log10': _handle_log10,
'sqrt': _handle_sqrt,
'asin': _handle_asin,
'acos': _handle_acos,
'tan': _handle_tan,
'acosh': _handle_acosh,
'atanh': _handle_atanh,
}
# Unary functions that can never be ill-defined:
_unary_functions_unrestricted = {
# Being a subclass of UnaryFunctionExpression, 'abs' does not end up here - this
# error check is available only for direct users of UnaryFunctionExpression.
'ceil',
'floor',
'exp',
'sin',
'cos',
'sinh',
'cosh',
'tanh',
'atan',
'asinh',
}