Redundancy models¶
Learning this for the first time?
This page is the reference. The ideas behind it are taught step by step, with worked examples and exercises, in Lesson 5 (when redundancy disappoints).
Parallel branches in a diagram model active redundancy: every unit runs, and each fails independently. Real redundancy is often not like that. Spares wait in standby, identical parts are replicated, and units that share a load wear each other out. The models on this page capture each of these as a single node. Theory: Concepts and load sharing.
import numpy as np
import surpyval as surv
from repyability import (
LoadSharingModel,
NonRepairableRBD,
RepeatedNode,
RepeatedStandbyNode,
StandbyModel,
)
pump = surv.Weibull.from_params([100, 2])
Standby: cold, warm and hot¶
A StandbyModel holds n units of which k
operate at a time. When an operating unit fails, the next spare in list order
is switched in; the arrangement fails when fewer than k units survive.
dormancy_factor sets how fast a dormant spare ages, relative to an
operating one:
cold = StandbyModel([pump, pump]) # dormancy_factor=0
warm = StandbyModel([pump, pump], dormancy_factor=0.3)
hot = StandbyModel([pump, pump], dormancy_factor=1.0)
cold.sf(150) # -> 0.6342 the spare only starts ageing when switched in
warm.sf(150) # -> 0.4815 the spare ages at 30% of the rate while dormant
hot.sf(150) # -> 0.1997 exactly two units in parallel
- Cold (
0, the default): a dormant spare does not age. Withk = 1, the lifetime is the sum of the units' lifetimes. - Warm (between 0 and 1): a dormant spare ages at that fraction of the operating rate, so it can fail latent, dead before it is ever needed (it is skipped when its turn comes).
- Hot (
1): spares age as fast as operating units, which is exactlyk-out-of-nactive parallel.
k operating units at once is supported for every dormancy:
two_of_three = StandbyModel([pump, pump, pump], k=2)
two_of_three.sf(100) # -> 0.5172
Imperfect switching¶
switching_probability is the probability that each switch-over succeeds,
one value for all or one per spare; a failed switch ends the arrangement. It
is supported for cold standby, with any k:
unreliable_switch = StandbyModel([pump, pump], switching_probability=0.9)
unreliable_switch.sf(150) # -> 0.5813 against 0.6342 with a perfect switch
StandbyModel([pump, pump, pump], k=2, switching_probability=0.9).sf(100)
# -> 0.479 against 0.5172
Warm or hot standby with imperfect switching raises NotImplementedError.
How the survival function is obtained¶
| Arrangement | Method |
|---|---|
Identical Exponential units, cold, perfect switching (any k) |
Exact: Erlang. |
| Identical Exponential units, warm or hot | Exact: hypoexponential. |
Hot, any units (any k) |
Exact: k-out-of-n of the units, each working independently. |
Cold, k = 1 (any units, including imperfect switching) |
Numerical convolution of the units' lifetimes: deterministic, and accurate to about 1e-6 (1e-5 for the steepest early-life densities, a gamma with shape 0.5 or less). |
Cold, k ≥ 2, identical units (including imperfect switching) |
Numerical: each operating position runs a renewal process of the units' lives, and the arrangement fails at the n − k + 1-th failure in all; the counts come from the same convolution. |
Cold, k = 2, different units (including imperfect switching) |
Numerical: a recursion over the switch-ins on the time and the other operating unit's start (the newcomer starts new), accurate to about 1e-4 (1e-3 for lives with a steep start, such as a Weibull of shape below 1). |
Warm, k = 1 (any units) |
Numerical: a recursion over the spares' switch-ins on a time grid (a spare switched in at τ has aged dormancy_factor · τ), accurate to about 1e-5. |
Everything else (warm with k ≥ 2, cold k ≥ 3 of different units) |
None: the arrangement draws lifetimes for the system's simulations, and is_simulated is True. |
An arrangement with no exact or numerical survival function is simulated
where it is used. Its sf, ff, cs and mean() raise
NotImplementedError, as do the analyses of a diagram that need its
reliability, naming the simulations that take it: random,
mean(method="simulate") and unreliability_interval of a
NonRepairableRBD, or availability and cost of a RepairableRBD.
Until 0.12 such an arrangement was given a Kaplan–Meier fit to simulated
lifetimes, which carried their Monte-Carlo error into every exact analysis
of the system; the mc_samples, lower and seed that set the fit went
in 0.13.
mean() and random(size, seed=None) give the arrangement's mean lifetime
and draw lifetimes; cs(x, X) is its conditional survival. A standby node
cannot take a condition-based state.
mean(method="simulate", mc_samples=..., seed=...) estimates its mean
life from that many new draws: the way to a simulated arrangement's mean,
and a check on an exact one.
A StandbyModel is one lifetime: in a repairable RBD the whole arrangement
is replaced as one unit when it fails. For a duty unit and a standby that
are repaired one at a time, the failed unit returning as the new spare, make
the node a standby group instead.
Repeated nodes: n identical copies¶
RepeatedNode(model, repeats, kind) is
repeats independent copies of model in "series" or "parallel", as one
node. It saves drawing them out one by one:
three_in_series = RepeatedNode(pump, 3, "series")
three_in_parallel = RepeatedNode(pump, 3, "parallel")
three_in_series.sf(50) # -> 0.4724 = pump.sf(50) ** 3
three_in_parallel.sf(50) # -> 0.9892 = 1 − pump.ff(50) ** 3
Its reliability is exact, and so is its mean(), the area under it;
mean(method="simulate", mc_samples=1_000_000) estimates it from that many
draws instead. A RepeatedNode is n distinct copies; for the
same component in several places, see
One component in several places.
Repeated standby¶
RepeatedStandbyNode(model, repeats,
switching_probability=1.0) is repeats identical units in cold standby,
one operating at a time: the same as a StandbyModel of repeats copies of
one model with k = 1, and computed by the same numerical convolution.
RepeatedStandbyNode(pump, 2).sf(150) # -> 0.6342
RepeatedStandbyNode(pump, 2, switching_probability=0.9).sf(150) # -> 0.5813
Load sharing¶
Units that share a load fail dependently: when one fails, the survivors take
over its share, run harder, and wear faster. A
LoadSharingModel makes such a group one
node. Each unit is an accelerated-failure-time model fitted in surpyval
with the load as its covariate; with s survivors sharing a total load L,
each carries L / s, and the group works while at least k survive.
from repyability import RegressionNode
# Field data: life at various loads (higher load, shorter life).
rng = np.random.default_rng(0)
load = rng.uniform(0.5, 2.0, size=800)
life = rng.exponential(scale=100.0 / np.exp(0.6 * (load - 1.0)), size=800)
unit = surv.ExponentialAFT.fit(life + 1e-3, Z=load.reshape(-1, 1))
# Three units share a total load of 3.0; the group needs two.
group = LoadSharingModel([unit, unit, unit], load=3.0, k=2)
group.sf(50) # -> 0.5842
group.is_simulated # False: identical Exponential baselines have a closed form
group.mean() # -> 70.36
Treating the three units as independent, each pinned at its initial share (load 1.0), would overstate the group:
p = RegressionNode(unit, covariates=[1.0]).sf(50)
3 * p**2 - 2 * p**3 # -> 0.6608 two-out-of-three, ignoring the load transfer
- Identical units with an Exponential baseline have an exact
(hypoexponential) group lifetime. Other identical units (one baseline and
one load response) have a numerical one: they all age alike, so they fail
in the order of their exposures to failure, and a recursion over the
failures on a grid of exposure and time gives the lifetime's
distribution, to about 1e-4. Either way
is_simulatedisFalse. - Different units have no exact or numerical survival function, and
is_simulatedisTrue: as for standby, the group draws lifetimes for the system's simulations, itssf,ff,csandmean()raiseNotImplementedError, andmean(method="simulate", mc_samples=..., seed=...)estimates its mean life. - With no load effect the units neither share stress nor accelerate, and the
group reduces exactly to
k-out-of-nparallel. - The units must be AFT models (they need the time-scaling
phi(load)); anything else raisesValueError. - Warm standby uses the same cumulative-exposure (virtual-age) machinery.
Using them in an RBD¶
Each of these is an ordinary node:
line = NonRepairableRBD(
[("s", "pumps"), ("pumps", "filters"), ("filters", "t")],
{"pumps": cold, "filters": three_in_parallel},
)
line.sf(50) # -> 0.9798
line.is_analytically_solvable() # True: no node's reliability is simulated
is_analytically_solvable() is False only when some node has no exact or
numerical reliability, and get_non_analytic_nodes() names those nodes; analysis_routes() says how each analysis is computed (see
Is the system time-dependent, and is it exact?).
All of these models save with the RBD.