Source code for pyomo.core.kernel.conic

#  ___________________________________________________________________________
#
#  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.
#  ___________________________________________________________________________
"""Various conic constraint implementations."""

from pyomo.core.expr.numvalue import is_numeric_data
from pyomo.core.expr import value, exp
from pyomo.core.kernel.block import block
from pyomo.core.kernel.variable import IVariable, variable, variable_tuple
from pyomo.core.kernel.constraint import (
    IConstraint,
    linear_constraint,
    constraint,
    constraint_tuple,
)


def _build_linking_constraints(v, v_aux):
    assert len(v) == len(v_aux)
    c_aux = []
    for vi, vi_aux in zip(v, v_aux):
        assert vi_aux.ctype is IVariable
        if vi is None:
            continue
        elif is_numeric_data(vi):
            c_aux.append(
                linear_constraint(variables=(vi_aux,), coefficients=(1,), rhs=vi)
            )
        elif isinstance(vi, IVariable):
            c_aux.append(
                linear_constraint(variables=(vi_aux, vi), coefficients=(1, -1), rhs=0)
            )
        else:
            c_aux.append(constraint(body=vi_aux - vi, rhs=0))
    return constraint_tuple(c_aux)


class _ConicBase(IConstraint):
    """Base class for a few conic constraints that
    implements some shared functionality. Derived classes
    are expected to declare any necessary slots."""

    _ctype = IConstraint
    _linear_canonical_form = False
    __slots__ = ()

    def __init__(self):
        self._parent = None
        self._storage_key = None
        self._active = True
        # the body expression is only built if necessary
        # (i.e., when someone asks for it via the body
        # property method)
        self._body = None

    @classmethod
    def as_domain(cls, *args, **kwds):
        """Builds a conic domain"""
        raise NotImplementedError  # pragma:nocover

    def _body_function(self, *args):
        """A function that defines the body expression"""
        raise NotImplementedError  # pragma:nocover

    def _body_function_variables(self, values=False):
        """Returns variables in the order they should be
        passed to the body function. If values is True, then
        return the current value of each variable in place
        of the variables themselves."""
        raise NotImplementedError  # pragma:nocover

    def check_convexity_conditions(self, relax=False):
        """Returns True if all convexity conditions for the
        conic constraint are satisfied. If relax is True,
        then variable domains are ignored and it is assumed
        that all variables are continuous."""
        raise NotImplementedError  # pragma:nocover

    #
    # Define the IConstraint abstract methods
    #

    @property
    def body(self):
        """The body of the constraint"""
        if self._body is None:
            self._body = self._body_function(
                *self._body_function_variables(values=False)
            )
        return self._body

    @property
    def lower(self):
        """The expression for the lower bound of the constraint"""
        return None

    @property
    def upper(self):
        """The expression for the upper bound of the constraint"""
        return 0.0

    @property
    def lb(self):
        """The value of the lower bound of the constraint"""
        return None

    @property
    def ub(self):
        """The value of the upper bound of the constraint"""
        return 0.0

    @property
    def rhs(self):
        """The right-hand side of the constraint"""
        raise ValueError(
            "The rhs property can not be read because this "
            "is not an equality constraint"
        )

    @property
    def equality(self):
        return False

    #
    # Override a the default __call__ method on IConstraint
    # to avoid building the body expression, if possible
    #

    def __call__(self, exception=True):
        try:
            # we wrap the result with value(...) as the
            # alpha term used by some of the constraints
            # may be a parameter
            return value(
                self._body_function(*self._body_function_variables(values=True))
            )
        except (ValueError, TypeError):
            if exception:
                raise ValueError("one or more terms could not be evaluated")
            return None


[docs]class quadratic(_ConicBase): """A quadratic conic constraint of the form: x[0]^2 + ... + x[n-1]^2 <= r^2, which is recognized as convex for r >= 0. Parameters ---------- r : :class:`variable` A variable. x : list[:class:`variable`] An iterable of variables. """ __slots__ = ( "_parent", "_storage_key", "_active", "_body", "_r", "_x", "__weakref__", ) def __init__(self, r, x): super(quadratic, self).__init__() self._r = r self._x = tuple(x) assert isinstance(self._r, IVariable) assert all(isinstance(xi, IVariable) for xi in self._x)
[docs] @classmethod def as_domain(cls, r, x): """Builds a conic domain. Input arguments take the same form as those of the conic constraint, but in place of each variable, one can optionally supply a constant, linear expression, or None. Returns ------- block A block object with the core conic constraint (block.q) expressed using auxiliary variables (block.r, block.x) linked to the input arguments through auxiliary constraints (block.c). """ b = block() b.r = variable(lb=0) b.x = variable_tuple([variable() for i in range(len(x))]) b.c = _build_linking_constraints([r] + list(x), [b.r] + list(b.x)) b.q = cls(r=b.r, x=b.x) return b
@property def r(self): return self._r @property def x(self): return self._x # # Define the _ConicBase abstract methods # def _body_function(self, r, x): """A function that defines the body expression""" return sum(xi**2 for xi in x) - r**2 def _body_function_variables(self, values=False): """Returns variables in the order they should be passed to the body function. If values is True, then return the current value of each variable in place of the variables themselves.""" if not values: return self.r, self.x else: return self.r.value, tuple(xi.value for xi in self.x)
[docs] def check_convexity_conditions(self, relax=False): """Returns True if all convexity conditions for the conic constraint are satisfied. If relax is True, then variable domains are ignored and it is assumed that all variables are continuous.""" return ( relax or (self.r.is_continuous() and all(xi.is_continuous() for xi in self.x)) ) and (self.r.has_lb() and value(self.r.lb) >= 0)
[docs]class rotated_quadratic(_ConicBase): """A rotated quadratic conic constraint of the form: x[0]^2 + ... + x[n-1]^2 <= 2*r1*r2, which is recognized as convex for r1,r2 >= 0. Parameters ---------- r1 : :class:`variable` A variable. r2 : :class:`variable` A variable. x : list[:class:`variable`] An iterable of variables. """ __slots__ = ( "_parent", "_storage_key", "_active", "_body", "_r1", "_r2", "_x", "__weakref__", ) def __init__(self, r1, r2, x): super(rotated_quadratic, self).__init__() self._r1 = r1 self._r2 = r2 self._x = tuple(x) assert isinstance(self._r1, IVariable) assert isinstance(self._r2, IVariable) assert all(isinstance(xi, IVariable) for xi in self._x)
[docs] @classmethod def as_domain(cls, r1, r2, x): """Builds a conic domain. Input arguments take the same form as those of the conic constraint, but in place of each variable, one can optionally supply a constant, linear expression, or None. Returns ------- block A block object with the core conic constraint (block.q) expressed using auxiliary variables (block.r1, block.r2, block.x) linked to the input arguments through auxiliary constraints (block.c). """ b = block() b.r1 = variable(lb=0) b.r2 = variable(lb=0) b.x = variable_tuple([variable() for i in range(len(x))]) b.c = _build_linking_constraints([r1, r2] + list(x), [b.r1, b.r2] + list(b.x)) b.q = cls(r1=b.r1, r2=b.r2, x=b.x) return b
@property def r1(self): return self._r1 @property def r2(self): return self._r2 @property def x(self): return self._x # # Define the _ConicBase abstract methods # def _body_function(self, r1, r2, x): """A function that defines the body expression""" return sum(xi**2 for xi in x) - 2 * r1 * r2 def _body_function_variables(self, values=False): """Returns variables in the order they should be passed to the body function. If values is True, then return the current value of each variable in place of the variables themselves.""" if not values: return self.r1, self.r2, self.x else: return self.r1.value, self.r2.value, tuple(xi.value for xi in self.x)
[docs] def check_convexity_conditions(self, relax=False): """Returns True if all convexity conditions for the conic constraint are satisfied. If relax is True, then variable domains are ignored and it is assumed that all variables are continuous.""" return ( ( relax or ( self.r1.is_continuous() and self.r2.is_continuous() and all(xi.is_continuous() for xi in self.x) ) ) and (self.r1.has_lb() and value(self.r1.lb) >= 0) and (self.r2.has_lb() and value(self.r2.lb) >= 0) )
[docs]class primal_exponential(_ConicBase): """A primal exponential conic constraint of the form: x1*exp(x2/x1) <= r, which is recognized as convex for x1,r >= 0. Parameters ---------- r : :class:`variable` A variable. x1 : :class:`variable` A variable. x2 : :class:`variable` A variable. """ __slots__ = ( "_parent", "_storage_key", "_active", "_body", "_r", "_x1", "_x2", "__weakref__", ) def __init__(self, r, x1, x2): super(primal_exponential, self).__init__() self._r = r self._x1 = x1 self._x2 = x2 assert isinstance(self._r, IVariable) assert isinstance(self._x1, IVariable) assert isinstance(self._x2, IVariable)
[docs] @classmethod def as_domain(cls, r, x1, x2): """Builds a conic domain. Input arguments take the same form as those of the conic constraint, but in place of each variable, one can optionally supply a constant, linear expression, or None. Returns ------- block A block object with the core conic constraint (block.q) expressed using auxiliary variables (block.r, block.x1, block.x2) linked to the input arguments through auxiliary constraints (block.c). """ b = block() b.r = variable(lb=0) b.x1 = variable(lb=0) b.x2 = variable() b.c = _build_linking_constraints([r, x1, x2], [b.r, b.x1, b.x2]) b.q = cls(r=b.r, x1=b.x1, x2=b.x2) return b
@property def r(self): return self._r @property def x1(self): return self._x1 @property def x2(self): return self._x2 # # Define the _ConicBase abstract methods # def _body_function(self, r, x1, x2): """A function that defines the body expression""" return x1 * exp(x2 / x1) - r def _body_function_variables(self, values=False): """Returns variables in the order they should be passed to the body function. If values is True, then return the current value of each variable in place of the variables themselves.""" if not values: return self.r, self.x1, self.x2 else: return self.r.value, self.x1.value, self.x2.value
[docs] def check_convexity_conditions(self, relax=False): """Returns True if all convexity conditions for the conic constraint are satisfied. If relax is True, then variable domains are ignored and it is assumed that all variables are continuous.""" return ( ( relax or ( self.x1.is_continuous() and self.x2.is_continuous() and self.r.is_continuous() ) ) and (self.x1.has_lb() and value(self.x1.lb) >= 0) and (self.r.has_lb() and value(self.r.lb) >= 0) )
[docs]class primal_power(_ConicBase): """A primal power conic constraint of the form: sqrt(x[0]^2 + ... + x[n-1]^2) <= (r1^alpha)*(r2^(1-alpha)) which is recognized as convex for r1,r2 >= 0 and 0 < alpha < 1. Parameters ---------- r1 : :class:`variable` A variable. r2 : :class:`variable` A variable. x : list[:class:`variable`] An iterable of variables. alpha : float, :class:`parameter`, etc. A constant term. """ __slots__ = ( "_parent", "_storage_key", "_active", "_body", "_r1", "_r2", "_x", "_alpha", "__weakref__", ) def __init__(self, r1, r2, x, alpha): super(primal_power, self).__init__() self._r1 = r1 self._r2 = r2 self._x = tuple(x) self._alpha = alpha assert isinstance(self._r1, IVariable) assert isinstance(self._r2, IVariable) assert all(isinstance(xi, IVariable) for xi in self._x) if not is_numeric_data(self._alpha): raise TypeError( "The type of the alpha parameter of a conic " "constraint is restricted numeric data or " "objects that store numeric data." )
[docs] @classmethod def as_domain(cls, r1, r2, x, alpha): """Builds a conic domain. Input arguments take the same form as those of the conic constraint, but in place of each variable, one can optionally supply a constant, linear expression, or None. Returns ------- block A block object with the core conic constraint (block.q) expressed using auxiliary variables (block.r1, block.r2, block.x) linked to the input arguments through auxiliary constraints (block.c). """ b = block() b.r1 = variable(lb=0) b.r2 = variable(lb=0) b.x = variable_tuple([variable() for i in range(len(x))]) b.c = _build_linking_constraints([r1, r2] + list(x), [b.r1, b.r2] + list(b.x)) b.q = cls(r1=b.r1, r2=b.r2, x=b.x, alpha=alpha) return b
@property def r1(self): return self._r1 @property def r2(self): return self._r2 @property def x(self): return self._x @property def alpha(self): return self._alpha # # Define the _ConicBase abstract methods # def _body_function(self, r1, r2, x): """A function that defines the body expression""" alpha = self.alpha return (sum(xi**2 for xi in x) ** 0.5) - (r1**alpha) * (r2 ** (1 - alpha)) def _body_function_variables(self, values=False): """Returns variables in the order they should be passed to the body function. If values is True, then return the current value of each variable in place of the variables themselves.""" if not values: return self.r1, self.r2, self.x else: return self.r1.value, self.r2.value, tuple(xi.value for xi in self.x)
[docs] def check_convexity_conditions(self, relax=False): """Returns True if all convexity conditions for the conic constraint are satisfied. If relax is True, then variable domains are ignored and it is assumed that all variables are continuous.""" alpha = value(self.alpha, exception=False) return ( ( relax or ( self.r1.is_continuous() and self.r2.is_continuous() and all(xi.is_continuous() for xi in self.x) ) ) and (self.r1.has_lb() and value(self.r1.lb) >= 0) and (self.r2.has_lb() and value(self.r2.lb) >= 0) and ((alpha is not None) and (0 < alpha < 1)) )
class primal_geomean(_ConicBase): """A primal geometric mean conic constraint of the form: (r[0]*...*r[n-2])^(1/(n-1)) >= |x[n-1]| Parameters ---------- r : :class:`variable` An iterable of variables. x : :class:`variable` A scalar variable. """ __slots__ = ( "_parent", "_storage_key", "_active", "_body", "_r", "_x", "__weakref__", ) def __init__(self, r, x): super(primal_geomean, self).__init__() self._r = tuple(r) self._x = x assert isinstance(self._x, IVariable) assert all(isinstance(ri, IVariable) for ri in self._r) @classmethod def as_domain(cls, r, x): """Builds a conic domain. Input arguments take the same form as those of the conic constraint, but in place of each variable, one can optionally supply a constant, linear expression, or None. Returns ------- block A block object with the core conic constraint (block.q) expressed using auxiliary variables (block.r, block.x) linked to the input arguments through auxiliary constraints (block.c).""" b = block() b.r = variable_tuple([variable(lb=0) for i in range(len(r))]) b.x = variable() b.c = _build_linking_constraints(list(r) + [x], list(b.r) + [x]) b.q = cls(r=b.r, x=b.x) return b @property def r(self): return self._r @property def x(self): return self._x
[docs]class dual_exponential(_ConicBase): """A dual exponential conic constraint of the form: -x2*exp((x1/x2)-1) <= r which is recognized as convex for x2 <= 0 and r >= 0. Parameters ---------- r : :class:`variable` A variable. x1 : :class:`variable` A variable. x2 : :class:`variable` A variable. """ __slots__ = ( "_parent", "_storage_key", "_active", "_body", "_r", "_x1", "_x2", "__weakref__", ) def __init__(self, r, x1, x2): super(dual_exponential, self).__init__() self._r = r self._x1 = x1 self._x2 = x2 assert isinstance(self._r, IVariable) assert isinstance(self._x1, IVariable) assert isinstance(self._x2, IVariable)
[docs] @classmethod def as_domain(cls, r, x1, x2): """Builds a conic domain. Input arguments take the same form as those of the conic constraint, but in place of each variable, one can optionally supply a constant, linear expression, or None. Returns ------- block A block object with the core conic constraint (block.q) expressed using auxiliary variables (block.r, block.x1, block.x2) linked to the input arguments through auxiliary constraints (block.c). """ b = block() b.r = variable(lb=0) b.x1 = variable() b.x2 = variable(ub=0) b.c = _build_linking_constraints([r, x1, x2], [b.r, b.x1, b.x2]) b.q = cls(r=b.r, x1=b.x1, x2=b.x2) return b
@property def r(self): return self._r @property def x1(self): return self._x1 @property def x2(self): return self._x2 # # Define the _ConicBase abstract methods # def _body_function(self, r, x1, x2): """A function that defines the body expression""" return -x2 * exp((x1 / x2) - 1) - r def _body_function_variables(self, values=False): """Returns variables in the order they should be passed to the body function. If values is True, then return the current value of each variable in place of the variables themselves.""" if not values: return self.r, self.x1, self.x2 else: return self.r.value, self.x1.value, self.x2.value
[docs] def check_convexity_conditions(self, relax=False): """Returns True if all convexity conditions for the conic constraint are satisfied. If relax is True, then variable domains are ignored and it is assumed that all variables are continuous.""" return ( ( relax or ( self.x1.is_continuous() and self.x2.is_continuous() and self.r.is_continuous() ) ) and (self.x2.has_ub() and value(self.x2.ub) <= 0) and (self.r.has_lb() and value(self.r.lb) >= 0) )
[docs]class dual_power(_ConicBase): """A dual power conic constraint of the form: sqrt(x[0]^2 + ... + x[n-1]^2) <= ((r1/alpha)^alpha) * ((r2/(1-alpha))^(1-alpha)) which is recognized as convex for r1,r2 >= 0 and 0 < alpha < 1. Parameters ---------- r1 : :class:`variable` A variable. r2 : :class:`variable` A variable. x : list[:class:`variable`] An iterable of variables. alpha : float, :class:`parameter`, etc. A constant term. """ __slots__ = ( "_parent", "_storage_key", "_active", "_body", "_r1", "_r2", "_x", "_alpha", "__weakref__", ) def __init__(self, r1, r2, x, alpha): super(dual_power, self).__init__() self._r1 = r1 self._r2 = r2 self._x = tuple(x) self._alpha = alpha assert isinstance(self._r1, IVariable) assert isinstance(self._r2, IVariable) assert all(isinstance(xi, IVariable) for xi in self._x) if not is_numeric_data(self._alpha): raise TypeError( "The type of the alpha parameter of a conic " "constraint is restricted numeric data or " "objects that store numeric data." )
[docs] @classmethod def as_domain(cls, r1, r2, x, alpha): """Builds a conic domain. Input arguments take the same form as those of the conic constraint, but in place of each variable, one can optionally supply a constant, linear expression, or None. Returns ------- block A block object with the core conic constraint (block.q) expressed using auxiliary variables (block.r1, block.r2, block.x) linked to the input arguments through auxiliary constraints (block.c). """ b = block() b.r1 = variable(lb=0) b.r2 = variable(lb=0) b.x = variable_tuple([variable() for i in range(len(x))]) b.c = _build_linking_constraints([r1, r2] + list(x), [b.r1, b.r2] + list(b.x)) b.q = cls(r1=b.r1, r2=b.r2, x=b.x, alpha=alpha) return b
@property def r1(self): return self._r1 @property def r2(self): return self._r2 @property def x(self): return self._x @property def alpha(self): return self._alpha # # Define the _ConicBase abstract methods # def _body_function(self, r1, r2, x): """A function that defines the body expression""" alpha = self.alpha return (sum(xi**2 for xi in x) ** 0.5) - ((r1 / alpha) ** alpha) * ( (r2 / (1 - alpha)) ** (1 - alpha) ) def _body_function_variables(self, values=False): """Returns variables in the order they should be passed to the body function. If values is True, then return the current value of each variable in place of the variables themselves.""" if not values: return self.r1, self.r2, self.x else: return self.r1.value, self.r2.value, tuple(xi.value for xi in self.x)
[docs] def check_convexity_conditions(self, relax=False): """Returns True if all convexity conditions for the conic constraint are satisfied. If relax is True, then variable domains are ignored and it is assumed that all variables are continuous.""" alpha = value(self.alpha, exception=False) return ( ( relax or ( self.r1.is_continuous() and self.r2.is_continuous() and all(xi.is_continuous() for xi in self.x) ) ) and (self.r1.has_lb() and value(self.r1.lb) >= 0) and (self.r2.has_lb() and value(self.r2.lb) >= 0) and ((alpha is not None) and (0 < alpha < 1)) )
class dual_geomean(_ConicBase): """A dual geometric mean conic constraint of the form: (n-1)*(r[0]*...*r[n-2])^(1/(n-1)) >= |x[n-1]| Parameters ---------- r : :class:`variable` An iterable of variables. x : :class:`variable` A scalar variable. """ __slots__ = ( "_parent", "_storage_key", "_active", "_body", "_r", "_x", "__weakref__", ) def __init__(self, r, x): super(dual_geomean, self).__init__() self._r = tuple(r) self._x = x assert isinstance(self._x, IVariable) assert all(isinstance(ri, IVariable) for ri in self._r) @classmethod def as_domain(cls, r, x): """Builds a conic domain. Input arguments take the same form as those of the conic constraint, but in place of each variable, one can optionally supply a constant, linear expression, or None. Returns ------- block A block object with the core conic constraint (block.q) expressed using auxiliary variables (block.r, block.x) linked to the input arguments through auxiliary constraints (block.c).""" b = block() b.r = variable_tuple([variable(lb=0) for i in range(len(r))]) b.x = variable() b.c = _build_linking_constraints(list(r) + [x], list(b.r) + [x]) b.q = cls(r=b.r, x=b.x) return b @property def r(self): return self._r @property def x(self): return self._x class svec_psdcone(_ConicBase): """A domain consisting of vectorizations of the lower-triangular part of a positive semidefinite matrx, with the non-diagonal elements additionally rescaled. In other words, if a vector 'x' of length n = d*(d+1)/2 belongs to this cone, then the matrix: sMat(x) = [[ x[1], x[2]/sqrt(2), ..., x[d]/sqrt(2)], [x[2]/sqrt(2), x[d+1], ..., x[2d-1]/sqrt(2)], ... [x[d]/sqrt(2), x[2d-1]/sqrt(2), ..., x[d*(d+1)/2]/sqrt(2)]] will be restricted to be a positive-semidefinite matrix. Parameters ---------- x : :class:`variable` An iterable of variables with length d*(d+1)/2. """ __slots__ = ("_parent", "_storage_key", "_active", "_body", "_x", "__weakref__") def __init__(self, x): super(svec_psdcone, self).__init__() self._x = tuple(x) assert all(isinstance(xi, IVariable) for xi in self._x) @classmethod def as_domain(cls, x): """Builds a conic domain. Input arguments take the same form as those of the conic constraint, but in place of each variable, one can optionally supply a constant, linear expression, or None. Returns ------- block A block object with the core conic constraint (block.q) expressed using auxiliary variables (block.r, block.x) linked to the input arguments through auxiliary constraints (block.c).""" b = block() b.x = variable_tuple([variable() for i in range(len(x))]) b.c = _build_linking_constraints(list(x), list(b.x)) b.q = cls(x=b.x) return b @property def x(self): return self._x