Source code for disropt.functions.affine_form
import numpy as np
import warnings
from typing import Union
from .abstract_function import AbstractFunction
from .quadratic_form import QuadraticForm
from .utilities import check_input
[docs]class AffineForm(AbstractFunction):
"""Makes an affine transformation
.. math::
f(x)=\\langle A, x\\rangle + b=A^\\top x + b
with :math:`A\\in \\mathbb{R}^{n\\times m}`, :math:`b\\in \\mathbb{R}^{m}`
and :math:`x: \\mathbb{R}^{n}`. It can also be instantiated as::
A @ x + b
Args:
fn (AbstractFunction): input function
A (numpy.ndarray): input matrix
b (numpy.ndarray): input bias
Raises:
TypeError: first argument must be a AbstractFunction object
TypeError: second argument must be numpy.ndarray
ValueError: the number of columns of A must be equal to the number of
rows of the output of fn
"""
def __init__(self, fn: AbstractFunction, A: np.ndarray = None, b: np.ndarray = None):
if not isinstance(fn, AbstractFunction):
raise TypeError("First argument must be a AbstractFunction object")
if not fn.is_differentiable:
warnings.warn(
'Composition with a nondifferentiable function will lead to \
an error when asking for a jacobian or a subgradient')
self.differentiable = False
else:
self.differentiable = True
if A is not None:
if not isinstance(A, np.ndarray):
raise TypeError("Second argument must be a numpy.ndarray")
if A.shape[0] != fn.output_shape[0]:
raise ValueError(
"Dimension mismatch. Input matrix must have shape {}".format(
(fn.output_shape[0], "Any")))
else:
A = np.eye(fn.output_shape[0])
if b is not None:
if not isinstance(b, np.ndarray):
if isinstance(b, (int, float)):
b = np.ndarray(b).reshape(1, 1)
else:
raise TypeError("Second argument must be a numpy.ndarray")
if b.shape[0] != A.shape[1]:
raise ValueError(
"Dimension mismatch. Input bias must have shape {}".format(
(A.shape[1], 1)))
else:
b = np.zeros([A.shape[1], 1])
if fn.is_affine:
from .variable import Variable
self.affine = True
fn_A, fn_b = fn.get_parameters()
# A @ (B @ x) = A^T B^T x = (BA)^T x = (B @ A) @ x
self.A = fn_A @ A
self.b = b + A.transpose() @ fn_b
self.fn = Variable(fn.input_shape[0])
else:
self.affine = False
self.A = A
self.b = b
self.fn = fn
self.input_shape = self.fn.input_shape
self.output_shape = (A.shape[1], self.fn.output_shape[1])
super().__init__()
def _expression(self):
expression = 'AffineForm({}, A, b)'.format(self.fn._expression())
return expression
def _to_cvxpy(self):
return self.A.transpose() @ self.fn._to_cvxpy() + self.b.flatten()
def _extend_variable(self, n_var, axis, pos):
return AffineForm(self.fn._extend_variable(n_var, axis, pos), self.A, self.b)
def __neg__(self):
A = -1 * self.A
b = -1 * self.b
return AffineForm(self.fn, A, b)
def __add__(self, other):
if isinstance(other, (int, float, np.ndarray)):
A = self.A
b = self.b + other
return AffineForm(self.fn, A, b)
elif isinstance(other, AbstractFunction):
if self.is_affine and other.is_affine:
other_A, other_b = other.get_parameters()
if self.A.shape == other_A.shape:
A = self.A + other_A
b = self.b + other_b
return AffineForm(self.fn, A, b)
else:
raise ValueError("Incompatible dimensions")
if self.is_affine and other.is_quadratic:
other_P, other_q, other_r = other.get_parameters()
if other_q.shape == self.A.shape:
P = other_P
q = self.A + other_q
r = self.b + other_r
return QuadraticForm(self.fn, P, q, r)
else:
raise ValueError("Incompatible dimensions")
else:
return super().__add__(other)
else:
raise TypeError
def __radd__(self, other):
return self.__add__(other)
def __iadd__(self, other):
return self.__add__(other)
def __sub__(self, other):
if isinstance(other, (int, float, np.ndarray)):
A = self.A
b = self.b - other
return AffineForm(self.fn, A, b)
elif isinstance(other, AbstractFunction):
if self.is_affine and other.is_affine:
other_A, other_b = other.get_parameters()
if self.A.shape == other_A.shape:
A = self.A - other_A
b = self.b - other_b
return AffineForm(self.fn, A, b)
else:
raise ValueError("Incompatible dimensions")
if self.is_affine and other.is_quadratic:
other_P, other_q, other_r = other.get_parameters()
if other_q.shape == self.A.shape:
P = -other_P
q = self.A - other_q
r = self.b - other_r
return QuadraticForm(self.fn, P, q, r)
else:
raise ValueError("Incompatible dimensions")
else:
return super().__sub__(other)
else:
raise TypeError
def __rsub__(self, other):
return self.__neg__().__add__(other)
def __isub__(self, other):
return self.__sub__(other)
def __mul__(self, other):
if isinstance(other, (int, float)):
A = other * self.A
b = other * self.b
return AffineForm(self.fn, A, b)
else:
return super().__mul__(other)
def __rmul__(self, other: Union[int, float]):
return self.__mul__(other)
def __imul__(self, other):
return self.__mul__(other)
def __matmul__(self, other):
# if isinstance(other, (np.ndarray)):
# if other.shape == self.output_shape:
# # B @ (A @ x) = B^T A^T x = (AB)^T x = (A @ B) @ x
# A = self.A @ other
# b = other.transpose() @ self.b
# return AffineForm(self.fn, A, b)
# else:
# raise NotImplementedError
if isinstance(other, AbstractFunction):
if self.is_affine and other.is_affine:
# TODO: check
other_A, other_b = other.get_parameters()
if self.A.shape == other_A.shape:
P = self.A @ other_A.transpose()
q = (self.b.transpose() @ other_A.transpose() +
other_b.transpose() @ self.A.transpose()).transpose()
r = self.b.transpose() @ other_b
return QuadraticForm(self.fn, P, q, r)
else:
raise ValueError("Incompatible dimensions")
else:
return super().__matmul__(other)
else:
return super().__matmul__(other)
def __rmatmul__(self, other):
if isinstance(other, np.ndarray):
# C^T (A^T x + b) = C^T A^T x + C^T b
if other.shape[0] != self.output_shape[0]:
raise ValueError("Incompatible shapes")
else:
A = self.A @ other
b = other.transpose() @ self.b
return AffineForm(self.fn, A, b)
elif isinstance(other, AbstractFunction):
from .constant import Constant
if isinstance(other, Constant):
# TODO check dimensions
A = self.A @ other.eval()
b = other.eval().transpose() @ self.b
return AffineForm(self.fn, A, b)
else:
return super().__rmatmul__(other)
else:
raise TypeError
def __imatmul__(self, other):
return self.__matmul__(other)
[docs] @check_input
def eval(self, x: np.ndarray) -> np.ndarray:
return (self.A.transpose() @ self.fn.eval(x)).reshape(self.output_shape) + self.b
@check_input
def _alternative_jacobian(self, x: np.ndarray, **kwargs) -> np.ndarray:
if not self.fn.is_differentiable:
warnings.warn("Composition of non affine functions. The Jacobian may be not correct.")
# A^T \Jac fn(x)
return self.A.transpose() @ self.fn.jacobian(x, **kwargs)
def aggregate_affine_form(list_of_affine: list) -> AffineForm:
from .variable import Variable
input_shape = None
x = None
A = None
b = None
for item in list_of_affine:
if not isinstance(item, AffineForm):
raise TypeError("A list of AffineForm objects is required.")
if input_shape is None:
input_shape = item.input_shape
x = Variable(input_shape[0])
if item.input_shape != input_shape:
raise ValueError("All AffineForm objects in the list must have the same input_shape.")
item_A, item_b = item.get_parameters()
if A is None:
A = item_A
b = item_b
else:
A = np.hstack([A, item_A])
b = np.vstack([b, item_b])
return AffineForm(x, A, b)