def test_posterior_mean_CG_equivalency(self):
        """The probabilistic linear solver(s) should recover CG iterates as a posterior
        mean for specific covariances."""

        # Linear system
        A, b = self.poisson_linear_system

        # Callback function to return CG iterates
        cg_iterates = []

        def callback_iterates_CG(xk):
            cg_iterates.append(
                np.eye(np.shape(A)[0]) @ xk
            )  # identity hack to actually save different iterations

        # Solve linear system

        # Initial guess as chosen by PLS: x0 = Ainv.mean @ b
        x0 = b

        # Conjugate gradient method
        xhat_cg, info_cg = scipy.sparse.linalg.cg(
            A=A, b=b, x0=x0, tol=10 ** -6, callback=callback_iterates_CG
        )
        cg_iters_arr = np.array([x0] + cg_iterates)

        # Matrix priors (encoding weak symmetric posterior correspondence)
        Ainv0 = rvs.Normal(
            mean=linops.Identity(A.shape[1]),
            cov=linops.SymmetricKronecker(A=linops.Identity(A.shape[1])),
        )
        A0 = rvs.Normal(
            mean=linops.Identity(A.shape[1]),
            cov=linops.SymmetricKronecker(A),
        )
        for kwargs in [{"assume_A": "sympos", "rtol": 10 ** -6}]:
            with self.subTest():
                # Define callback function to obtain search directions
                pls_iterates = []

                # pylint: disable=cell-var-from-loop
                def callback_iterates_PLS(
                    xk, Ak, Ainvk, sk, yk, alphak, resid, **kwargs
                ):
                    pls_iterates.append(xk.mean)

                # Probabilistic linear solver
                xhat_pls, _, _, info_pls = linalg.problinsolve(
                    A=A,
                    b=b,
                    Ainv0=Ainv0,
                    A0=A0,
                    callback=callback_iterates_PLS,
                    **kwargs
                )
                pls_iters_arr = np.array([x0] + pls_iterates)

                self.assertAllClose(xhat_pls.mean, xhat_cg, rtol=10 ** -12)
                self.assertAllClose(pls_iters_arr, cg_iters_arr, rtol=10 ** -12)
Beispiel #2
0
    def test_symmkronecker_transpose(self):
        """Kronecker product transpose property: (A (x) B)^T = A^T (x) B^T."""
        for A, B in self.symmkronecker_matrices:
            with self.subTest():
                W = linops.SymmetricKronecker(A=A, B=B)
                V = linops.SymmetricKronecker(A=A.T, B=B.T)

                self.assertAllClose(W.T.todense(), V.todense())
Beispiel #3
0
    def test_symmkronecker_commutation(self):
        """Symmetric Kronecker products fulfill A (x)_s B = B (x)_s A"""
        for A, B in self.symmkronecker_matrices:
            with self.subTest():
                W = linops.SymmetricKronecker(A=A, B=B)
                V = linops.SymmetricKronecker(A=B, B=A)

                self.assertAllClose(W.todense(), V.todense())
Beispiel #4
0
    def setUp(self):
        """Resources for tests."""
        # Seed
        np.random.seed(seed=42)

        # Parameters
        m = 7
        n = 3
        self.constants = [-1, -2.4, 0, 200, np.pi]
        sparsemat = scipy.sparse.rand(m=m, n=n, density=0.1, random_state=1)
        self.normal_params = [
            # Univariate
            (-1.0, 3.0),
            (1, 3),
            # Multivariate
            (np.random.uniform(size=10), np.eye(10)),
            (np.random.uniform(size=10), random_spd_matrix(10)),
            # Matrixvariate
            (
                np.random.uniform(size=(2, 2)),
                linops.SymmetricKronecker(
                    A=np.array([[1.0, 2.0], [2.0, 1.0]]),
                    B=np.array([[5.0, -1.0], [-1.0, 10.0]]),
                ).todense(),
            ),
            # Operatorvariate
            (
                np.array([1.0, -5.0]),
                linops.Matrix(A=np.array([[2.0, 1.0], [1.0, -0.1]])),
            ),
            (
                linops.Matrix(A=np.array([[0.0, -5.0]])),
                linops.Identity(shape=(2, 2)),
            ),
            (
                np.array([[1.0, 2.0], [-3.0, -0.4], [4.0, 1.0]]),
                linops.Kronecker(A=np.eye(3), B=5 * np.eye(2)),
            ),
            (
                linops.Matrix(A=sparsemat.todense()),
                linops.Kronecker(0.1 * linops.Identity(m), linops.Identity(n)),
            ),
            (
                linops.Matrix(A=np.random.uniform(size=(2, 2))),
                linops.SymmetricKronecker(
                    A=np.array([[1.0, 2.0], [2.0, 1.0]]),
                    B=np.array([[5.0, -1.0], [-1.0, 10.0]]),
                ),
            ),
            # Symmetric Kronecker Identical Factors
            (
                linops.Identity(shape=25),
                linops.SymmetricKronecker(A=linops.Identity(25)),
            ),
        ]
Beispiel #5
0
def get_randvar(rv_name):
    """Return a random variable for a given distribution name."""
    # Distribution Means and Covariances

    mean_0d = np.random.rand()
    mean_1d = np.random.rand(5)
    mean_2d_mat = SPD_MATRIX_5x5
    mean_2d_linop = linops.MatrixMult(SPD_MATRIX_5x5)
    cov_0d = np.random.rand() + 10**-12
    cov_1d = SPD_MATRIX_5x5
    cov_2d_kron = linops.Kronecker(A=SPD_MATRIX_5x5, B=SPD_MATRIX_5x5)
    cov_2d_symkron = linops.SymmetricKronecker(A=SPD_MATRIX_5x5)

    if rv_name == "univar_normal":
        randvar = rvs.Normal(mean=mean_0d, cov=cov_0d)
    elif rv_name == "multivar_normal":
        randvar = rvs.Normal(mean=mean_1d, cov=cov_1d)
    elif rv_name == "matrixvar_normal":
        randvar = rvs.Normal(mean=mean_2d_mat, cov=cov_2d_kron)
    elif rv_name == "symmatrixvar_normal":
        randvar = rvs.Normal(mean=mean_2d_mat, cov=cov_2d_symkron)
    elif rv_name == "operatorvar_normal":
        randvar = rvs.Normal(mean=mean_2d_linop, cov=cov_2d_symkron)
    else:
        raise ValueError("Random variable not found.")

    return randvar
Beispiel #6
0
    def setUp(self):
        """Resources for tests."""
        # Random Seed
        np.random.seed(42)

        # Scalars, arrays and operators
        self.scalars = [0, int(1), 0.1, -4.2, np.nan, np.inf]
        self.arrays = [np.random.normal(size=[5, 4]), np.array([[3, 4], [1, 5]])]

        def mv(v):
            return np.array([2 * v[0], v[0] + 3 * v[1]])

        self.mv = mv
        self.ops = [
            linops.MatrixMult(np.array([[-1.5, 3], [0, -230]])),
            linops.LinearOperator(shape=(2, 2), matvec=mv),
            linops.Identity(shape=4),
            linops.Kronecker(
                A=linops.MatrixMult(np.array([[2, -3.5], [12, 6.5]])),
                B=linops.Identity(shape=3),
            ),
            linops.SymmetricKronecker(
                A=linops.MatrixMult(np.array([[1, -2], [-2.2, 5]])),
                B=linops.MatrixMult(np.array([[1, -3], [0, -0.5]])),
            ),
        ]
    def _symmetric_matrix_based_update(self, matrix: randvars.Normal,
                                       action: np.ndarray,
                                       observ: np.ndarray) -> randvars.Normal:
        """Symmetric matrix-based inference update for linear information."""
        if not isinstance(matrix.cov, linops.SymmetricKronecker):
            raise ValueError(
                f"Covariance must have symmetric Kronecker structure, but is '{type(matrix.cov).__name__}'."
            )

        pred = matrix.mean @ action
        resid = observ - pred
        covfactor_Ms = matrix.cov.A @ action
        gram = action.T @ covfactor_Ms
        gram_pinv = 1.0 / gram if gram > 0.0 else 0.0
        gain = covfactor_Ms * gram_pinv
        covfactor_update = linops.aslinop(gain[:, None]) @ linops.aslinop(
            covfactor_Ms[None, :])
        resid_gain = linops.aslinop(resid[:, None]) @ linops.aslinop(
            gain[None, :])

        return randvars.Normal(
            mean=matrix.mean + resid_gain + resid_gain.T -
            linops.aslinop(gain[:, None]) @ linops.aslinop(
                (action.T @ resid_gain)[None, :]),
            cov=linops.SymmetricKronecker(A=matrix.cov.A - covfactor_update),
        )
Beispiel #8
0
    def test_matrixprior(self):
        """Solve random linear system with a matrix-based linear solver."""
        np.random.seed(1)
        # Linear system
        n = 10
        A = np.random.rand(n, n)
        A = A.dot(
            A.T) + n * np.eye(n)  # Symmetrize and make diagonally dominant
        x_true = np.random.normal(size=(n, ))
        b = A @ x_true

        # Prior distributions on A
        covA = linops.SymmetricKronecker(A=np.eye(n))
        Ainv0 = rvs.Normal(mean=np.eye(n), cov=covA)

        for matblinsolve in self.matblinsolvers:
            with self.subTest():
                x, Ahat, Ainvhat, info = matblinsolve(A=A, Ainv0=Ainv0, b=b)

                self.assertAllClose(
                    x.mean,
                    x_true,
                    rtol=1e-6,
                    atol=1e-6,
                    msg=
                    "Solution for matrixvariate prior does not match true solution.",
                )
    def setUp(self) -> None:
        """Scalars, arrays, linear operators and random variables for tests."""
        # Seed
        np.random.seed(42)

        # Random variable instantiation
        self.scalars = [0, int(1), 0.1, np.nan, np.inf]
        self.arrays = [np.empty(2), np.zeros(4), np.array([]), np.array([1, 2])]

        # Random variable arithmetic
        self.arrays2d = [
            np.empty(2),
            np.zeros(2),
            np.array([np.inf, 1]),
            np.array([1, -2.5]),
        ]
        self.matrices2d = [np.array([[1, 2], [3, 2]]), np.array([[0, 0], [1.0, -4.3]])]
        self.linops2d = [linops.Matrix(A=np.array([[1, 2], [4, 5]]))]
        self.randvars2d = [
            randvars.Normal(mean=np.array([1, 2]), cov=np.array([[2, 0], [0, 5]]))
        ]
        self.randvars2x2 = [
            randvars.Normal(
                mean=np.array([[-2, 0.3], [0, 1]]),
                cov=linops.SymmetricKronecker(A=np.eye(2), B=np.ones((2, 2))),
            ),
        ]

        self.scipyrvs = [
            scipy.stats.bernoulli(0.75),
            scipy.stats.norm(4, 2.4),
            scipy.stats.multivariate_normal(np.random.randn(10), np.eye(10)),
            scipy.stats.gamma(0.74),
            scipy.stats.dirichlet(alpha=np.array([0.1, 0.1, 0.2, 0.3])),
        ]
Beispiel #10
0
def symmetric_matrixvariate_normal(
    shape: ShapeArgType, precompute_cov_cholesky: bool, rng: np.random.Generator
) -> randvars.Normal:
    rv = randvars.Normal(
        mean=random_spd_matrix(dim=shape[0], rng=rng),
        cov=linops.SymmetricKronecker(A=random_spd_matrix(dim=shape[0], rng=rng)),
    )
    if precompute_cov_cholesky:
        rv.precompute_cov_cholesky()
    return rv
Beispiel #11
0
def case_state_symmetric_matrix_based(rng: np.random.Generator, ):
    """State of a symmetric matrix-based linear solver."""
    prior = linalg.solvers.beliefs.LinearSystemBelief(
        A=randvars.Normal(
            mean=linops.Matrix(linsys.A),
            cov=linops.SymmetricKronecker(A=linops.Identity(n)),
        ),
        x=(Ainv @ b[:, None]).reshape((n, )),
        Ainv=randvars.Normal(
            mean=linops.Identity(n),
            cov=linops.SymmetricKronecker(A=linops.Identity(n)),
        ),
        b=b,
    )
    state = linalg.solvers.LinearSolverState(problem=linsys, prior=prior)
    state.action = rng.standard_normal(size=state.problem.A.shape[1])
    state.observation = rng.standard_normal(size=state.problem.A.shape[1])

    return state
Beispiel #12
0
 def test_symmkronecker_todense_symmetric(self):
     """Dense matrix from symmetric Kronecker product of two symmetric matrices must be symmetric."""
     C = np.array([[5, 1], [1, 10]])
     D = np.array([[-2, 0.1], [0.1, 8]])
     Ws = linops.SymmetricKronecker(A=C, B=C)
     Ws_dense = Ws.todense()
     self.assertArrayEqual(
         Ws_dense,
         Ws_dense.T,
         msg=
         "Symmetric Kronecker product of symmetric matrices is not symmetric.",
     )
Beispiel #13
0
    def _symmetric_kronecker_identical_factors_cov_cholesky(
        self,
        damping_factor: Optional[FloatArgType] = COV_CHOLESKY_DAMPING,
    ) -> linops.SymmetricKronecker:
        assert (isinstance(self._cov, linops.SymmetricKronecker)
                and self._cov.identical_factors)

        A = self._cov.A.todense()

        return linops.SymmetricKronecker(A=scipy.linalg.cholesky(
            A + damping_factor * np.eye(A.shape[0], dtype=self.dtype),
            lower=True,
        ), )
Beispiel #14
0
    def _symmetric_kronecker_identical_factors_cov_cholesky(
        self,
    ) -> linops.SymmetricKronecker:
        assert isinstance(self._cov, linops.SymmetricKronecker) and self._cov._ABequal

        A = self._cov.A.todense()

        return linops.SymmetricKronecker(
            A=scipy.linalg.cholesky(
                A + COV_CHOLESKY_DAMPING * np.eye(A.shape[0], dtype=self.dtype),
                lower=True,
            ),
            dtype=self.dtype,
        )
def test_induced_solution_belief(rng: np.random.Generator):
    """Test whether a consistent belief over the solution is inferred from a belief over
    the inverse."""
    n = 5
    A = randvars.Constant(random_spd_matrix(dim=n, rng=rng))
    Ainv = randvars.Normal(
        mean=linops.Scaling(factors=1 / np.diag(A.mean)),
        cov=linops.SymmetricKronecker(linops.Identity(n)),
    )
    b = randvars.Constant(rng.normal(size=(n, 1)))
    prior = LinearSystemBelief(A=A, Ainv=Ainv, x=None, b=b)

    x_infer = Ainv @ b
    np.testing.assert_allclose(prior.x.mean, x_infer.mean)
    np.testing.assert_allclose(prior.x.cov.todense(), x_infer.cov.todense())
Beispiel #16
0
    def dense_matrix_based_update(matrix: randvars.Normal, action: np.ndarray,
                                  observ: np.ndarray):
        pred = matrix.mean @ action
        resid = observ - pred
        covfactor_Ms = matrix.cov.A @ action
        gram = action.T @ covfactor_Ms
        gram_pinv = 1.0 / gram if gram > 0.0 else 0.0
        gain = covfactor_Ms * gram_pinv
        covfactor_update = gain @ covfactor_Ms.T
        resid_gain = np.outer(resid, gain)

        return randvars.Normal(
            mean=matrix.mean + resid_gain + resid_gain.T -
            np.outer(gain, action.T @ resid_gain),
            cov=linops.SymmetricKronecker(A=matrix.cov.A - covfactor_update),
        )
Beispiel #17
0
    def test_symmetric_samples(self):
        """Samples from a normal distribution with symmetric Kronecker kernels of two
        symmetric matrices are symmetric."""

        n = 3
        A = self.rng.uniform(size=(n, n))
        A = 0.5 * (A + A.T) + n * np.eye(n)
        rv = randvars.Normal(
            mean=np.eye(A.shape[0]),
            cov=linops.SymmetricKronecker(A=A),
        )
        rv = rv.sample(rng=self.rng, size=10)
        for i, B in enumerate(rv):
            self.assertAllClose(
                B,
                B.T,
                atol=1e-5,
                rtol=1e-5,
                msg=
                "Sample {} from symmetric Kronecker distribution is not symmetric."
                .format(i),
            )
Beispiel #18
0
import numpy as np
from pytest_cases import case

from probnum import linalg, linops, randvars
from probnum.problems.zoo.linalg import random_linear_system, random_spd_matrix

# Problem
n = 10
linsys = random_linear_system(
    rng=np.random.default_rng(42), matrix=random_spd_matrix, dim=n
)

# Prior
Ainv = randvars.Normal(
    mean=linops.Identity(n), cov=linops.SymmetricKronecker(linops.Identity(n))
)
b = randvars.Constant(linsys.b)
prior = linalg.solvers.beliefs.LinearSystemBelief(
    A=randvars.Constant(linsys.A),
    Ainv=Ainv,
    x=(Ainv @ b[:, None]).reshape(
        (n,)
    ),  # TODO: This can be replaced by Ainv @ b once https://github.com/probabilistic-numerics/probnum/issues/456 is fixed
    b=b,
)


@case(tags=["initial"])
def case_initial_state(
    rng: np.random.Generator,
Beispiel #19
0
    def _get_output_randvars(self, Y_list, sy_list, phi=None, psi=None):
        """Return output random variables x, A, Ainv from their means and
        covariances."""

        if self.iter_ > 0:
            # Observations and inner products in A-space between actions
            Y = np.hstack(Y_list)
            sy = np.vstack(sy_list).ravel()

            # Posterior covariance factors
            if self.is_calib_covclass and (not phi is None) and (
                    not psi is None):
                # Ensure prior covariance class only acts in span(S) like A
                @linops.LinearOperator.broadcast_matvec
                def _matmul(x):
                    # First term of calibration covariance class: AS(S'AS)^{-1}S'A
                    return (Y * sy**-1) @ (Y.T @ x.ravel())

                _A_covfactor0 = linops.LinearOperator(
                    shape=(self.n, self.n),
                    dtype=np.result_type(Y, sy),
                    matmul=_matmul,
                )

                @linops.LinearOperator.broadcast_matvec
                def _matmul(x):
                    # Term in covariance class: A_0^{-1}Y(Y'A_0^{-1}Y)^{-1}Y'A_0^{-1}
                    # TODO: for efficiency ensure that we dont have to compute
                    # (Y.T Y)^{-1} two times! For a scalar mean this is the same as in
                    # the null space projection
                    YAinv0Y_inv_YAinv0x = np.linalg.solve(
                        Y.T @ (self.Ainv_mean0 @ Y),
                        Y.T @ (self.Ainv_mean0 @ x))
                    return self.Ainv_mean0 @ (Y @ YAinv0Y_inv_YAinv0x)

                _Ainv_covfactor0 = linops.LinearOperator(
                    shape=(self.n, self.n),
                    dtype=np.result_type(Y, self.Ainv_mean0),
                    matmul=_matmul,
                )

                # Set degrees of freedom based on uncertainty calibration in unexplored
                # space
                (
                    calibration_term_A,
                    calibration_term_Ainv,
                ) = self._get_calibration_covariance_update_terms(phi=phi,
                                                                  psi=psi)

                _A_covfactor = (_A_covfactor0 - self._A_covfactor_update_term +
                                calibration_term_A)
                _Ainv_covfactor = (_Ainv_covfactor0 -
                                   self._Ainv_covfactor_update_term +
                                   calibration_term_Ainv)
            else:
                # No calibration
                _A_covfactor = self.A_covfactor
                _Ainv_covfactor = self.Ainv_covfactor
        else:
            # Converged before making any observations
            _A_covfactor = self.A_covfactor0
            _Ainv_covfactor = self.Ainv_covfactor0

        # Create output random variables
        A = randvars.Normal(mean=self.A_mean,
                            cov=linops.SymmetricKronecker(A=_A_covfactor))

        Ainv = randvars.Normal(
            mean=self.Ainv_mean,
            cov=linops.SymmetricKronecker(A=_Ainv_covfactor),
        )
        # Induced distribution on x via Ainv
        # Exp(x) = Ainv b, Cov(x) = 1/2 (W b'Wb + Wbb'W)
        Wb = _Ainv_covfactor @ self.b
        bWb = np.squeeze(Wb.T @ self.b)

        def _matmul(x):
            return 0.5 * (bWb * _Ainv_covfactor @ x + Wb @ (Wb.T @ x))

        cov_op = linops.LinearOperator(
            shape=(self.n, self.n),
            dtype=np.result_type(Wb.dtype, bWb.dtype),
            matmul=_matmul,
        )

        x = randvars.Normal(mean=self.x_mean.ravel(), cov=cov_op)

        # Compute trace of solution covariance: tr(Cov(x))
        self.trace_sol_cov = np.real_if_close(
            self._compute_trace_solution_covariance(bWb=bWb, Wb=Wb)).item()

        return x, A, Ainv