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 |
required |
nodes
|
Iterable
|
Node names that must be in the diagram as well as those in
|
None
|
k
|
dict[Any, int]
|
The k-out-of-n value of nodes, keyed by node name, by default None
(every node has |
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 |
None
|
on_infeasible_rbd
|
str
|
What to do if the structure is infeasible, by default |
'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 |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
G |
RBDGraph
|
The diagram, a |
input_node |
Hashable
|
The input node, or None if it could not be found (only possible
when an infeasible RBD is built with |
output_node |
Hashable
|
The output node, or None if it could not be found. |
in_or_out |
list
|
|
nodes |
list
|
The intermediate nodes, in the order they were added to the graph
(the same list |
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. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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
|
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 |
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
|
|
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the system is working, otherwise False. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
KeyError
|
If a node needed for the evaluation has no entry in
|
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
|
|
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 |
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
|
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
|
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'
|
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 |
KeyError
|
If an intermediate node has no entry in |
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 |
KeyError
|
If an intermediate node has no entry in |
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 |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
The allocated probability (a float) of every key of
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
KeyError
|
If |
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 |
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 |
KeyError
|
If |
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 |
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 |
required |
max_probabilities
|
dict
|
The most each node's probability can be raised to, |
None
|
feasibility
|
dict
|
Each node's feasibility |
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 |
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
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If a node is in both |
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 |
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 |
{}
|
Returns:
| Type | Description |
|---|---|
str or None
|
The JSON document, or None once written to |
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
If a node model cannot be serialised (see
|
AttributeError
|
If called on a bare |
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 |
required |
Returns:
| Type | Description |
|---|---|
RBD
|
The reconstructed |
Raises:
| Type | Description |
|---|---|
ValueError
|
If called on a subclass and |
KeyError
|
If a required entry is missing, e.g. |
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 |
required |
Returns:
| Type | Description |
|---|---|
RBD
|
The reconstructed |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
FileNotFoundError
|
If |
KeyError
|
If a required entry is missing (see |
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