NonRepairableRBD¶
NonRepairableRBD ¶
NonRepairableRBD(edges, reliabilities, k=None, input_node=None, output_node=None, on_infeasible_rbd='raise', ccf_groups=None, capacity=None)
Bases: RBD
A reliability block diagram (RBD) of non-repairable components.
The diagram is a directed acyclic graph with exactly one input node (no
predecessors) and one output node (no successors). Every other node is
a component with a reliability model, and the system works while a
path of working components joins the input to the output (k makes
a node k-out-of-n). Components fail independently, except within the
common-cause groups given in ccf_groups.
The system reliability sf is
computed exactly from the node reliabilities, by a decomposition of
the structure into modules (see the guide's concepts); the other
analytic methods build on it. The mean
time to failure mean integrates
it (method="simulate" estimates it from simulated lifetimes
instead), and random draws
lifetimes for the simulations. analysis_routes() says how each
analysis is computed.
A node model can be:
- a surpyval distribution: parametric (e.g.
surpyval.Weibull.from_params([100, 2])or a fitted model), aMixtureModel, a fitted Wiener or gamma degradation process (its time to the threshold), or a fixed per-demand probability (surpyval.FixedEventProbability); not a non-parametric fit (e.g. asurpyval.KaplanMeierfit), which is refused: fit a parametric distribution in surpyval; - a composite node: a
StandbyModel,LoadSharingModel,RepeatedNode,RepeatedStandbyNode,RegressionNodeor anotherNonRepairableRBDnested as a single node; - one unit of a fitted surpyval joint model (readings and failures),
given its readings: its
DynamicPrediction(predict), or aJointNode, whose reliability is from now; PerfectReliabilityorPerfectUnreliability.
If every node is a fixed probability the RBD is fixed-probability (see
is_fixed) and the time
argument of its methods may be omitted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
edges
|
Iterable[tuple[Hashable, Hashable]]
|
The directed edges |
required |
reliabilities
|
dict
|
|
required |
k
|
dict[Any, int]
|
|
None
|
input_node
|
Hashable
|
The input node. By default it is inferred as the unique node with no predecessors; naming it does not relax that requirement. A name not in the diagram, or a node with predecessors, raises a ValueError. |
None
|
output_node
|
Hashable
|
The output node. By default it is inferred as the unique node with no successors; naming it does not relax that requirement. A name not in the diagram, or a node with successors, raises a ValueError. |
None
|
on_infeasible_rbd
|
str
|
What to do if the diagram is invalid: |
'raise'
|
ccf_groups
|
Iterable[CCFGroup]
|
Common-cause failure groups ( |
None
|
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 a dict |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
reliabilities |
dict
|
|
repeated |
dict
|
|
ccf_groups |
list[CCFGroup]
|
The validated common-cause groups. |
capacity |
dict
|
The capacities given, keyed by node name, as floats. |
nodes |
list
|
The component nodes: every node except the input and output nodes and the repeated nodes (each is the component it repeats). |
input_node |
Hashable
|
The input node. |
output_node |
Hashable
|
The output node. |
structure_check |
dict
|
The findings of the structural validation, e.g. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
Two pumps in parallel, feeding a valve in series:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> pump = surv.Weibull.from_params([1000, 1.5])
>>> valve = surv.Exponential.from_params([1e-4])
>>> rbd = NonRepairableRBD(
... [("in", "p1"), ("in", "p2"), ("p1", "v"), ("p2", "v"),
... ("v", "out")],
... {"p1": pump, "p2": pump, "v": valve},
... )
>>> rbd.input_node, rbd.output_node, rbd.nodes
('in', 'out', ['p1', 'p2', 'v'])
>>> round(rbd.sf(100), 4)
0.9891
A 2-out-of-3 system of fixed-probability units, so x may be
omitted:
>>> from surpyval import FixedEventProbability
>>> unit = FixedEventProbability.from_params(0.1)
>>> edges = [("in", u) for u in "abc"] + [(u, "out") for u in "abc"]
>>> two_of_three = NonRepairableRBD(
... edges, {u: unit for u in "abc"}, k={"out": 2}
... )
>>> round(two_of_three.sf(), 4)
0.972
A repeated node: one power supply drawn in both branches ("psu2"
repeats "psu"), so its failure fails both:
>>> shared = NonRepairableRBD(
... [("in", "a"), ("a", "psu"), ("psu", "out"),
... ("in", "b"), ("b", "psu2"), ("psu2", "out")],
... {"a": unit, "b": unit, "psu": unit, "psu2": "psu"},
... )
>>> shared.repeated
{'psu2': 'psu'}
>>> round(shared.sf(), 4)
0.891
A beta-factor common cause coupling two parallel units lowers the independent result of 0.99:
>>> from repyability import BetaFactor, CCFGroup
>>> coupled = NonRepairableRBD(
... [("in", "a"), ("in", "b"), ("a", "out"), ("b", "out")],
... {"a": unit, "b": unit},
... ccf_groups=[CCFGroup(["a", "b"], BetaFactor(0.1))],
... )
>>> round(coupled.sf(), 4)
0.982
is_fixed
property
¶
is_fixed
Whether the system reliability is constant in time.
True when every node is a fixed (per-demand) probability, such as a
surpyval.FixedEventProbability, a
RepeatedNode of one, or a nested
fixed-probability RBD; perfect nodes do not count either way. Any
other model (a lifetime distribution, a standby or load-sharing
node, ...) makes the RBD time-varying.
For a fixed-probability RBD the time argument x of the
reliability and importance methods may be omitted, and the
methods that solve for a time (time_to_reliability,
bx_life, remaining_life) raise a ValueError. Decided when
the RBD is built.
is_time_varying
property
¶
is_time_varying
Whether the system reliability varies with time.
The complement of
is_fixed: True when at
least one node's reliability depends on time, in which case the
time argument x must be given.
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
sf ¶
sf(x=None, working_nodes=None, broken_nodes=None, method='p')
System reliability (survival function) at time/s x.
The probability that the system is still working at x,
computed exactly from each node's reliability sf(x) by a
Shannon decomposition of the structure's modules. Nodes are
independent apart from any
common-cause groups: with ccf_groups each group is conditioned
on within the smallest module holding its members, or its shared
causes written out as events of their own where a module's groups
would multiply their outcomes (see the common-cause guide),
so the cost grows with the number of groups rather than doubling.
working_nodes and broken_nodes condition on the state of
some components by setting their reliability to 1 or 0, e.g. to
see the system reliability once a component has failed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s as a number or an array. May be omitted only for a
fixed-probability RBD (see
|
None
|
working_nodes
|
Collection[Hashable]
|
Nodes to treat as working (reliability 1), by default none. |
None
|
broken_nodes
|
Collection[Hashable]
|
Nodes to treat as failed (reliability 0), by default none. |
None
|
method
|
str
|
|
'p'
|
Returns:
| Type | Description |
|---|---|
float or ndarray
|
The system reliability: a float for scalar |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
If a working/broken node is a member of a common-cause group. |
Examples:
Two components in parallel, each 90% reliable, so the system is
1 - 0.1 * 0.1 = 0.99 reliable:
>>> 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.1),
... },
... )
>>> round(rbd.sf(), 4)
0.99
Conditioning on node "a" having failed leaves only "b":
>>> round(rbd.sf(broken_nodes=["a"]), 4)
0.9
ff ¶
ff(x=None, working_nodes=None, broken_nodes=None, method='p')
System unreliability (failure probability) at time/s x.
The probability that the system has failed by x: 1 - sf(x),
exact, and honouring common-cause groups and the working/broken
nodes, as sf does. It is
worked out in its own right, from each node's probability of
failing (its model's ff), as a sum of products, so a small
one keeps its full relative precision: a system that fails with
probability 1e-15 gets 1e-15, where 1 - sf(x) would
lose it (sf rounds to within 1e-16 of 1). A node's own
precision limits it: a numerical one, such as a cold-standby group
of non-exponential units (a convolution), is accurate to about
1e-6, so a smaller probability through it needs
unreliability_interval.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s as a number or an array. May be omitted only for a fixed-probability RBD. |
None
|
working_nodes
|
Collection[Hashable]
|
Nodes to treat as working (unreliability 0), by default none. |
None
|
broken_nodes
|
Collection[Hashable]
|
Nodes to treat as failed (unreliability 1), by default none. |
None
|
method
|
str
|
|
'p'
|
Returns:
| Type | Description |
|---|---|
float or ndarray
|
The system unreliability: a float for scalar |
Raises:
| Type | Description |
|---|---|
ValueError
|
As for |
NotImplementedError
|
As for |
Examples:
>>> 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.1),
... },
... )
>>> round(rbd.ff(), 4)
0.01
A small probability keeps its precision: three components in
parallel, each failing with probability 1e-6, fail together
with probability 1e-18:
>>> rbd = NonRepairableRBD(
... [("s", n) for n in "abc"] + [(n, "t") for n in "abc"],
... {n: FixedEventProbability.from_params(1e-6) for n in "abc"},
... )
>>> f"{rbd.ff():.6g}"
'1e-18'
capacity_distribution ¶
capacity_distribution(x=None, working_nodes=None, broken_nodes=None)
The exact distribution of the system's capacity at time/s x.
Each node carries its capacity (given when the RBD was built)
while it works, at one level or several, and nothing once it has
failed; the system's capacity is the most that can flow through the
working nodes from the input to the output. A node given no
capacity limits nothing, unless its model has capacities of its own
(a DegradingNode's stages, or a nested RBD's capacities).
With three pumps of half the demand each, one failure costs nothing
and two cost half the output. The capacity is positive exactly when
the system works, so the probability that it is positive is
sf.
The distribution is exact, from each node's reliability at x,
worked out as sf is (see
RBD.system_capacity), and
honours common-cause groups.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s as a number or an array. May be omitted only for a fixed-probability RBD. |
None
|
working_nodes
|
Collection[Hashable]
|
Nodes to treat as working, by default none. |
None
|
broken_nodes
|
Collection[Hashable]
|
Nodes to treat as failed, by default none. |
None
|
Returns:
| Type | Description |
|---|---|
CapacityDistribution
|
The capacities the system can have, and their probabilities: one
per level for a scalar |
A node forced working is at the levels it can work at, in
|
|
proportion to their probabilities at ``x``.
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If no node has a capacity, |
NotImplementedError
|
If a working/broken node is a member of a common-cause group, or a member of one has a capacity distribution from its model. |
Examples:
Three pumps of 50 each in parallel, each 90% reliable at the time of interest, feeding a pipe that carries 120:
>>> from surpyval import FixedEventProbability
>>> from repyability import NonRepairableRBD
>>> pump = FixedEventProbability.from_params(0.1)
>>> pipe = FixedEventProbability.from_params(0.0)
>>> plant = NonRepairableRBD(
... [("in", "a"), ("in", "b"), ("in", "c"),
... ("a", "pipe"), ("b", "pipe"), ("c", "pipe"),
... ("pipe", "out")],
... {"a": pump, "b": pump, "c": pump, "pipe": pipe},
... capacity={"a": 50, "b": 50, "c": 50, "pipe": 120},
... )
>>> capacity = plant.capacity_distribution()
>>> capacity.levels.tolist()
[0.0, 50.0, 100.0, 120.0]
>>> round(capacity.meets(100), 4) # two pumps or more
0.972
>>> round(capacity.mean, 2)
113.13
sf_uncertainty ¶
sf_uncertainty(x=None, uncertainty=None, *, n_draws=1000, seed=None, sampling='random')
System reliability over plausible node models (parameter uncertainty).
A node's model is estimated from data, so its parameters are
uncertain: this is epistemic uncertainty, about what the model
is, as opposed to the aleatory variability the model describes.
Each of the n_draws draws gives every uncertain node a
plausible model, and the system reliability at x is computed
exactly for that draw (all draws at once, by the vectorised exact
engine). The spread of the results, and its percentiles
(interval), say how well the system reliability is known.
RePyability does not fit models: the draws use what the fit, done in surpyval, provides. A node's uncertainty is one of:
"fit": its parameters are drawn from the normal approximation of its maximum-likelihood fit (surpyval'scovariance()), on the log scale for a parameter that must be positive and the logit scale for one in (0, 1), so that every draw is valid: the distribution's parameters and a limited failure population'slfp_por a zero-inflated fit'sf0; an offset, which the covariance leaves out, keeps its fitted value, with a warning;{parameter name: distribution}: each named parameter is drawn from its distribution (anything withqforppf, such as a surpyval or scipy.stats distribution), the others kept;- a list of models (e.g. refits to bootstrap resamples, or posterior draws, made in surpyval), drawn with replacement.
Nodes of one population, such as identical pumps fitted to the same data, share their uncertainty: give them together as a tuple of node names, and each draw gives them the same model. Drawing them independently would understate the uncertainty, which is about the one population's parameters; so would drawing one and leaving the others as fitted, which is warned about when they hold the same model object.
With common-cause groups, each draw is worked out as sf works
it out, with the groups. A group's members carry one model, so
their uncertainty is given together, in one tuple (of all of them,
and perhaps others of their population). The group's common-cause
model may be uncertain too: give the
CCFGroup itself as a key, with
{parameter name: distribution} over its model's parameters
("beta", an MGL model's "gamma",
"delta", ..., an AlphaFactor
model's "alpha_2", ...), each drawn in [0, 1], or a list
of models (BetaFactor, MGL or AlphaFactor, for the
group's size and with its model's basis), drawn with
replacement. The common-cause models
are drawn after the nodes', so the nodes' draws are the same
whether or not they are uncertain.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s, a number or an array. May be left out when every node model, and every drawn model, is a fixed probability. |
None
|
uncertainty
|
dict
|
|
None
|
n_draws
|
int
|
The number of draws, by default 1000. |
1000
|
seed
|
int
|
Seed for the draws, for reproducible results. |
None
|
sampling
|
str
|
|
'random'
|
Returns:
| Type | Description |
|---|---|
UncertaintyResult
|
The system reliability of every draw ( |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
Two pumps in parallel, of one type whose exponential failure rate is uncertain (known only to lie between 1 and 3 per 1000 h), in series with a valve. Both pumps share the one uncertain rate:
>>> import scipy.stats as st
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> edges = [
... ("s", "p1"), ("s", "p2"), ("p1", "v"), ("p2", "v"),
... ("v", "t"),
... ]
>>> models = {
... "p1": surv.Exponential.from_params([0.002]),
... "p2": surv.Exponential.from_params([0.002]),
... "v": surv.FixedEventProbability.from_params(0.01),
... }
>>> rbd = NonRepairableRBD(edges, models)
>>> rate = {"failure_rate": st.uniform(0.001, 0.002)}
>>> result = rbd.sf_uncertainty(
... 500, {("p1", "p2"): rate}, n_draws=10_000, seed=1
... )
>>> round(result.nominal, 4) # at the nominal rate, 2 per 1000 h
0.5944
>>> lower, upper = result.interval(0.9)
>>> round(lower, 3), round(upper, 3)
(0.409, 0.813)
If the pumps share a tenth of their failure rate, and that fraction
is itself uncertain, the group is given with a distribution over
its beta:
>>> from repyability import BetaFactor, CCFGroup
>>> group = CCFGroup(["p1", "p2"], BetaFactor(0.1, basis="rate"))
>>> shared = NonRepairableRBD(edges, models, ccf_groups=[group])
>>> result = shared.sf_uncertainty(
... 500,
... {("p1", "p2"): rate, group: {"beta": st.beta(2, 18)}},
... n_draws=10_000,
... seed=1,
... )
>>> round(result.nominal, 4)
0.5803
>>> lower, upper = result.interval(0.9)
>>> round(lower, 3), round(upper, 3)
(0.401, 0.795)
mean_uncertainty ¶
mean_uncertainty(uncertainty=None, *, n_draws=1000, seed=None, sampling='random')
The system's MTTF over plausible node models (parameter uncertainty).
As sf_uncertainty
does for the reliability: each of the n_draws draws gives every
uncertain node a plausible model, and the system's MTTF is worked
out exactly for that draw, as mean works it out (the area under
the draw's system reliability). The spread of the results says how
well the MTTF is known from the data behind the models, which the
simulation error of mean_time_to_failure_interval does not:
that interval narrows with more simulations, this one only with
more data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
uncertainty
|
dict
|
|
None
|
n_draws
|
int
|
The number of draws, by default 1000. |
1000
|
seed
|
int
|
Seed for the draws, for reproducible results. |
None
|
sampling
|
str
|
|
'random'
|
Returns:
| Type | Description |
|---|---|
UncertaintyResult
|
The MTTF of every draw ( |
Raises:
| Type | Description |
|---|---|
ValueError
|
As for |
NotImplementedError
|
If a common-cause group splits a failure probability (the
default basis), as for |
Examples:
Two pumps of one type in parallel, whose exponential failure rate is known only to lie between 1 and 3 per 1000 h:
>>> import scipy.stats as st
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> pump = surv.Exponential.from_params([0.002])
>>> rbd = NonRepairableRBD(
... [("s", "p1"), ("s", "p2"), ("p1", "t"), ("p2", "t")],
... {"p1": pump, "p2": pump},
... )
>>> rate = {"failure_rate": st.uniform(0.001, 0.002)}
>>> result = rbd.mean_uncertainty({("p1", "p2"): rate}, seed=1)
>>> round(result.nominal, 6) # 1.5 / 0.002
750.0
>>> lower, upper = result.interval(0.9)
>>> round(lower), round(upper)
(515, 1352)
time_to_reliability_uncertainty ¶
time_to_reliability_uncertainty(target, uncertainty=None, *, n_draws=1000, seed=None, sampling='random', upper_bound=None)
The time at which the system reliability falls to target,
over plausible node models (parameter uncertainty).
Each of the n_draws draws gives every uncertain node a plausible
model (see
sf_uncertainty),
and the time is found exactly for that draw, as
time_to_reliability finds it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
float or array_like
|
The reliability level, in (0, 1), or levels: each a row of draws, from the same parameter draws. |
required |
uncertainty
|
dict
|
|
None
|
n_draws
|
int
|
The number of draws, by default 1000. |
1000
|
seed
|
int
|
Seed for the draws, for reproducible results. |
None
|
sampling
|
str
|
|
'random'
|
upper_bound
|
float
|
An upper bound for each draw's search, at which its reliability
is below |
None
|
Returns:
| Type | Description |
|---|---|
UncertaintyResult
|
The time of every draw ( |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
A pump fitted (in surpyval) to 20 failure times: the time to 90% reliability, over the fit's uncertainty:
>>> import numpy as np
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> times = surv.Weibull.from_params([100, 2]).qf(
... np.linspace(0.025, 0.975, 20)
... )
>>> rbd = NonRepairableRBD(
... [("s", "pump"), ("pump", "t")],
... {"pump": surv.Weibull.fit(times)},
... )
>>> result = rbd.time_to_reliability_uncertainty(
... 0.9, {"pump": "fit"}, seed=0
... )
>>> round(result.nominal, 2)
33.64
>>> lower, upper = result.interval(0.9)
>>> round(lower, 1), round(upper, 1)
(21.6, 49.3)
bx_life_uncertainty ¶
bx_life_uncertainty(x, uncertainty=None, *, n_draws=1000, seed=None, sampling='random', upper_bound=None)
The Bx life, the time by which x percent of systems have
failed (the B10 life for x = 10), over plausible node models
(parameter uncertainty).
time_to_reliability_uncertainty(1 - x / 100, ...): see
time_to_reliability_uncertainty.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
float or array_like
|
The percentage failed, in (0, 100), or percentages: each a row of draws, from the same parameter draws. |
required |
uncertainty
|
dict
|
|
None
|
n_draws
|
int
|
The number of draws, by default 1000. |
1000
|
seed
|
int
|
Seed for the draws, for reproducible results. |
None
|
sampling
|
str
|
|
'random'
|
upper_bound
|
float
|
An upper bound for each draw's search, found by doubling if None. |
None
|
Returns:
| Type | Description |
|---|---|
UncertaintyResult
|
The Bx life of every draw, the nominal one, and their summaries. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> import numpy as np
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> times = surv.Weibull.from_params([100, 2]).qf(
... np.linspace(0.025, 0.975, 20)
... )
>>> rbd = NonRepairableRBD(
... [("s", "pump"), ("pump", "t")],
... {"pump": surv.Weibull.fit(times)},
... )
>>> b10 = rbd.bx_life_uncertainty(10, {"pump": "fit"}, seed=0)
>>> round(b10.nominal, 2) # the same as the time to 90% reliability
33.64
uncertainty_importance ¶
uncertainty_importance(x=None, uncertainty=None, *, of='sf', method='delta', n_draws=1000, seed=None, sampling='random', rel_step=0.0001, upper_bound=None)
Which input's parameter uncertainty makes a system quantity
uncertain: each uncertain input's share of the quantity's variance,
the inputs as for sf_uncertainty.
In the guide's Greeks it is vega: whose uncertainty widens the answer (see Sensitivities: the Greeks).
of is the quantity: "sf", the reliability at the time/s
x (as sf_uncertainty); "mean", the MTTF (as
mean_uncertainty; no x); "bx_life", the time by which
x percent have failed; "time_to_reliability", the time the
reliability falls to x.
method="delta" (the default) linearises the quantity in the
inputs' parameters: Var(Q) ~ sum_k g_k^T Sigma_k g_k, g_k
its gradient in input k's parameters (central differences of
the exact quantity, with the parameters moved by rel_step of
themselves) and Sigma_k their covariance: a fit's (surpyval's
covariance()) for "fit", or the variances of the distributions
given. The inputs are independent, so each one's part is its own
term and the shares add up to 1. A list of models has no
parameters to move: it needs method="sobol".
method="sobol" draws the inputs as sf_uncertainty does and
estimates the variance-based (Sobol) indices: n_draws draws of
two independent sets, and one more set for each input with it
taken from the second (Jansen's estimators), n_draws * (inputs +
2) evaluations in all. It
takes the nonlinearity and the inputs' interactions in, at the
cost of sampling error, which shrinks as 1/sqrt(n_draws) (with
few draws, a first-order estimate can even pass its total).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
What the quantity takes: the time/s for |
None
|
uncertainty
|
dict
|
|
None
|
of
|
str
|
|
'sf'
|
method
|
str
|
|
'delta'
|
n_draws
|
int
|
With |
1000
|
seed
|
int
|
With |
None
|
sampling
|
str
|
With |
'random'
|
rel_step
|
float
|
With |
0.0001
|
upper_bound
|
float
|
For |
None
|
Returns:
| Type | Description |
|---|---|
UncertaintyImportance
|
The |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown |
Examples:
A pump, fitted to 10 failures, in series with a valve fitted to 40: the pump's estimate is the vaguer, and most of the system's uncertainty is its.
>>> import numpy as np
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> def fitted(scale, n):
... u = (np.arange(n) + 0.5) / n
... return surv.Weibull.fit(scale * (-np.log(1 - u)) ** 0.5)
>>> rbd = NonRepairableRBD(
... [("s", "pump"), ("pump", "valve"), ("valve", "t")],
... {"pump": fitted(100, 10), "valve": fitted(120, 40)},
... )
>>> parts = rbd.uncertainty_importance(50)
>>> {n: round(share, 2) for n, share in parts.first_order.items()}
{'pump': 0.86, 'valve': 0.14}
allocate_redundancy ¶
allocate_redundancy(costs, *, budget=None, target=None, minimise=None, t=None, max_units=None, required=None, strategy='active', switching_probability=1.0, mixing=True, method='exact')
Choose how many redundant copies of each node to fit.
Solves the Redundancy Allocation Problem: pick how many independent
copies of each node in costs to fit in active parallel, to either
- maximise system reliability within the
budget, or - minimise total cost with system reliability at least
target(and within thebudget, if one is also given).
A copy may use one resource (a number: its cost) or several (a dict
such as {"cost": 4000, "weight": 12}, as in Fyffe, Hines & Lee,
1968), and the budget then limits each resource it names. A node
may also be given a choice of component types, a list of
ComponentOption with their own
reliabilities and costs: its copies are then all of one type or,
with mixing, any combination of types (Coit & Smith, 1996).
Copies of reliabilities p_1, ..., p_n in active parallel have
reliability 1 - (1 - p_1) ... (1 - p_n) (1 - (1 - p) ** n
for n identical copies); a node may instead need several of its
copies working (required, k-out-of-n: Coit & Liu, 2000), and its
spares may be cold standby rather than active (strategy: Coit,
2001), or either, whichever is better (Coit, 2003). The system
reliability of each candidate allocation is computed exactly from
its nodes' reliabilities, so any RBD structure works; only a cold
standby node's own reliability may be simulated (see
strategy). Nodes not in costs stay as they are.
Reliability is evaluated at the single mission time t.
With common-cause groups, a copy of a member joins its group, so the
shared cause fails it too, and each design is scored exactly over
the groups' outcomes. A BetaFactor group's beta holds at
any size, so its members can be copied: active copies of their own
model (with any number of them required). An MGL or
AlphaFactor model's parameters are for its group's size, so
copies of its member are refused, as are options and cold
spares for a member.
method="exact" returns a proven optimum. When every costed node
is in series with the rest of the system (it lies on every path, as
in the textbook series of subsystems), the system reliability is the
product of the costed nodes' reliabilities and the rest's, and a
dynamic program over the nodes finds the optimum however many
costed nodes there are. On other structures an exhaustive search
finds it, which suits a handful of costed nodes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
costs
|
dict
|
|
required |
budget
|
float or dict
|
The limit on the one resource (a number), or on each resource a
dict names (resources it leaves out are not limited). With no
|
None
|
target
|
float
|
Minimise the total of one resource subject to system reliability
|
None
|
minimise
|
Hashable
|
The resource a |
None
|
t
|
float
|
The mission time at which reliability is evaluated, a single number. Required for a time-varying RBD (or time-varying options); not needed when every node and option is a fixed probability. |
None
|
max_units
|
int or dict
|
The most copies allowed (at least 1) of every costed node (an int) or of particular costed nodes (a dict; nodes it leaves out are unlimited), counting copies of every type, e.g. for space limits. By default unlimited; the budget or target bounds the search, so every node (every option) must use some of a limited (or, with a target, the minimised) resource, or have a cap. |
None
|
required
|
int or dict
|
The number of a node's copies that must work (k-out-of-n), for every costed node (an int) or particular ones (a dict; others need 1). By default 1. Every design has at least this many copies. |
None
|
strategy
|
str or dict
|
How a node's copies are arranged, for every costed node (a
string) or particular ones (a dict; others are active):
|
'active'
|
switching_probability
|
float or dict
|
For cold standby, the probability that switching onto a spare succeeds, for every cold node (a number) or particular ones (a dict). By default 1.0 (perfect switching). Imperfect switching is supported with one unit required. |
1.0
|
mixing
|
bool
|
For nodes with options: whether their copies may be of different types (the default), or must all be of one type. |
True
|
method
|
str
|
|
'exact'
|
Returns:
| Type | Description |
|---|---|
RedundancyAllocation
|
The chosen |
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither |
NotImplementedError
|
If a costed node is in a common-cause group whose model is an
|
References
D. E. Fyffe, W. W. Hines and N. K. Lee, "System reliability allocation and a computational algorithm", IEEE Transactions on Reliability, 17(2), 64-69, 1968.
J. D. Kettelle, "Least-cost allocations of reliability investment", Operations Research, 10(2), 249-265, 1962 (dominance in a dynamic program over the subsystems of a series system).
D. W. Coit and A. E. Smith, "Reliability optimization of series-parallel systems using a genetic algorithm", IEEE Transactions on Reliability, 45(2), 254-260, 1996 (component mixing).
D. W. Coit and J. Liu, "System reliability optimization with k-out-of-n subsystems", International Journal of Reliability, Quality and Safety Engineering, 7(2), 129-142, 2000.
D. W. Coit, "Cold-standby redundancy optimization for nonrepairable systems", IIE Transactions, 33(6), 471-478, 2001.
D. W. Coit, "Maximization of system reliability with a choice of redundancy strategies", IIE Transactions, 35(6), 535-543, 2003.
Examples:
Two components in series, 90% and 80% reliable, one cost unit each. With a budget of 3 the extra copy goes to the weaker component:
>>> from surpyval import FixedEventProbability
>>> from repyability import ComponentOption, NonRepairableRBD
>>> rbd = NonRepairableRBD(
... [("s", "a"), ("a", "b"), ("b", "t")],
... {
... "a": FixedEventProbability.from_params(0.1),
... "b": FixedEventProbability.from_params(0.2),
... },
... )
>>> best = rbd.allocate_redundancy({"a": 1.0, "b": 1.0}, budget=3)
>>> best.units
{'a': 1, 'b': 2}
>>> round(best.reliability, 4)
0.864
The cheapest design that is at least 90% reliable needs two of each:
>>> rbd.allocate_redundancy({"a": 1.0, "b": 1.0}, target=0.9).units
{'a': 2, 'b': 2}
With weight limited as well as cost, a second a (3 kg) no longer
fits, and the budget goes on more copies of b:
>>> costs = {
... "a": {"cost": 1, "weight": 3},
... "b": {"cost": 1, "weight": 1},
... }
>>> best = rbd.allocate_redundancy(
... costs, budget={"cost": 4, "weight": 6}
... )
>>> best.units, round(best.reliability, 4), best.resources
({'a': 1, 'b': 3}, 0.8928, {'cost': 4.0, 'weight': 6.0})
b may instead be built from a standard part (80%, cost 1) or a
premium one (98%, cost 2). Within a budget of 5 the best design
mixes them; with every copy of b of one type it is less
reliable:
>>> choice = [
... ComponentOption("standard", 0.8, cost=1),
... ComponentOption("premium", 0.98, cost=2),
... ]
>>> best = rbd.allocate_redundancy({"a": 1, "b": choice}, budget=5)
>>> best.units, best.mix, round(best.reliability, 5)
({'a': 2, 'b': 2}, {'b': {'standard': 1, 'premium': 1}}, 0.98604)
>>> same = rbd.allocate_redundancy(
... {"a": 1, "b": choice}, budget=5, mixing=False
... )
>>> same.mix, round(same.reliability, 5)
({'b': {'standard': 3}}, 0.98208)
A pump that must deliver with two units running (2-out-of-n), as active copies or with cold spares, whichever is better. At 1000 hours cold spares are better:
>>> import surpyval as surv
>>> station = NonRepairableRBD(
... [("s", "pumps"), ("pumps", "t")],
... {"pumps": surv.Exponential.from_params([1 / 2000])},
... )
>>> best = station.allocate_redundancy(
... {"pumps": 1}, budget=4, t=1000, required=2, strategy="choose"
... )
>>> best.units, best.strategy, round(best.reliability, 4)
({'pumps': 4}, {'pumps': 'cold'}, 0.9197)
redundancy_front ¶
redundancy_front(costs, *, budget=None, t=None, max_units=None, required=None, strategy='active', switching_probability=1.0, mixing=True)
The whole cost-reliability trade-off of redundancy allocation.
Where allocate_redundancy returns the one best design for a
budget or a target, this returns every design that no other beats
by using no more of every resource while being at least as reliable
(the Pareto front of multi-objective redundancy allocation, e.g.
Taboada et al., 2007). With one resource
it is the best reliability for each level of spending, so a design
can be chosen by looking at the whole curve; the best design within
any budget, and the cheapest meeting any target, are on it. With
several resources it is the best reliability for each combination
of them.
The front is exact: when every costed node is in series with the
rest of the system it comes from the dynamic program behind
allocate_redundancy, and otherwise from evaluating every
combination of node designs within the budget (giving up with an
explanatory error beyond 500,000 of them).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
costs
|
dict
|
What one copy of each costed node uses, as for
|
required |
budget
|
float or dict
|
Limits on the resources, as for |
None
|
t
|
float
|
The mission time, as for |
None
|
max_units
|
int or dict
|
The most copies of each node, as for |
None
|
required
|
int or dict
|
The copies of each node that must work, as for
|
None
|
strategy
|
str or dict
|
|
'active'
|
switching_probability
|
float or dict
|
For cold standby, as for |
1.0
|
mixing
|
bool
|
Whether a node's copies may mix types, as for
|
True
|
Returns:
| Type | Description |
|---|---|
list of RedundancyAllocation
|
One per non-dominated design, by increasing |
Raises:
| Type | Description |
|---|---|
ValueError
|
On invalid input, as for |
NotImplementedError
|
As for |
References
H. A. Taboada, F. Baheranwala, D. W. Coit and N. Wattanapongsakorn, "Practical solutions for multi-objective optimization: an application to system reliability design problems", Reliability Engineering & System Safety, 92(3), 314-322, 2007.
Examples:
Two components in series, 90% and 80% reliable, one cost unit each, spending up to 5:
>>> from surpyval import FixedEventProbability
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
... [("s", "a"), ("a", "b"), ("b", "t")],
... {
... "a": FixedEventProbability.from_params(0.1),
... "b": FixedEventProbability.from_params(0.2),
... },
... )
>>> front = rbd.redundancy_front({"a": 1.0, "b": 1.0}, budget=5)
>>> for design in front:
... print(design.cost, design.units, round(design.reliability, 4))
2.0 {'a': 1, 'b': 1} 0.72
3.0 {'a': 1, 'b': 2} 0.864
4.0 {'a': 2, 'b': 2} 0.9504
5.0 {'a': 2, 'b': 3} 0.9821
allocate_reliability_redundancy ¶
allocate_reliability_redundancy(uses, *, budget, bounds, t=None, max_units=None)
Choose each node's component reliability and number of copies together: the reliability-redundancy allocation problem (RRAP).
Joins reliability allocation (what each component must achieve) to
redundancy allocation (how many copies to fit): for each node in
uses, pick a component reliability r within its bounds
and a number of active copies n, giving the node reliability
1 - (1 - r) ** n, to maximise system reliability within the
budget. What the copies use is a function of both, typically rising
steeply as r approaches 1 (Tillman, Hwang & Kuo, 1977). The
node's own model in the RBD is not used; the other nodes stay as
they are, evaluated at the mission time t. Any structure works:
the system reliability and its gradient come from the exact engine.
It is solved exactly over the copies by branch and bound: each copy vector that fits the budget at the lowest reliabilities is bounded above by the system reliability with every node at the highest reliability it could afford alone, and the vectors are solved in decreasing order of that bound -- each a continuous problem for the reliabilities, solved by SLSQP with the exact gradient from the lowest reliabilities -- until no bound beats the best found. The continuous problem is solved to a local optimum, which for a series system with costs convex in the reliabilities is the global one. On the classic benchmarks (series, series-parallel, bridge and overspeed protection systems) it returns the best published solutions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
uses
|
dict
|
|
required |
budget
|
float or dict
|
The limit on the one resource (a number), or on each resource a dict names (resources it leaves out are not limited). |
required |
bounds
|
tuple or dict
|
The lowest and highest component reliability, |
required |
t
|
float
|
The mission time at which the other nodes' reliabilities are evaluated. Required for a time-varying RBD. |
None
|
max_units
|
int or dict
|
The most copies of every node (an int) or of particular nodes (a dict). By default the budget bounds them. |
None
|
Returns:
| Type | Description |
|---|---|
ReliabilityRedundancyAllocation
|
The |
Raises:
| Type | Description |
|---|---|
ValueError
|
On invalid input: empty |
NotImplementedError
|
If a node in |
References
F. A. Tillman, C.-L. Hwang and W. Kuo, "Determining component reliability and redundancy for optimum system reliability", IEEE Transactions on Reliability, 26(3), 162-165, 1977.
Examples:
Two components in series. A component's reliability r is
chosen in [0.5, 0.999]; each copy costs 10 to fit, plus
(-1 / log(r)) ** 1.5, which rises steeply as r nears 1.
Within a budget of 100, three copies of each at r = 0.754 is
best:
>>> import math
>>> from surpyval import FixedEventProbability
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
... [("s", "a"), ("a", "b"), ("b", "t")],
... {
... "a": FixedEventProbability.from_params(0.1),
... "b": FixedEventProbability.from_params(0.2),
... },
... )
>>> def cost(r, n):
... return n * (10 + (-1 / math.log(r)) ** 1.5)
>>> best = rbd.allocate_reliability_redundancy(
... {"a": cost, "b": cost}, budget=100, bounds=(0.5, 0.999)
... )
>>> best.units
{'a': 3, 'b': 3}
>>> [round(r, 3) for r in best.component_reliability.values()]
[0.754, 0.754]
>>> round(best.reliability, 4)
0.9705
unreliability ¶
unreliability(x=None, *args, **kwargs)
System unreliability at time/s x; the same as ff.
1 - sf(x), the probability that the system has failed by x,
worked out in its own right, so that a small one keeps its
precision (see ff).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s as a number or an array. May be omitted only for a fixed-probability RBD. |
None
|
*args
|
Further positional arguments of |
()
|
|
**kwargs
|
Keyword arguments of |
{}
|
Returns:
| Type | Description |
|---|---|
float or ndarray
|
The system unreliability: a float for scalar |
Raises:
| Type | Description |
|---|---|
ValueError
|
As for |
NotImplementedError
|
As for |
reliability ¶
reliability(x=None, *args, **kwargs)
System reliability at time/s x; the same as sf.
The probability that the system is still working at x (see
sf).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s as a number or an array. May be omitted only for a fixed-probability RBD. |
None
|
*args
|
Further positional arguments of |
()
|
|
**kwargs
|
Keyword arguments of |
{}
|
Returns:
| Type | Description |
|---|---|
float or ndarray
|
The system reliability: a float for scalar |
Raises:
| Type | Description |
|---|---|
ValueError
|
As for |
NotImplementedError
|
As for |
cs ¶
cs(x, X, *args, **kwargs)
Conditional survival of the system.
The probability that the system survives a further x given it
has survived to age X: R(x | X) = sf(X + x) / sf(X). The
whole system is conditioned on having survived, not each component
on its own age; for per-component ages use
sf_given_state.
The result is clipped to [0, 1] and is 0 where sf(X) is 0.
x and X broadcast against each other.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
The further duration/s at which conditional survival is evaluated. |
required |
X
|
array_like
|
The age/s the system is known to have survived to. |
required |
*args
|
Further positional arguments of |
()
|
|
**kwargs
|
Keyword arguments of |
{}
|
Returns:
| Type | Description |
|---|---|
float or ndarray
|
The conditional survival probability: a float if both |
Raises:
| Type | Description |
|---|---|
ValueError
|
As for |
NotImplementedError
|
As for |
Examples:
A component 50 hours old survives the next 10 hours with a higher probability than it had of surviving from new to 60 hours:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
... [("s", "c"), ("c", "t")],
... {"c": surv.Weibull.from_params([100, 2])},
... )
>>> round(rbd.cs(10, 50), 4)
0.8958
>>> round(rbd.sf(60), 4)
0.6977
Hf ¶
Hf(x=None, working_nodes=None, broken_nodes=None, method='p')
System cumulative hazard H(x) = -ln R(x) at time/s x.
Exact given the exact system reliability R(x) from
sf (so it honours
common-cause groups); +inf wherever the system reliability has
reached zero. While the system is more likely to work than not, it
is -log1p(-F(x)), from the unreliability F of
ff, so that a small one keeps
its precision.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s as a number or an array. May be omitted only for a fixed-probability RBD. |
None
|
working_nodes
|
Collection[Hashable]
|
Nodes to treat as working, by default none (as for |
None
|
broken_nodes
|
Collection[Hashable]
|
Nodes to treat as failed, by default none (as for |
None
|
method
|
str
|
|
'p'
|
Returns:
| Type | Description |
|---|---|
float or ndarray
|
The cumulative hazard: a float for scalar |
Raises:
| Type | Description |
|---|---|
ValueError
|
As for |
NotImplementedError
|
As for |
Examples:
For a single exponential component H(x) is the failure rate
times x:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
... [("s", "c"), ("c", "t")],
... {"c": surv.Exponential.from_params([0.01])},
... )
>>> round(rbd.Hf(50), 6)
0.5
df ¶
df(x=None, dx=1e-06, working_nodes=None, broken_nodes=None, method='p')
System failure density f(x) = -dR/dx at time/s x.
Computed by a central finite difference of the system reliability
from sf (so it honours
common-cause groups), with step h = dx * |x|, relative to
x (at 0, dx of the shortest of the nodes' typical failure
times), so that it does not depend on the unit of time. While the
system is more likely to work than not, the difference is taken of its
unreliability from ff
instead, which keeps its precision where the reliability's change
would be lost to rounding. The lower point is clipped at 0, so the
step never crosses into negative time (the difference is one-sided
near 0), and negative results (numerical noise) are clipped to 0.
It assumes R is smooth near x, which does not hold where a
node model's reliability has a kink or a step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s as a number or an array. May be omitted only for a fixed-probability RBD (whose density is 0). |
None
|
dx
|
float
|
Relative finite-difference step, by default 1e-6. |
1e-06
|
working_nodes
|
Collection[Hashable]
|
Nodes to treat as working, by default none (as for |
None
|
broken_nodes
|
Collection[Hashable]
|
Nodes to treat as failed, by default none (as for |
None
|
method
|
str
|
|
'p'
|
Returns:
| Type | Description |
|---|---|
float or ndarray
|
The failure density: a float for scalar |
Raises:
| Type | Description |
|---|---|
ValueError
|
As for |
NotImplementedError
|
As for |
Examples:
For a single exponential component f(x) = rate * exp(-rate * x):
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
... [("s", "c"), ("c", "t")],
... {"c": surv.Exponential.from_params([0.01])},
... )
>>> round(rbd.df(50), 6)
0.006065
hf ¶
hf(x=None, dx=1e-06, **kwargs)
System hazard rate h(x) = f(x) / R(x) at time/s x.
The numerical failure density from
df over the exact system
reliability from sf (both
honour common-cause groups). It is +inf wherever the system
reliability has reached zero.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s as a number or an array. May be omitted only for a fixed-probability RBD. |
None
|
dx
|
float
|
Relative finite-difference step passed to |
1e-06
|
**kwargs
|
Keyword arguments of |
{}
|
Returns:
| Type | Description |
|---|---|
float or ndarray
|
The hazard rate: a float for scalar |
Raises:
| Type | Description |
|---|---|
ValueError
|
As for |
NotImplementedError
|
As for |
Examples:
A single exponential component has a constant hazard rate:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
... [("s", "c"), ("c", "t")],
... {"c": surv.Exponential.from_params([0.01])},
... )
>>> round(rbd.hf(50), 6)
0.01
node_sf ¶
node_sf(x=None, *args, **kwargs)
Reliability of each node at time/s x.
Each node model's own sf(x), including the input and output
nodes (always 1). A repeated node appears only under the node it
repeats.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s as a number or an array. May be omitted only for a fixed-probability RBD. |
None
|
*args
|
Accepted but ignored, so |
()
|
|
**kwargs
|
Accepted but ignored. |
{}
|
Returns:
| Type | Description |
|---|---|
dict[Any, float | ndarray]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
... [("s", "c"), ("c", "t")],
... {"c": surv.Weibull.from_params([100, 2])},
... )
>>> {k: round(v, 4) for k, v in sorted(rbd.node_sf(50).items())}
{'c': 0.7788, 's': 1.0, 't': 1.0}
node_ff ¶
node_ff(x=None, *args, **kwargs)
Unreliability of each node at time/s x.
Each node model's own ff(x), including the input and output
nodes (always 0). A repeated node appears only under the node it
repeats.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s as a number or an array. May be omitted only for a fixed-probability RBD. |
None
|
*args
|
Accepted but ignored, so |
()
|
|
**kwargs
|
Accepted but ignored. |
{}
|
Returns:
| Type | Description |
|---|---|
dict[Any, float | ndarray]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
get_non_analytic_nodes ¶
get_non_analytic_nodes()
The nodes with no exact or numerical reliability.
Such a node is a StandbyModel or
LoadSharingModel with no closed
form or numerical method (see their is_simulated), or a repeated
node or nested RBD of one: only the simulations take it, and the
analyses that need its reliability refuse. analysis_routes says
how each analysis of the RBD is computed.
Returns:
| Type | Description |
|---|---|
dict[Any, str]
|
|
is_analytically_solvable ¶
is_analytically_solvable()
Whether every node has an exact or numerical reliability.
The exact system reliability (sf and the methods built on it)
is worked out from each node's own sf(t): a closed form for
surpyval distributions, for
RegressionNode and the perfect
nodes, and a closed form or a numerical method for most standby
and load-sharing arrangements. The rest have none (see
get_non_analytic_nodes), which makes this False: the analyses
that need their reliability refuse, and the simulations take them.
analysis_routes says how each analysis is computed.
The result is also stored at construction in
structure_check["is_analytically_solvable"].
Returns:
| Type | Description |
|---|---|
bool
|
True if no node's reliability is simulated; False otherwise
( |
Examples:
One cold spare for one unit is a numerical convolution; two units of three needed working, with one warm spare, has no reliability but the simulations':
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD, StandbyModel
>>> unit = surv.Weibull.from_params([100, 2])
>>> pair = NonRepairableRBD(
... [("s", "a"), ("a", "t")], {"a": StandbyModel([unit, unit])}
... )
>>> pair.is_analytically_solvable()
True
>>> trio = NonRepairableRBD(
... [("s", "a"), ("a", "t")],
... {
... "a": StandbyModel([unit] * 3, k=2, dormancy_factor=0.5)
... },
... )
>>> trio.is_analytically_solvable()
False
>>> trio.get_non_analytic_nodes()
{'a': 'StandbyModel'}
analysis_routes ¶
analysis_routes()
How each analysis of this RBD is computed, found without running it: exactly, numerically, by simulation, or not at all.
The route follows from the method, then from the nodes' models:
the structure is always evaluated exactly, whatever the diagram, but
a system value is only as exact as the node values it is made of.
So a node whose reliability is a numerical convolution makes the
analyses built on it numerical, and one with no reliability makes
them refuse, as it does (see get_non_analytic_nodes). A refusal
is found by the check the method itself runs, and its reason is the
message it would raise.
Returns:
| Type | Description |
|---|---|
dict[str, AnalysisRoute]
|
For each public analysis, by method name: its |
Examples:
Two units needed of three, with the third a warm spare, have no exact or numerical reliability, so what is built on it is refused, and the simulations take the diagram:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD, StandbyModel
>>> unit = surv.Weibull.from_params([100, 2])
>>> rbd = NonRepairableRBD(
... [("s", "a"), ("a", "b"), ("b", "t")],
... {
... "a": StandbyModel([unit] * 3, k=2, dormancy_factor=0.5),
... "b": unit,
... },
... )
>>> routes = rbd.analysis_routes()
>>> routes["sf"].route, routes["sf"].nodes
('refused', ('a',))
>>> routes["structural_importance"].route
'exact'
>>> routes["mean_time_to_failure_interval"].route
'simulated'
random ¶
random(size, seed=None, *, antithetic=False, n_jobs=None)
Draw size random system lifetimes (Monte-Carlo).
Each node's lifetime is drawn independently from its own model's
random, and the system fails when its last working minimal path
set breaks: each sample is the maximum, over the minimal path sets,
of the minimum lifetime of the path set's members (k-out-of-n and
repeated nodes are accounted for). When every
node's draws can be replayed as one block (e.g. surpyval parametric
distributions, and composite nodes built from them), all samples
are computed at once; otherwise they are simulated one at a time by
failing nodes in time order until the system fails. Both paths draw
the same random numbers in the same order, so they give identical
results. Either way the system is worked out through the diagram's
modules, without listing its path sets, so a large redundant diagram
is sampled about as fast as a small one.
A common-cause group that splits the failure rate
(basis="rate") is drawn with its shared shocks: in the
members' cumulative hazard each cause strikes at an exponential
time, and a member fails at the first of its causes, through its
model's quantile function. A group that splits a probability (the
default) is left out, without a warning: its basic-event model
assumes a small failure probability, while a lifetime runs to
Q = 1. The same applies to mean(method="simulate"),
mean_time_to_failure_interval and compare; the exact
mean refuses such a group.
A fixed-probability node fails at the start (a lifetime of 0), with
its probability, or never (inf), as sf takes it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
size
|
int
|
Number of system lifetimes to draw. |
required |
seed
|
int or None
|
If given, seeds numpy's global RNG for the duration of the draw so
the result is reproducible (surpyval's |
None
|
antithetic
|
bool
|
Draw the lifetimes in antithetic pairs, by default False: the
second of each pair (samples |
False
|
n_jobs
|
int
|
Draw in parallel, in blocks of |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
If |
Examples:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> unit = surv.Weibull.from_params([100, 2])
>>> rbd = NonRepairableRBD(
... [("s", "a"), ("s", "b"), ("a", "t"), ("b", "t")],
... {"a": unit, "b": unit},
... )
>>> lifetimes = rbd.random(5, seed=1)
>>> lifetimes.shape
(5,)
>>> bool((lifetimes == rbd.random(5, seed=1)).all())
True
random_block ¶
random_block(block, seed, *, antithetic=False)
Block block of the lifetimes random(size, seed=seed,
n_jobs=...) draws: lifetimes block * RANDOM_BLOCK to
(block + 1) * RANDOM_BLOCK - 1 of them, for any n_jobs.
With n_jobs, random draws its lifetimes in blocks of
RANDOM_BLOCK (10 000), each seeded from seed and its
position alone. This draws one, so a large draw can be split across
machines, or any block of it drawn again to check it. A last,
shorter block of a draw is the start of the full block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
block
|
int
|
The block's position, from 0. |
required |
seed
|
int
|
The draw's seed, required. |
required |
antithetic
|
bool
|
Draw in antithetic pairs, as |
False
|
Returns:
| Type | Description |
|---|---|
ndarray
|
The block's |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
As for |
Examples:
>>> import numpy as np
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> unit = surv.Weibull.from_params([100, 2])
>>> rbd = NonRepairableRBD(
... [("s", "a"), ("s", "b"), ("a", "t"), ("b", "t")],
... {"a": unit, "b": unit},
... )
>>> whole = rbd.random(30_000, seed=5, n_jobs=1)
>>> second = rbd.random_block(1, seed=5)
>>> bool((second == whole[10_000:20_000]).all())
True
mean ¶
mean(mc_samples=None, seed=None, *, method='exact', tolerance=None, confidence=None, max_samples=None, antithetic=False, n_jobs=None)
Mean time to failure (MTTF) of the system.
Exact by default: the area under the system reliability,
MTTF = integral from 0 to infinity of R(t) dt, with R the
exact sf, integrated by
adaptive Gauss-Legendre quadrature to about 1e-10, relative. It
is as exact as the node reliabilities it is made of: a node whose
reliability is a numerical convolution, or a fit to simulated
lifetimes, brings its own error (see analysis_routes). A system
that may never fail has an infinite MTTF, inf: one that needs
only nodes some of whose units never fail (a
limited-failure-population model, PerfectReliability). A
fixed-probability node counts as working, or failed, from the start
and for good, as sf takes it. This is also the MTTF this RBD
brings to another it is nested in (see node_mttf).
method="simulate" estimates the MTTF instead, as the mean of
mc_samples simulated lifetimes (see
random, which leaves out
common-cause groups that split a probability); the estimate's
standard error is the
lifetimes' standard deviation over sqrt(mc_samples).
mean_time_to_failure_interval gives such an estimate with its
confidence interval.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mc_samples
|
int
|
With |
None
|
seed
|
int or None
|
With |
None
|
method
|
(exact, simulate)
|
How to find the MTTF, by default |
"exact"
|
tolerance
|
float
|
With |
None
|
confidence
|
float
|
With |
None
|
max_samples
|
int
|
With |
None
|
antithetic
|
bool
|
With |
False
|
n_jobs
|
int
|
With |
None
|
Returns:
| Type | Description |
|---|---|
float
|
The MTTF ( |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
For the exact MTTF, if the RBD (or an RBD nested in it) has
common-cause groups that split a failure probability: they
assume it is small (see |
TypeError
|
If a simulation option is given without |
Examples:
A single exponential component with failure rate 0.01 has an MTTF of 100:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
... [("s", "c"), ("c", "t")],
... {"c": surv.Exponential.from_params([0.01])},
... )
>>> round(rbd.mean(), 9)
100.0
>>> estimate = rbd.mean(method="simulate", mc_samples=10_000, seed=1)
>>> print(f"{estimate:.1f}")
98.9
Two in parallel last 100 + 100 - 50 = 150 on average:
>>> pair = NonRepairableRBD(
... [("s", "a"), ("s", "b"), ("a", "t"), ("b", "t")],
... {n: surv.Exponential.from_params([0.01]) for n in "ab"},
... )
>>> round(pair.mean(), 9)
150.0
mean_time_to_failure ¶
mean_time_to_failure(mc_samples=None, seed=None, *, method='exact', tolerance=None, confidence=None, max_samples=None, antithetic=False, n_jobs=None)
Mean time to failure (MTTF) of the system; the same as mean.
Exact by default, and with method="simulate" a Monte-Carlo
estimate from mc_samples simulated lifetimes, which leaves out
common-cause groups that split a probability (see
mean).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mc_samples
|
int
|
With |
None
|
seed
|
int or None
|
With |
None
|
method
|
(exact, simulate)
|
How to find the MTTF, by default |
"exact"
|
tolerance
|
float
|
With |
None
|
confidence
|
float
|
With |
None
|
max_samples
|
int
|
With |
None
|
antithetic
|
bool
|
With |
False
|
n_jobs
|
int
|
With |
None
|
Returns:
| Type | Description |
|---|---|
float
|
The MTTF, or its estimate. |
Raises:
| Type | Description |
|---|---|
(ValueError, NotImplementedError, TypeError)
|
As for |
unreliability_interval ¶
unreliability_interval(x, *, method='auto', relative_tolerance=0.1, confidence=0.95, max_samples=10000000, seed=None)
The probability that the system has failed by x, P(T <=
x), estimated by simulation to a relative precision: for small
probabilities, which plain sampling estimates slowly.
The system's lifetime is drawn from a row of uniforms, as random
draws it (a standby or load-sharing node by its own logic), so the
probability is an integral over the unit cube of uniforms, which
these methods sample:
"plain": independent samples. For a probabilitypit takes aboutz**2 * (1 - p) / (p * relative_tolerance**2)lifetimes: some 4e10 forp = 1e-8and 10 %."latin_hypercube"and"sobol": Latin hypercube samples, and scrambled Sobol points (randomised quasi-Monte Carlo), in independent replicates, the error from their spread."cross_entropy": importance sampling from a Gaussian, in the standard normal space of the uniforms, shifted towards failure by the cross-entropy method, the samples weighted by the likelihood ratio."subset": subset simulation:pas a product of conditional probabilities of failing by ever earlier times, each about 0.1, sampled by Markov chains, in independent runs."auto"(the default): plain sampling when a pilot of 20 000 lifetimes sees at least 50 failures; else the cross-entropy method when the system fails in at most 8 ways (minimal cut sets), each of components that are distributions; else subset simulation (see the guide's measurements). The cross-entropy method's mixture follows each way of failing it is fitted to, and misses the others: asked for directly on a system with more, it underestimates, with an interval that does not show it.
Each runs until the half-width of the interval is at most
relative_tolerance times the estimate, or max_samples
lifetimes are spent (with a RuntimeWarning). Where the exact
unreliability is known, ff(x) gives it at once, to full
precision however small it is; this method is
for diagrams whose nodes are simulated (a standby or load-sharing
node without a closed form), and for checking.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
float
|
The time, at least 0. |
required |
method
|
str
|
|
'auto'
|
relative_tolerance
|
float
|
The half-width of the interval to reach, relative to the estimate, by default 0.1. |
0.1
|
confidence
|
float
|
The interval's confidence level, by default 0.95. |
0.95
|
max_samples
|
int
|
The most lifetimes to draw, by default 10 000 000. |
10000000
|
seed
|
int
|
Seeds the simulation, by default None (not reproducible). |
None
|
Returns:
| Type | Description |
|---|---|
ConfidenceInterval
|
The estimate, its interval (within [0, 1]) and standard error,
the lifetimes drawn ( |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
If a node's draws cannot be replayed from uniforms (a model that draws its own random numbers), or the diagram has common-cause groups that split a probability, which the simulation leaves out (a group splitting the failure rate is drawn). |
Examples:
Three units, any two of which keep the system up, fail by 10 with probability about 3e-6:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD, PerfectReliability
>>> unit = surv.Weibull.from_params([1000.0, 1.5])
>>> rbd = NonRepairableRBD(
... [("s", n) for n in "abc"] + [(n, "v") for n in "abc"]
... + [("v", "t")],
... {"a": unit, "b": unit, "c": unit, "v": PerfectReliability},
... k={"v": 2},
... )
>>> estimate = rbd.unreliability_interval(10.0, seed=1)
>>> estimate.method
'cross_entropy'
>>> bool(abs(estimate.estimate / rbd.ff(10.0) - 1) < 0.2)
True
mean_time_to_failure_interval ¶
mean_time_to_failure_interval(mc_samples=None, confidence=0.95, seed=None, *, tolerance=None, max_samples=None, antithetic=False, n_jobs=None)
Monte-Carlo MTTF estimate with a confidence interval.
mean gives the MTTF exactly; this estimates it by simulation, as
mean(method="simulate") does, and says how precise the estimate
is. The MTTF is the mean of mc_samples simulated system lifetimes
(see random; common-cause
groups are ignored). By the central limit theorem its sampling
error is normal with standard error
sample std / sqrt(mc_samples) (of the antithetic pairs' means,
over the square root of their number, with antithetic), from
which the two-sided interval estimate +/- z * standard_error is
built; the lower bound is clipped at 0. The interval describes the
simulation error only, not uncertainty in the node models.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mc_samples
|
int
|
Number of Monte-Carlo samples, by default 100_000. |
None
|
confidence
|
float
|
The confidence level, in (0, 1), by default 0.95. |
0.95
|
seed
|
int or None
|
Seed for reproducibility (see |
None
|
tolerance
|
float
|
Simulate until the interval is at most |
None
|
max_samples
|
int
|
The most lifetimes a run to |
None
|
antithetic
|
bool
|
Draw the lifetimes in antithetic pairs (see |
False
|
n_jobs
|
int
|
Draw in parallel over |
None
|
Returns:
| Type | Description |
|---|---|
ConfidenceInterval
|
The estimate, bounds, confidence level, standard error and
sample count (see
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
With |
Examples:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
... [("s", "c"), ("c", "t")],
... {"c": surv.Exponential.from_params([0.01])},
... )
>>> ci = rbd.mean_time_to_failure_interval(mc_samples=10_000, seed=1)
>>> print(f"{ci.estimate:.1f} ({ci.lower:.1f}, {ci.upper:.1f})")
98.9 (96.9, 100.8)
Simulate until the MTTF is known to within 0.5 either side:
>>> ci = rbd.mean_time_to_failure_interval(
... mc_samples=10_000, seed=1, tolerance=0.5
... )
>>> ci.n_samples, round(ci.upper - ci.estimate, 2)
(160000, 0.49)
compare ¶
compare(other, mc_samples=100000, seed=None, *, confidence=0.95, method=None)
How much longer (or shorter) this system's mean time to failure
is than other's: exactly where both MTTFs are worked out
exactly, and otherwise by simulation with common random numbers.
By default, where mean works out both systems' MTTFs (exactly
or numerically), the difference is theirs, with no error
(method="exact" in the result), and nothing is simulated,
as mean is exact by default. method="simulate"
simulates it whatever.
Simulated, both systems' lifetimes are drawn mc_samples times,
and in each sample a component with the same name in both draws the
same random numbers in both: the same lifetime where its model is
the same, and a matching one (the same quantile of its own model)
where it is not. The differences between the two systems' lifetimes
then come from how the systems differ, not from chance, so their
mean is a more precise estimate of the difference in MTTF than the
difference of two independent estimates of the same size (the more
the systems share, the more precise). The reliabilities themselves
need no simulation: compare sf for those.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
NonRepairableRBD
|
The system to compare with. |
required |
mc_samples
|
int
|
Simulated, the number of lifetimes of each system, by default 100_000. An exact difference simulates nothing. |
100000
|
seed
|
int
|
Simulated, the seed for a reproducible comparison, by default None. |
None
|
confidence
|
float
|
The confidence level of the interval, by default 0.95. |
0.95
|
method
|
str
|
None (the default): the exact difference where |
None
|
Returns:
| Type | Description |
|---|---|
ConfidenceInterval
|
The difference in MTTF (this system's minus |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
Simulated, if a node's draws cannot be replayed from uniforms (a
node model sampled its own way, whose random numbers the two
systems would not share); with |
Examples:
A third unit in parallel with two:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> unit = surv.Weibull.from_params([100, 2])
>>> def parallel(n):
... names = [f"u{i}" for i in range(n)]
... edges = [("s", u) for u in names] + [(u, "t") for u in names]
... return NonRepairableRBD(edges, {u: unit for u in names})
>>> gain = parallel(3).compare(parallel(2))
>>> round(gain.estimate, 2), gain.method
(14.46, 'exact')
The integral of the difference in reliability. Simulated with common random numbers:
>>> gain = parallel(3).compare(
... parallel(2), mc_samples=20_000, seed=0, method="simulate"
... )
>>> round(gain.estimate, 1), round(gain.standard_error, 2)
(14.8, 0.21)
Two independent estimates from 20_000 lifetimes each would give it with a standard error of about 0.42.
time_to_reliability ¶
time_to_reliability(target, upper_bound=None, **kwargs)
Time at which the system reliability falls to target.
Solves R(t) = target for t >= 0 (the inverse of
sf, so it honours common-cause
groups). System reliability is non-increasing in time, so the
crossing is bracketed between 0 and an upper bound and found with
Brent's method (scipy.optimize.brentq). Unless upper_bound
is given, the bound is found by doubling from t = 1.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
float or array_like
|
The reliability level to solve for, in (0, 1), or several. |
required |
upper_bound
|
float
|
An upper bound for the search, at which the reliability must be
below |
None
|
**kwargs
|
Keyword arguments of |
{}
|
Returns:
| Type | Description |
|---|---|
float or ndarray
|
The time at which |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
As for |
Examples:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
... [("s", "c"), ("c", "t")],
... {"c": surv.Weibull.from_params([100, 2])},
... )
>>> round(rbd.time_to_reliability(0.9), 2)
32.46
bx_life ¶
bx_life(x, **kwargs)
Bx life: the time by which x percent of systems have failed.
The time at which R(t) = 1 - x / 100, found with
time_to_reliability(1 - x / 100). For example bx_life(10)
is the B10 life (10% failed, 90% reliability).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
float or array_like
|
The percentage failed (10 for the B10 life, not a fraction), in (0, 100), or several. |
required |
**kwargs
|
Keyword arguments of |
{}
|
Returns:
| Type | Description |
|---|---|
float or ndarray
|
The time at which |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
As for |
Examples:
The B10 life (10% failed) of a single Weibull component:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
... [("s", "c"), ("c", "t")],
... {"c": surv.Weibull.from_params([100, 2])},
... )
>>> round(rbd.bx_life(10), 2)
32.46
sf_given_state ¶
sf_given_state(x=None, state=None, method='p')
System reliability over the next x, given each node's state.
The condition-based ("digital twin") generalisation of
sf: instead of assuming every
component is new, each component conditions on its own current
NodeState (e.g. streamed from sensors),
and the conditioned node reliabilities are propagated exactly
through the system:
R_i(x | X_i) = R_i(X_i + x) / R_i(X_i)
R_sys(x | state) = system reliability from the R_i(x | X_i)
A node that is alive at age X_i contributes R_i(x | X_i)
(0 where R_i(X_i) is 0), a failed node (alive=False)
contributes 0, and a node left out of state contributes its
ordinary reliability R_i(x), as if new.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
The further duration/s at which reliability is evaluated
(measured from now, so |
None
|
state
|
dict[Hashable, NodeState]
|
|
None
|
method
|
str
|
|
'p'
|
Returns:
| Type | Description |
|---|---|
float or ndarray
|
System reliability given the state: a float for scalar |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
NotImplementedError
|
If the RBD has common-cause (CCF) groups. |
Notes
Only lifetime (time-varying) distributions age. A fixed-probability
component stated alive contributes reliability 1 going forward (its
per-demand uncertainty is resolved by observing it alive), whereas
left out of state it contributes its fixed probability.
Composite / dynamic nodes (standby, load-sharing, repeated, nested
RBD) are not supported here in this release: they may be left out
of state (and are then unconditioned) but raise if given a
state.
Examples:
A component 40 hours into life is less reliable over the next 50 than a new one would be:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD, NodeState
>>> rbd = NonRepairableRBD(
... [("s", "c"), ("c", "t")],
... {"c": surv.Weibull.from_params([100, 2])},
... )
>>> round(rbd.sf_given_state(50, {"c": NodeState(age=40)}), 4)
0.522
remaining_life ¶
remaining_life(target, state=None, upper_bound=None)
Remaining useful life: the time until reliability falls to target.
The condition-based analogue of time_to_reliability: it solves
sf_given_state(t, state) == target for t, by the same
bracketing and Brent's method. Because
sf_given_state is
measured from now, the result is the time remaining from the
current state. remaining_life(1 - x / 100, state) is the
conditional Bx life; mean_residual_life(state) is the mean
remaining life.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
float or array_like
|
The system reliability level to solve for, in (0, 1), or several. |
required |
state
|
dict[Hashable, NodeState]
|
|
None
|
upper_bound
|
float
|
An upper bound for the search, at which the reliability must be
below |
None
|
Returns:
| Type | Description |
|---|---|
float or ndarray
|
The remaining time until |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
TypeError
|
If |
NotImplementedError
|
If the RBD has common-cause (CCF) groups. |
Examples:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD, NodeState
>>> rbd = NonRepairableRBD(
... [("s", "c"), ("c", "t")],
... {"c": surv.Weibull.from_params([100, 2])},
... )
>>> round(rbd.remaining_life(0.9, {"c": NodeState(age=40)}), 2)
11.51
mean_residual_life ¶
mean_residual_life(state=None)
The mean remaining life: the expected time from now until the system fails, given its components' current states.
remaining_life gives the time until the reliability given the
state falls to a target, a percentile of the remaining life; this
is its mean, the area under
sf_given_state
from now, by quadrature (to about 1e-10), as mean integrates
sf. With every node new it is mean().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
dict[Hashable, NodeState]
|
|
None
|
Returns:
| Type | Description |
|---|---|
float
|
The mean remaining life: |
Raises:
| Type | Description |
|---|---|
TypeError
|
As for |
ValueError
|
If the RBD is fixed-probability (its reliability does not
change with time), or as for |
NotImplementedError
|
As for |
Examples:
A Weibull unit with a mean life of 88.6 has 59.4 more to go on average at age 40:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD, NodeState
>>> rbd = NonRepairableRBD(
... [("s", "c"), ("c", "t")],
... {"c": surv.Weibull.from_params([100, 2])},
... )
>>> round(rbd.mean_residual_life(), 2)
88.62
>>> round(rbd.mean_residual_life({"c": NodeState(age=40)}), 2)
59.45
importances_given_state ¶
importances_given_state(x=None, state=None, kind='failure')
Birnbaum and criticality importance given each node's state.
Shows how each component's importance over a forward horizon x
shifts once the current wear on every component is accounted for.
The Birnbaum and criticality importance measures are evaluated at
the conditioned node reliabilities R_i(x | X_i) (see
sf_given_state)
rather than at the as-new reliabilities, so the rankings reflect
the current state. With F = 1 - R:
birnbaum_i = R_sys(x | state, i working)
- R_sys(x | state, i failed)
criticality_i = birnbaum_i * F_i(x | X_i) / F_sys(x | state)
the failure-oriented criticality: the share of the system failures
over the horizon that node i accounts for. kind="success"
gives the success-oriented form,
birnbaum_i * R_i(x | X_i) / R_sys(x | state), instead. These
are the conventions of birnbaum_importance and
criticality_importance, applied to the system as it is now. A
failed node's success-oriented criticality is 0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
The forward horizon/s over which importance is evaluated (from now). May be omitted only for a fixed-probability RBD. |
None
|
state
|
dict[Hashable, NodeState]
|
|
None
|
kind
|
str
|
The criticality's form: |
'failure'
|
Returns:
| Type | Description |
|---|---|
dict[str, dict[Any, float | ndarray]]
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
NotImplementedError
|
If the RBD has common-cause (CCF) groups. |
Examples:
Two identical units in parallel, one of them 60 hours old: over the next 20 hours the system now depends more on the new unit:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD, NodeState
>>> unit = surv.Weibull.from_params([100, 2])
>>> rbd = NonRepairableRBD(
... [("s", "old"), ("s", "new"), ("old", "t"), ("new", "t")],
... {"old": unit, "new": unit},
... )
>>> imp = rbd.importances_given_state(20, {"old": NodeState(age=60)})
>>> {k: round(v, 4) for k, v in sorted(imp["birnbaum"].items())}
{'new': 0.2442, 'old': 0.0392}
node_mttf ¶
node_mttf()
Mean time to failure (MTTF) of each component node.
Each node's MTTF comes from its own model, without simulating:
- a nested
NonRepairableRBDor aRepeatedNode: the area under its reliability (seemean); - a
StandbyModelorLoadSharingModel: itsmean(), exact where it has a closed form or convolution, and otherwise the mean of the lifetimes simulated when it was built, which its reliability is fitted to; - a fixed-probability node: 0.0, as it has no time dimension;
- any other model: its own
mean(), e.g. the exact mean of a surpyval distribution, except that a limited-failure-population model's is infinite: some of its units never fail. (surpyval'smean()of one is the defective mean, its failing units' mean weighted by their fraction.)
Common-cause groups do not affect a node's own MTTF. A junction (a
node given PerfectReliability,
such as a k-out-of-n vote point) never fails and is left out, as
from the importance measures.
Returns:
| Type | Description |
|---|---|
dict[Any, float]
|
|
Raises:
| Type | Description |
|---|---|
AttributeError
|
If a node's model has no |
NotImplementedError
|
If a nested RBD has common-cause groups that split a
probability (see |
Examples:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
... [("s", "a"), ("a", "b"), ("b", "t")],
... {
... "a": surv.Exponential.from_params([0.01]),
... "b": surv.FixedEventProbability.from_params(0.1),
... },
... )
>>> {k: round(v, 2) for k, v in sorted(rbd.node_mttf().items())}
{'a': 100.0, 'b': 0.0}
birnbaum_importance ¶
birnbaum_importance(x=None, working_nodes=None, broken_nodes=None)
Birnbaum importance of each node at time/s x.
In the guide's Greeks it is delta: how far the system moves with each node (see Sensitivities: the Greeks).
B_i = R_sys(i working) - R_sys(i failed): the rate at which the
system reliability changes with node i's reliability, which is
also the probability that node i is critical (the system works
if i works and fails if i fails). It does not depend on node
i's own reliability. Exact; it assumes independent nodes. It is
worked out for every node at once, as the derivative of the system
reliability: a sum of products of the nodes' reliabilities and
probabilities of failing (their models' sf and ff), so a
small one keeps its full relative precision, where the difference
above would cancel in a reliable system.
With common-cause groups, a member's state says something of the
others', so its R_sys(i working) and R_sys(i failed) are
the system's reliability given that it works and that it has
failed: sums over the groups' shock outcomes, each weighted by the
member's chance of that state under it. A node outside the groups
is held working and failed, as without them. The other measures
are built on the same two values, and beta = 0 gives them as
without the group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s as a number or an array. May be omitted only for a fixed-probability RBD. |
None
|
working_nodes
|
Collection[Hashable]
|
Nodes to treat as working (reliability 1), by default none. |
None
|
broken_nodes
|
Collection[Hashable]
|
Nodes to treat as failed (reliability 0), by default none. |
None
|
Returns:
| Type | Description |
|---|---|
dict[Any, float | ndarray]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
If a common-cause group's member is held working or broken. |
Examples:
In a two-component parallel system each node's Birnbaum importance is the probability the other node has failed:
>>> 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.1),
... },
... )
>>> bi = rbd.birnbaum_importance()
>>> {k: round(v, 4) for k, v in sorted(bi.items())}
{'a': 0.1, 'b': 0.1}
Once "b" has failed, the system depends entirely on "a":
>>> bi = rbd.birnbaum_importance(broken_nodes=["b"])
>>> {k: round(v, 4) for k, v in sorted(bi.items())}
{'a': 1.0, 'b': 0.1}
improvement_potential ¶
improvement_potential(x=None, working_nodes=None, broken_nodes=None)
Improvement potential of each node at time/s x.
IP_i = R_sys(i working) - R_sys: how much the system reliability
would rise if node i were made perfect. It equals
B_i * (1 - R_i), with B_i the Birnbaum importance, and is
worked out as that product (with 1 - R_i from the node's model's
ff), so a small one keeps its precision. Exact; it assumes
independent nodes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s as a number or an array. May be omitted only for a fixed-probability RBD. |
None
|
working_nodes
|
Collection[Hashable]
|
Nodes to treat as working (reliability 1), by default none. |
None
|
broken_nodes
|
Collection[Hashable]
|
Nodes to treat as failed (reliability 0), by default none. |
None
|
Returns:
| Type | Description |
|---|---|
dict[Any, float | ndarray]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
If a common-cause group's member is held working or broken. |
Examples:
Two pumps in parallel (each failing with probability 0.1) feeding a valve in series (0.05): a perfect valve gains the most.
>>> 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),
... },
... )
>>> ip = rbd.improvement_potential()
>>> {k: round(v, 4) for k, v in sorted(ip.items())}
{'p1': 0.0095, 'p2': 0.0095, 'v': 0.0495}
risk_achievement_worth ¶
risk_achievement_worth(x=None, working_nodes=None, broken_nodes=None)
Risk achievement worth (RAW) of each node at time/s x.
RAW_i = (1 - R_sys(i failed)) / (1 - R_sys), per Modarres &
Kaminskiy: the factor by which the system unreliability would grow
if node i were failed. It is at least 1. Where the system
unreliability is 0 the division gives inf or nan, with a numpy
RuntimeWarning. Exact; it assumes independent nodes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s as a number or an array. May be omitted only for a fixed-probability RBD. |
None
|
working_nodes
|
Collection[Hashable]
|
Nodes to treat as working (reliability 1), by default none. |
None
|
broken_nodes
|
Collection[Hashable]
|
Nodes to treat as failed (reliability 0), by default none. |
None
|
Returns:
| Type | Description |
|---|---|
dict[Any, float | ndarray]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
If a common-cause group's member is held working or broken. |
Examples:
Two pumps in parallel (each failing with probability 0.1) feeding a valve in series (0.05): a failed valve fails the system outright.
>>> 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),
... },
... )
>>> raw = rbd.risk_achievement_worth()
>>> {k: round(v, 4) for k, v in sorted(raw.items())}
{'p1': 2.437, 'p2': 2.437, 'v': 16.8067}
risk_reduction_worth ¶
risk_reduction_worth(x=None, working_nodes=None, broken_nodes=None)
Risk reduction worth (RRW) of each node at time/s x.
RRW_i = (1 - R_sys) / (1 - R_sys(i working)), per Modarres &
Kaminskiy: the factor by which the system unreliability would
shrink if node i were made perfect. It is at least 1, and inf
(with a numpy RuntimeWarning) where a perfect node i makes the
system perfect, e.g. a node that alone forms a path. Exact; it
assumes independent nodes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s as a number or an array. May be omitted only for a fixed-probability RBD. |
None
|
working_nodes
|
Collection[Hashable]
|
Nodes to treat as working (reliability 1), by default none. |
None
|
broken_nodes
|
Collection[Hashable]
|
Nodes to treat as failed (reliability 0), by default none. |
None
|
Returns:
| Type | Description |
|---|---|
dict[Any, float | ndarray]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
If a common-cause group's member is held working or broken. |
Examples:
Two pumps in parallel (each failing with probability 0.1) feeding a valve in series (0.05):
>>> 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),
... },
... )
>>> rrw = rbd.risk_reduction_worth()
>>> {k: round(v, 4) for k, v in sorted(rrw.items())}
{'p1': 1.19, 'p2': 1.19, 'v': 5.95}
criticality_importance ¶
criticality_importance(x=None, working_nodes=None, broken_nodes=None, kind='failure')
Criticality importance of each node at time/s x.
With B_i the Birnbaum importance, R_i the node's reliability
and R_sys the system's:
kind="failure"(the default) gives the failure-oriented form (Rausand & Høyland),CI_i = B_i * (1 - R_i) / (1 - R_sys): the probability that nodeihas failed and is critical, given that the system has failed -- the share of system failures that nodeiaccounts for. It ranks nodes in series by how unreliable they are. It is computed from the node unreliabilities, through the minimal cut sets, so the system unreliability is not lost to cancellation in1 - R_syshowever reliable the system is. It isnanwhere the system cannot fail (e.g. atx = 0).kind="success"gives the success-oriented form,CI_i = B_i * R_i / R_sys: the probability that nodeiis working and critical, given that the system works. It is 1 for every node in series with the rest of the system, however unreliable, so it cannot rank them. It isnanwhere the system cannot work.
Exact; it assumes independent nodes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s as a number or an array. May be omitted only for a fixed-probability RBD. |
None
|
working_nodes
|
Collection[Hashable]
|
Nodes to treat as working (reliability 1), by default none. |
None
|
broken_nodes
|
Collection[Hashable]
|
Nodes to treat as failed (reliability 0), by default none. |
None
|
kind
|
str
|
|
'failure'
|
Returns:
| Type | Description |
|---|---|
dict[Any, float | ndarray]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
If a common-cause group's member is held working or broken. |
References
M. Rausand and A. Høyland, System Reliability Theory: Models, Statistical Methods, and Applications, 2nd edition, Wiley, 2004.
Examples:
Two pumps in parallel (each failing with probability 0.1) feeding a valve in series (0.05): the valve accounts for 83% of the system's failures (the shares add to more than 1 here, as both pumps are critical in the same failures).
>>> 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),
... },
... )
>>> ci = rbd.criticality_importance()
>>> {k: round(v, 4) for k, v in sorted(ci.items())}
{'p1': 0.1597, 'p2': 0.1597, 'v': 0.8319}
The success-oriented form gives the valve, like any node in series, exactly 1:
>>> ci = rbd.criticality_importance(kind="success")
>>> {k: round(v, 4) for k, v in sorted(ci.items())}
{'p1': 0.0909, 'p2': 0.0909, 'v': 1.0}
fussell_vesely ¶
fussell_vesely(x=None, fv_type='c', working_nodes=None, broken_nodes=None, method='exact')
Fussell-Vesely importance of each node at time/s x.
With fv_type="c" (the default) this is the usual cut-set
measure: the probability that a minimal cut set containing node
i has failed (every node in it), given that the system has
failed, the share of the system's failure probability that involves
node i:
FV_i = P(some minimal cut set containing i has failed)
/ (1 - R_sys)
It is exact (method="exact", the default), from the exact
engine, and between 0 and 1. method="rare_event" gives the
rare-event approximation that many PRA tools report, summing the
probabilities of those cut sets instead of taking their union:
FV_i = sum over minimal cut sets C containing i of
prod over j in C of (1 - R_j), divided by (1 - R_sys)
The sum over-estimates the union, so it can exceed 1 when the
failure probabilities are not small (up to 2 on a bridge whose
system is near failure). Each 1 - R is worked out in its own
right (a node's from its model's ff, the system's as
ff is), and so is the union, so
small ones keep their precision.
With fv_type="p" the minimal path sets take the place of the cut
sets: the numerator is the probability that every member of some
minimal path set containing i has failed (or, with
"rare_event", the sum of those probabilities). This value is
not bounded by 1: for a node in a parallel pair it is
(1 - R_i) / (1 - R_sys).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s as a number or an array. May be omitted only for a fixed-probability RBD. |
None
|
fv_type
|
str
|
|
'c'
|
working_nodes
|
Collection[Hashable]
|
Nodes to treat as working (reliability 1), by default none. |
None
|
broken_nodes
|
Collection[Hashable]
|
Nodes to treat as failed (reliability 0), by default none. |
None
|
method
|
str
|
|
'exact'
|
Returns:
| Type | Description |
|---|---|
dict[Any, float | ndarray]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
If a common-cause group's member is held working or broken. |
Examples:
Two pumps in parallel (each failing with probability 0.1) feeding a valve in series (0.05): the valve alone is a cut set, and it contributes most of the system's failure probability.
>>> 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),
... },
... )
>>> fv = rbd.fussell_vesely()
>>> {k: round(v, 4) for k, v in sorted(fv.items())}
{'p1': 0.1681, 'p2': 0.1681, 'v': 0.8403}
Here no two cut sets share a node, so the rare-event sum is the same. In a bridge they overlap, and once failures are likely the sum passes 1 while the exact share cannot:
>>> F = FixedEventProbability.from_params
>>> bridge = NonRepairableRBD(
... [("s", "a"), ("s", "b"), ("a", "c"), ("b", "c"),
... ("a", "d"), ("c", "d"), ("b", "e"), ("c", "e"),
... ("d", "t"), ("e", "t")],
... {node: F(0.8) for node in "abcde"},
... )
>>> round(bridge.fussell_vesely()["a"], 4)
0.8146
>>> round(bridge.fussell_vesely(method="rare_event")["a"], 4)
1.264
parameter_sensitivity ¶
parameter_sensitivity(x=None, working_nodes=None, broken_nodes=None, rel_step=1e-05)
Sensitivity of system reliability to each node's parameters.
In the guide's Greeks it is the levers' deltas (see Sensitivities: the Greeks).
For node i with parameter theta, the sensitivity at time/s
x is
d R_sys / d theta = B_i(x) * d sf_i(x; theta) / d theta
where B_i is the Birnbaum importance of node i (how much the
system reliability moves per unit change in that node's reliability)
and d sf_i / d theta is how much the node's reliability moves per
unit change in the parameter. The parameter derivative is taken
numerically by a central finite difference with step
rel_step * |theta| (rel_step if theta is 0), rebuilding
the distribution with from_params, so it applies to any
parametric surpyval model without per-distribution formulae. If one
perturbation is not a valid parameter value (e.g. a probability
leaving [0, 1]) a one-sided difference is used; if neither is, the
result is nan. It answers "which fitted parameter, if it were a
little different, would move system reliability the most" -- e.g.
to target data collection or to gauge the impact of estimation
uncertainty.
Only nodes with reconstructable surpyval distribution parameters
are included, fixed-probability nodes among them (their parameter
is the failure probability); levers() lists them, with their
values and ranges, and with_levers builds the RBD with them
moved. A regression node's levers are its covariates
("covariate.<name>"), at fixed covariates. Composite nodes
(a nested RBD, a standby, load-sharing or repeated node, a
regression node along a schedule) and the input and output nodes
have no parameters to perturb and are omitted. A node forced via
working_nodes/broken_nodes is pinned independently of its
parameters, so its sensitivities are reported as zero.
The members of a common-cause group carry one model, so its
parameters are the group's: they are reported once, under the
tuple of the group's members, each the derivative of the system
reliability (with the groups, as sf works it out) as the
parameter moves for every member at once. So are the parameters of
the group's common-cause model, named "ccf_beta" (and for an
MGL model "ccf_gamma", "ccf_delta",
..., and for an AlphaFactor model
"ccf_alpha_2", ..., alpha_1 being the remainder), by a
central difference in each; a parameter at the edge of
[0, 1] is differenced on its one side. Each difference is of
the system's probability of failing where that is the smaller, so
a small one keeps its precision.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s as a number or an array. May be omitted only for a fixed-probability RBD. |
None
|
working_nodes
|
Collection[Hashable]
|
Nodes to treat as working (reliability 1), by default none. |
None
|
broken_nodes
|
Collection[Hashable]
|
Nodes to treat as failed (reliability 0), by default none. |
None
|
rel_step
|
float
|
Relative step used for the finite difference, by default |
1e-05
|
Returns:
| Type | Description |
|---|---|
dict[Any, dict[str, float | ndarray]]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
If a common-cause group's member is held working or broken. |
Examples:
For a single exponential component R = exp(-rate * x), so
dR/d rate = -x * exp(-rate * x), about -9.048 at x = 10
with rate 0.01:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
... [("s", "c"), ("c", "t")],
... {"c": surv.Exponential.from_params([0.01])},
... )
>>> sens = rbd.parameter_sensitivity(10)
>>> {name: round(v, 3) for name, v in sens["c"].items()}
{'failure_rate': -9.048}
Two pumps in parallel, each failing with probability p = 0.05,
a fraction beta = 0.1 of their failures shared. Their one
model's p and the group's beta are reported together, as
derivatives of the system reliability
1 - beta p - (1 - beta p) ((1 - beta) p) ** 2:
>>> from surpyval import FixedEventProbability
>>> from repyability import BetaFactor, CCFGroup
>>> pump = FixedEventProbability.from_params(0.05)
>>> pair = NonRepairableRBD(
... [("s", "a"), ("s", "b"), ("a", "t"), ("b", "t")],
... {"a": pump, "b": pump},
... ccf_groups=[CCFGroup(["a", "b"], BetaFactor(0.1))],
... )
>>> sens = pair.parameter_sensitivity()
>>> {name: round(v, 4) for name, v in sens[("a", "b")].items()}
{'p': -0.1804, 'ccf_beta': -0.0454}
levers ¶
levers()
The parameters that parameter_sensitivity moves, in the order
it reports them: each node's model's, by surpyval's names,
and each common-cause group's under the tuple of its members (the
parameters of the one model its members carry, then its own,
"ccf_beta", ...).
Each is a Lever: whose it is and its name, as
parameter_sensitivity keys them, its value and the range of its
values. Nothing is worked out, so a report can name the parameters
and show their values without the sensitivities;
with_levers sets them.
The levers are the nodes' models' own parameters: those of the
units inside a composite model (a StandbyModel, a
LoadSharingModel, a DegradingNode) or of a nested diagram
are not reached (see the guide's Greeks).
Returns:
| Type | Description |
|---|---|
list of Lever
|
The levers, in |
Examples:
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
... [("s", "c"), ("c", "t")],
... {"c": surv.Weibull.from_params([100, 2])},
... )
>>> for lever in rbd.levers():
... print(lever.key, lever.name, lever.value, lever.bounds)
c alpha 100.0 (0.0, inf)
c beta 2.0 (0.0, inf)
with_levers ¶
with_levers(values)
A copy of this RBD with parameters at new values, each
moved as parameter_sensitivity moves it: a node's model rebuilt
with the parameter changed, a common-cause group's members' one
model for every member, a group's own ("ccf_beta", ...) in its
model. levers() lists the parameters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
dict
|
Each parameter's new value, by its
|
required |
Returns:
| Type | Description |
|---|---|
NonRepairableRBD
|
The new RBD, built as this one was with the parameters moved; this one is unchanged. |
Raises:
| Type | Description |
|---|---|
ValueError
|
For a parameter the RBD does not have (with the closest name),
or a value its model refuses (one outside the lever's
|
TypeError
|
If |
Examples:
Twice the Weibull scale: at 100 the unit survives with probability
exp(-(100 / 200) ** 2), about 0.7788, where it did with
exp(-1):
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
... [("s", "c"), ("c", "t")],
... {"c": surv.Weibull.from_params([100, 2])},
... )
>>> longer = rbd.with_levers({("c", "alpha"): 200.0})
>>> round(float(longer.sf(100)), 4)
0.7788
differential_importance ¶
differential_importance(x=None, working_nodes=None, broken_nodes=None, *, over='components', change='uniform', kind='failure', improving=False, groups=None, rel_step=1e-05)
Each node's (or parameter's) share of the change in the system reliability when they all change together: the differential importance measure (DIM, Borgonovo & Apostolakis, 2001).
In the guide's Greeks it is DIM, the shares of a change (see Sensitivities: the Greeks).
DIM_i = dR/dtheta_i dtheta_i / sum_j dR/dtheta_j dtheta_j, so
the shares add up to 1, and a group's share is the sum of its
members' (groups): what share of a possible gain lies in the
valves, say, or in all the shape parameters. change="uniform"
moves every theta by as much; "proportional" each by the
same fraction of itself.
Over the nodes (over="components"), theta is each node's
probability of failing (kind="failure") or of working
(kind="success"). A uniform change shares out the Birnbaum
importance, either way; a proportional one the criticality
importance of that kind (the failure-oriented one is the
improvement potential over the system's unreliability). A node
held working or failed takes no part (its share is 0).
Over the parameters (over="parameters"), the derivatives are
parameter_sensitivity's, keyed (node, parameter) (a
common-cause group's under the tuple of its members), and a
proportional change moves each parameter by the same fraction of
its value.
Effects that oppose (parameters that lower the reliability and
others that raise it) give shares of either sign, and some beyond
1; where they cancel, there is nothing to share out, and the
shares are NaN. improving=True moves each parameter instead
the way that raises the reliability (a scale up, a probability of
failing down), so that every share is of a gain, between 0 and 1.
A uniform change adds the same amount to parameters of different
units (a scale in hours, a shape with none): over parameters, the
proportional change is usually the one to ask for.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s, as for |
None
|
working_nodes
|
Collection[Hashable]
|
Nodes held working, as for |
None
|
broken_nodes
|
Collection[Hashable]
|
Nodes held failed, likewise. |
None
|
over
|
str
|
|
'components'
|
change
|
str
|
|
'uniform'
|
kind
|
str
|
Over the nodes, with a proportional change, which of each
node's probabilities moves in proportion: |
'failure'
|
improving
|
bool
|
Move each parameter the way that raises the reliability, so that the shares are of a gain (each one's size, shared out), instead of every one up together (the default, the measure as defined). The nodes' shares are the same either way. |
False
|
groups
|
dict
|
|
None
|
rel_step
|
float
|
The parameters' finite-difference step, as for
|
1e-05
|
Returns:
| Type | Description |
|---|---|
dict
|
The shares, by node, |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown |
Examples:
Two nodes in series, failing with probabilities 0.1 and 0.2: a
uniform change moves the reliability by 0.8 dq_a + 0.9 dq_b
(the Birnbaum importances), so a holds 0.8 / 1.7 of it:
>>> from surpyval import FixedEventProbability
>>> from repyability import NonRepairableRBD
>>> F = FixedEventProbability.from_params
>>> rbd = NonRepairableRBD(
... [("s", "a"), ("a", "b"), ("b", "t")],
... {"a": F(0.1), "b": F(0.2)},
... )
>>> shares = rbd.differential_importance()
>>> {node: round(share, 4) for node, share in shares.items()}
{'a': 0.4706, 'b': 0.5294}
A proportional change weighs each by its probability of failing,
0.1 * 0.8 against 0.2 * 0.9:
>>> shares = rbd.differential_importance(change="proportional")
>>> round(shares["b"], 4)
0.6923
joint_importance ¶
joint_importance(x=None, working_nodes=None, broken_nodes=None)
The joint (second-order) importance of each pair of nodes: whether improving the two together is worth more than improving each (Hong & Lie, 1993; Armstrong, 1995).
In the guide's Greeks it is gamma: complements or substitutes (see Sensitivities: the Greeks).
JRI(i, j) = d2R / dR_i dR_j = R(1_i, 1_j) - R(1_i, 0_j) -
R(0_i, 1_j) + R(0_i, 0_j), with R(1_i, 0_j) the system
reliability with node i working and node j failed: how much
node j's Birnbaum importance rises when node i goes from
failed to working. Positive, the two are complements, as in series:
improving either makes improving the other worth more. Negative,
they are substitutes, as in parallel: either does the other's job.
0 where neither changes what the other is worth.
The system reliability being multilinear in the nodes', it is exact: each node's Birnbaum importance with the other held working, less that with it held failed (twice as many evaluations as there are nodes, not as pairs).
With common-cause groups, the system's reliability given each of
the pair working and failed is conditioned on their states, as
birnbaum_importance conditions on one: a member's state says
something of its group's shocks, so the system given it is a sum
over the shock outcomes, and two members of one group are
conditioned on their joint state (see Common-cause
failures). A node outside the groups is
held. beta = 0 gives the values without the group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s, as for |
None
|
working_nodes
|
Collection[Hashable]
|
Nodes held working: their pairs are 0. |
None
|
broken_nodes
|
Collection[Hashable]
|
Nodes held failed, likewise. |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
As |
NotImplementedError
|
If a common-cause group's member is held working or broken. |
Examples:
Two pumps in parallel, then a valve: the pumps are substitutes, and each is a complement of the valve.
>>> from surpyval import FixedEventProbability
>>> from repyability import NonRepairableRBD
>>> F = FixedEventProbability.from_params
>>> rbd = NonRepairableRBD(
... [("s", "p1"), ("s", "p2"), ("p1", "v"), ("p2", "v"),
... ("v", "t")],
... {"p1": F(0.2), "p2": F(0.2), "v": F(0.1)},
... )
>>> {pair: round(v, 4) for pair, v in rbd.joint_importance().items()}
{('p1', 'p2'): -0.9, ('p1', 'v'): 0.2, ('p2', 'v'): 0.2}
A shared cause striking both pumps (a tenth of their failures)
makes one's failure a sign of the other's: with p1 failed,
p2 is likelier to have failed too, so the valve is worth less
then, and more of a complement of each pump. The pumps' pair is
as before: given their states, the valve alone remains.
>>> from repyability import BetaFactor, CCFGroup
>>> grouped = NonRepairableRBD(
... [("s", "p1"), ("s", "p2"), ("p1", "v"), ("p2", "v"),
... ("v", "t")],
... {"p1": F(0.2), "p2": F(0.2), "v": F(0.1)},
... ccf_groups=[CCFGroup(["p1", "p2"], BetaFactor(0.1))],
... )
>>> joint = grouped.joint_importance()
>>> round(joint[("p1", "p2")], 4), round(joint[("p1", "v")], 4)
(-0.9, 0.2635)
reliability_rate ¶
reliability_rate(x, working_nodes=None, broken_nodes=None)
How fast the system reliability is falling at each time x,
and which nodes are bringing it down.
In the guide's Greeks it is theta: what is moving the system now (see Sensitivities: the Greeks).
The nodes failing independently, the system reliability is multilinear in theirs, so
dR/dt = sum_i I_B^i(t) dR_i/dt = -sum_i I_B^i(t) f_i(t)
exactly: the system's failure density, f(t) = -dR/dt, split
among the nodes, each term the density with which that node's
failure is the one that fails the system then. Integrated over
time, it is barlow_proschan_importance. A node's density is its
model's own (df) where it has one, else by differences of its
reliability, as df takes the system's; with common-cause groups
a group's part (its members together, under the tuple of their
names) is the system's change with its members' lives moved on and
the rest held, by differences too.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s, a number or an array. |
required |
working_nodes
|
Collection[Hashable]
|
Nodes held working: their part is 0. |
None
|
broken_nodes
|
Collection[Hashable]
|
Nodes held failed, likewise. |
None
|
Returns:
| Type | Description |
|---|---|
RateBreakdown
|
The system reliability's |
Raises:
| Type | Description |
|---|---|
ValueError
|
As |
Examples:
Two pumps in parallel, then a valve: at 50 the valve is bringing the system down about as fast as the two pumps together.
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
... [("s", "p1"), ("s", "p2"), ("p1", "v"), ("p2", "v"),
... ("v", "t")],
... {
... "p1": surv.Weibull.from_params([100, 2]),
... "p2": surv.Weibull.from_params([100, 2]),
... "v": surv.Weibull.from_params([200, 1.5]),
... },
... )
>>> rate = rbd.reliability_rate(50)
>>> {node: round(part, 5) for node, part in rate.node_rate.items()}
{'p1': -0.00152, 'p2': -0.00152, 'v': -0.00315}
>>> round(rate.rate, 5) == round(-rbd.df(50), 5)
True
barlow_proschan_importance ¶
barlow_proschan_importance(x=None, working_nodes=None, broken_nodes=None)
Each node's Barlow-Proschan importance: the probability that the
system's failure is caused by the node's (Barlow & Proschan,
1975), given that the system fails by x, or over its whole life.
In the guide's Greeks it is theta, integrated: who caused the failures (see Sensitivities: the Greeks).
A node's failure fails the system when the node is critical then,
so the probability that the system has failed by x, through
node i, is integral_0^x I_B^i(t) f_i(t) dt: the
node's part of the system's failure density
(reliability_rate), integrated. Its share of the sum over the
nodes, the system's unreliability by x, is the node's
importance; the shares add up to 1. With x left out the
integrals run over the whole life. A common-cause group's part is
its members' together, under the tuple of their names.
The integrals are by adaptive Gauss-Legendre quadrature, as
mean integrates the reliability, to about 1e-9, relative.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
Time/s by which the system has failed, a number or an array; by default its whole life. |
None
|
working_nodes
|
Collection[Hashable]
|
Nodes held working (they cause nothing). |
None
|
broken_nodes
|
Collection[Hashable]
|
Nodes held failed (likewise). |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
Each node's share (and each common-cause group's): floats for a
number |
Raises:
| Type | Description |
|---|---|
ValueError
|
For a negative time, if every node is a fixed probability (the
system then has no lifetimes), or as |
Examples:
Two nodes in series, with failure rates 1 and 3: the second causes three quarters of the system's failures.
>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> E = surv.Exponential.from_params
>>> rbd = NonRepairableRBD(
... [("s", "a"), ("a", "b"), ("b", "t")],
... {"a": E([1.0]), "b": E([3.0])},
... )
>>> shares = rbd.barlow_proschan_importance()
>>> {node: round(share, 6) for node, share in shares.items()}
{'a': 0.25, 'b': 0.75}