luk036/ellpy

View on GitHub
src/ellpy/oracles/chol_ext.py

Summary

Maintainability
A
45 mins
Test Coverage
# -*- coding: utf-8 -*-
import math
from typing import Callable, Union

import numpy as np

Arr = Union[np.ndarray]


class chol_ext:
    """LDLT factorization (mainly for LMI oracles)

    - LDL^T square-root-free version
    - Option allow semidefinite
    - Cholesky–Banachiewicz style, row-based
    - Lazy evaluation
    - A matrix A in R^{m x m} is positive definite
                         iff v' A v > 0 for all v in R^n.
    - O(p^3) per iteration, independent of N
    """

    __slots__ = ("p", "v", "_n", "_T", "allow_semidefinite")

    def __init__(self, N: int):
        """Construct a new chol ext object

        Arguments:
            N (int): dimension
        """
        self.allow_semidefinite = False
        self.p = (0, 0)
        self.v: Arr = np.zeros(N)

        self._n: int = N
        self._T: Arr = np.zeros((N, N))  # pre-allocate storage

    def factorize(self, A: Arr) -> bool:
        """Perform Cholesky Factorization

        Arguments:
            A (np.array): Symmetric Matrix

         If $A$ is positive definite, then $p$ is zero.
         If it is not, then $p$ is a positive integer,
         such that $v = R^{-1} e_p$ is a certificate vector
         to make $v'*A[:p,:p]*v < 0$
        """
        return self.factor(lambda i, j: A[i, j])

    def factor(self, getA: Callable[[int, int], float]) -> bool:
        """Perform Cholesky Factorization (square-root free version)

        Arguments:
            getA (callable): function to access symmetric matrix

         Construct $A(i, j)$ on demand, lazy evalution
        """
        start = 0  # range start
        self.p = (0, 0)
        for i in range(self._n):
            # j = start
            d = getA(i, start)
            for j in range(start, i):
                self._T[j, i] = d  # keep it for later use
                self._T[i, j] = d / self._T[j, j]  # the L[i, j]
                s = j + 1
                d = getA(i, s) - (self._T[i, start:s] @ self._T[start:s, s])
            self._T[i, i] = d
            if d > 0.0:
                continue
            if d < 0.0 or not self.allow_semidefinite:
                self.p = start, i + 1
                break
            start = i + 1  # T[i, i] == 0 (very unlikely), restart at i+1
        return self.is_spd()

    def is_spd(self):
        """Is $A$ symmetric positive definite (spd)

        Returns:
            bool: True if $A$ is a spd
        """
        return self.p[1] == 0

    def witness(self):
        """witness that certifies $A$ is not symmetric positive definite (spd)
            (square-root-free version)

           evidence: v' A v = -ep

        Raises:
            AssertionError: $A$ indeeds a spd matrix

        Returns:
            array: v
            float: ep
        """
        if self.is_spd():
            raise AssertionError()
        start, n = self.p
        m = n - 1
        self.v[m] = 1.0
        for i in range(m, start, -1):
            self.v[i - 1] = -(self._T[i:n, i - 1] @ self.v[i:n])
        return -self._T[m, m]

    def sym_quad(self, A: Arr):
        """[summary]

        Arguments:
            v ([type]): [description]
            A ([type]): [description]

        Returns:
            [type]: [description]
        """
        s, n = self.p
        v = self.v[s:n]
        return v @ A[s:n, s:n] @ v

    def sqrt(self) -> Arr:
        """Return upper triangular matrix R where A = R' * R

        Raises:
            AssertionError: [description]

        Returns:
            Arr: [description]
        """
        if not self.is_spd():
            raise AssertionError()
        M = np.zeros((self._n, self._n))
        for i in range(self._n):
            M[i, i] = math.sqrt(self._T[i, i])
            for j in range(i + 1, self._n):
                M[i, j] = self._T[j, i] * M[i, i]
        return M