Skip to content

Commit 8dcb3da

Browse files
authored
Remove overwrite bug in SVD (#233)
* Wrote a test * Remove overwriting array in SVD --------- Co-authored-by: tommyod <tommy.odland>
1 parent bc49a89 commit 8dcb3da

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

src/iterative_ensemble_smoother/esmda_inversion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ def inversion_subspace(
456456
# Eqn (68)
457457
# TODO: Approximately 50% of the time in the function is spent here
458458
# consider using randomized svd for further speed gains
459-
U, w, _ = sp.linalg.svd(D_delta, overwrite_a=True, full_matrices=False)
459+
U, w, _ = sp.linalg.svd(D_delta, full_matrices=False)
460460

461461
# Clip the singular value decomposition
462462
idx = singular_values_to_keep(w, truncation=truncation)

tests/test_esmda.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,69 @@ def g(X):
563563
assert np.allclose(X_posterior_highlevel_API, X_posterior_lowlevel_API)
564564

565565

566+
@pytest.mark.parametrize("inversion", ESMDA._inversion_methods.keys())
567+
def test_row_by_row_assimilation_order(inversion):
568+
"""A regression test for issue #232.
569+
570+
The problem was that in a SVD call we had
571+
572+
sp.linalg.svd(a, overwrite_a=True, ...)
573+
574+
but the array a was used later on in the function. This bug was not spotted
575+
because when the input array is contiguous (C-order), then no actual overwrite
576+
is done by the SVD. However, in F-order it does overwrite the input array,
577+
which does lead to the wrong values being used later on in the function.
578+
579+
The fix is to carefully review all usage of 'overwrite_a' and only keep
580+
overwriting if the input array is not used later on, and is not part
581+
of the input arguments to a function.
582+
"""
583+
rng = np.random.default_rng(42)
584+
585+
num_outputs = 4
586+
num_inputs = 5
587+
num_ensemble = 3
588+
589+
A = rng.normal(size=(num_outputs, num_inputs))
590+
591+
def g(X):
592+
return A @ X
593+
594+
X_prior = rng.normal(size=(num_inputs, num_ensemble))
595+
covariance = np.exp(rng.normal(size=num_outputs))
596+
observations = A @ np.linspace(0, 1, num=num_inputs) + rng.normal(
597+
size=num_outputs, scale=0.01
598+
)
599+
600+
# We need two instances because each call to 'compute_transition_matrix'
601+
# increments the random number generator.
602+
smoother1 = ESMDA(
603+
covariance=covariance,
604+
observations=observations,
605+
alpha=2,
606+
inversion=inversion,
607+
seed=1,
608+
)
609+
610+
smoother2 = ESMDA(
611+
covariance=covariance,
612+
observations=observations,
613+
alpha=2,
614+
inversion=inversion,
615+
seed=1,
616+
)
617+
618+
X = np.copy(X_prior)
619+
alpha_i = 2.5
620+
621+
# Test that no matter which order the input is in, the output is equal
622+
K1 = smoother1.compute_transition_matrix(
623+
Y=np.ascontiguousarray(g(X)), alpha=alpha_i
624+
)
625+
K2 = smoother2.compute_transition_matrix(Y=np.asfortranarray(g(X)), alpha=alpha_i)
626+
assert np.allclose(K1, K2)
627+
628+
566629
if __name__ == "__main__":
567630
import pytest
568631

0 commit comments

Comments
 (0)