Basic 2 parameter estimation#

Here is a simple example where amplitude and change factor parameters of n exponential function are estimated:

[1]:
import numpy as np
from pyesmda import ESMDA, ESMDA_RS, ESMDA_DMC, ESMDAInversionType, FixedLocalization
import logging
import covmats

# set logging level
logging.basicConfig(level=logging.INFO)


def exponential(p, x):
    """
    Simple exponential function with an amplitude and change factor.

    Parameters
    ----------
    p : tuple, list
        Parameters vector: amplitude i.e. initial value and change factor.
    x : np.array
        Independent variable (e.g. time).

    Returns
    -------
    np.array
        Result.

    """
    return p[0] * np.exp(x * p[1])


def forward_model(m_ensemble, x):
    """
    Wrap the non-linear observation model (forward model).

    Function calling the non-linear observation model (forward model).
    for all ensemble members and returning the predicted data for
    each ensemble member.

    Parameters
    ----------
    m_ensemble : np.array
        Initial ensemble of N_{e} parameters vector..
    x : np.array
        Independent variable (e.g. time).

    Returns
    -------
    d_pred: np.array
        Predicted data for each ensemble member.
    """
    # Initiate an array of predicted results.
    d_pred = np.zeros([x.shape[0], m_ensemble.shape[1]])
    for j in range(m_ensemble.shape[1]):
        # Calling the forward model for each member of the ensemble
        d_pred[:, j] = exponential(m_ensemble[:, j], x)
    return d_pred
[2]:
# seed for the reproductibility
seed = 0
rng = np.random.default_rng(seed=seed)

a = 10.0
b = -0.0020
# timesteps
x = np.arange(500)
obs = exponential((a, b), x) + rng.normal(0.0, 1.0, 500)
# Initiate an ensemble of (a, b) parameters
n_ensemble = 100  # size of the ensemble
# Uniform law for the parameter a ensemble
ma = rng.uniform(low=-10.0, high=50.0, size=n_ensemble)
# ma = rng.normal(loc=20.0, scale=20.0, size=n_ensemble)
# Uniform law for the parameter b ensemble
mb = rng.uniform(low=-0.001, high=0.01, size=n_ensemble)
# mb = rng.normal(loc=-.002, scale=.005, size=n_ensemble)
# Prior ensemble
m_ensemble = np.stack((ma, mb), axis=0)

# Observation error covariance matrix
cov_obs = covmats.CovViaDiagonal(np.ones(obs.size) * 2.0)
# cov_obs = np.diag([0.5] * obs.shape[0])

# cov_obs = np.ones(obs.size)

# Bounds on parameters (size m * 2)
m_bounds = np.array([[0.0, 50.0], [-1.0, 1.0]])

# Number of assimilations
n_assimilations = 4

# Use a geometric suite (see procedure un evensen 2018) to compte alphas.
# Also explained in Torrado 2021 (see her PhD manuscript.)
cov_obs_inflation_geo = 1.2
cov_obs_inflation_factors: list[float] = [1.1]
for l in range(1, n_assimilations):
    cov_obs_inflation_factors.append(
        cov_obs_inflation_factors[l - 1] / cov_obs_inflation_geo
    )
scaling_factor: float = np.sum(1 / np.array(cov_obs_inflation_factors))
cov_obs_inflation_factors = [
    alpha * scaling_factor for alpha in cov_obs_inflation_factors
]

np.testing.assert_almost_equal(sum(1.0 / np.array(cov_obs_inflation_factors)), 1.0)

# This is just for the test
cov_mm_inflation_factor = 1.2

solver = ESMDA(
    obs,
    m_ensemble,
    cov_obs,
    forward_model,
    forward_model_args=(x,),
    forward_model_kwargs={},
    n_assimilations=n_assimilations,
    cov_obs_inflation_factors=cov_obs_inflation_factors,
    cov_mm_inflation_factor=cov_mm_inflation_factor,
    m_bounds=m_bounds,
    save_ensembles_history=True,
    inversion_type=ESMDAInversionType.EXACT_CHOLESKY,
    seed=seed,
    truncation=0.99,
    logger=logging.getLogger("ESMDA"),
)
# Call the ES-MDA solver
solver.solve()

# Assert that the parameters are found with a 5% accuracy.
assert np.isclose(np.average(solver.m_prior, axis=1), np.array([a, b]), rtol=5e-2).all()

# Get the approximated parameters
a_approx, b_approx = np.average(solver.m_prior, axis=1)

# Get the uncertainty on the parameters
a_std, b_std = np.sqrt(np.diagonal(solver.cov_mm))


solver.logger.info(f"a = {a_approx:.5f} +/- {a_std:.4E}")
solver.logger.info(f"b = {b_approx:.5f} +/- {b_std: 4E}")
INFO:ESMDA:Assimilation # 1
INFO:ESMDA:Assimilation # 2
INFO:ESMDA:Assimilation # 3
INFO:ESMDA:Assimilation # 4
INFO:ESMDA:Forecast for the final ensemble
INFO:ESMDA:a = 10.03181 +/- 1.3964E-01
INFO:ESMDA:b = -0.00202 +/-  7.653077E-05
  • Same example but with ESMDA_RS

[3]:
# This is to defined

# A priori estimated parameters covariance
std_m_prior = np.array([30.0, 0.01])

# Note: no inflation factor provided nor number of assimilations

# This is just for the test
cov_mm_inflation_factor: float = 0.9

solver = ESMDA_RS(
    obs,
    m_ensemble,
    cov_obs,
    forward_model,
    forward_model_args=(x,),
    forward_model_kwargs={},
    cov_mm_inflation_factor=cov_mm_inflation_factor,
    m_bounds=m_bounds,
    C_MD_localization=FixedLocalization(np.ones((m_ensemble.shape[0], obs.size))),
    C_DD_localization=FixedLocalization(np.ones((obs.size, obs.size))),
    save_ensembles_history=True,
    std_m_prior=std_m_prior,
    random_state=123,
    batch_size=1,
    is_parallel_analyse_step=True,
    logger=logging.getLogger("ESMDA-RS"),
)
# Call the ES-MDA-RS solver
solver.solve()

# Assert that the parameters are found with a 5% accuracy.
assert np.isclose(np.average(solver.m_prior, axis=1), np.array([a, b]), rtol=1e-1).all()

# Get the approximated parameters
a_approx, b_approx = np.average(solver.m_prior, axis=1)

# Get the uncertainty on the parameters
a_std, b_std = np.sqrt(np.diagonal(solver.cov_mm))


solver.logger.info(f"a = {a_approx:.5f} +/- {a_std:.4E}")
solver.logger.info(f"b = {b_approx:.5f} +/- {b_std: 4E}")
INFO:ESMDA-RS:Assimilation # 1
INFO:ESMDA-RS:- Inflation factor = 4132.540
INFO:ESMDA-RS:Assimilation # 2
INFO:ESMDA-RS:- Inflation factor = 71.034
INFO:ESMDA-RS:Assimilation # 3
INFO:ESMDA-RS:- Inflation factor = 4.377
INFO:ESMDA-RS:Assimilation # 4
INFO:ESMDA-RS:- Inflation factor = 1.321
INFO:ESMDA-RS:Forecast for the final ensemble
INFO:ESMDA-RS:a = 9.85206 +/- 1.2995E-01
INFO:ESMDA-RS:b = -0.00192 +/-  6.263513E-05
[4]:
# Note: no inflation factor provided nor number of assimilations
# This is just for the test
cov_mm_inflation_factor: float = 0.9

solver = ESMDA_DMC(
    obs,
    m_ensemble,
    cov_obs,
    forward_model,
    forward_model_args=(x,),
    forward_model_kwargs={},
    cov_mm_inflation_factor=cov_mm_inflation_factor,
    m_bounds=m_bounds,
    C_MD_localization=FixedLocalization(np.ones((m_ensemble.shape[0], obs.size))),
    C_DD_localization=FixedLocalization(np.ones((obs.size, obs.size))),
    save_ensembles_history=True,
    random_state=123,
    batch_size=1,
    is_parallel_analyse_step=True,
    logger=logging.getLogger("ESMDA-DMC"),
)
# Call the ES-MDA-RS solver
solver.solve()

# Assert that the parameters are found with a 5% accuracy.
assert np.isclose(np.average(solver.m_prior, axis=1), np.array([a, b]), rtol=5e-2).all()

# Get the approximated parameters
a_approx, b_approx = np.average(solver.m_prior, axis=1)

# Get the uncertainty on the parameters
a_std, b_std = np.sqrt(np.diagonal(solver.cov_mm))


solver.logger.info(f"a = {a_approx:.5f} +/- {a_std:.4E}")
solver.logger.info(f"b = {b_approx:.5f} +/- {b_std: 4E}")
INFO:ESMDA-DMC:Assimilation # 1
INFO:ESMDA-DMC:- Inflation factor = 33060.322
INFO:ESMDA-DMC:Assimilation # 2
INFO:ESMDA-DMC:- Inflation factor = 737.164
INFO:ESMDA-DMC:Assimilation # 3
INFO:ESMDA-DMC:- Inflation factor = 125.379
INFO:ESMDA-DMC:Assimilation # 4
INFO:ESMDA-DMC:- Inflation factor = 11.016
INFO:ESMDA-DMC:Assimilation # 5
INFO:ESMDA-DMC:- Inflation factor = 1.153
INFO:ESMDA-DMC:Assimilation # 6
INFO:ESMDA-DMC:- Inflation factor = 30.593
INFO:ESMDA-DMC:Forecast for the final ensemble
INFO:ESMDA-DMC:a = 9.85543 +/- 1.3871E-01
INFO:ESMDA-DMC:b = -0.00193 +/-  6.096230E-05