CamPetro

Inversion Uncertainty and Confidence

On this page

Summary

The uncertainty of an inversion has two parts: how log errors and endmember errors propagate into the volumes, and how well the model fits the data. The first is controlled by the Condition number of the inversion matrix, the second by the model error QC. Confidence combines both. The calculator gives the standard deviation of the three-component example from the log errors.

Inputs and outputs

Item Units
Input Quartz density endmember g/cm³
Input Calcite density endmember g/cm³
Input Pore fluid density g/cm³
Input Quartz U endmember barns/cm³
Input Calcite U endmember barns/cm³
Input Fluid U endmember barns/cm³
Input Density log uncertainty g/cm³
Input U uncertainty barns/cm³
Output Porosity standard deviation (inversion) v/v
Output Quartz volume standard deviation v/v
Output Calcite volume standard deviation v/v

Equations

Propagation of log errors. For the linear least-squares problem on the Mineral Inversion page, if the errors are independent with standard deviations \(\sigma_i\) and the model is right, the covariance of the volumes is:

\[ \mathrm{Cov}(\hat{\mathbf V}) = \left(\mathbf A^T \mathbf W \mathbf A\right)^{-1}, \qquad \mathbf W = \mathrm{diag}\!\left(1/\sigma_i^{2}\right) \]

The standard deviation of each volume is the square root of the diagonal. This holds when no non-negativity constraint is active. When one is, the volume at its bound has a smaller spread than this gives.

Conditioning. The Condition number of the weighted matrix is the ratio of its largest to its smallest singular value:

\[ \kappaCond = \frac{s_{max}}{s_{min}} \]

A large \(\kappa\) means that some combination of volumes changes the logs very little, so that a small log error produces a large change in that combination.

Propagation of endmember errors. To first order, an error \(\delta\mathbf A\) in the matrix changes the volumes by:

\[ \delta\mathbf V \approx -\left(\mathbf A^T \mathbf W \mathbf A\right)^{-1}\mathbf A^T \mathbf W\,\delta\mathbf A\,\mathbf V \]

This error is systematic, not random, and it is not reduced by averaging over depth.

The calculator is the three-component case with errors in the bulk density and in \(U\) only (closure is exact). The coefficients of the volumes with respect to the data are the entries of the inverse matrix, which are the numerators of the formulas on the Mineral Inversion page divided by \(D\). The standard deviations are:

\[ \sigVinvQ = \frac{\sqrt{\left[(\uCc - \uFl)\,\sigRhob\right]^{2} + \left[(\rhoFl - \rhoCc)\,\sigU\right]^{2}}}{|D|} \]
\[ \sigVinvC = \frac{\sqrt{\left[(\uFl - \uQz)\,\sigRhob\right]^{2} + \left[(\rhoQz - \rhoFl)\,\sigU\right]^{2}}}{|D|} \]
\[ \sigPhiInv = \frac{\sqrt{\left[(\uQz - \uCc)\,\sigRhob\right]^{2} + \left[(\rhoCc - \rhoQz)\,\sigU\right]^{2}}}{|D|} \]

with \(D = \rhoQz(\uCc - \uFl) - \rhoCc(\uQz - \uFl) + \rhoFl(\uQz - \uCc)\) as before. The results do not depend on the measured values, only on the endmembers and the log uncertainties.

Symbol Variable Units Typical range
\(\rho_q\) Quartz density endmember g/cm³ 2.64 to 2.65
\(\rho_c\) Calcite density endmember g/cm³ 2.71 to 2.73
\(\rho_f\) Pore fluid density g/cm³ 0.2 to 1.2
\(U_q\) Quartz U endmember barns/cm³ 4.7 to 4.9
\(U_c\) Calcite U endmember barns/cm³ 13.5 to 14
\(U_f\) Fluid U endmember barns/cm³ 0.1 to 1
\(\sigma_{\rho}\) Density log uncertainty g/cm³ 0.01 to 0.05
\(\sigma_U\) U uncertainty barns/cm³ 0.5 to 2
\(\sigma_{\phi}\) Porosity standard deviation (inversion) v/v 0.01 to 0.05
\(\sigma_{V_q}\) Quartz volume standard deviation v/v 0.05 to 1
\(\sigma_{V_c}\) Calcite volume standard deviation v/v 0.05 to 1
\(\kappa\) Condition number 1 to 1000

Single-value calculator

Behavior

With the default endmembers and uncertainties of 0.03 g/cm³ and 1 barn/cm³, the porosity is determined to 0.019 but quartz and calcite to 0.120 and 0.113. This is the practical meaning of a poorly conditioned system: the sum of the two minerals is known, the split is not. The plot sweeps the calcite \(U\). As it comes down toward the quartz value of 4.8 the minerals become harder to tell apart: the quartz standard deviation is 0.12 at 13.8, 0.21 at 10, 0.34 at 8 and 1.00 at 6 barns/cm³, while the porosity standard deviation hardly moves (0.019, 0.020, 0.023 and 0.041). The density difference between quartz and calcite is only 0.06 g/cm³, so the density log cannot help; the photoelectric log carries the separation, and its accuracy sets the answer.

Parameter guidance

Log uncertainties. Use the repeatability and accuracy of each curve after environmental corrections, not the bit resolution. Values used in practice are of the order 0.02 to 0.03 g/cm³ for density, 0.02 to 0.03 for neutron porosity, 3 to 5 µs/ft for sonic and about 1 barn/cm³ for \(U\). They also act as weights: a curve with a larger uncertainty has less influence on the answer, so raise the uncertainty of curves that are less reliable (for example the sonic in poor hole).

Constraint uncertainties. The uncertainty on the clay volume and kerogen volume rows should reflect the accuracy of the upstream result, about 0.03 and 0.01 here. They may be reduced if the upstream result is calibrated to core.

Confidence classes. A usable confidence flag needs both measures. As a starting point, for the model error \(E_{avg}\) and the propagated standard deviation of the result of interest:

Confidence Average normalized error Standard deviation of the volume
High below 1 below 0.03
Medium 1 to 2 0.03 to 0.10
Low above 2, or a non-negativity bound active above 0.10

Use the class of the worse of the two. The limits are starting points for review.

Worked example

First, the effect of the choice of logs on a three-component problem (quartz, calcite, water) and, second, a five-component problem solved with and without the clay and kerogen constraints, comparing a Monte Carlo result (300 trials with noise on the logs) with the linear propagation:

import itertools
import numpy as np

# Endmember responses. Illustrative mid-range values, not a recommendation.
#             density  neutron  sonic   U
#             g/cm3    v/v      us/ft   barns/cm3
END = {
    "quartz":  [2.65, -0.02,  55.5,  4.8],
    "calcite": [2.71,  0.00,  47.5, 13.8],
    "clay":    [2.55,  0.35, 110.0,  9.0],
    "kerogen": [1.26,  0.60, 160.0,  0.25],
    "water":   [1.00,  1.00, 189.0,  0.4],
}
LOGS = ["rhob", "nphi", "dt", "u"]
SIGMA = {"rhob": 0.03, "nphi": 0.03, "dt": 5.0, "u": 1.0, "vcl": 0.03, "vker": 0.01, "unity": 0.001}


def system(minerals, curves=LOGS, constraints=("vcl", "vker")):
    """Rows: the log curves, then the constraint rows, then the closure row."""
    rows, sig, labels = [], [], []
    for c in curves:
        rows.append([END[m][LOGS.index(c)] for m in minerals])
        sig.append(SIGMA[c]); labels.append(c)
    for c, name in (("vcl", "clay"), ("vker", "kerogen")):
        if c in constraints:
            rows.append([1.0 if m == name else 0.0 for m in minerals])
            sig.append(SIGMA[c]); labels.append(c)
    rows.append([1.0] * len(minerals)); sig.append(SIGMA["unity"]); labels.append("unity")
    return np.array(rows), np.array(sig), labels


def solve(A, sig, d):
    """Weighted least squares with V >= 0: try every active set, keep the best feasible one."""
    Aw, dw = A / sig[:, None], d / sig
    n = A.shape[1]
    best, best_cost = None, np.inf
    for k in range(1, n + 1):
        for subset in itertools.combinations(range(n), k):
            x = np.linalg.lstsq(Aw[:, subset], dw, rcond=None)[0]
            if (x < -1e-12).any():
                continue
            v = np.zeros(n)
            v[list(subset)] = x
            cost = np.sum((Aw @ v - dw) ** 2)
            if cost < best_cost:
                best, best_cost = v, cost
    return best, best_cost


truth = {"quartz": 0.45, "calcite": 0.20, "clay": 0.15, "kerogen": 0.05, "water": 0.15}

# 1. Three unknowns (quartz, calcite, water): how the choice of logs changes the conditioning.
print("quartz-calcite-water, no constraints, linear error propagation")
print(f"{'logs':14s} {'cond':>7s} {'sd quartz':>10s} {'sd calcite':>11s} {'sd water':>9s}")
for curves in (("rhob", "nphi"), ("rhob", "u"), ("rhob", "nphi", "u")):
    A, sig, _ = system(["quartz", "calcite", "water"], curves, constraints=())
    Aw = A / sig[:, None]
    cov = np.linalg.inv(Aw.T @ Aw)
    sd = np.sqrt(np.diag(cov))
    print(f"{'+'.join(curves):14s} {np.linalg.cond(Aw):7.0f} {sd[0]:10.3f} {sd[1]:11.3f} {sd[2]:9.3f}")

# 2. Five unknowns: Monte Carlo on the log errors against the linear estimate.
minerals = list(truth)
v_true = np.array([truth[m] for m in minerals])
rng = np.random.default_rng(11)
print()
print("five components, standard deviation of the result (volume fraction)")
print(f"{'case':26s}" + "".join(f"{m:>9s}" for m in minerals))
for label, cons in (("logs + closure only", ()), ("logs + VWCL + kerogen", ("vcl", "vker"))):
    A, sig, _ = system(minerals, constraints=cons)
    d0 = A @ v_true
    out = []
    for _ in range(300):
        out.append(solve(A, sig, d0 + rng.normal(0.0, sig))[0])
    out = np.array(out)
    Aw = A / sig[:, None]
    lin = np.sqrt(np.diag(np.linalg.inv(Aw.T @ Aw)))
    print(f"{label + ' (MC)':26s}" + "".join(f"{x:9.3f}" for x in out.std(axis=0)))
    print(f"{label + ' (linear)':26s}" + "".join(f"{x:9.3f}" for x in lin))
    print(f"{'  condition number':26s}{np.linalg.cond(Aw):9.0f}")

Output

quartz-calcite-water, no constraints, linear error propagation
logs              cond  sd quartz  sd calcite  sd water
rhob+nphi         1535      0.632       0.618     0.020
rhob+u             287      0.120       0.113     0.019
rhob+nphi+u        277      0.115       0.111     0.016

five components, standard deviation of the result (volume fraction)
case                         quartz  calcite     clay  kerogen    water
logs + closure only (MC)      0.118    0.116    0.099    0.096    0.084
logs + closure only (linear)    0.174    0.126    0.142    0.231    0.201
  condition number              794
logs + VWCL + kerogen (MC)    0.113    0.106    0.029    0.009    0.018
logs + VWCL + kerogen (linear)    0.115    0.112    0.029    0.010    0.018
  condition number              358

Assumptions and limitations

  • Log errors are independent, random, and have the assigned standard deviations. Real errors are correlated across logs (a washout affects density, neutron and sonic together) and systematic.
  • The model is right: the right components and the right endmembers. Model error is not propagated, and shows up as misfit.
  • The linear propagation ignores the non-negativity bounds. Near a bound the real spread is smaller and skewed, as the Monte Carlo columns in the example show.
  • Uncertainties of the constraint rows (clay and kerogen volume) are known and independent of the log errors, although they depend on the same logs in practice.
  • The three-component calculator ignores error in the endmembers.

QC checks

  • The propagated standard deviation of porosity is a few hundredths or less, and is smaller than the porosity itself in tight rock.
  • Quartz and calcite (or any similar pair) standard deviations are reported with the volumes. A pair with deviations far above its volumes is not resolved and should be merged or reported as a sum.
  • Adding a log, or a constraint, lowers the standard deviations. If it does not, it carries no new information about the volume of interest.
  • The condition number does not change much from well to well unless the component list or logs change.
  • Monte Carlo and linear results agree where no bound is active.

Going Deeper

Two measures matter and they are different. The condition number and the propagated standard deviation are a property of the model and the data errors, and can be computed before looking at any logs. The model error is a property of the fit and is computed from the logs afterwards. A system can be well conditioned and fit badly (a missing mineral), or fit perfectly and be badly conditioned (too many similar minerals), and in both cases the volumes are uncertain. In a full Bayesian treatment the same quantities appear as a posterior covariance, and priors (for example on clay volume and kerogen volume) work as extra rows, as they do in the inversion method.

References

  1. Aster, R.C., Borchers, B. and Thurber, C.H., 2018. Parameter Estimation and Inverse Problems, 3rd edition. Elsevier.
  2. Lawson, C.L. and Hanson, R.J., 1974. Solving Least Squares Problems. Prentice-Hall, Englewood Cliffs, NJ (reprinted by SIAM, 1995).

Python reference implementation

Python reference implementation

The Python reference implementation is available to registered users with a verified email address. Register or sign in to view it.