Skip to content

Commit 4378ef9

Browse files
authored
Sampling arbitrary Precision matrices, not just diagonal (#164)
1 parent b20c7a1 commit 4378ef9

1 file changed

Lines changed: 47 additions & 14 deletions

File tree

graphite_maps/utils.py

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import logging
22

33
import numpy as np
4+
import scipy as sp
45
from numpy.typing import NDArray
56
from scipy.sparse import sparray
7+
from sksparse.cholmod import cholesky
68

79
log = logging.getLogger(__name__)
810
log.addHandler(logging.NullHandler())
@@ -25,28 +27,59 @@ def generate_gaussian_noise(
2527
-------
2628
np.ndarray
2729
The Gaussian noise array of shape (n, m), where Prec has shape (m, m).
30+
31+
Examples
32+
--------
33+
>>> F = sp.sparse.random_array(shape=(6, 6), density=0.3, random_state=42)
34+
>>> Prec = sp.sparse.csc_array(F.T @ F + np.diag([1, 2, 4, 8, 16, 32]))
35+
>>> generate_gaussian_noise(n=3, Prec=Prec, seed=1).round(2)
36+
array([[ 0.35, 0.19, -0.33, -0.19, -0.32, 0.1 ],
37+
[ 0.82, 0.02, -0.07, 0.2 , 0.23, 0.01],
38+
[ 0.33, 0.36, -0.19, 0.11, 0.11, -0.05]])
39+
40+
>>> Prec = sp.sparse.diags_array(Prec.diagonal())
41+
>>> generate_gaussian_noise(n=3, Prec=Prec, seed=2).round(2)
42+
array([[ 0.19, -0.34, -0.17, -0.84, 0.45, 0.2 ],
43+
[-0.33, 0.51, 0.12, -0.19, 0.24, -0.05],
44+
[-0.33, -0.52, 0.19, -0.03, 0.14, -0.1 ]])
2845
"""
2946
if n < 1:
3047
raise ValueError(f"`n` should be g.e. 1, got {n}")
3148

3249
m = Prec.shape[0]
3350
rng = np.random.default_rng(seed)
34-
eps = rng.normal(
35-
loc=0,
36-
scale=np.sqrt(np.linalg.inv(Prec.toarray()).diagonal()),
37-
size=(n, m),
38-
)
3951

40-
# standard_normal_samples = rng.normal(size=(n, m))
41-
# cholesky_factor = cholesky(Prec)
52+
# If precision matrix is diagonal
53+
row_idx, col_idx, _ = sp.sparse.find(Prec)
54+
if np.all(row_idx == col_idx):
55+
# Scale is the standard deviations. diag(Prec) = 1 / variances
56+
scale = np.sqrt(1 / Prec.diagonal())
57+
return rng.normal(
58+
loc=0,
59+
scale=scale,
60+
size=(n, m),
61+
)
62+
63+
# General case: precision matrix is not diagonal
64+
z = rng.normal(size=(m, n))
65+
66+
# The simplest, but naive, way to sample is to compute:
67+
# cov = np.linalg.inv(Prec.todense())
68+
# C = np.linalg.cholesky(cov)
69+
# assert np.allclose(C @ C.T, cov)
70+
# return (np.linalg.cholesky(cov) @ z).T
4271

43-
# Transform the samples using the inverse Cholesky factor
44-
# This transformation results in samples from N(0, Prec^-1)
45-
# eps = cholesky_factor.solve_A(standard_normal_samples.T).T
72+
# Suppose C @ C.T = Cov, then our samples are eps = C @ z.
73+
# If we take cholesky of Prec, we obtain L @ L.T = P @ Prec @ P.T.
74+
# Rearrange to (P.T @ L) @ (P.T @ L).T = Prec, then take inverse
75+
# to see that C = inv((P.T @ L).T). Thus the equation eps = C @ z
76+
# becomes the system (P.T @ L).T @ eps = z, which we solve for eps below
77+
factor = cholesky(Prec)
78+
v = factor.solve_Lt(z, use_LDLt_decomposition=False)
79+
return factor.apply_Pt(v).T
4680

47-
assert eps.shape == (n, m), "Sampling returns wrong size"
4881

49-
log.info("Sampling with seed=%s", seed)
50-
log.info("Sampled Gaussian noise with shape=%s", eps.shape)
82+
if __name__ == "__main__":
83+
import pytest
5184

52-
return eps
85+
pytest.main(args=[__file__, "--doctest-modules", "-v"])

0 commit comments

Comments
 (0)