Skip to content

Commit a096ee1

Browse files
author
Rolf Johan Lorentzen
committed
Added batch generation of multiple samples
1 parent 3f6c8e3 commit a096ee1

File tree

1 file changed

+35
-42
lines changed

1 file changed

+35
-42
lines changed

src/geostat/gaussian_sim.py

Lines changed: 35 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
from 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

Comments
 (0)