Skip to content

RBD (the base class)

The methods every diagram has. NonRepairableRBD and RepairableRBD list them again, with their own.

RBD

RBD(edges, nodes=None, k=None, input_node=None, output_node=None, on_infeasible_rbd='raise', capacity=None)

Reliability block diagram structure: the base of the RBD classes.

An RBD is a directed acyclic graph from a single input node (the only node with no incoming edges) to a single output node (the only node with no outgoing edges). Every other node is an intermediate node, i.e. a component. The input and output nodes are not components: they are perfectly reliable, and are left out of node_names, of the node probabilities the methods need, and of the nodes that can be forced working or broken. The system works when the output node is reached: the input node is always reached, and any other node is reached when it works and at least k of its predecessors are reached, where k is its k-out-of-n value (1 unless set with k). Node names can be any hashable, e.g. strings or integers.

This class holds what depends only on the structure, or on the structure and given per-node probabilities: minimal path and cut sets, the structure function, the exact system probability, structural importance, reliability allocation and serialisation. It is usually used through NonRepairableRBD or RepairableRBD, which add node models (their model argument takes the place of nodes). It can also be built from edges alone to analyse a structure with no models. Probability calculations assume the nodes are independent.

Nodes can also be given a capacity, their throughput while they work, for the capacity analysis (see system_capacity): the system's capacity is then the most that can flow from the input to the output.

The structure is validated on construction. It is infeasible if it has a cycle; if it has not exactly one node with no incoming edges, or not exactly one with no outgoing edges (e.g. a node in nodes that is in no edge); if a k is 0 or greater than the node's number of incoming edges; or if k names a node not in the diagram. The subclasses add that every node in the edges but the input and output has a model, and that every model is for a node in the edges. on_infeasible_rbd sets what then happens: the error or warning lists each finding on a line of its own (a model in no edge with the node it was likely meant for), and the full report is kept in structure_check. On construction the diagram is also reduced to its modules: the series, parallel and k-out-of-n parts, each of which has a closed form, leaving only the rest (e.g. a bridge) to be worked out from its minimal path sets. This keeps large redundant diagrams fast, and a series-parallel diagram never needs its path sets, however many there are (see system_probability). The minimal path and cut sets are found on first use and cached, so repeated evaluations are cheap.

Parameters:

Name Type Description Default
edges Iterable[tuple[Hashable, Hashable]]

The directed edges (from_node, to_node) of the diagram, e.g. [("s", "a"), ("a", "t")] for the single component "a" between input "s" and output "t".

required
nodes Iterable

Node names that must be in the diagram as well as those in edges, by default None. A name that is in no edge is an isolated node, which makes the structure infeasible. (The subclasses check their models' names against the edges instead.)

None
k dict[Any, int]

The k-out-of-n value of nodes, keyed by node name, by default None (every node has k = 1).

None
input_node Any

The input node, by default None: it is found as the node with no incoming edges. If given, it must be that node: a name not in the diagram, or a node with incoming edges, raises a ValueError. It cannot make a diagram with several such nodes feasible.

None
output_node Any

The output node, by default None: it is found as the node with no outgoing edges. The same rules as for input_node apply.

None
on_infeasible_rbd str

What to do if the structure is infeasible, by default "raise": "raise" raises a ValueError, "warn" issues a UserWarning containing structure_check and builds the RBD anyway, and "ignore" builds it silently. An infeasible RBD may give errors or wrong results later.

'raise'
capacity dict[Any, float or dict]

Each node's capacity, keyed by node name, by default None: the throughput it passes while it works, a positive number in any unit (the same for every node); or, for a node that works at several levels, a dict {level: probability} of the levels and the probability of each while it works (adding up to 1). A failed node passes nothing, and a node with no capacity given limits nothing: it passes whatever reaches it while it works (inf). The edges carry any amount, so a series chain passes the least of its nodes' capacities and a parallel group the sum; a k-out-of-n node passes flow only while at least k of its inputs are reached. Only the capacity analysis uses them.

None

Attributes:

Name Type Description
G RBDGraph

The diagram, a networkx.DiGraph subclass; each node's "k" attribute is its k-out-of-n value.

input_node Hashable

The input node, or None if it could not be found (only possible when an infeasible RBD is built with "warn" or "ignore").

output_node Hashable

The output node, or None if it could not be found.

in_or_out list

[input_node, output_node].

nodes list

The intermediate nodes, in the order they were added to the graph (the same list node_names returns).

capacity dict

The capacities given, keyed by node name: floats, or dicts of levels and their probabilities (in increasing order of level).

structure_check dict

The validation report, e.g. "is_valid", "has_cycles", "cycles", "koon_errors", "koon_warnings", "irrelevant_nodes" and "is_too_meshed": whether the core is too meshed to work out exactly, so that only the simulations run (its irrelevant nodes are then not known). The subclasses add their own entries.

Raises:

Type Description
ValueError

If input_node or output_node is not a node of the diagram, or is not its source or sink (whatever on_infeasible_rbd is); if a capacity is not a positive number or a dict of positive levels with probabilities adding up to 1, or is given for the input or output node or a node not in the diagram; if the structure is infeasible and on_infeasible_rbd is "raise" (the message lists what is wrong, one finding a line); or if on_infeasible_rbd is not one of its three values.

Examples:

Two redundant pumps feeding a valve. The subclasses supply the node models; the structure methods come from this class:

>>> from surpyval import FixedEventProbability
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
...     [("s", "p1"), ("s", "p2"), ("p1", "v"), ("p2", "v"), ("v", "t")],
...     {
...         "p1": FixedEventProbability.from_params(0.1),
...         "p2": FixedEventProbability.from_params(0.1),
...         "v": FixedEventProbability.from_params(0.05),
...     },
... )
>>> rbd.input_node, rbd.output_node
('s', 't')
>>> rbd.node_names()
['p1', 'p2', 'v']
>>> sorted(sorted(c) for c in rbd.get_min_cut_sets())
[['p1', 'p2'], ['v']]

The structure alone, with no node models, is enough for structural analysis:

>>> from repyability import RBD
>>> structure = RBD(
...     [("s", "p1"), ("s", "p2"), ("p1", "v"), ("p2", "v"), ("v", "t")]
... )
>>> si = structure.structural_importance()
>>> {k: round(v, 4) for k, v in sorted(si.items())}
{'p1': 0.25, 'p2': 0.25, 'v': 0.75}

find_irrelevant_components

find_irrelevant_components()

Return the nodes that cannot affect whether the system works.

A node is irrelevant when it is in no minimal path set, e.g. a node in parallel with a direct edge, which is a connection that never fails, or with a path through junctions alone (a PerfectReliability node, such as a vote point, never fails either). Whether such a node works never changes whether the system works, so its Birnbaum and structural importance are zero. They are found by the reduction to modules, with the junctions folded in as always working, without listing the path sets. An irrelevant node is still a node of the RBD (it is in node_names); a junction is no component, so it is never listed. The same set is found on construction and stored in structure_check["irrelevant_nodes"], with structure_check["has_irrelevant_nodes"].

Returns:

Type Description
set

The irrelevant node names; empty if every node is relevant.

Raises:

Type Description
ValueError

If no set of working nodes can reach the output node.

Examples:

Node "b" is bypassed by the direct edge from "a" to "t":

>>> from repyability import RBD
>>> rbd = RBD([("s", "a"), ("a", "t"), ("a", "b"), ("b", "t")])
>>> rbd.find_irrelevant_components()
{'b'}

get_all_path_sets

get_all_path_sets()

Iterate over every path from the input node to the output node.

A thin wrapper of networkx.all_simple_paths: each path is a list of node names in order, from the input node to the output node. These are paths of the graph, not reliability path sets: they ignore k-out-of-n values (a path through a node with k > 1 does not by itself make the system work) and need not be minimal. For those, use get_min_path_sets. The number of paths can grow exponentially with the size of the diagram, so avoid exhausting the iterator on very large RBDs.

Returns:

Type Description
Iterator[list[Hashable]]

A generator of paths, each a list of node names.

Examples:

>>> from repyability import RBD
>>> rbd = RBD([("s", "a"), ("s", "b"), ("a", "t"), ("b", "t")])
>>> sorted(rbd.get_all_path_sets())
[['s', 'a', 't'], ['s', 'b', 't']]

get_min_path_sets

get_min_path_sets(include_in_out_nodes=True)

Return the minimal path sets of the RBD.

A path set is a set of nodes whose working is enough for the system to work; it is minimal when no node can be left out. k-out-of-n values are accounted for: a minimal path set through a node with k-out-of-n value k combines path sets reaching k of its predecessors. The sets are expanded from the diagram's modules (see system_probability) on first use and cached; each call returns a new set. Their number can grow exponentially with redundancy (n stages of duplicated units in series have 2 ** n), but nothing else needs them: the system probability, the importance measures and the cut sets are found without listing them. A junction (a node given PerfectReliability, such as a k-out-of-n vote point) always works, so no path set needs it, and none lists it.

Parameters:

Name Type Description Default
include_in_out_nodes bool

Whether each path set includes the input and output nodes, by default True. Pass False for the components only. (The default differs from get_min_cut_sets.)

True

Returns:

Type Description
set[frozenset[Hashable]]

The minimal path sets, each a frozenset of node names.

Raises:

Type Description
ValueError

If there is no path set, i.e. no set of working nodes can reach the output node (e.g. a k greater than the node's number of incoming edges).

Examples:

A 2-out-of-3 vote "v" needs two of "a", "b" and "c":

>>> from repyability import RBD
>>> rbd = RBD(
...     [
...         ("s", "a"), ("s", "b"), ("s", "c"),
...         ("a", "v"), ("b", "v"), ("c", "v"), ("v", "t"),
...     ],
...     k={"v": 2},
... )
>>> paths = rbd.get_min_path_sets(include_in_out_nodes=False)
>>> sorted(sorted(p) for p in paths)
[['a', 'b', 'v'], ['a', 'c', 'v'], ['b', 'c', 'v']]

is_system_working

is_system_working(component_status, method)

Return whether the system works, given which components work.

This is the structure function. It is evaluated through the diagram's modules (see system_probability): a series module works when all of its members do, a parallel one when any does, a k-out-of-n one when at least k do, and what is left (e.g. a bridge) through its minimal path sets (method="p": some path set has every member working) or cut sets (method="c": every cut set has a member working). Both give the same answer; "p" is typically faster as it does not need the cut sets. The input and output nodes need no entry. The layout is worked out once, so repeated calls (as in the simulations) are cheap.

Parameters:

Name Type Description Default
component_status dict[Any, bool]

Whether each component is working (truthy) or failed (falsy), keyed by node name. Every node in a minimal path set needs an entry; other keys are ignored.

required
method str

"p" (path sets) or "c" (cut sets). There is no default.

required

Returns:

Type Description
bool

True if the system is working, otherwise False.

Raises:

Type Description
ValueError

If method is not "p" or "c".

KeyError

If a node needed for the evaluation has no entry in component_status.

Examples:

Two parallel nodes "a" and "b" in series with "c":

>>> from repyability import RBD
>>> rbd = RBD(
...     [("s", "a"), ("s", "b"), ("a", "c"), ("b", "c"), ("c", "t")]
... )
>>> rbd.is_system_working({"a": False, "b": True, "c": True}, "p")
True
>>> rbd.is_system_working({"a": True, "b": True, "c": False}, "c")
False

system_timeline

system_timeline(timelines)

The system's up/down history from its components': the structure function followed over time.

The components' timelines are merged up the diagram's modules (see system_probability): a series module is up while all its members are, a parallel one while any is, a k-out-of-n one while at least k are, and what is left (e.g. a bridge) while one of its minimal path sets is all up. Each change of the system's timeline keeps its cause, the component whose change made it, and whether that change was planned; changes at the same time are taken in the order of the diagram's components, each one's in its own order (see repyability.timelines).

It takes any timelines: an outage log, a what-if edit of one, or simulated histories (see RepairableRBD.simulate_timelines).

Parameters:

Name Type Description Default
timelines Mapping

{node: timeline}: a Timeline or Timelines for each component the system depends on (all over one window; Timelines with as many histories each, and a Timeline among them standing for each history). A drawing junction (a perfectly reliable node) left out is up throughout; a component no path set needs is ignored.

required

Returns:

Type Description
Timeline or Timelines

The system's: a Timelines if any component's is one, else a Timeline. Its causes are node names.

Raises:

Type Description
ValueError

If a component the system depends on has no timeline, a key is not a component (the input or output node, a repeated node's copy, or an unknown name), or the timelines do not share a window or a number of histories.

TypeError

If timelines is not a mapping of Timeline or Timelines objects.

Examples:

Two pumps in parallel feeding a valve, from their outage logs:

>>> from repyability import RBD, Timeline
>>> rbd = RBD(
...     [("s", "a"), ("s", "b"), ("a", "v"), ("b", "v"), ("v", "t")]
... )
>>> logs = {
...     "a": Timeline.from_outages([(10, 30)], end=100),
...     "b": Timeline.from_outages([(20, 25), (60, 70)], end=100),
...     "v": Timeline.from_outages([(80, 81)], end=100),
... }
>>> plant = rbd.system_timeline(logs)
>>> plant.down_intervals.tolist()
[[20.0, 25.0], [80.0, 81.0]]
>>> plant.failures_by_cause()
{'a': 0, 'b': 1, 'v': 1}

minimal_path_sets

minimal_path_sets()

The minimal path sets of the components (no input or output node), as a list, smallest first (then by name): the same as get_min_path_sets (include_in_out_nodes=False), in the form a FaultTree's minimal_path_sets gives, so the same code runs on either.

Returns:

Type Description
list of frozenset

The minimal path sets.

Examples:

>>> from repyability import RBD
>>> rbd = RBD(
...     [("s", "a"), ("s", "b"), ("a", "c"), ("b", "c"), ("c", "t")]
... )
>>> [sorted(p) for p in rbd.minimal_path_sets()]
[['a', 'c'], ['b', 'c']]

minimal_cut_sets

minimal_cut_sets()

The minimal cut sets of the components, as a list, smallest first (then by name): the same as get_min_cut_sets(), in the form a FaultTree's minimal_cut_sets gives, so the same code runs on either.

Returns:

Type Description
list of frozenset

The minimal cut sets.

Examples:

>>> from repyability import RBD
>>> rbd = RBD(
...     [("s", "a"), ("s", "b"), ("a", "c"), ("b", "c"), ("c", "t")]
... )
>>> [sorted(c) for c in rbd.minimal_cut_sets()]
[['c'], ['a', 'b']]

get_min_cut_sets

get_min_cut_sets(include_in_out_nodes=False)

Return the minimal cut sets of the RBD.

A cut set is a set of nodes whose failure is enough for the system to fail; it is minimal when no node can be left out. The minimal cut sets are the minimal transversals (hitting sets) of the minimal path sets, and k-out-of-n values are accounted for. They are built from the diagram's modules without listing the path sets: a series module's cut sets are its members', a parallel module's combine one of each member's, and a k-out-of-n module's combine one of each of n - k + 1 members'; the part that is not series-parallel (e.g. a bridge) has its own read off the exact engine's Shannon decomposition (see minimal_cut_sets_from_path_sets in this module). They are worked out on first use and cached; each call returns a new set. A junction (a node given PerfectReliability, such as a k-out-of-n vote point) never fails, so a cut set with one never happens, and none is listed.

Parameters:

Name Type Description Default
include_in_out_nodes bool

Whether to derive the cut sets from the path sets including the input and output nodes, by default False (components only). If True, the input and output nodes each appear as an extra single-node cut set. (The default differs from get_min_path_sets.)

False

Returns:

Type Description
set[frozenset[Hashable]]

The minimal cut sets, each a frozenset of node names.

Raises:

Type Description
ValueError

If no set of working nodes can reach the output node.

Examples:

Two parallel nodes "a" and "b" in series with "c":

>>> from repyability import RBD
>>> rbd = RBD(
...     [("s", "a"), ("s", "b"), ("a", "c"), ("b", "c"), ("c", "t")]
... )
>>> sorted(sorted(c) for c in rbd.get_min_cut_sets())
[['a', 'b'], ['c']]
>>> cuts = rbd.get_min_cut_sets(include_in_out_nodes=True)
>>> sorted(sorted(c) for c in cuts)
[['a', 'b'], ['c'], ['s'], ['t']]

path_set_probabilities

path_set_probabilities(node_probabilities)

Return the probability that each minimal path set fully works.

For each minimal path set (components only) this is the product of its nodes' probabilities, i.e. the probability that all of them work, assuming independent nodes. The path sets share nodes, so these values do not add up to the system probability: use system_probability for that.

Parameters:

Name Type Description Default
node_probabilities dict

The probability that each node works (not a time: a reliability at some time, or an availability), keyed by node name, as floats or numpy arrays of equal length (not lists). Every node in a minimal path set needs an entry; other keys are ignored.

required

Returns:

Type Description
ndarray

One value per minimal path set, in the order minimal_path_sets lists them (smallest first, then by name), so the two pair up. The shape is (n_path_sets,) for float probabilities and (n_path_sets, n) for arrays of length n.

Raises:

Type Description
ValueError

If a node in a minimal path set has no entry.

Examples:

Two parallel nodes "a" and "b" in series with "c" have the minimal path sets {a, c} and {b, c}:

>>> from repyability import RBD
>>> rbd = RBD(
...     [("s", "a"), ("s", "b"), ("a", "c"), ("b", "c"), ("c", "t")]
... )
>>> probs = rbd.path_set_probabilities({"a": 0.9, "b": 0.8, "c": 0.95})
>>> sorted(round(float(p), 4) for p in probs)
[0.76, 0.855]

system_probability

system_probability(node_probabilities, method='p')

Return the exact system probability from each node's probability.

Given the probability that each node works -- e.g. its reliability at some time or its availability -- this returns the probability that the system works, assuming the nodes are independent. It is the engine behind the subclasses' reliability, availability and importance calculations.

The result is exact. On construction the diagram is reduced to modules, each with a closed form: a series chain works with probability p1 * p2 * ..., a parallel group with 1 - (1 - p1) * (1 - p2) * ..., and a k-out-of-n group by summing over how many of its members work. Whatever is not series-parallel (e.g. a bridge) is left as a core over modules and single nodes, and is expanded by a Shannon (pivotal) decomposition over its minimal path sets, with repeated sub-problems solved once. So a series-parallel diagram is evaluated without listing its path sets, however many there are (n stages of duplicated units in series have 2 ** n), and only the core pays the combinatorial price. Both depend only on the structure, so they are worked out once and reused.

With method="p" the probability that the system works is computed, and with method="c" the probability that it fails, whose complement is returned. Both give the same result (up to rounding); "p" is the default. Every step is a sum of products of node probabilities and their complements, so the probability computed keeps its full relative precision however close to 0 it is.

Parameters:

Name Type Description Default
node_probabilities Dict

The probability that each node works, keyed by node name: floats, or 1-d arrays (e.g. one value per time) that all have the same length. Every intermediate node needs an entry; entries for the input and output nodes, and any other keys, are not used. The dict is not modified.

required
method str

"p" or "paths" (the default), or "c" or "cuts": whether to compute the probability that the system works or that it fails.

'p'

Returns:

Type Description
ndarray

The system probability, a 1-d array with one value per element of the node arrays. It is always an array: float inputs give a one-element array.

Raises:

Type Description
ValueError

If method is not "p" or "c", or the node probability arrays are not all the same length.

KeyError

If an intermediate node has no entry in node_probabilities.

Examples:

Two parallel nodes "a" and "b" in series with "c" work with probability (1 - 0.1 * 0.2) * 0.95:

>>> from repyability import RBD
>>> rbd = RBD(
...     [("s", "a"), ("s", "b"), ("a", "c"), ("b", "c"), ("c", "t")]
... )
>>> p = rbd.system_probability({"a": 0.9, "b": 0.8, "c": 0.95})
>>> round(float(p[0]), 4)
0.931

Arrays give one system probability per element:

>>> import numpy as np
>>> p = rbd.system_probability(
...     {
...         "a": np.array([0.9, 0.5]),
...         "b": np.array([0.8, 0.5]),
...         "c": np.array([0.95, 0.5]),
...     }
... )
>>> [round(float(v), 4) for v in p]
[0.931, 0.375]

system_capacity

system_capacity(node_probabilities)

Return the exact distribution of the system's capacity, from each node's probability of working.

Each node carries its capacity while it works and nothing once it has failed (a node given none limits nothing), and the system's capacity is the most that can flow from the input to the output: the maximum flow of the diagram, which is the least total capacity of a cut. A series chain carries the least of its nodes' capacities and a parallel group their sum, and a k-out-of-n node passes flow only while at least k of its inputs are reached, so the capacity is positive exactly when the system works. With three pumps of half the demand each, one failure costs nothing and two cost half the output.

The distribution is exact, over every combination of the nodes' states, and assumes they are independent. It is worked out as the system probability is: each series, parallel or k-out-of-n module's distribution in closed form from its members' (the least of their capacities, their sum, their sum while at least k work), and what is left (e.g. a bridge) by conditioning on its parts' capacities one at a time. So it costs about what the system probability does. The subclasses give it the node probabilities: NonRepairableRBD.capacity_distribution at a time, and RepairableRBD.capacity_distribution in the long run.

Parameters:

Name Type Description Default
node_probabilities Dict

The probability that each node works, keyed by node name: floats, or 1-d arrays (e.g. one value per time) that all have the same length. Every intermediate node needs an entry; other keys are not used.

required

Returns:

Type Description
CapacityDistribution

The capacities the system can have and their probabilities: one per level for float probabilities, and one row per level and one column per element for arrays.

Raises:

Type Description
ValueError

If no node has a capacity, the diagram is not a valid RBD, the probability arrays are not all the same length, or a node takes its capacity from its model (a DegradingNode, or a nested RBD with capacities), which a probability of working does not describe.

KeyError

If an intermediate node has no entry in node_probabilities.

Examples:

Two pumps of 60 and 40 in parallel, feeding a pipe that carries 80:

>>> from repyability import RBD
>>> plant = RBD(
...     [("s", "p1"), ("s", "p2"), ("p1", "pipe"), ("p2", "pipe"),
...      ("pipe", "t")],
...     capacity={"p1": 60, "p2": 40, "pipe": 80},
... )
>>> up = {"p1": 0.9, "p2": 0.8, "pipe": 1.0}
>>> capacity = plant.system_capacity(up)
>>> capacity.levels.tolist()
[0.0, 40.0, 60.0, 80.0]
>>> capacity.probabilities.round(4).tolist()
[0.02, 0.08, 0.18, 0.72]

The pipe, not the pumps, limits the plant when both run.

improvement_allocation

improvement_allocation(target, node_probabilities, fixed=None, weights=None)

Improve the node probabilities just enough to meet a target.

Reliability allocation by a common improvement: every node that is not fixed has its unreliability 1 - p multiplied by exp(-x * w), where w is the node's weight and x is one scale factor shared by all nodes,

p_new = 1 - (1 - p) * exp(-x * w),

and x is solved for (by a bracketed root search) so that system_probability of the new probabilities equals target. With the default equal weights every free node's unreliability shrinks by the same factor, keeping their ratios. A larger weight changes a node more; a weight of 0 leaves it unchanged, as does a probability of exactly 1.

A target below the current system probability gives x < 0, which increases the unreliabilities, each capped at 1 (a node probability of 0): the result is then the lowest node probabilities that still meet the target. A target the scaling cannot reach (e.g. because fixed nodes limit the system probability) raises a ValueError giving the reachable range. A target equal to the best reachable probability gives the free nodes probability 1. The solver's result is stored on the RBD as res, a scipy OptimizeResult whose x holds x (inf when the free nodes are made perfect), replacing any earlier one.

Parameters:

Name Type Description Default
target float

The system probability to reach, in [0, 1].

required
node_probabilities Dict

The current probability that each node works, as a single float in [0, 1] per node, keyed by node name. An intermediate node with no entry starts at 0.5. Any other keys (e.g. the input and output nodes) are scaled like the rest and returned.

required
fixed list

Nodes whose probability is not changed, by default None (every node may change).

None
weights dict

A weight per node, by default None (1.0 for every node). If given, it needs an entry for every node that is not fixed.

None

Returns:

Type Description
dict

The allocated probability (a float) of every key of node_probabilities, and of any intermediate node that was missing from it. fixed nodes keep their probability.

Raises:

Type Description
ValueError

If target is above 1 or below 0, if it cannot be reached, or if a node probability is not a single value in [0, 1].

KeyError

If weights is given without an entry for a node that is not fixed.

Examples:

Raising two parallel nodes "a" and "b" in series with "c" from a system probability of 0.931 to 0.99:

>>> from repyability import RBD
>>> rbd = RBD(
...     [("s", "a"), ("s", "b"), ("a", "c"), ("b", "c"), ("c", "t")]
... )
>>> current = {"a": 0.9, "b": 0.8, "c": 0.95}
>>> new = rbd.improvement_allocation(0.99, current)
>>> {k: round(v, 4) for k, v in sorted(new.items())}
{'a': 0.9814, 'b': 0.9627, 'c': 0.9907}
>>> round(float(rbd.system_probability(new)[0]), 4)
0.99

Every unreliability was cut by the same factor:

>>> sorted({round((1 - new[k]) / (1 - current[k]), 4) for k in new})
[0.1863]

equal_allocation

equal_allocation(target)

Give every node the same probability, chosen to meet a target.

Finds the single probability p that, given to every intermediate node, makes system_probability equal target: e.g. target ** (1 / n) for n nodes in series and 1 - (1 - target) ** (1 / n) for n in parallel. Any node models are ignored. It runs improvement_allocation from 0.5 for every node, with equal weights and nothing fixed, so the solver's result is stored on the RBD as res. A target of 1 gives every node probability 1.

Parameters:

Name Type Description Default
target float

The system probability to reach, in [0, 1].

required

Returns:

Type Description
dict

The allocated probability (a float, the same for every node), keyed by intermediate node name.

Raises:

Type Description
ValueError

If target is above 1 or below 0.

Examples:

Three nodes in series each need 0.9 ** (1 / 3):

>>> from repyability import RBD
>>> rbd = RBD([("s", 1), (1, 2), (2, 3), (3, "t")])
>>> new = rbd.equal_allocation(0.9)
>>> {k: round(v, 4) for k, v in sorted(new.items())}
{1: 0.9655, 2: 0.9655, 3: 0.9655}

simple_allocation

simple_allocation(target, weights=None)

Meet a target with the smallest change in the node log-odds.

Starting from 0.5 for every node, finds the node probabilities that meet the target with the least weighted change on the log-odds scale. With s_i = log(p_i / (1 - p_i)) a node's log-odds (0 at 0.5) and w_i its weight, it minimises sum_i s_i ** 2 / w_i subject to system_probability equalling target. Any node models are ignored.

At the solution each node's change is proportional to its weight times the sensitivity of the system's log-odds to it, p_i (1 - p_i) I_B(i) / (R (1 - R)) with I_B(i) its Birnbaum importance: nodes that matter more to the system (e.g. a node in series with a redundant pair), and nodes with larger weights, move further from 0.5 (at equal sensitivity, twice the weight moves a node twice as far), and a node with weight 0 stays at 0.5. With equal weights, nodes placed symmetrically (e.g. all in series, or all in parallel) get the same value, as equal_allocation gives.

It is a structural allocation: it uses no component data, only the diagram, the target and the weights. With every node at 0.5 the Birnbaum importance equals the structural_importance, so a small change moves each node in proportion to its weight times its structural importance; for larger changes the importances are re-evaluated at the new probabilities. It is not one of the classic named methods; see cost_based_allocation and minimum_effort_allocation for those.

It is solved with scipy.optimize.minimize (trust-constr), from the point that moves every node in proportion to its weight just far enough to meet the target, with exact gradients; the system probability is handled on the log-odds scale from both ends of the exact engine, so it stays exact at any size. A target of 0 or 1 is approached to within 1e-12. The solver's result is stored on the RBD as res, replacing any earlier one; its x holds the log-odds of the nodes with a positive weight, in node order.

Parameters:

Name Type Description Default
target float

The system probability to reach, in [0, 1].

required
weights dict

A non-negative weight per intermediate node, by default None (1.0 for every node). If given, it needs an entry for every intermediate node.

None

Returns:

Type Description
dict

The allocated probability (a float) of every intermediate node, keyed by node name.

Raises:

Type Description
ValueError

If target is above 1 or below 0; if it cannot be reached because nodes with weight 0 hold the system back (the message gives the reachable range); or if a weight is negative or not finite.

KeyError

If weights is given without an entry for an intermediate node.

Examples:

With two parallel nodes "a" and "b" in series with "c", the series node "c" moves furthest:

>>> from repyability import RBD
>>> rbd = RBD(
...     [("s", "a"), ("s", "b"), ("a", "c"), ("b", "c"), ("c", "t")]
... )
>>> new = rbd.simple_allocation(0.99)
>>> {k: round(v, 4) for k, v in sorted(new.items())}
{'a': 0.9395, 'b': 0.9395, 'c': 0.9936}
>>> round(float(rbd.system_probability(new)[0]), 4)
0.99

minimum_effort_allocation

minimum_effort_allocation(target, node_probabilities)

Albert's minimum-effort allocation for a series system.

The minimization-of-effort algorithm (Albert, 1958; MIL-HDBK-338B) raises a series system to target with the least total effort: with the current probabilities in ascending order R_1 <= ... <= R_n, it raises the k least reliable nodes to one common level

R_0 = (target / (R_{k+1} * ... * R_n)) ** (1 / k),

where k is the largest j with R_j below (target / (R_{j+1} * ... * R_n)) ** (1 / j), and leaves the other nodes unchanged. The result holds for any effort function the nodes share that meets Albert's conditions (the effort of raising a reliability from x to y is non-negative, grows with y and adds up over successive steps, among others), such as y - x or log((1 - x) / (1 - y)): the answer does not depend on which. It needs no solver, so res is not changed.

Parameters:

Name Type Description Default
target float

The system probability to reach, in [0, 1].

required
node_probabilities Dict

The current probability that each intermediate node works (a single value in [0, 1] per node), keyed by node name. Every intermediate node needs an entry; other keys are not used.

required

Returns:

Type Description
dict

The allocated probability (a float) of every intermediate node, keyed by node name. A target the system already meets returns the current probabilities.

Raises:

Type Description
ValueError

If target is above 1 or below 0; if the diagram is not a series system (one path through every intermediate node; use cost_based_allocation for other structures); or if a node has no probability, or one that is not a single value in [0, 1].

References

A. Albert, "A measure of the effort required to increase reliability", Technical Report No. 43, Applied Mathematics and Statistics Laboratory, Stanford University, 1958.

MIL-HDBK-338B, Electronic Reliability Design Handbook, 1998: "Minimization of effort algorithm".

Examples:

The two weakest of four nodes in series are raised together; the others are already good enough:

>>> from repyability import RBD
>>> rbd = RBD([("s", "a"), ("a", "b"), ("b", "c"), ("c", "d"),
...            ("d", "t")])
>>> current = {"a": 0.7, "b": 0.8, "c": 0.9, "d": 0.95}
>>> new = rbd.minimum_effort_allocation(0.6, current)
>>> {k: round(v, 4) for k, v in new.items()}
{'a': 0.8377, 'b': 0.8377, 'c': 0.9, 'd': 0.95}
>>> round(float(rbd.system_probability(new)[0]), 4)
0.6

cost_based_allocation

cost_based_allocation(target, node_probabilities, max_probabilities=None, feasibility=None)

Mettas's cost-based allocation: the cheapest way to a target.

Reliability allocation as an optimisation (Mettas, 2000): find the node probabilities R_i that meet the system target at the least total cost sum_i c_i(R_i), where each node's cost of improvement is

c_i(R_i) = exp((1 - f_i) * (R_i - R_min_i) / (R_max_i - R_i)),

R_min_i is its current probability, R_max_i the most it can reach and f_i in [0, 1) its feasibility: how easily it can be improved relative to the others (the cost rises faster for a lower f_i). The cost is 1 at the current probability and grows without bound towards the maximum, so every node stays within its bounds and none reaches its maximum. It works on any structure: the system probability is exact, and the improvement goes where it is cheapest per unit of system probability, which depends on each node's importance, current value, maximum and feasibility.

It is solved with scipy.optimize.minimize (SLSQP), from the point where every node has closed the same fraction of its gap to its maximum, using exact gradients. The system probability is handled on the log-odds scale, computed from both ends of the exact engine, and the cost as the log of its sum, so neither loses precision nor overflows, however near 0 or 1 the probabilities are. The allocation returned meets the target. The solver's result is stored on the RBD as res, replacing any earlier one; its fun is the log of the total cost of the nodes allowed to change.

Parameters:

Name Type Description Default
target float

The system probability to reach, in [0, 1].

required
node_probabilities Dict

The current probability that each intermediate node works (a single value in [0, 1] per node), keyed by node name: the lower bound R_min. Every intermediate node needs an entry; other keys are not used.

required
max_probabilities dict

The most each node's probability can be raised to, R_max (at least its current probability; equal to it holds the node fixed). A node without an entry can approach 1.

None
feasibility dict

Each node's feasibility f in [0, 1): higher is easier to improve. A node without an entry gets 0.5.

None

Returns:

Type Description
dict

The allocated probability (a float) of every intermediate node, keyed by node name. A target the system already meets returns the current probabilities.

Raises:

Type Description
ValueError

If target is above 1 or below 0; if it cannot be reached (the message gives the reachable range: the maximum is only approached, at an ever-growing cost); if a node has no probability, or one that is not a single value in [0, 1]; if a maximum is below its node's current probability or a feasibility is outside [0, 1); or if max_probabilities or feasibility names a node that is not an intermediate node.

Warns:

Type Description
UserWarning

If the solver stops before converging; the allocation returned still meets the target, but may not be the cheapest.

References

A. Mettas, "Reliability allocation and optimization for complex systems", Proceedings of the Annual Reliability and Maintainability Symposium, 2000, pp. 216-221.

Examples:

Two parallel pumps in series with a valve, all at 0.9, to reach 0.99. The valve is on every path, so it must reach 0.99 itself; the cheapest allocation takes it only a little further and raises the pumps to supply the rest:

>>> from repyability import RBD
>>> rbd = RBD([("s", "p1"), ("s", "p2"), ("p1", "v"), ("p2", "v"),
...            ("v", "t")])
>>> current = {"p1": 0.9, "p2": 0.9, "v": 0.9}
>>> new = rbd.cost_based_allocation(0.99, current)
>>> {k: round(v, 4) for k, v in new.items()}
{'p1': 0.9808, 'p2': 0.9808, 'v': 0.9904}

A valve that is harder to improve stops even closer to 0.99, and the pumps do more:

>>> new = rbd.cost_based_allocation(
...     0.99, current, feasibility={"v": 0.1}
... )
>>> {k: round(v, 4) for k, v in new.items()}
{'p1': 0.9896, 'p2': 0.9896, 'v': 0.9901}

node_names

node_names()

Return the names of the intermediate (component) nodes.

Every node of the diagram except the input and output nodes, in the order the nodes were added to the graph (for a feasible RBD, the order of first appearance in edges). Irrelevant nodes are included. In a NonRepairableRBD a repeated node is the component it repeats, so only the latter is listed.

Returns:

Type Description
list[Hashable]

The intermediate node names, as a new list.

Examples:

>>> from repyability import RBD
>>> RBD([("s", "a"), ("a", "b"), ("b", "t")]).node_names()
['a', 'b']

structural_importance

structural_importance(working_nodes=None, broken_nodes=None)

Return the structural (probability-free) importance of every node.

The fraction of the states of the other nodes in which the node is pivotal -- the system works when the node works and fails when it fails, holding the others fixed. A perfect junction node (a PerfectReliability drawing device, such as a k-out-of-n vote) is always working, and is left out. It is computed exactly as the Birnbaum importance with every node probability at 1/2, so it depends only on the RBD's structure and not on any failure model -- useful at design time, before any life data exists. It is the same for a NonRepairableRBD and a RepairableRBD with the same diagram, and a common-cause group does not change it.

Parameters:

Name Type Description Default
working_nodes Iterable[Hashable]

Nodes to condition on as working (probability 1), by default None.

None
broken_nodes Iterable[Hashable]

Nodes to condition on as failed (probability 0), by default None. With either, the fraction is taken over the states of the nodes that are not forced. A forced node's own importance is still computed, given the other forced nodes.

None

Returns:

Type Description
dict[Any, float]

The structural importance of each intermediate node, in [0, 1], keyed by node name.

Raises:

Type Description
ValueError

If a node is in both working_nodes and broken_nodes, is the input or output node, or is not an intermediate node of the RBD. A NonRepairableRBD also rejects a repeated node.

Examples:

In a two-component parallel system each node is pivotal in half of the other node's states, independent of any failure model:

>>> from surpyval import FixedEventProbability
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
...     [("s", "a"), ("s", "b"), ("a", "t"), ("b", "t")],
...     {
...         "a": FixedEventProbability.from_params(0.1),
...         "b": FixedEventProbability.from_params(0.4),
...     },
... )
>>> si = rbd.structural_importance()
>>> {k: round(v, 4) for k, v in sorted(si.items())}
{'a': 0.5, 'b': 0.5}

Given that "a" has failed, "b" is pivotal in every state ("a"'s own value is unchanged, as it is taken over "b"):

>>> si = rbd.structural_importance(broken_nodes=["a"])
>>> {k: round(v, 4) for k, v in sorted(si.items())}
{'a': 0.5, 'b': 1.0}

to_dict

to_dict()

Serialise the RBD to a JSON-friendly dict.

The dict holds the RBD's type and the repyability_version that wrote it, plus the constructor inputs as given: the edges, the node models (or components), the k-out-of-n values and capacities, the input and output nodes, on_infeasible_rbd, and the common-cause groups or the downtime cost rate. from_dict rebuilds the RBD by calling its constructor again, so the round trip is faithful even for repeated nodes. Node models are serialised structurally: surpyval's parametric models in its own format, so an offset, p, f0 and a fit's covariance round-trip; the RePyability node models (standby, repeated, load-sharing, regression, NonRepairable, PerfectReliability and PerfectUnreliability) and nested RBDs recursively. Per-node values are stored as lists of entries, so integer and string node names both survive JSON. Only a NonRepairableRBD or RepairableRBD can be serialised.

Returns:

Type Description
dict

The serialised RBD.

Raises:

Type Description
NotImplementedError

If a node model cannot be serialised: one that is neither a surpyval model nor a RePyability node model.

AttributeError

If called on a bare RBD, which keeps no constructor inputs.

Examples:

>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
...     [("s", "c"), ("c", "t")],
...     {"c": surv.Weibull.from_params([100, 2])},
... )
>>> d = rbd.to_dict()
>>> d["type"], d["edges"]
('NonRepairableRBD', [['s', 'c'], ['c', 't']])
>>> saved = d["reliabilities"][0]["model"]  # surpyval's own format
>>> saved["kind"], saved["model"]["distribution"]
('surpyval', 'Weibull')

to_json

to_json(fp=None, **json_kwargs)

Serialise the RBD to JSON: a string, or written to a file.

Equivalent to json.dumps(self.to_dict(), **json_kwargs); see to_dict for what is stored. String, integer and tuple node names all survive: JSON turns a tuple into a list, and loading turns it back. With fp it is written there, as surpyval's models' to_json(fp) writes them.

Parameters:

Name Type Description Default
fp (str, PathLike or file)

A path, or a file opened for writing, to write the document to; by default None: it is returned.

None
**json_kwargs

Passed to json.dumps, e.g. indent=2.

{}

Returns:

Type Description
str or None

The JSON document, or None once written to fp.

Raises:

Type Description
NotImplementedError

If a node model cannot be serialised (see to_dict).

AttributeError

If called on a bare RBD.

TypeError

If a value cannot be encoded as JSON, e.g. a node name that JSON has no type for.

Examples:

>>> import json
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
...     [("s", "c"), ("c", "t")],
...     {"c": surv.Weibull.from_params([100, 2])},
... )
>>> json.loads(rbd.to_json(indent=2))["type"]
'NonRepairableRBD'

from_dict classmethod

from_dict(d)

Reconstruct an RBD from the output of to_dict.

The RBD is rebuilt by calling its constructor with the stored inputs, so it is validated again and is equivalent to the original. Called on a specific subclass, the document's type must match; called on RBD it dispatches to whichever type the document names.

Parameters:

Name Type Description Default
d dict

A dict made by to_dict (possibly after a JSON round trip).

required

Returns:

Type Description
RBD

The reconstructed NonRepairableRBD or RepairableRBD.

Raises:

Type Description
ValueError

If called on a subclass and d["type"] is not that subclass; if the RBD type or a node model kind is unknown; or if the constructor rejects the stored inputs.

KeyError

If a required entry is missing, e.g. "type" when called on RBD.

Examples:

The structure round-trips through a plain dict, so reliability is preserved:

>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
...     [("s", "c"), ("c", "t")],
...     {"c": surv.Weibull.from_params([100, 2])},
... )
>>> restored = NonRepairableRBD.from_dict(rbd.to_dict())
>>> round(restored.sf(50), 4) == round(rbd.sf(50), 4)
True

from_json classmethod

from_json(s)

Reconstruct an RBD from a JSON document made by to_json.

Equivalent to cls.from_dict(json.loads(s)), so the same type rules apply (see from_dict). s can be the document's text or, as surpyval's from_json takes, a path to the file holding it.

Parameters:

Name Type Description Default
s (str, PathLike or file)

A JSON document made by to_json: its text, a path to a file holding it, or a file opened for reading.

required

Returns:

Type Description
RBD

The reconstructed NonRepairableRBD or RepairableRBD.

Raises:

Type Description
ValueError

If s is not valid JSON (json.JSONDecodeError is a ValueError), or for the reasons given in from_dict.

FileNotFoundError

If s is neither a JSON document nor the path of a file.

KeyError

If a required entry is missing (see from_dict).

Examples:

RBD.from_json returns whichever RBD type the document names:

>>> import surpyval as surv
>>> from repyability import RBD, NonRepairableRBD
>>> rbd = NonRepairableRBD(
...     [("s", "c"), ("c", "t")],
...     {"c": surv.Weibull.from_params([100, 2])},
... )
>>> restored = RBD.from_json(rbd.to_json())
>>> type(restored).__name__
'NonRepairableRBD'
>>> round(restored.sf(50), 4)
0.7788