Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test_utils to make it easier for downstream implementations to check correctness #1825

Open
wants to merge 19 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 2 additions & 3 deletions RELEASE.md
Expand Up @@ -50,8 +50,7 @@ This release contains contributions from:

## Major Features and Improvements

* <INSERT MAJOR FEATURE HERE, USING MARKDOWN SYNTAX>
* <IF RELEASE CONTAINS MULTIPLE FEATURES FROM SAME AREA, GROUP THEM TOGETHER>
* Testing: new `test_utils` submodule that provides a `test_kernel` function that does basic correctness checks of custom `Kernel` implementations
st-- marked this conversation as resolved.
Show resolved Hide resolved

## Bug Fixes and Other Changes

Expand All @@ -62,7 +61,7 @@ This release contains contributions from:

This release contains contributions from:

ltiao, jesnie
ltiao, jesnie, st--


# Release 2.4.0
Expand Down
2 changes: 2 additions & 0 deletions gpflow/__init__.py
Expand Up @@ -32,6 +32,7 @@
posteriors,
probability_distributions,
quadrature,
test_utils,
utilities,
)
from .base import Module, Parameter
Expand Down Expand Up @@ -66,6 +67,7 @@
"probability_distributions",
"quadrature",
"set_trainable",
"test_utils",
"utilities",
"versions",
]
51 changes: 51 additions & 0 deletions gpflow/test_utils.py
@@ -0,0 +1,51 @@
import numpy as np

from .base import AnyNDArray
from .kernels import Kernel


def assert_psd_matrix(A: AnyNDArray, tol: float = 1e-12) -> None:
"""
Assert whether `A` is a positive semi-definite matrix, based on the
smallest eigenvalue being non-negative to tolerance `tol`.

:param A: the matrix to check for positive-semi-definiteness
:param tol: numeric tolerance to accept if the smallest eigenvalue is very
slightly negative.
"""
assert np.linalg.eigvals(A).min() > -tol, "test for positive semi definite matrix"


def test_kernel(kernel: Kernel, X: AnyNDArray, X2: AnyNDArray) -> None:
st-- marked this conversation as resolved.
Show resolved Hide resolved
"""
Test that the `kernel` satisfies basic requirements for a valid `Kernel`
implementation, such as consistency between the implementations for
`full_cov=True` and `full_cov=False`, and whether the returned covariance
matrix is positive semi-definite.

:param kernel: the kernel to test
:param X: a valid input matrix with the right dimensionality for this kernel
:param X2: a different valid input matrix; should be of different length to `X`
"""
N, D = X.shape
N2, D2 = X2.shape
assert D == D2
assert N != N2

kX = kernel(X).numpy()
assert kX.shape == (N, N)
kXX = kernel(X, X).numpy()
assert kXX.shape == (N, N)
np.testing.assert_allclose(kX, kXX)
np.testing.assert_allclose(kX, kX.T)
assert_psd_matrix(kX)

kXX2 = kernel(X, X2).numpy()
assert kXX2.shape == (N, N2)

kX2X = kernel(X2, X).numpy()
np.testing.assert_allclose(kXX2, kX2X.T)

kXdiag = kernel(X, full_cov=False).numpy()
assert kXdiag.shape == (N,)
np.testing.assert_allclose(np.diag(kX), kXdiag)
32 changes: 19 additions & 13 deletions tests/gpflow/kernels/test_positive_semidefinite.py
Expand Up @@ -20,7 +20,7 @@
from numpy.testing import assert_array_less

import gpflow.ci_utils
from gpflow import kernels
from gpflow import kernels, test_utils

KERNEL_CLASSES = [
kernel
Expand All @@ -32,23 +32,25 @@
rng = np.random.RandomState(42)


def pos_semidefinite(kernel: kernels.Kernel) -> None:
N, D = 100, 5
X = rng.randn(N, D)

cov = kernel(X)
eig = tf.linalg.eigvalsh(cov).numpy()
assert_array_less(-1e-12, eig)


@pytest.mark.parametrize("kernel_class", KERNEL_CLASSES)
def test_positive_semidefinite(kernel_class: Type[kernels.Kernel]) -> None:
def test_kernel_interface(kernel_class: Type[kernels.Kernel]) -> None:
"""
A valid kernel is positive semidefinite. Some kernels are only valid for
particular input shapes, see https://github.com/GPflow/GPflow/issues/1328
"""
N, N2, D = 101, 103, 5
X = rng.randn(N, D)
X2 = rng.randn(N2, D)
kernel = kernel_class()
pos_semidefinite(kernel)

if isinstance(kernel, kernels.White):
# The White kernel is special in that it is based on indices, not
# values, and hence White()(X, X2) is zero everywhere. This means we
# need to explicitly check psd-ness of just kernel(X) itself.
K = kernel(X)
test_utils.assert_psd_matrix(K)
else:
test_utils.test_kernel(kernel, X, X2)
st-- marked this conversation as resolved.
Show resolved Hide resolved


@pytest.mark.parametrize(
Expand All @@ -60,4 +62,8 @@ def test_positive_semidefinite_periodic(base_class: Type[kernels.IsotropicStatio
particular input shapes, see https://github.com/GPflow/GPflow/issues/1328
"""
kernel = kernels.Periodic(base_class())
pos_semidefinite(kernel)

N, N2, D = 101, 103, 5
X = rng.randn(N, D)
X2 = rng.randn(N2, D)
test_utils.test_kernel(kernel, X, X2)
st-- marked this conversation as resolved.
Show resolved Hide resolved