Source code for geometric_kernels.spaces.su

"""
This module provides the :class:`SpecialUnitary` space and the respective
:class:`~.eigenfunctions.Eigenfunctions` subclass :class:`SUEigenfunctions`.
"""

import itertools
import json
import math
import operator
from functools import reduce

import lab as B
import numpy as np
from beartype.typing import List, Tuple

from geometric_kernels.lab_extras import (
    complex_conj,
    create_complex,
    dtype_double,
    from_numpy,
    qr,
)
from geometric_kernels.spaces.eigenfunctions import Eigenfunctions
from geometric_kernels.spaces.lie_groups import (
    CompactMatrixLieGroup,
    LieGroupCharacter,
    WeylAdditionTheorem,
)
from geometric_kernels.utils.utils import chain, get_resource_file_path


[docs] class SUEigenfunctions(WeylAdditionTheorem): def __init__(self, n, num_levels, compute_characters=True): self.n = n self.dim = n**2 - 1 self.rank = n - 1 self.rho = np.arange(self.n - 1, -self.n, -2) * 0.5 super().__init__(n, num_levels, compute_characters) def _generate_signatures(self, num_levels: int) -> List[Tuple[int, ...]]: sign_vals_lim = 100 if self.n in (1, 2) else 30 if self.n == 3 else 10 signatures = list( itertools.combinations_with_replacement( range(sign_vals_lim, -1, -1), r=self.rank ) ) signatures = [sgn + (0,) for sgn in signatures] eig_and_signature = [ ( round(4 * (len(signature) ** 2) * self._compute_eigenvalue(signature)), signature, ) for signature in signatures ] eig_and_signature.sort() signatures = [eig_sgn[1] for eig_sgn in eig_and_signature][:num_levels] return signatures def _compute_dimension(self, signature: Tuple[int, ...]) -> int: rep_dim = reduce( operator.mul, ( reduce( operator.mul, ( signature[i - 1] - signature[j - 1] + j - i for j in range(i + 1, self.n + 1) ), ) / math.factorial(self.n - i) for i in range(1, self.n) ), ) return int(round(rep_dim)) def _compute_eigenvalue(self, signature: Tuple[int, ...]) -> B.Float: normalized_signature = np.asarray(signature, dtype=np.float64) - np.mean( signature ) lb_eigenvalue = ( np.linalg.norm(self.rho + normalized_signature) ** 2 - np.linalg.norm(self.rho) ** 2 ) return lb_eigenvalue def _compute_character( self, n: int, signature: Tuple[int, ...] ) -> LieGroupCharacter: return SUCharacter(n, signature) def _torus_representative(self, X): return B.eig(X, False)
[docs] def inverse(self, X: B.Numeric) -> B.Numeric: return SpecialUnitary.inverse(X)
[docs] class SUCharacter(LieGroupCharacter): """ The class that represents a character of the SU(n) group. Many of the characters on SU(n) are complex-valued. These are polynomials whose coefficients are precomputed and stored in a file. By default, there are 20 precomputed characters for n from 2 to 6. If you want more, use the `utils/compute_characters.py` script. :param n: The order n of the SO(n) group. :param signature: The signature that determines a particular character (and an irreducible unitary representation along with it). """ def __init__(self, n: int, signature: Tuple[int, ...]): self.signature = signature self.n = n self.coeffs, self.monoms = self._load() def _load(self): group_name = "SU({})".format(self.n) with get_resource_file_path("precomputed_characters.json") as file_path: with file_path.open("r") as file: character_formulas = json.load(file) try: cs, ms = character_formulas[group_name][str(self.signature)] coeffs, monoms = (np.array(data) for data in (cs, ms)) return coeffs, monoms except KeyError as e: raise KeyError( "Unable to retrieve character parameters for signature {} of {}, " "perhaps it is not precomputed." "Run compute_characters.py with changed parameters.".format( e.args[0], group_name ) ) from None
[docs] def __call__(self, gammas: B.Numeric) -> B.Numeric: char_val = B.zeros(B.dtype(gammas), *gammas.shape[:-1]) for coeff, monom in zip(self.coeffs, self.monoms): char_val += coeff * B.prod(gammas ** from_numpy(gammas, monom), axis=-1) return char_val
[docs] class SpecialUnitary(CompactMatrixLieGroup): r""" The GeometricKernels space representing the special unitary group SU(n) consisting of n by n complex unitary matrices with unit determinant. The elements of this space are represented as n x n unitary matrices with complex entries and unit determinant. .. note:: A tutorial on how to use this space is available in the :doc:`SpecialUnitary.ipynb </examples/SpecialUnitary>` notebook. :param n: The order n of the group SU(n). .. note:: We only support n >= 2. Mathematically, SU(1) is trivial, consisting of a single element (the identity), chances are you do not need it. For large values of n, you might need to run the `compute_characters.py` script to precompute the necessary mathematical quantities beyond the ones provided by default. .. admonition:: Citation If you use this GeometricKernels space in your research, please consider citing :cite:t:`azangulov2022`. """ def __init__(self, n: int): if n < 2: raise ValueError(f"Only n >= 2 is supported. n = {n} was provided.") self.n = n self.dim = n**2 - 1 self.rank = n - 1 super().__init__() @property def dimension(self) -> int: """ The dimension of the space, as that of a Riemannian manifold. :return: floor(n^2-1) where n is the order of the group SU(n). """ return self.dim
[docs] @staticmethod def inverse(X: B.Numeric) -> B.Numeric: return complex_conj( B.transpose(X) ) # B.transpose only inverses the last two dims.
[docs] def get_eigenfunctions(self, num: int) -> Eigenfunctions: """ Returns the :class:`~.SUEigenfunctions` object with `num` levels and order n. :param num: Number of levels. """ return SUEigenfunctions(self.n, num)
[docs] def get_eigenvalues(self, num: int) -> B.Numeric: eigenfunctions = SUEigenfunctions(self.n, num) eigenvalues = np.array( [eigenvalue for eigenvalue in eigenfunctions._eigenvalues] ) return B.reshape(eigenvalues, -1, 1) # [num, 1]
[docs] def get_repeated_eigenvalues(self, num: int) -> B.Numeric: eigenfunctions = SUEigenfunctions(self.n, num) eigenvalues = chain( eigenfunctions._eigenvalues, [rep_dim**2 for rep_dim in eigenfunctions._dimensions], ) return B.reshape(eigenvalues, -1, 1) # [M, 1]
[docs] def random(self, key: B.RandomState, number: int): if self.n == 2: # explicit parametrization via the double cover SU(2) = S_3 key, sphere_point = B.random.randn(key, dtype_double(key), number, 4) sphere_point /= B.reshape( B.sqrt(B.einsum("ij,ij->i", sphere_point, sphere_point)), -1, 1 ) a = create_complex(sphere_point[..., 0], sphere_point[..., 1]) b = create_complex(sphere_point[..., 2], sphere_point[..., 3]) r1 = B.stack(a, -complex_conj(b), axis=-1) r2 = B.stack(b, complex_conj(a), axis=-1) q = B.stack(r1, r2, axis=-1) return key, q else: key, real = B.random.randn(key, dtype_double(key), number, self.n, self.n) key, imag = B.random.randn(key, dtype_double(key), number, self.n, self.n) h = create_complex(real, imag) / B.sqrt(2) q, r = qr(h, mode="complete") r_diag = B.einsum("...ii->...i", r) r_diag_inv_phase = complex_conj(r_diag / B.abs(r_diag)) q *= r_diag_inv_phase[:, None] q_det = B.det(q) q_det_inv_phase = complex_conj((q_det / B.abs(q_det))) q[:, :, 0] *= q_det_inv_phase[:, None] return key, q
@property def element_shape(self): """ :return: [n, n]. """ return [self.n, self.n]