Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4f954e8
gp: Gaussian processes via jax
asher-m Jul 29, 2026
edf3913
gp: MWE for gps via JAX
asher-m Jul 30, 2026
9f0d554
gp: adaptive sampling coordinate selection
asher-m Aug 17, 2026
7120116
gp: efficient Kronecker-structure kernel solves
asher-m Aug 19, 2026
fc908c3
gp: cast ranges to array in posterior variance optimization
asher-m Aug 20, 2026
adabd1f
gp.kernel: revert @property decorator in CovarianceFactor
asher-m Aug 20, 2026
82ffc52
gp: add 1-d demo notebook
asher-m Aug 20, 2026
6a1a211
gp: clean up public namespace and method names
asher-m Aug 29, 2026
b2bfb19
gp.kernel: make CovarianceFactor(s) private classes inside kernel/gp …
asher-m Aug 29, 2026
374dd85
gp: missing factor of 1/2 on quadractic term in log marginal likelihood
asher-m Aug 29, 2026
58aae91
gp: add/correct docstrings across gp submodule
asher-m Aug 30, 2026
9b297fe
gp: mark out jax inexact problematic dtype statements
asher-m Aug 31, 2026
0e5396d
gp: break out 1-d visualization routines from demo
asher-m Sep 3, 2026
f150cb6
gp: add docs and support scripts
asher-m Sep 3, 2026
dc61b8d
gp: commentary in 1-d demo notebook
asher-m Sep 3, 2026
cd64b8e
docs: add clean target to Makefile
asher-m Sep 10, 2026
bb2a009
gp: get ready to add 1-d example notebook to docs
asher-m Sep 10, 2026
4cd1766
docs: recursively delete all occurences of 'generated' folder(s)
asher-m Sep 10, 2026
9b1056d
gp: cleanup some variable names in example notebook
asher-m Sep 10, 2026
b1c4d2f
docs: just copy stuff in the Makefile
asher-m Oct 1, 2026
ef4261e
project: bump python support to 3.12-3.14 (inclusive)
asher-m Oct 1, 2026
e8a0654
project: bump python to 3.12 in readthedocs config
asher-m Oct 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v6

- name: Setup Python 3.9
- name: Setup Python 3.12
uses: actions/setup-python@v6
with:
python-version: 3.9
python-version: 3.12
cache: 'pip' # optional, adds caching for faster builds

- name: Install pypa/build
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v6

- name: Setup Python 3.9
- name: Setup Python 3.12
uses: actions/setup-python@v6
with:
python-version: 3.9
python-version: 3.12
cache: 'pip' # optional, adds caching for faster builds

- name: Install UncertainSCI
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
python-version: ["3.9", "3.10", "3.11"]
python-version: ["3.12", "3.13", "3.14"]

steps:
- name: Checkout code
Expand Down Expand Up @@ -43,7 +43,7 @@ jobs:
coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_FLAG_NAME: ${{ matrix.os }}
COVERALLS_FLAG_NAME: ${{ matrix.os }}-${{ matrix.python-version }}
COVERALLS_PARALLEL: true

coveralls:
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ instance/
# Scrapy stuff:
.scrapy

# Sphinx documentation
# Docs:
docs/_build/
docs/**/copied/
docs/**/generated/

# PyBuilder
Expand Down
2 changes: 1 addition & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ version: 2
build:
os: ubuntu-22.04
tools:
python: "3.10"
python: "3.12"
apt_packages:
- pandoc

Expand Down
138 changes: 138 additions & 0 deletions UncertainSCI/_equinox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import abc
import equinox as eqx
import jax
import jax.numpy as jnp
import numpy.typing as npt


class ComputedArray(eqx.Module, metaclass=abc.ABCMeta):
"""
Computed array base class.

Primarily used to detect if PyTree nodes require (further) resolution to reach
underlying objects or data.
"""
is_static: bool = eqx.field(static=True)

@abc.abstractmethod
def __call__(self) -> jax.Array:
raise NotImplementedError

@classmethod
@abc.abstractmethod
def from_array(cls, a: jax.Array, is_static: bool=False, **kwargs):
"""
Convenience method for in-place replacement of types of
:class:`ComputedArray`, though not generally intended for use as a long-lived
instance; prefer direct instantiation when possible.

Args:
a (array):
The array data of this :class:`ComputedArray`.
is_static (bool, optional):
If static.
**kwargs:
Kwargs to hand off to :meth:`__init__` (generally depends
on child class).
"""
raise NotImplementedError


class PositiveDefinite(ComputedArray):
"""
A positive-definite matrix.

Module contains some niceties that guarantee matrix remains positive definite
during training.

Attributes:
L_log_diag (array or None):
Log of diagonal of Cholesky factor of positive definite matrix.
L_off_diag (array or None):
Elements below main diagonal of Cholesky factor of positive definite.
"""
L_log_diag: jax.Array
L_off_diag: jax.Array

def __init__(
self,
*, # TODO: make this NOT kwargs-only.
D: jax.Array | npt.NDArray | None = None,
L_log_diag: jax.Array | npt.NDArray | None = None,
L_off_diag: jax.Array | npt.NDArray | None = None,
is_static: bool = False,
):
"""
Create a positive definite matrix.

All args must be passed as kwargs to avoid name collisions with parent
methods.

Args:
D (array or None):
Positive definite matrix; defined only if ``L_log_diag is None`` and
``L_off_diag is None``.
L_log_diag (array or None):
Log of diagonal of Cholesky factor of positive definite matrix;
defined only if ``D is None``.
L_off_diag (array or None):
Elements below main diagonal of Cholesky factor of positive definite
matrix; defined only if ``D is None``.
is_static (bool):
If the matrix is static.
"""
if not (
(D is not None) ^
(L_log_diag is not None and L_off_diag is not None)
):
raise ValueError('D xor (L_log_diag and L_off_diag) must be defined!')

if D is not None:
if D.ndim != 2:
raise ValueError(f'Expected ndim = 2 for matrix, got D.ndim = {D.ndim}!')
if D.shape[0] != D.shape[1]:
raise ValueError(f'Expected square matrix, got D.shape = {D.shape}!')

n = D.shape[0]
L = jnp.linalg.cholesky(D)
self.L_log_diag = jnp.log(jnp.diag(L))
self.L_off_diag = L[jnp.tril_indices(n, k=-1)]

elif L_log_diag is not None and L_off_diag is not None:
if L_log_diag.ndim != 1:
raise ValueError(
f'Expected ndim = 1 for L_log_diag, got L_log_diag.ndim = {L_log_diag.ndim}!'
)
if L_off_diag.ndim != 1:
raise ValueError(
f'Expected ndim = 1 for L_off_diag, got L_off_diag.ndim = {L_off_diag.ndim}!'
)


n = L_log_diag.shape[0]
if L_off_diag.shape[0] != (n) * (n - 1) / 2:
raise ValueError(
f'Expected L_off_diag.shape[0] = (n) * (n - 1) / 2 = '
f'{(n) * (n - 1) / 2} (numel below main diagonal), got '
f'L_off_diag.shape[0] = {L_off_diag.shape[0]}!'
)

self.L_log_diag = jnp.asarray(L_log_diag)
self.L_off_diag = jnp.asarray(L_off_diag)

else:
raise ValueError('Got unexpected combination of parameters; check caller!')

super().__init__(is_static=is_static)

def __call__(self) -> jax.Array:
n = self.L_log_diag.shape[0]
L = jnp.diag(jnp.exp(self.L_log_diag))
L = L.at[jnp.tril_indices(n, k=-1)].set(self.L_off_diag)
D = L @ L.T

return jax.lax.stop_gradient(D) if self.is_static else D

@classmethod
def from_array(cls, a, is_static: bool = False, **kwargs):
return cls(D=a, is_static=is_static, **kwargs)
80 changes: 80 additions & 0 deletions UncertainSCI/_linalg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import jax
import jax.numpy as jnp


@jax.jit
def d_foerstner(a: jax.Array, b: jax.Array) -> jax.Array:
r"""
Compute the Förstner distance between covariance matrices ``a`` and ``b``.

The Förstner metric is defined as

.. math::

d_\text{Förstner}(A, B) = \sqrt{ \sum_i \log^2 \lambda_i }

for :math:`\lambda_i` eigenvalues of the generalized eigenvalue problem

.. math::

\operatorname{det}\left( A - \lambda B \right) = 0.

See [A Metric on Covariance Matrices](https://doi.org/10.1007/978-3-662-05296-9_31)
for more details.

Args:
a (array):
Matrix ``a``.
b (array):
Matric ``b``.

Returns:
A scalar array (``ndim == 0`` and ``shape == ()``) of the Förstner distance
between ``a`` and ``b`` of complex float.
"""
eig_vals, _ = eigh_generalized(a, b)
return jnp.sqrt(jnp.sum(jnp.log(eig_vals) ** 2))


@jax.jit
def eigh_generalized(
a: jax.Array,
b: jax.Array
) -> tuple[jax.Array, jax.Array]:
r"""
Solves the generalized eigenvalue problem :math:`A v = \lambda B v`
for symmetric/Hermitian matrices where :math:`B` is positive-definite
in a JAX-compatible way.

This exists because :func:`jax.scipy.linalg.eigh` does not support
``b is not None`` (in particular, it supports only the standard eigenvalue
problem).

Args:
a (array):
Symmetric or Hermitian matrix of shape (N, N).
b (array):
Symmetric positive-definite matrix of shape (N, N).

Returns:
eigenvalues (array):
Array of shape (N,) sorted in ascending order.
eigenvectors (array):
Matrix of shape (N, N) of columns of eigenvectors.
"""
l = jnp.linalg.cholesky(b)
x = jax.scipy.linalg.solve_triangular(l, a, lower=True)
c = jax.scipy.linalg.solve_triangular(l, x.conj().T, lower=True)
evals, w = jnp.linalg.eigh(c)
evecs = jax.scipy.linalg.solve_triangular(l.conj().T, w, lower=False)

return evals, evecs


@jax.jit
def solve_cholesky(
l: jax.Array,
y: jax.Array
):
w = jax.scipy.linalg.solve_triangular(l, y, lower=True)
return jax.scipy.linalg.solve_triangular(l.conj().T, w)
Loading
Loading