Source code for cvxpy.constraints.power

"""
Copyright 2021 the CVXPY developers

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

from typing import List, Tuple

import numpy as np

from cvxpy.constraints.cones import Cone
from cvxpy.expressions import cvxtypes
from cvxpy.utilities import scopes


[docs]class PowCone3D(Cone): """ An object representing a collection of 3D power cone constraints x[i]**alpha[i] * y[i]**(1-alpha[i]) >= |z[i]| for all i x >= 0, y >= 0 If the parameter alpha is a scalar, it will be promoted to a vector matching the (common) sizes of x, y, z. The numeric value of alpha (or its components, in the vector case) must be a number in the open interval (0, 1). We store flattened representations of the arguments (x, y, z, and alpha) as Expression objects. We construct dual variables with respect to these flattened representations. """ def __init__(self, x, y, z, alpha, constr_id=None) -> None: Expression = cvxtypes.expression() self.x = Expression.cast_to_const(x) self.y = Expression.cast_to_const(y) self.z = Expression.cast_to_const(z) for val in [self.x, self.y, self.z]: if not (val.is_affine() and val.is_real()): raise ValueError('All arguments must be affine and real.') alpha = Expression.cast_to_const(alpha) if alpha.is_scalar(): if self.x.shape: alpha = cvxtypes.promote()(alpha, self.x.shape) else: # when `alpha` is a naked float, it has to be cast into a # 1-D array to be compatible with downstream (vectorized) # processing alpha = cvxtypes.promote()(alpha, (1,)) self.alpha = alpha if np.any(self.alpha.value <= 0) or np.any(self.alpha.value >= 1): msg = "Argument alpha must have entries in the open interval (0, 1)." raise ValueError(msg) if alpha.shape == (1,): arg_shapes = [self.x.shape, self.y.shape, self.z.shape, ()] else: arg_shapes = [self.x.shape, self.y.shape, self.z.shape, self.alpha.shape] if any(arg_shapes[0] != s for s in arg_shapes[1:]): msg = ("All arguments must have the same shapes. Provided arguments have" "shapes %s" % str(arg_shapes)) raise ValueError(msg) super(PowCone3D, self).__init__([self.x, self.y, self.z], constr_id) def __str__(self) -> str: return "Pow3D(%s, %s, %s; %s)" % (self.x, self.y, self.z, self.alpha) @property def residual(self): # TODO: The projection should be implemented directly. from cvxpy import Minimize, Problem, Variable, hstack, norm2 if self.x.value is None or self.y.value is None or self.z.value is None: return None x = Variable(self.x.shape) y = Variable(self.y.shape) z = Variable(self.z.shape) constr = [PowCone3D(x, y, z, self.alpha)] obj = Minimize(norm2(hstack([x, y, z]) - hstack([self.x.value, self.y.value, self.z.value]))) problem = Problem(obj, constr) return problem.solve(solver='SCS', eps=1e-8) def get_data(self): return [self.alpha, self.id] def is_imag(self) -> bool: return False def is_complex(self) -> bool: return False @property def size(self) -> int: return 3 * self.num_cones() def num_cones(self): return self.x.size def cone_sizes(self) -> List[int]: return [3]*self.num_cones()
[docs] def is_dcp(self, dpp: bool = False) -> bool: if dpp: with scopes.dpp_scope(): args_ok = all(arg.is_affine() for arg in self.args) exps_ok = not isinstance(self.alpha, cvxtypes.parameter()) return args_ok and exps_ok return all(arg.is_affine() for arg in self.args)
def is_dgp(self, dpp: bool = False) -> bool: return False def is_dqcp(self) -> bool: return self.is_dcp() @property def shape(self) -> Tuple[int, ...]: s = (3,) + self.x.shape # Note: this can be a 3-tuple of x.ndim == 2. return s def save_dual_value(self, value) -> None: value = np.reshape(value, newshape=(3, -1)) dv0 = np.reshape(value[0, :], newshape=self.x.shape) dv1 = np.reshape(value[1, :], newshape=self.y.shape) dv2 = np.reshape(value[2, :], newshape=self.z.shape) self.dual_variables[0].save_value(dv0) self.dual_variables[1].save_value(dv1) self.dual_variables[2].save_value(dv2) # TODO: figure out why the reshaping had to be done differently, # relative to ExpCone constraints. def _dual_cone(self, *args): """Implements the dual cone of PowCone3D See Pg 85 of the MOSEK modelling cookbook for more information""" if args is None: PowCone3D(self.dual_variables[0]/self.alpha, self.dual_variables[1]/(1-self.alpha), self.dual_variables[2], self.alpha) else: # some assertions for verifying `args` args_shapes = [arg.shape for arg in args] instance_args_shapes = [arg.shape for arg in self.args] assert len(args) == len(self.args) assert args_shapes == instance_args_shapes return PowCone3D(args[0]/self.alpha, args[1]/(1-self.alpha), args[2], self.alpha)
[docs]class PowConeND(Cone): """ Represents a collection of N-dimensional power cone constraints that is *mathematically* equivalent to the following code snippet (which makes incorrect use of numpy functions on cvxpy objects): np.prod(np.power(W, alpha), axis=axis) >= np.abs(z), W >= 0 All arguments must be Expression-like, and z must satisfy z.ndim <= 1. The columns (rows) of alpha must sum to 1 when axis=0 (axis=1). Note: unlike PowCone3D, we make no attempt to promote alpha to the appropriate shape. The dimensions of W and alpha must match exactly. Note: Dual variables are not currently implemented for this type of constraint. """ _TOL_ = 1e-6 def __init__(self, W, z, alpha, axis: int = 0, constr_id=None) -> None: Expression = cvxtypes.expression() W = Expression.cast_to_const(W) if not (W.is_real() and W.is_affine()): msg = "Invalid first argument; W must be affine and real." raise ValueError(msg) z = Expression.cast_to_const(z) if z.ndim > 1 or not (z.is_real() and z.is_affine()): msg = ("Invalid second argument. z must be affine, real, " "and have at most one z.ndim <= 1.") raise ValueError(msg) # Check z has one entry per cone. if (W.ndim <= 1 and z.size > 1) or \ (W.ndim == 2 and z.size != W.shape[1-axis]) or \ (W.ndim == 1 and axis == 1): raise ValueError( "Argument dimensions %s and %s, with axis=%i, are incompatible." % (W.shape, z.shape, axis)) if W.ndim == 2 and W.shape[axis] <= 1: msg = "PowConeND requires left-hand-side to have at least two terms." raise ValueError(msg) alpha = Expression.cast_to_const(alpha) if alpha.shape != W.shape: raise ValueError("Argument dimensions %s and %s are not equal." % (W.shape, alpha.shape)) if np.any(alpha.value <= 0): raise ValueError("Argument alpha must be entry-wise positive.") if np.any(np.abs(1 - np.sum(alpha.value, axis=axis)) > PowConeND._TOL_): raise ValueError("Argument alpha must sum to 1 along axis %s." % axis) self.W = W self.z = z self.alpha = alpha self.axis = axis if z.ndim == 0: z = z.flatten() super(PowConeND, self).__init__([W, z], constr_id) def __str__(self) -> str: return "PowND(%s, %s; %s)" % (self.W, self.z, self.alpha) def is_imag(self) -> bool: return False def is_complex(self) -> bool: return False def get_data(self): return [self.alpha, self.axis, self.id] @property def residual(self): # TODO: The projection should be implemented directly. from cvxpy import Minimize, Problem, Variable, hstack, norm2 if self.W.value is None or self.z.value is None: return None W = Variable(self.W.shape) z = Variable(self.z.shape) constr = [PowConeND(W, z, self.alpha, axis=self.axis)] obj = Minimize(norm2(hstack([W.flatten(), z.flatten()]) - hstack([self.W.flatten().value, self.z.flatten().value]))) problem = Problem(obj, constr) return problem.solve(solver='SCS', eps=1e-8) def num_cones(self): return self.z.size @property def size(self) -> int: cone_size = 1 + self.args[0].shape[self.axis] return cone_size * self.num_cones() def cone_sizes(self) -> List[int]: cone_size = 1 + self.args[0].shape[self.axis] return [cone_size] * self.num_cones()
[docs] def is_dcp(self, dpp: bool = False) -> bool: """A power cone constraint is DCP if each argument is affine. """ if dpp: with scopes.dpp_scope(): args_ok = self.args[0].is_affine() and self.args[1].is_affine() exps_ok = not isinstance(self.alpha, cvxtypes.parameter()) return args_ok and exps_ok return True
def is_dgp(self, dpp: bool = False) -> bool: return False def is_dqcp(self) -> bool: return self.is_dcp() def save_dual_value(self, value) -> None: dW = value[:, :-1] dz = value[:, -1] if self.axis == 0: dW = dW.T dz = dz.T if dW.shape[1] == 1: #NOTE: Targetting problems where duals have the shape # (n, 1) --- dropping the extra dimension is crucial for # the `_dual_cone` and `dual_residual` methods to work properly dW = np.squeeze(dW) self.dual_variables[0].save_value(dW) self.dual_variables[1].save_value(dz) def _dual_cone(self, *args): """Implements the dual cone of PowConeND See Pg 85 of the MOSEK modelling cookbook for more information""" if args is None or args == (): scaled_duals = self.dual_variables[0]/self.alpha return PowConeND(scaled_duals, self.dual_variables[1], self.alpha, axis=self.axis) else: # some assertions for verifying `args` args_shapes = [arg.shape for arg in args] instance_args_shapes = [arg.shape for arg in self.args] assert len(args) == len(self.args) assert args_shapes == instance_args_shapes assert args[0].value.shape == self.alpha.value.shape return PowConeND(args[0]/self.alpha, args[1], self.alpha, axis=self.axis)