44from scipy .linalg import toeplitz
55
66
7- def fast_gaussian (dimension , sdev , corr ):
8-
9- # print('Fast Gaussian!')
7+ def fast_gaussian (dimension , sdev , corr , num_samples = 1 ):
108 """
9+
1110 Generates random vector from distribution satisfying Gaussian variogram in dimension up to 3-d.
1211
1312 Parameters
@@ -23,10 +22,14 @@ def fast_gaussian(dimension, sdev, corr):
2322 If a single float is provided, it represents the correlation length in all directions.
2423 If an array-like object with length 3 is provided, it represents the correlation length in the x-, y-, and z-directions.
2524
25+ num_samples : int, optional
26+ Number of samples to generate. Default is 1.
27+ If greater than 1, the function will return an array of shape (dimension, num_samples).
28+
2629 Returns
2730 -------
2831 x : array-like
29- The generated random vector .
32+ The generated random vectors of shape (dimension, num_samples) .
3033
3134 Notes
3235 -----
@@ -39,15 +42,15 @@ def fast_gaussian(dimension, sdev, corr):
3942 Want to generate a field on a 3-d grid with dimension m x n x p, with correlation length a along first coordinate
4043 axis, b along second coordinate axis, c alone third coordinate axis, and standard deviation sigma:
4144
42- >>> x=fast_gaussian(np.array([m, n, p]),np.array([sigma]),np.array([a b c]))
45+ x=fast_gaussian(np.array([m, n, p]),np.array([sigma]),np.array([a b c]))
4346
4447 If the dimension is n x 1 one can write
4548
46- >>> x=fast_gaussian(np.array([n]),np.array([sigma]),np.array([a]))
49+ x=fast_gaussian(np.array([n]),np.array([sigma]),np.array([a]))
4750
4851 If the correlation length is the same in all directions:
4952
50- >>> x=fast_gaussian(np.array([m n p]),np.array([sigma]),np.array([a]))
53+ x=fast_gaussian(np.array([m n p]),np.array([sigma]),np.array([a]))
5154
5255 The properties on the Kronecker product behind this algorithm can be found in
5356 Horn & Johnson: Topics in Matrix Analysis, Cambridge UP, 1991.
@@ -58,9 +61,9 @@ def fast_gaussian(dimension, sdev, corr):
5861 Also note that reshape with order='F' is used to keep the code identical to the Matlab code.
5962
6063 The method was invented and implemented in Matlab by Geir Nævdal in 2011.
64+ Memory-efficient implementation for batch generation of samples was added in 2025.
6165 """
6266
63- # Initialize dimension:
6467 if len (dimension ) == 0 :
6568 sys .exit ("fast_gaussian: Wrong input, dimension should have length at least 1" )
6669 m = dimension [0 ]
@@ -75,13 +78,11 @@ def fast_gaussian(dimension, sdev, corr):
7578 if len (dimension ) > 3 :
7679 sys .exit ("fast_gaussian: Wrong input, dimension should have length at most 3" )
7780
78- # Compute standard deviation
79- if len (sdev ) > 1 : # check input
81+ if len (sdev ) > 1 :
8082 std = 1
8183 else :
82- std = sdev # the variance will come out through the kronecker product.
84+ std = sdev
8385
84- # Initialize correlation length
8586 if len (corr ) == 0 :
8687 sys .exit ("fast_gaussian: Wrong input, corr should have length at least 1" )
8788 if len (corr ) == 1 :
@@ -90,60 +91,52 @@ def fast_gaussian(dimension, sdev, corr):
9091 corr = np .append (corr , corr [1 ])
9192 corr = np .maximum (corr , 1 )
9293
93- # first generate the covariance matrix for first dimension
94- dist1 = np .arange (m )
95- dist1 = dist1 / corr [0 ]
94+ dist1 = np .arange (m ) / corr [0 ]
9695 t1 = toeplitz (dist1 )
97- # to avoid problem with Cholesky factorization when the matrix is close to singular we add a small number on the
98- # diagonal entries
9996 t1 = std * np .exp (- t1 ** 2 ) + 1e-10 * np .eye (m )
100- # Cholesky decomposition
10197 cholt1 = np .linalg .cholesky (t1 )
10298
103- # generate the covariance matrix for the second dimension
104- # to save time - use a copy if possible
10599 if corr [0 ] == corr [1 ] and n == m :
106100 cholt2 = cholt1
107101 else :
108- dist2 = np .arange (n )
109- dist2 = dist2 / corr [1 ]
102+ dist2 = np .arange (n ) / corr [1 ]
110103 t2 = toeplitz (dist2 )
111104 t2 = std * np .exp (- t2 ** 2 ) + 1e-10 * np .eye (n )
112105 cholt2 = np .linalg .cholesky (t2 )
113106
114- # generate the covariance matrix for the third dimension if required
115107 cholt3 = None
116108 if p is not None :
117- # use std = 1 to get the correct value
118- dist3 = np .arange (p )
119- dist3 = dist3 / corr [2 ]
109+ dist3 = np .arange (p ) / corr [2 ]
120110 t3 = toeplitz (dist3 )
121111 t3 = np .exp (- t3 ** 2 ) + 1e-10 * np .eye (p )
122112 cholt3 = np .linalg .cholesky (t3 )
123113
124- # draw a random variable
125- x = np .random .randn (dim , 1 )
114+ z = np .random .randn (dim , num_samples )
126115
127- # adjust to get the correct covariance matrix, applying Lemma 4.3.1. in Horn & Johnson:
128- # we need to adjust to get the correct covariance matrix
129- if p is None : # 2-d
130- x = np .dot (np .dot (cholt1 , np .reshape (x , (m , n ))), cholt2 .T )
131- else : # 3-d
132- # either dimension 1 and 2 or 2 and 3 need to be grouped together.
116+ # Memory-efficient multiplication without explicit large Kronecker product
117+ if p is None :
118+ x = z .reshape (m , n , num_samples , order = 'F' )
119+ x = np .tensordot (cholt1 , x , axes = ([1 ], [0 ]))
120+ x = np .tensordot (cholt2 , x , axes = ([1 ], [1 ]))
121+ else :
122+ x = z .reshape (m , n , p , num_samples , order = 'F' )
133123 if n <= p :
134- x = np .dot (np .dot (np .kron (cholt2 , cholt1 ),
135- np .reshape (x , (m * n , p ))), cholt3 .T )
124+ x = np .tensordot (cholt1 , x , axes = ([1 ], [0 ]))
125+ x = np .tensordot (cholt2 , x , axes = ([1 ], [1 ]))
126+ x = np .tensordot (cholt3 , x , axes = ([1 ], [2 ]))
136127 else :
137- x = np .dot (np .dot (cholt1 , np .reshape (x , (m , n * p ))),
138- np .kron (cholt3 .T , cholt2 .T ))
128+ x = np .tensordot (cholt1 , x , axes = ([1 ], [0 ]))
129+ x = np .tensordot (cholt2 , x , axes = ([1 ], [1 ]))
130+ x = np .tensordot (cholt3 , x , axes = ([1 ], [2 ]))
139131
140- # reshape back
141- x = np .reshape (x , (dim ,), order = 'F ' )
132+ # Reshape back to (dim, num_samples) (order='C' is used here to match the original function's output)
133+ x = x .reshape ((dim , num_samples ), order = 'C ' )
142134
143135 if len (sdev ) > 1 :
144- if len (sdev ) == len ( x ) :
145- x = sdev * x
136+ if len (sdev ) == dim :
137+ x = sdev [:, None ] * x
146138 else :
147139 sys .exit ('fast_gaussian: Inconsistent dimension of sdev' )
148140
149141 return x
142+
0 commit comments