11import logging
22
33import numpy as np
4+ import scipy as sp
45from numpy .typing import NDArray
56from scipy .sparse import sparray
7+ from sksparse .cholmod import cholesky
68
79log = logging .getLogger (__name__ )
810log .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