Source code for polynomials_on_simplices.calculus.affine_map
r"""
Functionality for dealing with affine maps :math:`\Phi : \mathbb{R}^m \to \mathbb{R}^n` with
.. math:: \Phi(x) = Ax + b,
where :math:`A \in \mathbb{R}^{n \times m}, b \in \mathbb{R}^n`.
"""
import numbers
import numpy as np
[docs]def create_affine_map(a, b, multiple_arguments=False):
r"""
Generate the affine map :math:`\Phi : \mathbb{R}^m \to \mathbb{R}^n, \Phi(x) = Ax + b` from a matrix A and
a vector b (or scalars a and b in the case m = n = 1).
:param a: Matrix or scalar defining the linear part of the affine map.
:param b: Vector or scalar defining the translation part of the affine map.
:param bool multiple_arguments: For a multivariate affine map, this argument determines if the generated map should
take m scalar arguments or 1 m-dimensional vector as argument. For example with a 2-dimensional domain the
generated map could have the signature :math:`\Phi([x, y])` (if `multiple_arguments` is False) or
:math:`\Phi(x, y)` (if `multiple_arguments` is True).
:return: Map :math:`\Phi` which takes an m-dimensional vector as input and returns an n-dimensional vector (or
scalar input and output for m = n = 1).
:rtype: Callable :math:`\Phi(x)`.
"""
if isinstance(a, numbers.Number):
# R -> R
assert isinstance(b, numbers.Number)
def phi(x):
return a * x + b
return phi
if len(a.shape) == 1:
try:
len(b)
# R -> R^n, n > 1
def phi(x):
return a * x + b
return phi
except TypeError:
# R^n -> R, n > 1
assert isinstance(b, numbers.Number)
if multiple_arguments:
def phi(*x):
return np.dot(a, np.array(x)) + b
else:
def phi(x):
return np.dot(a, x) + b
return phi
assert len(a.shape) == 2
n, m = a.shape
if m == 1:
if n == 1:
# Invalid input, pass a and b as scalars instead
assert False
else:
assert len(b) == n
def phi(x):
return a.flatten() * x + b
return phi
else:
if n == 1:
assert isinstance(b, numbers.Number)
if multiple_arguments:
def phi(*x):
return np.dot(a, np.array(x))[0] + b
else:
def phi(x):
return np.dot(a, x)[0] + b
return phi
else:
assert len(b) == n
if multiple_arguments:
def phi(*x):
return np.dot(a, np.array(x)) + b
else:
def phi(x):
return np.dot(a, x) + b
return phi
[docs]def inverse_affine_transformation(a, b):
r"""
Generate the matrix and vector defining the inverse of a given affine map
:math:`\Phi : \mathbb{R}^n \to \mathbb{R}^n`,
.. math:: \Phi(x) = Ax + b.
The inverse of the affine map :math:`\Phi(x)` is given by :math:`\Phi^{-1}(x) = A^{-1}x - A^{-1}b`.
This function returns the matrix :math:`A^{-1}` and vector :math:`-A^{-1}b` defining the inverse map (or
scalars 1 / A and -b / A for scalar input A, b).
:param a: Matrix or scalar defining the linear part of the affine map we want to invert.
:param b: Vector or scalar defining the translation part of the affine map we want to invert.
:return: Tuple of A and b.
"""
# Handle 1d case separately
if isinstance(a, numbers.Number):
a_inv = 1 / a
b_inv = -b * a_inv
return a_inv, b_inv
a_inv = np.linalg.inv(a)
b_inv = -np.dot(a_inv, b)
return a_inv, b_inv
def _is_invertible(a):
"""
Check if a given matrix is invertible.
:param a: Square matrix.
:return: Whether or not the matrix is invertible
:rtype: bool
"""
try:
np.linalg.inv(a)
return True
except np.linalg.LinAlgError:
return False
[docs]def pseudoinverse_affine_transformation(a, b):
r"""
Generate the matrix and vector defining the pseudoinverse of a given affine map.
The inverse of an affine map :math:`\Phi(x) = Ax + b` is given by :math:`\Phi^{-1}(x) = A^{-1}x - A^{-1}b`.
Assuming the matrix A is not invertible, but :math:`A^T A` is, so that it has pseudoinverse
:math:`A^+ = (A^T A)^{-1} A^T`. Then the affine map :math:`\Phi^+(x) = A^+x - A^+b` satisfies
:math:`\Phi^+(\Phi(x)) = x`.
This function returns the matrix :math:`A^+` and vector :math:`-A^+b` defining this pseudoinverse map.
:param a: Matrix or scalar defining the linear part of the affine map we want to invert.
:param b: Vector or scalar defining the translation part of the affine map we want to invert.
:return: Tuple of A and b.
"""
assert _is_invertible(np.dot(a.T, a)) or isinstance(np.dot(a.T, a), numbers.Number)
if len(a.shape) == 1:
return pseudoinverse_affine_transformation(np.reshape(a, (len(a), 1)), b)
n, m = a.shape
a_inv = np.linalg.pinv(a)
if m == 1:
b_inv = -np.dot(a_inv, b)[0]
else:
b_inv = -np.dot(a_inv, b)
return a_inv, b_inv
[docs]def affine_composition(phi1, phi2):
r"""
Compute the matrix and vector defining the composition of two affine maps.
:math:`\Phi_1(x) = A_1x + b_1, \Phi_2(x) = A_2x + b_2, \Phi(x) = (\Phi_2 \circ \Phi_1)(x) = Ax + b`.
:math:`A = A_2 A_1, b = A_2 b_1 + b_2`.
:param phi1: Tuple of A matrix and b vector for the first affine transformation.
:param phi2: Tuple of A matrix and b vector for the second affine transformation.
:return: Tuple of A and b.
"""
a1, b1 = phi1
a2, b2 = phi2
return np.dot(a2, a1), np.dot(a2, b1) + b2