Skip to content

Fault trees

Learning this for the first time?

This page is the reference. The idea behind fault trees, and how they relate to block diagrams, is taught step by step in Lesson 3.

A reliability block diagram says how a system works. A fault tree says how it fails: it starts from an undesired top event (the system failing) and works down, through gates, to the basic events that cause it (component failures). Safety and risk analyses are usually written this way. FaultTree evaluates static fault trees exactly, with the same engine as the diagrams, and converts them to and from NonRepairableRBDs.

Building a tree

A tree is its gates, keyed by name, and its basic events, keyed by name with a probability or a lifetime model:

import numpy as np
import surpyval as surv
from repyability import FaultTree

cooling = FaultTree(
    {
        "no cooling": ("or", ["no flow", "valve"]),       # either will do
        "no flow": ("and", ["pump 1", "pump 2"]),         # needs both
    },
    {"pump 1": 0.1, "pump 2": 0.1, "valve": 0.05},        # probability each has occurred
)
cooling.top                        # 'no cooling'
cooling.top_event_probability()    # -> 0.0595   1 − (1 − 0.1²)(1 − 0.05)
cooling.ff()                       # -> 0.0595   the same, by an RBD's name
cooling.sf()                       # -> 0.9405   worked out in its own right

A tree answers to an RBD's names too, so the same code runs on either: ff and sf, and the cut and path sets as a list (minimal_cut_sets(), which an RBD has as well) or a set (get_min_cut_sets(), as an RBD gives them).

Gate Written Occurs when
OR ("or", inputs) any input occurs
AND ("and", inputs) every input occurs
VOTE ("vote", k, inputs) at least k of the inputs occur (k out of n)

An input is a gate or a basic event. The top event is the one gate that is no other gate's input, or pass top=. Gates must not form a loop, every gate and event must be below the top event, and a name cannot be both a gate and an event; mistakes raise ValueError.

A basic event is a probability in [0, 1] that it has occurred, or a lifetime model whose ff(t) is the probability that it has occurred by time t: a surpyval distribution (Weibull, FixedEventProbability, ...) or any RePyability node model. With lifetime models every method takes a time, a number or an array:

pump = surv.Weibull.from_params([8000, 1.8])
valve = surv.Weibull.from_params([20000, 1.5])
timed = FaultTree(
    {"no cooling": ("or", ["no flow", "valve"]),
     "no flow": ("and", ["pump 1", "pump 2"])},
    {"pump 1": pump, "pump 2": pump, "valve": valve},
)
timed.top_event_probability(5000)   # -> 0.2249   failed within 5000 h
timed.top_event_probability(np.array([1000, 5000, 10000]))
# array([0.01166, 0.22494, 0.72021])

The top event probability by time t is the unreliability of the system the tree describes.

Cut sets

A minimal cut set is a smallest set of basic events whose joint occurrence makes the top event occur. They are the failure combinations the tree allows, and a cut set of one event is a single point of failure:

cooling.minimal_cut_sets()
# [frozenset({'valve'}), frozenset({'pump 1', 'pump 2'})]
cooling.ranked_cut_sets()           # most likely first
# [(frozenset({'valve'}), 0.05), (frozenset({'pump 1', 'pump 2'}), 0.01)]
timed.ranked_cut_sets(5000)         # at 5000 h the pumps dominate
# [(frozenset({'pump 1', 'pump 2'}), 0.1217), (frozenset({'valve'}), 0.1175)]

A cut set's probability is the product of its events' (with common causes, that of its events occurring together). minimal_path_sets() gives the dual: the smallest sets of events whose not occurring keeps the top event from occurring. occurs(events) evaluates the tree's logic for one combination: whether the top event occurs when exactly events have.

Repeated events

An event may feed several gates. Here two channels, either of which will do, share a power supply:

shared = FaultTree(
    {
        "no output": ("and", ["channel A", "channel B"]),
        "channel A": ("or", ["supply", "pump A"]),
        "channel B": ("or", ["supply", "pump B"]),
    },
    {"supply": 0.01, "pump A": 0.1, "pump B": 0.1},
)
shared.repeated_events           # frozenset({'supply'})
shared.top_event_probability()   # -> 0.0199   0.01 + 0.99 × 0.1²

Multiplying the channels' probabilities as if they were independent, (1 − 0.99 × 0.9)² = 0.0119, would be 40% too low: one supply failure takes out both channels at once. The tree is evaluated exactly, repeated events included. Every gate below which nothing is shared with the rest of the tree is a module with a closed form (an OR gate is 1 − ∏(1 − q_i), an AND gate ∏ q_i), and what the repeated events tie together is worked out by a binary decision diagram built from the gates themselves, which branches on one event at a time (the pivotal, or Shannon, decomposition, as for the core of a block diagram: see Concepts). The cut and path sets are not listed to build it, as they multiply with the shared events: an OR of fifty AND gates over twenty-five shared events is built and evaluated in a fraction of a second. A tree whose diagram would pass two million nodes (repyability.fault_tree.DIAGRAM_LIMIT) is refused, with the advice to convert it with to_rbd() and simulate the diagram. There is no rare-event or min-cut upper-bound approximation.

Common causes

Redundant pumps of one design, in one room, also fail together: from one flood, one bad batch of seals, one maintenance error. ccf_groups (#184) declares such groups over basic events, with the common-cause models a block diagram takes: a BetaFactor or MGL model splits each member's probability into the part that is its own and the shared causes that fail several members at once.

from repyability import BetaFactor, CCFGroup

grouped = FaultTree(
    {"no cooling": ("or", ["no flow", "valve"]),
     "no flow": ("and", ["pump 1", "pump 2"])},
    {"pump 1": 0.1, "pump 2": 0.1, "valve": 0.05},
    ccf_groups=[CCFGroup(["pump 1", "pump 2"], BetaFactor(0.1))],
)
grouped.top_event_probability()   # -> 0.0671   against 0.0595 independent
grouped.ranked_cut_sets()
# [(frozenset({'valve'}), 0.05), (frozenset({'pump 1', 'pump 2'}), 0.0180)]
grouped.criticality_importance()
# {'pump 1': 0.255, 'pump 2': 0.255, 'valve': 0.7315}

The pumps' pair now fails with probability 0.1 × 0.1 + 0.99 × 0.09² = 0.0180, nearly twice the independent 0.01: the shared cause fails both with β Q, and without it each fails on its own with (1 − β) Q. A BetaFactor group is that cause drawn as a repeated event (OR(pump 1's own, cause) and OR(pump 2's own, cause)), which the group spares you writing; an MGL group of three or more has a cause for each set of members, as the models assign.

  • Exact, as for a diagram: the top event probability and every importance measure sum over the groups' shock outcomes, given each of which the events are independent; a member's importance is conditioned on its own state through them. A tree and its to_rbd() diagram, which carries the groups, give the same values.
  • The cut sets stay sets of basic events. ranked_cut_sets gives each the probability that its events occur together, the shared causes included, so a pair of members often leads the ranking. (PRA tools that draw each shared cause as an event of its own list it as a cut set of one.)
  • Rules, as a diagram's: a member is a basic event, in one group at most, and the members have one model. A group splitting a probability warns once its members' probability passes 0.1, and basis="rate" splits the failure rate over a lifetime (Over a lifetime).
  • Conversions keep them: to_rbd() passes the groups to the diagram and from_rbd takes the diagram's, refusing a group with a member that cannot affect the system (it is no event of the tree, but the group's shocks would still strike it) and dropping one whose members none can.

Importance measures

Each measure returns {event: value} (floats for a number t, arrays for an array), with Q the top event probability and q_e the event's:

Method Definition
birnbaum_importance(t) Q(e occurred) − Q(e did not): how much Q depends on the event.
criticality_importance(t) I_B(e) · q_e / Q: the share of the top event the event accounts for.
fussell_vesely(t) The probability that some minimal cut set containing e has occurred, over Q (method="rare_event": their probabilities summed, which can pass 1).
risk_achievement_worth(t) Q(e occurred) / Q.
risk_reduction_worth(t) Q / Q(e did not occur).
cooling.criticality_importance()
# {'pump 1': 0.1597, 'pump 2': 0.1597, 'valve': 0.8319}
cooling.risk_achievement_worth()["valve"]   # -> 16.81   top event certain once the valve fails

They are the same measures, with the same definitions, as the importance measures of a NonRepairableRBD: a tree and the diagram of the same system give the same values.

From diagrams to trees and back

FaultTree.from_rbd(rbd) turns a NonRepairableRBD into the tree of its failure. Its series blocks become OR gates, parallel blocks AND gates, and k-out-of-n blocks VOTE gates on n − k + 1 failures; a part that is not series-parallel (a bridge) becomes an OR gate over its minimal cut sets. The nodes' models become the events', and nodes that can never fail (PerfectReliability junctions) or never matter are left out:

from repyability import NonRepairableRBD

fail = surv.FixedEventProbability.from_params
pumps_valve = NonRepairableRBD(
    [("in", "p1"), ("in", "p2"), ("p1", "v"), ("p2", "v"), ("v", "out")],
    {"p1": fail(0.1), "p2": fail(0.1), "v": fail(0.05)},
)
tree = FaultTree.from_rbd(pumps_valve)
tree.gates                        # {'G1': ('and', ['p1', 'p2']), 'TOP': ('or', ['G1', 'v'])}
tree.top_event_probability()      # -> 0.0595   = 1 − pumps_valve.sf()

to_rbd() goes the other way: an OR gate becomes its inputs in series, an AND gate in parallel, and a VOTE gate a junction node (PerfectReliability, named after the gate) that needs n − k + 1 of its inputs working. A repeated event becomes a repeated node:

diagram = cooling.to_rbd()
diagram.sf()                      # -> 0.9405
shared.to_rbd().repeated          # {'supply (2)': 'supply'}

An event or gate that feeds several gates is drawn once for each place, its later appearances as repeated nodes that the diagram treats as the one component, so every tree converts exactly: the diagram's reliability is one minus the tree's top event probability. The tree of a bridge, for example, whose events each appear in several cut sets, converts back to a diagram with the bridge's logic.

Saving

to_json() / FaultTree.from_json() (and to_dict / from_dict) save the gates, the top event and the events, probabilities as numbers and models as RePyability saves a diagram's node models, and the common-cause groups as a diagram saves them.

The model and its limits

  • Static gates only. OR, AND and VOTE. Dynamic gates (priority-AND, spares, sequence dependence) and NOT gates are not supported; for standby and load sharing, see Redundancy models.
  • Independent basic events, but for the events they share and their common causes.
  • Non-repairable. A tree gives the probability that the top event has occurred by t. For availability with repair, build a RepairableRBD.