Source code for leaspy.utils.linalg
import torch
from leaspy.exceptions import LeaspyModelInputError
__all__ = [
"compute_orthonormal_basis",
]
[docs]
def compute_orthonormal_basis(
dgamma_t0: torch.Tensor, G_metric: torch.Tensor, *, strip_col: int = 0
):
"""
Householder decomposition, adapted for a non-Euclidean inner product defined by:
(1) :math:`< x, y >Metric(p) = < x, G(p) y >Eucl = xT G(p) y`, where:
:math:`G(p)` is the symmetric positive-definite (SPD) matrix defining the metric at point `p`.
The Euclidean case is the special case where `G` is the identity matrix.
Product-metric is a special case where `G(p)` is a diagonal matrix (identified to a vector)
whose components are all > 0.
It is used to compute and set in-place the ``orthonormal_basis`` attribute
given the time-derivative of the geodesic at initial time and the `G_metric`.
The first component of the full orthonormal basis is a vector collinear `G_metric x dgamma_t0` that we get rid of.
The orthonormal basis we construct is always orthonormal for the Euclidean canonical inner product.
But all (but first) vectors of it lie in the sub-space orthogonal (for canonical inner product) to `G_metric * dgamma_t0`
which is the same thing that being orthogonal to `dgamma_t0` for the inner product implied by the metric.
[We could do otherwise if we'd like a full orthonormal basis, w.r.t. the non-Euclidean inner product.
But it'd imply to compute G^(-1/2) & G^(1/2) which may be computationally costly in case we don't have direct access to them
(for the special case of product-metric it is easy - just the component-wise inverse (sqrt'ed) of diagonal)
TODO are there any advantages/drawbacks of one method over the other except this one?
TODO are there any biases between features when only considering Euclidean orthonormal basis?]
Parameters
----------
dgamma_t0 : :class:`torch.FloatTensor` 1D
Time-derivative of the geodesic at initial time.
It may also be a vector collinear to it without any change to the result.
G_metric : scalar, `torch.FloatTensor` 0D, 1D or 2D-square
The `G(p)` defining the metric as referred in equation (1) just before :
* If 0D (scalar): `G` is proportional to the identity matrix
* If 1D (vector): `G` is a diagonal matrix (diagonal components > 0)
* If 2D (square matrix): `G` is general (SPD)
strip_col : :obj:`int` in 0..model_dimension-1 (default 0)
Which column of the basis should be the one collinear to `dgamma_t0` (that we get rid of)
Returns
-------
torch.Tensor of shape (dimension, dimension - 1)
Raises
------
:exc:`.LeaspyModelInputError`
if incoherent metric `G_metric`
"""
assert (
dgamma_t0.ndim == 1
), f"Expecting vector for velocities but got {dgamma_t0.ndim}D tensor"
(dimension,) = dgamma_t0.shape
## enforce `G_metric` to be a tensor
# if not isinstance(G_metric, torch.Tensor):
# G_metric = torch.tensor(G_metric) # convert from scalar...
# compute the vector that others columns should be orthogonal to, w.r.t canonical inner product
G_shape = G_metric.shape
if len(G_shape) == 0: # 0D
if G_metric.item() <= 0:
raise LeaspyModelInputError("Incoherent negative scalar metric.")
dgamma_t0 = G_metric.item() * dgamma_t0 # homothetic
elif len(G_shape) == 1: # 1D
if not (G_metric > 0).all():
raise LeaspyModelInputError("Incoherent 1D metric with negative values.")
if G_shape != (dimension,):
raise LeaspyModelInputError(
f"Incoherent 1D metric size: {G_shape[0]} != {dimension}."
)
dgamma_t0 = G_metric * dgamma_t0 # component-wise multiplication of vectors
elif len(G_shape) == 2: # matrix (general case)
# no check on positivity of matrix to remain light
if G_shape != (dimension, dimension):
raise LeaspyModelInputError(
f"Incoherent 2D metric shape: {G_shape} != {(dimension, dimension)}."
)
dgamma_t0 = G_metric @ dgamma_t0 # matrix multiplication
else:
raise LeaspyModelInputError(
"Unexpected metric of dim > 2 when computing orthonormal basis."
)
"""
Automatically choose the best column to strip?
<!> Not a good idea because it could fluctuate over iterations making mixing_matrix unstable!
(betas should slowly readapt to the permutation...)
#strip_col = dgamma_t0.abs().argmax().item()
#strip_col = v_metric_normalization.argmin().item()
"""
assert isinstance(strip_col, int) and 0 <= strip_col < dimension, (
strip_col,
dimension,
)
ej = torch.zeros_like(dgamma_t0)
ej[strip_col] = 1.0
alpha = -torch.sign(dgamma_t0[strip_col]) * torch.norm(dgamma_t0)
u_vector = dgamma_t0 - alpha * ej
v_vector = u_vector / torch.norm(u_vector)
## Classical Householder method (to get an orthonormal basis for the canonical inner product)
## Q = I_n - 2 v • v'
q_matrix = torch.eye(dimension) - 2 * v_vector.view(-1, 1) * v_vector
# concat columns (get rid of the one collinear to `dgamma_t0`)
return torch.cat((q_matrix[:, :strip_col], q_matrix[:, strip_col + 1 :]), dim=1)