System Reliability#
This notebook demonstrates the pystra.system topology layer on classical system-reliability examples. The key distinction is that Pystra asks the user to encode the system event they intend to analyse; it does not infer the structural topology automatically.
The examples cover:
Example |
Reference |
Purpose |
|---|---|---|
Series system as a minimum of branch limit states |
||
Classical equal-load-sharing benchmark |
||
k-of-n system |
Event-counting topology |
|
Minimal cut-set encoding |
||
Bounds from component and pairwise event probabilities |
[1]:
import itertools
import sys
from pathlib import Path
import numpy as np
from scipy.stats import binom
for candidate in (Path.cwd(), *Path.cwd().parents):
src = candidate / "src"
if (src / "pystra").exists():
sys.path.insert(0, str(src))
break
import pystra as ra
rng = np.random.default_rng(20260425)
Four-Branch Series System#
The four-branch case is a standard benchmark for algorithms that must find several disconnected failure regions. It is naturally a series system:
For independent \(X_1, X_2 \sim \mathcal{N}(0, 1)\), the branch functions are:
[2]:
sqrt2 = np.sqrt(2.0)
four_branch = ra.SeriesSystem(
[
ra.Component("g1", lambda X1, X2: 3.0 + 0.1 * (X1 - X2) ** 2 - (X1 + X2) / sqrt2),
ra.Component("g2", lambda X1, X2: 3.0 + 0.1 * (X1 - X2) ** 2 + (X1 + X2) / sqrt2),
ra.Component("g3", lambda X1, X2: (X1 - X2) + 6.0 / sqrt2),
ra.Component("g4", lambda X1, X2: (X2 - X1) + 6.0 / sqrt2),
],
name="four_branch",
)
n_samples = 200_000
x1 = rng.standard_normal(n_samples)
x2 = rng.standard_normal(n_samples)
failed = four_branch.failure_mask(X1=x1, X2=x2)
pf_hat = failed.mean()
cov_hat = np.sqrt((1.0 - pf_hat) / (n_samples * pf_hat))
float(pf_hat), float(cov_hat)
[2]:
(0.00434, 0.0338684769766651)
The estimate should be close to the usual reference probability of about \(4.5 \times 10^{-3}\). The precise Monte Carlo value changes with the sample size and seed; the point here is that the system topology is encoded directly from the branch functions.
Daniels Equal-Load-Sharing Bundle#
Daniels’ six-thread example gives the individual thread strengths
For an equal-load-sharing bundle, sort the strengths in descending order and compute \(r\sigma_r\). The bundle strength is the maximum of those products.
[3]:
thread_strengths = np.array([3.2, 5.2, 4.4, 8.2, 6.1, 5.7])
ordered_strengths = np.sort(thread_strengths)[::-1]
remaining_threads = np.arange(1, len(ordered_strengths) + 1)
bundle_loads = remaining_threads * ordered_strengths
np.column_stack([remaining_threads, ordered_strengths, bundle_loads]), float(bundle_loads.max())
[3]:
(array([[ 1. , 8.2, 8.2],
[ 2. , 6.1, 12.2],
[ 3. , 5.7, 17.1],
[ 4. , 5.2, 20.8],
[ 5. , 4.4, 22. ],
[ 6. , 3.2, 19.2]]),
22.0)
The maximum is 22.0, matching Daniels’ published example. This is not a KofNSystem: after each thread breaks, the load redistributes, so the limit state depends on order statistics rather than only a fixed number of failed components.
k-of-n Topology#
A k-of-n system fails when at least k components have failed. This event-counting representation is useful for simulation and exact Boolean enumeration, but it is discontinuous and should not be treated as a smooth FORM/SORM limit state.
[4]:
n_components = 6
k_failures = 3
component_pf = 0.05
components = [
ra.Component(f"E{i}", lambda event=f"E{i}", **kwargs: kwargs[event])
for i in range(n_components)
]
k_of_n = ra.KofNSystem(components, k=k_failures, name="three_of_six")
states = np.array(list(itertools.product([False, True], repeat=n_components))).T
state_values = {f"E{i}": np.where(states[i], -1.0, 1.0) for i in range(n_components)}
weights = np.prod(np.where(states, component_pf, 1.0 - component_pf), axis=0)
pf_enumerated = weights[k_of_n.failure_mask(**state_values)].sum()
pf_binomial = binom.sf(k_failures - 1, n_components, component_pf)
float(pf_enumerated), float(pf_binomial)
[4]:
(0.00222984375, 0.00222984375)
The exact enumeration agrees with the binomial result because the component events were independent and identically distributed in this example.
Minimal Cut Sets#
Song and Der Kiureghian’s rigid-plastic cantilever-bar example uses the system failure event
Pystra can encode this topology directly. The probability calculation in the paper uses component and joint event probabilities with linear-programming bounds; those LP bounds are a later extension. The topology itself is already usable for simulation when the component limit states are available.
[5]:
cut_components = {
name: ra.Component(name, lambda event=name, **kwargs: kwargs[event])
for name in ("E1", "E2", "E3", "E4", "E5")
}
cantilever_system = ra.CutSetSystem(
[["E1", "E2"], ["E3", "E4"], ["E3", "E5"]],
components=cut_components,
name="cantilever_cut_sets",
)
example_events = {
"E1": np.array([False, True, False, False]),
"E2": np.array([False, True, False, True]),
"E3": np.array([True, False, True, True]),
"E4": np.array([False, False, True, False]),
"E5": np.array([False, False, False, True]),
}
example_values = {name: np.where(mask, -1.0, 1.0) for name, mask in example_events.items()}
cantilever_system.failure_mask(**example_values)
[5]:
array([False, True, True, True])
The four samples above fail only when at least one of the supplied cut sets is complete.
Ditlevsen Bounds#
Ditlevsen bounds use component event probabilities and pairwise intersections to bound a union of failure events. For two events the result collapses to exact inclusion-exclusion.
[6]:
ra.ditlevsen_bounds([0.1, 0.2], {(0, 1): 0.03})
[6]:
(0.27, 0.27)
Mainçon’s identical 100-element series-system example reports component failure probability \(P_1 = 1.969 \times 10^{-3}\) and pairwise intersection probability \(P_{12} = 1.361 \times 10^{-3}\). With equal pairwise intersections, the Ditlevsen upper bound is approximately \(6.216 \times 10^{-2}\).
[7]:
n_events = 100
p1 = 1.969e-3
p12 = 1.361e-3
probabilities = np.full(n_events, p1)
intersections = np.full((n_events, n_events), p12)
np.fill_diagonal(intersections, p1)
ra.ditlevsen_bounds(probabilities, intersections)
[7]:
(0.0025769999999999994, 0.06216099999999983)
These bounds are deliberately separate from the topology classes. A topology tells Pystra which samples fail; Ditlevsen bounds require marginal and pairwise event probabilities, usually obtained from component analyses, simulation, or reference data.