14. Fault Tree Uncertainties#

In addition to what’s in Anaconda, this lecture will need the following libraries:

!pip install quantecon tabulate

Hide code cell output

Collecting quantecon
  Downloading quantecon-0.12.0-py3-none-any.whl.metadata (5.5 kB)
Requirement already satisfied: tabulate in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (0.10.0)
Requirement already satisfied: numba>=0.56.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (0.65.1)
Requirement already satisfied: numpy>=1.17.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (2.4.6)
Requirement already satisfied: requests in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (2.34.2)
Requirement already satisfied: scipy>=1.5.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (1.18.0)
Requirement already satisfied: llvmlite<0.48,>=0.47.0dev0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from numba>=0.56.0->quantecon) (0.47.0)
Requirement already satisfied: charset_normalizer<4,>=2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (3.4.7)
Requirement already satisfied: idna<4,>=2.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (3.18)
Requirement already satisfied: urllib3<3,>=1.26 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (2.7.0)
Requirement already satisfied: certifi>=2023.5.7 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (2026.6.17)
Downloading quantecon-0.12.0-py3-none-any.whl (351 kB)
Installing collected packages: quantecon
Successfully installed quantecon-0.12.0

14.1. Overview#

This lecture puts elementary tools to work to approximate probability distributions of the annual failure rates of a system consisting of a number of critical parts.

We’ll use log normal distributions to approximate probability distributions of critical component parts.

To approximate the probability distribution of the sum of \(n\) lognormal random variables (representing the system’s total failure rate), we compute the convolution of these distributions.

We’ll use the following concepts and tools:

  • lognormal distributions

  • the convolution theorem that describes the probability distribution of the sum of independent random variables

  • fault tree analysis for approximating a failure rate of a multi-component system

  • a hierarchical probability model for describing uncertain probabilities

  • Fourier transforms and inverse Fourier transforms as efficient ways of computing convolutions of sequences

See also

For more on Fourier transforms, see Circulant Matrices as well as Covariance Stationary Processes and Estimation of Spectra.

El-Shanawany et al. [2018] and Greenfield and Sargent [1993] applied these methods to approximate failure probabilities of safety systems in nuclear facilities.

These techniques respond to recommendations by Apostolakis [1990] for quantifying uncertainty in safety system reliability.

We will use the following imports and settings throughout this lecture:

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import fftconvolve
from tabulate import tabulate
import quantecon as qe

14.2. The lognormal distribution#

If random variable \(x\) follows a normal distribution with mean \(\mu\) and variance \(\sigma^2\), then \(y = \exp(x)\) follows a lognormal distribution with parameters \(\mu, \sigma^2\).

Note

We refer to \(\mu\) and \(\sigma^2\) as parameters rather than mean and variance because:

  • \(\mu\) and \(\sigma^2\) are the mean and variance of \(x = \log(y)\)

  • They are not the mean and variance of \(y\)

  • The mean of \(y\) is \(\exp(\mu + \frac{1}{2}\sigma^2)\) and the variance is \((e^{\sigma^2} - 1) e^{2\mu + \sigma^2}\)

A lognormal random variable \(y\) is always nonnegative.

The probability density function for \(y\) is

(14.1)#\[f(y) = \frac{1}{y \sigma \sqrt{2 \pi}} \exp \left( \frac{- (\log y - \mu)^2 }{2 \sigma^2} \right), \quad y \geq 0\]

Important properties of a lognormal random variable are:

(14.2)#\[\begin{split}\begin{aligned} \text{Mean:} & \quad e ^{\mu + \frac{1}{2} \sigma^2} \\ \text{Variance:} & \quad (e^{\sigma^2} - 1) e^{2 \mu + \sigma^2} \\ \text{Median:} & \quad e^\mu \\ \text{Mode:} & \quad e^{\mu - \sigma^2} \\ \text{0.95 quantile:} & \quad e^{\mu + 1.645 \sigma} \\ \text{0.95/0.05 quantile ratio:} & \quad e^{3.29 \sigma} \end{aligned}\end{split}\]

14.2.1. Stability properties#

Recall that independent normally distributed random variables have the following stability property:

If \(x_1 \sim N(\mu_1, \sigma_1^2)\) and \(x_2 \sim N(\mu_2, \sigma_2^2)\) are independent, then \(x_1 + x_2 \sim N(\mu_1 + \mu_2, \sigma_1^2 + \sigma_2^2)\).

Independent lognormal distributions have a different stability property: the product of independent lognormal random variables is also lognormal.

Specifically, if \(y_1\) is lognormal with parameters \((\mu_1, \sigma_1^2)\) and \(y_2\) is lognormal with parameters \((\mu_2, \sigma_2^2)\), then \(y_1 y_2\) is lognormal with parameters \((\mu_1 + \mu_2, \sigma_1^2 + \sigma_2^2)\).

Warning

While the product of two lognormal distributions is lognormal, the sum of two lognormal distributions is not lognormal.

This observation motivates the central challenge of this lecture: approximating the probability distribution of sums of independent lognormal random variables.

14.3. The convolution theorem#

Let \(x\) and \(y\) be independent random variables with probability densities \(f(x)\) and \(g(y)\), where \(x, y \in \mathbb{R}\).

Let \(z = x + y\).

Then the probability density of \(z\) is

(14.3)#\[h(z) = (f * g)(z) \equiv \int_{-\infty}^\infty f(\tau) g(z - \tau) d\tau\]

where \((f*g)\) denotes the convolution of \(f\) and \(g\).

For nonnegative random variables, this specializes to

(14.4)#\[h(z) = (f * g)(z) \equiv \int_{0}^z f(\tau) g(z - \tau) d\tau\]

14.3.1. Discrete convolution#

We will use a discretized version of the convolution formula.

We replace both \(f\) and \(g\) with discretized counterparts, normalized to sum to 1.

The discrete convolution formula is

(14.5)#\[h_n = (f*g)_n = \sum_{m=0}^n f_m g_{n-m}, \quad n \geq 0\]

This computes the probability mass function of the sum of two discrete random variables.

14.3.2. Example: discrete distributions#

Consider two probability mass functions:

\[ f_j = \mathbb{P}\{X = j\}, \quad j = 0, 1 \]

and

\[ g_j = \mathbb{P}\{Y = j\}, \quad j = 0, 1, 2, 3 \]

The distribution of \(Z = X + Y\) is given by the convolution \(h = f * g\).

# Define probability mass functions
f = [0.75, 0.25]
g = [0.0, 0.6, 0.0, 0.4]

# Compute convolution using two methods
h = np.convolve(f, g)
hf = fftconvolve(f, g)

print(f"f = {f}, sum = {np.sum(f):.3f}")
print(f"g = {g}, sum = {np.sum(g):.3f}")
print(f"h = {h}, sum = {np.sum(h):.3f}")
print(f"hf = {hf}, sum = {np.sum(hf):.3f}")
f = [0.75, 0.25], sum = 1.000
g = [0.0, 0.6, 0.0, 0.4], sum = 1.000
h = [0.   0.45 0.15 0.3  0.1 ], sum = 1.000
hf = [0.   0.45 0.15 0.3  0.1 ], sum = 1.000

Both numpy.convolve and scipy.signal.fftconvolve produce the same result, but fftconvolve is much faster for long sequences.

We will use fftconvolve throughout this lecture for efficiency.

14.4. Approximating continuous distributions#

We now verify that discretized distributions can accurately approximate samples from underlying continuous distributions.

We generate samples of size 25,000 from three independent lognormal random variables and compute their pairwise and triple-wise sums.

We then compare histograms of the samples with histograms of the discretized distributions.

# Set parameters for lognormal distributions
μ, σ = 5.0, 1.0
n_samples = 25000

# Generate samples
rng = np.random.default_rng(1234)
s1 = rng.lognormal(μ, σ, n_samples)
s2 = rng.lognormal(μ, σ, n_samples)
s3 = rng.lognormal(μ, σ, n_samples)

# Compute sums
ssum2 = s1 + s2
ssum3 = s1 + s2 + s3

# Plot histogram of s1
fig, ax = plt.subplots()
ax.hist(s1, 1000, density=True, alpha=0.6)
ax.set_xlabel('value')
ax.set_ylabel('density')
plt.show()
_images/1f9e7e72294307e506e78c0b61ae7f100a56b05def5e7436902e6418e70afbd2.png

Fig. 14.1 Sample histogram of one lognormal#

# Plot histogram of sum of two lognormal distributions
fig, ax = plt.subplots()
ax.hist(ssum2, 1000, density=True, alpha=0.6)
ax.set_xlabel('value')
ax.set_ylabel('density')
plt.show()
_images/c84ca41e209e8f09d0323cce2ea87b32fa4fca457501b688cfffd4f83fac63b5.png

Fig. 14.2 Histogram of a sum of two lognormals#

# Plot histogram of sum of three lognormal distributions
fig, ax = plt.subplots()
ax.hist(ssum3, 1000, density=True, alpha=0.6)
ax.set_xlabel('value')
ax.set_ylabel('density')
plt.show()
_images/b8185cbbdd3066c468fb7b650eb74222ae9c27b1d6c2cbecd16c7a863d869ba7.png

Fig. 14.3 Histogram of a sum of three lognormals#

Let’s verify that the sample mean matches the theoretical mean:

samp_mean = np.mean(s2)
theoretical_mean = np.exp(μ + σ**2 / 2)

print(f"Theoretical mean: {theoretical_mean:.3f}")
print(f"Sample mean: {samp_mean:.3f}")
Theoretical mean: 244.692
Sample mean: 243.197

14.5. Discretizing the lognormal distribution#

We define helper functions to create discretized versions of lognormal probability density functions.

We write out the density by hand to keep the formula (14.1) in view; scipy.stats.lognorm(s=σ, scale=np.exp(μ)).pdf(x) computes the same thing.

def lognormal_pdf(x, μ, σ):
    """
    Compute lognormal probability density function.
    """
    p = 1 / (σ * x * np.sqrt(2 * np.pi)) \
            * np.exp(-0.5 * ((np.log(x) - μ) / σ)**2)
    return p


def discretize_lognormal(μ, σ, I, m):
    """
    Discretize a lognormal distribution on the grid 0, m, 2m, ..., up to I.

    Parameters
    ----------
    μ, σ : parameters of the lognormal distribution
    I    : upper end of the grid, which truncates the right tail
    m    : spacing between grid points, which sets the resolution

    Returns
    -------
    p_array      : the density evaluated on the grid
    p_array_norm : the implied probability mass function, summing to one
    x            : the grid itself, with I / m points
    """
    x = np.arange(1e-7, I, m)
    p_array = lognormal_pdf(x, μ, σ)
    p_array_norm = p_array / np.sum(p_array)
    return p_array, p_array_norm, x

Two separate choices govern the quality of this approximation, and it pays to keep them straight.

  • \(I\) fixes where the grid stops, so it controls how much of the right tail we throw away

  • \(m\) fixes the spacing between grid points, so it controls resolution

The grid has \(I/m\) points, so raising \(I\) at fixed \(m\) buys range, while lowering \(m\) at fixed \(I\) buys accuracy.

Once \(I\) is large enough that almost no probability mass lies beyond it, further increases change nothing, and only \(m\) matters.

Exercise 14.1 asks you to verify this.

Note

scipy.signal.fftconvolve pads its inputs to a convenient length internally, so there is no need to choose \(I/m\) to be a power of two.

# Set grid parameters
p = 15
I = 2**p  # where the grid stops: truncates the right tail
m = 0.1   # spacing between grid points: sets the resolution

Let’s visualize how well the discretized distribution approximates the continuous lognormal distribution:

# Compute discretized PDF
pdf, pdf_norm, x = discretize_lognormal(μ, σ, I, m)

# Plot discretized PDF against histogram
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(x, pdf, 'r-', lw=2, label='discretized PDF')
ax.hist(s1, 1000, density=True, alpha=0.6, label='sample histogram')
ax.set_xlim(0, 2500)
ax.set_xlabel('value')
ax.set_ylabel('density')
ax.legend()
plt.show()
_images/6e48ad081653c3aef63a04db0b75bdefa146008ae01134d1cc07693c79f53e1c.png

Fig. 14.4 Discretized density against sample#

Now let’s verify that the discretized distribution has the correct mean:

# Compute mean from discretized PDF
mean_discrete = np.sum(x * pdf_norm)
mean_theory = np.exp(μ + 0.5 * σ**2)

print(f"Theoretical mean: {mean_theory:.3f}")
print(f"Discretized mean: {mean_discrete:.3f}")
Theoretical mean: 244.692
Discretized mean: 244.691

14.6. Convolving probability mass functions#

Now let’s use the convolution theorem to compute the probability distribution of a sum of the two lognormal random variables we have parameterized above.

We’ll also compute the probability distribution of a sum of three log normal distributions constructed above.

For long sequences, scipy.signal.fftconvolve is much faster than numpy.convolve because it uses fast Fourier transforms.

Let’s define the Fourier transform and the inverse Fourier transform first

14.6.1. The fast Fourier transform#

The Fourier transform of a sequence \(\{x_t\}_{t=0}^{T-1}\) is

(14.6)#\[x(\omega_j) = \sum_{t=0}^{T-1} x_t \exp(-i \omega_j t)\]

where \(\omega_j = \frac{2\pi j}{T}\) for \(j = 0, 1, \ldots, T-1\).

The inverse Fourier transform of the sequence \(\{x(\omega_j)\}_{j=0}^{T-1}\) is

(14.7)#\[x_t = T^{-1} \sum_{j=0}^{T-1} x(\omega_j) \exp(i \omega_j t)\]

The sequences \(\{x_t\}_{t=0}^{T-1}\) and \(\{x(\omega_j)\}_{j=0}^{T-1}\) contain the same information.

The pair of equations (14.6) and (14.7) tell how to recover one series from its Fourier partner.

The program scipy.signal.fftconvolve deploys the theorem that a convolution of two sequences \(\{f_k\}, \{g_k\}\) can be computed in the following way:

  • Compute Fourier transforms \(F(\omega), G(\omega)\) of the \(\{f_k\}\) and \(\{g_k\}\) sequences, respectively

  • Form the product \(H (\omega) = F(\omega) G (\omega)\)

  • The convolution of \(f * g\) is the inverse Fourier transform of \(H(\omega)\)

The fast Fourier transform and the associated inverse fast Fourier transform execute these calculations very quickly.

This is the algorithm used by fftconvolve.

Let’s do a warmup calculation that compares the times taken by numpy.convolve and scipy.signal.fftconvolve

Our three components are identically distributed, so a single discretization serves for all of them.

# Discretize the lognormal distribution; the three components are IID
_, pmf1, x = discretize_lognormal(μ, σ, I, m)
pmf2 = pmf3 = pmf1

# Direct convolution costs O(N²), so we time it on a short prefix
short = pmf1[:20_000]

with qe.Timer() as timer_numpy:
    np.convolve(short, short)
time_numpy = timer_numpy.elapsed

with qe.Timer() as timer_fft:
    fftconvolve(short, short)
time_fft = timer_fft.elapsed

print(f"On {len(short):,} points:")
print(f"  np.convolve: {time_numpy:.4f} seconds")
print(f"  fftconvolve: {time_fft:.4f} seconds")
print(f"  speedup:     {time_numpy / time_fft:.0f}x")
0.0655 seconds elapsed
0.0026 seconds elapsed
On 20,000 points:
  np.convolve: 0.0655 seconds
  fftconvolve: 0.0026 seconds
  speedup:     25x

The gap widens rapidly with the length of the sequences, because direct convolution costs \(O(N^2)\) operations while the FFT approach costs \(O(N \log N)\).

On the full grid used below, the direct method would be far slower still.

# The full calculation, done the fast way
conv_fft = fftconvolve(fftconvolve(pmf1, pmf2), pmf3)
print(f"grid points per component: {len(pmf1):,}")
grid points per component: 327,680

Now let’s plot our computed probability mass function approximation for the sum of two log normal random variables against the histogram of the sample that we formed above

# Compute convolution of two distributions for comparison
conv2 = fftconvolve(pmf1, pmf2)

fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(x, conv2[:len(x)] / m, 'r-', lw=2, label='convolution (FFT)')
ax.hist(ssum2, 1000, density=True, alpha=0.6, label='sample histogram')
ax.set_xlim(0, 5000)
ax.set_xlabel('value')
ax.set_ylabel('density')
ax.legend()
plt.show()
_images/814c8d8da009864aa741730ebc0c20768465111358b87280770c4b27650f0eed.png

Fig. 14.5 Convolution against sample, two components#

Now we present the plot for the sum of three lognormal random variables:

fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(x, conv_fft[:len(x)] / m, 'r-', lw=2, label='convolution (FFT)')
ax.hist(ssum3, 1000, density=True, alpha=0.6, label='sample histogram')
ax.set_xlim(0, 5000)
ax.set_xlabel('value')
ax.set_ylabel('density')
ax.legend()
plt.show()
_images/3c3a9825b30300f5d0ffd1b97bc3c7df01d24d4768c3efa4d129cfeab2bb2a4b.png

Fig. 14.6 Convolution against sample, three components#

Let’s verify that the means are correct

# Mean of sum of two distributions
mean_conv2 = np.sum(x * conv2[:len(x)])
mean_theory2 = 2 * np.exp(μ + 0.5 * σ**2)

print(f"Sum of two distributions:")
print(f"  Theoretical mean: {mean_theory2:.3f}")
print(f"  Computed mean: {mean_conv2:.3f}")
Sum of two distributions:
  Theoretical mean: 489.384
  Computed mean: 489.381
# Mean of sum of three distributions
mean_conv3 = np.sum(x * conv_fft[:len(x)])
mean_theory3 = 3 * np.exp(μ + 0.5 * σ**2)

print(f"Sum of three distributions:")
print(f"  Theoretical mean: {mean_theory3:.3f}")
print(f"  Computed mean: {mean_conv3:.3f}")
Sum of three distributions:
  Theoretical mean: 734.076
  Computed mean: 734.071

14.7. Fault tree analysis#

We shall soon apply the convolution theorem to compute the probability of a top event in a failure tree analysis.

Before applying the convolution theorem, we first describe the model that connects constituent events to the top event whose failure rate we seek to quantify.

Fault tree analysis is a widely used technique for assessing system reliability, as described by El-Shanawany et al. [2018].

To construct the statistical model, we repeatedly use what is called the rare event approximation.

14.7.1. The rare event approximation#

We want to compute the probability of an event \(A \cup B\).

For events \(A\) and \(B\), the probability of the union is

\[ P(A \cup B) = P(A) + P(B) - P(A \cap B) \]

where \(A \cup B\) is the event that \(A\) or \(B\) occurs, and \(A \cap B\) is the event that \(A\) and \(B\) both occur.

If \(A\) and \(B\) are independent, then \(P(A \cap B) = P(A) P(B)\).

When \(P(A)\) and \(P(B)\) are both small, \(P(A) P(B)\) is even smaller.

The rare event approximation is

\[ P(A \cup B) \approx P(A) + P(B) \]

This approximation is widely used in system failure analysis.

14.7.2. System failure probability#

Consider a system with \(n\) critical components where system failure occurs when any component fails.

We assume:

  • The failure probability \(P(A_i)\) of each component \(A_i\) is small

  • Component failures are statistically independent

We repeatedly apply a rare event approximation to obtain the following formula for the probability of a system failure:

\[ P(F) \approx P(A_1) + P (A_2) + \cdots + P (A_n) \]

or

(14.8)#\[P(F) \approx \sum_{i=1}^n P(A_i)\]

where \(P(F)\) is the system failure probability.

Probabilities for each event are recorded as failure rates per year.

Note

Strictly speaking, a failure rate per year and a failure probability within a year are different objects.

For rare events they nearly coincide, because \(1 - e^{-\lambda} \approx \lambda\) when \(\lambda\) is small.

The same approximation that lets us add probabilities across components also lets us move between rates and probabilities, so we follow the reliability literature in using the two words interchangeably here.

14.8. Failure rates unknown#

Now we come to the problem that really interests us, following El-Shanawany et al. [2018] and Greenfield and Sargent [1993] in the spirit of Apostolakis [1990].

The component failure rates \(P(A_i)\) are not known precisely and must be estimated.

We address this problem by specifying probabilities of probabilities that capture one notion of not knowing the constituent probabilities that are inputs into a failure tree analysis.

Thus, we assume that a system analyst is uncertain about the failure rates \(P(A_i), i =1, \ldots, n\) for components of a system.

The analyst copes with this situation by regarding the system’s failure probability \(P(F)\) and each of the component probabilities \(P(A_i)\) as random variables.

  • dispersions of the probability distribution of \(P(A_i)\) characterizes the analyst’s uncertainty about the failure probability \(P(A_i)\)

  • the dispersion of the implied probability distribution of \(P(F)\) characterizes his uncertainty about the probability of a system’s failure.

This leads to what is sometimes called a hierarchical model in which the analyst has probabilities about the probabilities \(P(A_i)\).

Note

Two distinct kinds of randomness appear in this model, and it is worth keeping them apart.

Aleatory uncertainty is the randomness in whether a component fails during a given year; it is described by the failure rate \(P(A_i)\).

Epistemic uncertainty is the analyst’s ignorance about the value of that rate; it is described by the lognormal distribution that he places over \(P(A_i)\).

The distribution that we compute below is an epistemic object: it describes what the analyst knows about a failure rate, not how often the system fails.

Separating the two is the central recommendation of Apostolakis [1990].

The analyst formalizes his uncertainty by assuming that

  • the failure probability \(P(A_i)\) is itself a log normal random variable with parameters \((\mu_i, \sigma_i)\).

  • failure rates \(P(A_i)\) and \(P(A_j)\) are statistically independent for all pairs with \(i \neq j\).

The analyst calibrates the parameters \((\mu_i, \sigma_i)\) for the failure events \(i = 1, \ldots, n\) by reading reliability studies in engineering papers that have studied historical failure rates of components that are as similar as possible to the components being used in the system under study.

The analyst assumes that such information about the observed dispersion of annual failure rates, or times to failure, can inform him of what to expect about parts’ performances in his system.

The analyst assumes that the random variables \(P(A_i)\) are statistically mutually independent.

Warning

Independence is a strong assumption and it is the one that reliability analysts worry about most.

A design flaw, a shared power supply, a common maintenance crew, or a single environmental shock can push many components toward failure at once.

Such common-cause failures make the upper tail of the distribution of \(P(F)\) much fatter than the independent calculation suggests, which is precisely the region that a safety regulator cares about.

Exercise 14.5 quantifies how much difference this makes.

The analyst wants to approximate a probability mass function and cumulative distribution function of the system’s failure probability \(P(F)\).

  • We say probability mass function because of how we discretize each random variable, as described earlier.

The analyst calculates the probability mass function for the top event \(F\), i.e., a system failure, by repeatedly applying the convolution theorem to compute the probability distribution of a sum of independent log normal random variables, as described in equation (14.8).

14.9. Application: waste hoist failure rate#

We now analyze a real-world example with \(n = 14\) components.

The application estimates the annual failure rate of a critical hoist at a nuclear waste facility.

A regulatory agency requires the system to be designed so that the top event failure rate is small with high probability.

14.9.1. Model specification#

This example is Design Option B-2 (Case I) described in Table 10 on page 27 of Greenfield and Sargent [1993].

The table describes parameters \(\mu_i, \sigma_i\) for fourteen log normal random variables that consist of seven pairs of random variables that are identically and independently distributed.

  • Within a pair, parameters \(\mu_i, \sigma_i\) are the same

  • As described in table 10 of Greenfield and Sargent [1993] p. 27, parameters of log normal distributions for the seven unique probabilities \(P(A_i)\) have been calibrated to be the values in the following Python code:

# Component failure rate parameters 
# (see Table 10 of Greenfield & Sargent 1993)
params = [
    (4.28, 1.1947),   # Component type 1
    (3.39, 1.1947),   # Component type 2
    (2.795, 1.1947),  # Component type 3
    (2.717, 1.1947),  # Component type 4
    (2.717, 1.1947),  # Component type 5
    (1.444, 1.4632),  # Component type 6
    (-0.040, 1.4632), # Component type 7 (appears 8 times)
]

Note

Since failure rates are very small, these lognormal distributions actually describe \(P(A_i) \times 10^{-9}\).

So the probabilities that we’ll put on the \(x\) axis of the probability mass function and associated cumulative distribution function should be multiplied by \(10^{-09}\)

We define a helper function to find array indices:

def find_nearest(array, value):
    """
    Index of the array element nearest to the given value.

    Applied to a cumulative distribution function, this returns the grid point
    whose cumulative probability is closest to a target, which for a finely
    discretized distribution is indistinguishable from the usual definition of
    a quantile as the smallest x with CDF(x) >= q.
    """
    array = np.asarray(array)
    idx = (np.abs(array - value)).argmin()
    return idx

We compute the required thirteen convolutions in the following code.

(Please feel free to try different values of the power parameter \(p\) that we use to set the number of points in our grid for constructing the probability mass functions that discretize the continuous log normal distributions.)

# Set grid parameters
p = 15
I = 2**p
m = 0.05

# Discretize all component failure rate distributions
# First 6 components use unique parameters, last 8 share the same parameters
component_pmfs = []
for μ, σ in params[:6]:
    _, pmf, x = discretize_lognormal(μ, σ, I, m)
    component_pmfs.append(pmf)

# Add 8 copies of component type 7
μ7, σ7 = params[6]
_, pmf7, x = discretize_lognormal(μ7, σ7, I, m)
component_pmfs.extend([pmf7] * 8)

# Compute system failure distribution via sequential convolution
with qe.Timer() as timer:
    system_pmf = component_pmfs[0]
    for pmf in component_pmfs[1:]:
        system_pmf = fftconvolve(system_pmf, pmf)

print(f"Time for 13 convolutions: {timer.elapsed:.4f} seconds")

# the convolution lives on the same grid spacing, but extends much further
system_grid = np.arange(len(system_pmf)) * m
print(f"grid points in the answer: {len(system_pmf):,}")
7.5745 seconds elapsed
Time for 13 convolutions: 7.5745 seconds
grid points in the answer: 9,175,027

Before plotting the cumulative distribution function, let’s look at the density itself.

fig, ax = plt.subplots(figsize=(10, 6))
upper = 2000
ax.plot(system_grid[:int(upper/m)], system_pmf[:int(upper/m)] / m, 'b-', lw=2)
ax.set_xlabel(r'failure rate ($\times 10^{-9}$ per year)')
ax.set_ylabel('density')
plt.show()
_images/1d4c092094ca93f790d54331b8d8957dd2a8512fd56cd3466b1bbcfb66da0bac.png

Fig. 14.7 Density of the system failure rate#

The density is strongly skewed to the right: a long upper tail stretches far beyond the bulk of the distribution.

This asymmetry is what makes a single point estimate of a failure rate a poor summary, and it is why the analyst reports quantiles instead.

We now plot a counterpart to the cumulative distribution function (CDF) in figure 5 on page 29 of Greenfield and Sargent [1993]

# Compute cumulative distribution function
cdf = np.cumsum(system_pmf)

# Plot CDF
Nx = 1400
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(x[:int(Nx / m)], cdf[:int(Nx / m)], 'b-', lw=2)

# Add reference lines for key quantiles
quantile_levels = [0.05, 0.10, 0.50, 0.90, 0.95]
for q in quantile_levels:
    ax.axhline(q, color='gray', linestyle='--', alpha=0.5)

ax.set_xlim(0, Nx)
ax.set_ylim(0, 1)
ax.set_xlabel(r'failure rate ($\times 10^{-9}$ per year)')
ax.set_ylabel('cumulative probability')
plt.show()
_images/e76d46cd188f184bb0156391fc63ab436a76d7f0ca33eadf20bf76ee21d21a8b.png

Fig. 14.8 CDF of the system failure rate#

We also present a counterpart to their Table 11 on page 28 of Greenfield and Sargent [1993], which lists key quantiles of the system failure rate distribution

# Percentiles reported in Table 11 of Greenfield and Sargent (1993),
# together with their published values, in units of 10^-9 per year
reference = {1.0: 77, 10.0: 130, 50.0: 263, 66.5: 341,
             85.0: 513, 95.0: 811, 99.0: 1480, 99.78: 2490}

table_data = []
for pc, published in reference.items():
    ours = system_grid[find_nearest(cdf, pc/100)]
    table_data.append([f"{pc}%", f"{ours:.1f}", published,
                       f"{100*(ours - published)/published:+.1f}%"])

print("\nSystem failure rate quantiles (×10^-9 per year):")
print(tabulate(table_data,
      headers=['Percentile', 'Computed here', 'Greenfield-Sargent', 'Difference'],
      tablefmt='grid'))
System failure rate quantiles (×10^-9 per year):
+--------------+-----------------+----------------------+--------------+
| Percentile   |   Computed here |   Greenfield-Sargent | Difference   |
+==============+=================+======================+==============+
| 1.0%         |            76.2 |                   77 | -1.1%        |
+--------------+-----------------+----------------------+--------------+
| 10.0%        |           128.2 |                  130 | -1.4%        |
+--------------+-----------------+----------------------+--------------+
| 50.0%        |           260.6 |                  263 | -0.9%        |
+--------------+-----------------+----------------------+--------------+
| 66.5%        |           338.6 |                  341 | -0.7%        |
+--------------+-----------------+----------------------+--------------+
| 85.0%        |           509.4 |                  513 | -0.7%        |
+--------------+-----------------+----------------------+--------------+
| 95.0%        |           807.6 |                  811 | -0.4%        |
+--------------+-----------------+----------------------+--------------+
| 99.0%        |          1470.2 |                 1480 | -0.7%        |
+--------------+-----------------+----------------------+--------------+
| 99.78%       |          2474.9 |                 2490 | -0.6%        |
+--------------+-----------------+----------------------+--------------+

Our quantiles reproduce the published ones to within one and a half per cent, and slightly understate each of them.

The small discrepancies reflect the precision of the reported parameters \(\mu_i, \sigma_i\), the grid spacing \(m\), and the point at which the grid is truncated.

14.9.2. Reading the answer#

The numbers in this table, rather than any single one of them, are the output of the analysis.

The median failure rate is about \(261 \times 10^{-9}\) per year, while the 95th percentile is about \(808 \times 10^{-9}\), three times larger.

That spread is not a statement about how often the hoist fails; it is a statement about how little the analyst knows about how often the hoist fails.

Notice also where the mean of the distribution falls.

mean_rate = np.sum(system_grid * system_pmf)
mean_percentile = 100 * cdf[find_nearest(system_grid, mean_rate)]

print(f"mean failure rate: {mean_rate:.1f} × 10⁻⁹ per year")
print(f"the mean sits at the {mean_percentile:.1f}th percentile")
mean failure rate: 338.1 × 10⁻⁹ per year
the mean sits at the 66.4th percentile

Because the distribution is skewed, the mean lies well above the median, at about the 66th percentile.

This is why Table 11 of Greenfield and Sargent [1993] records the mean alongside the 66.5th percentile.

The practical implication is the one that motivated the original study.

# The point estimate used in the U.S. Department of Energy's 1990 risk
# assessment, expressed in the same units
doe_estimate = 220    # 2.2 × 10^-7 per year

pct = 100 * cdf[find_nearest(system_grid, doe_estimate)]
print(f"the DOE point estimate of {doe_estimate} × 10⁻⁹ lies at "
      f"the {pct:.0f}th percentile")
print(f"so the analyst assigns probability {100-pct:.0f}% to the "
      f"true rate exceeding it")
the DOE point estimate of 220 × 10⁻⁹ lies at the 39th percentile
so the analyst assigns probability 61% to the true rate exceeding it

An analysis that reports a single number in place of a distribution conveys none of this.

Greenfield and Sargent [1993] made exactly this point: reading their figure, they put the Department of Energy’s point estimate at the 36th percentile and concluded that there was roughly a 64 per cent chance that the true failure rate was higher.

14.10. Exercises#

Exercise 14.1

Our discretization involves two separate choices: where the grid stops, \(I = 2^p\), and how finely it is spaced, \(m\).

Investigate what each one controls.

  1. Holding \(m = 0.05\) fixed, compute the median, the 95th percentile and the 99.78th percentile of the system failure rate for \(p = 10, 11, \ldots, 15\). For each \(p\), also compute how much probability mass the truncation discards, using \(\sum_i \mathbb{P}\{P(A_i) > I\}\).

  2. Holding \(p = 14\) fixed, repeat for \(m = 0.4, 0.2, 0.1, 0.05, 0.025\).

  3. Which statistic is sensitive to which choice, and why? Are the values \(p = 15\), \(m = 0.05\) used in the lecture well chosen?

Exercise 14.2

The rare event approximation replaces \(P(A \cup B)\) by \(P(A) + P(B)\), discarding \(P(A \cap B)\).

Assess how good it is here.

  1. Using the mean failure rate of each of the fourteen components as a representative value, compare \(\sum_i p_i\) with the exact probability that at least one component fails, \(1 - \prod_i (1 - p_i)\).

  2. Repeat with all the rates multiplied by \(10^3\), \(10^6\) and \(10^7\), and report the relative error in each case.

  3. At what order of magnitude does the approximation start to matter?

Exercise 14.3

A regulator who learns that the 95th percentile of the failure rate is too high will ask which components to improve.

Answer that question by computing, for each of the seven component types, the 95th percentile of the system failure rate when that type is removed from the system entirely.

Rank the component types by how much they contribute to the upper tail, and compare the ranking with the components’ mean failure rates.

Exercise 14.4

We could have computed the distribution of the system failure rate by simulation instead of by convolution.

Draw samples of all fourteen component rates, sum them, and compare the resulting quantiles with those from the convolution, for sample sizes \(10^4\), \(10^5\) and \(10^6\).

Compare the median, the 95th percentile and the 99.78th percentile.

Which method would you prefer, and why?

Exercise 14.5

The entire calculation assumes that the fourteen component failure rates are statistically independent.

Investigate what happens when they are not.

Suppose that

\[ \log P(A_i) = \mu_i + \sigma_i \left( \sqrt{\rho}\, z_0 + \sqrt{1-\rho}\, z_i \right), \]

where \(z_0\) is a shock common to all components and \(z_1, \ldots, z_{14}\) are idiosyncratic, all standard normal.

Each component still has exactly its original marginal distribution, but any two of them now have correlation \(\rho\) in logs.

Simulate the system failure rate for \(\rho = 0, 0.2, 0.5, 0.8\) and report the median, the 95th, the 99th and the 99.9th percentiles.

Explain what happens and why it matters for a safety analysis.