Skip to content

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), a MixtureModel, 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. a surpyval.KaplanMeier fit), which is refused: fit a parametric distribution in surpyval;
  • a composite node: a StandbyModel, LoadSharingModel, RepeatedNode, RepeatedStandbyNode, RegressionNode or another NonRepairableRBD nested as a single node;
  • one unit of a fitted surpyval joint model (readings and failures), given its readings: its DynamicPrediction (predict), or a JointNode, whose reliability is from now;
  • PerfectReliability or PerfectUnreliability.

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 (from_node, to_node) of the diagram, e.g. [("in", "a"), ("a", "out")].

required
reliabilities dict

{node: model} for every component node (see above). A value that is the name of another node in this dict makes the key a repeated node: the same physical component drawn a second time. It stays where it is drawn, and every appearance is the one component: it works, or has failed, in all of them at once. That node cannot itself be a repeat. The input and output nodes need no entry: they are always perfectly reliable. A model given for one (other than PerfectReliability) is refused as an invalid structure, since it would be ignored: most likely a component's edge to the output node, or from the input node, is missing.

required
k dict[Any, int]

{node: k} for k-out-of-n nodes: a node with n predecessors is reached only when at least k of them are reached through working components (and it must work itself). By default k is 1 for every node. For example, three parallel units feeding the output node "out" with k={"out": 2} form a 2-out-of-3 system.

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" (the default) raises a ValueError, while "warn" and "ignore" carry on, after emitting a UserWarning that lists the problems or silently. The results of an invalid RBD are not meaningful. Invalid diagrams include a cycle, more than one input or output node, a node without a model, and a k that is zero, exceeds the node's number of incoming branches or names an unknown node. The findings are kept in structure_check.

'raise'
ccf_groups Iterable[CCFGroup]

Common-cause failure groups (CCFGroup), by default none. Each member must be a component node (not the input or output node, nor a repeated node) in at most one group, and a group's members must have identical models (compared by their serialised form; the check is skipped for models that cannot be serialised). The groups are honoured by sf/ff and the methods computed from them (reliability, unreliability, df, hf, Hf, cs, time_to_reliability, bx_life), by the importance measures (a member conditioned on its state through the groups' shock outcomes), by parameter_sensitivity (a group's parameters reported together), by the parameter-uncertainty methods (a group's members drawn together, and its model too if asked) and by the redundancy allocations (a BetaFactor member's copies join its group). A group splitting the failure rate (basis="rate") is also in mean, the MTTF methods and random; one splitting a probability (the default) mean refuses and random leaves out, and the RBD warns when its members' probability of failing passes 0.1. The condition-based methods raise NotImplementedError.

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 {level: probability} of the levels it works at and the probability of each while it works; for capacity_distribution. A failed node passes nothing. A node with no capacity given limits nothing (inf), unless its model has capacities of its own: a DegradingNode's stages, or a nested NonRepairableRBD with capacities, whose distribution it then has. A repeated node has the capacity of the node it repeats, wherever it is drawn: give that node's.

None

Attributes:

Name Type Description
reliabilities dict

{node: model} as used: it includes the input and output nodes (as PerfectReliability) and leaves out the repeated nodes.

repeated dict

{repeated_node: node_it_repeats}.

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. "is_valid", "has_cycles", "nodes_with_no_reliability_distribution", "all_distributions_fixed" and "non_analytic_nodes".

Raises:

Type Description
ValueError

If on_infeasible_rbd is not an allowed value, a node's model is its own name, input_node or output_node is not in the diagram, the diagram is invalid (with on_infeasible_rbd="raise"), ccf_groups is invalid, or a capacity is not a positive number or is for a node that cannot take one (the input or output node, a repeated node, or one not in the diagram).

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 get_min_cut_sets.)

True

Returns:

Type Description
set[frozenset[Hashable]]

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

Raises:

Type Description
ValueError

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

Examples:

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

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

is_system_working

is_system_working(component_status, method)

Return whether the system works, given which components work.

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

Parameters:

Name Type Description Default
component_status dict[Any, bool]

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

required
method str

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

required

Returns:

Type Description
bool

True if the system is working, otherwise False.

Raises:

Type Description
ValueError

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

KeyError

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

Examples:

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

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

system_timeline

system_timeline(timelines)

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

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

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

Parameters:

Name Type Description Default
timelines Mapping

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

required

Returns:

Type Description
Timeline or Timelines

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

Raises:

Type Description
ValueError

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

TypeError

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

Examples:

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

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

minimal_path_sets

minimal_path_sets()

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

Returns:

Type Description
list of frozenset

The minimal path sets.

Examples:

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

minimal_cut_sets

minimal_cut_sets()

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

Returns:

Type Description
list of frozenset

The minimal cut sets.

Examples:

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

get_min_cut_sets

get_min_cut_sets(include_in_out_nodes=False)

Return the minimal cut sets of the RBD.

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

Parameters:

Name Type Description Default
include_in_out_nodes bool

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

False

Returns:

Type Description
set[frozenset[Hashable]]

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

Raises:

Type Description
ValueError

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

Examples:

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

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

path_set_probabilities

path_set_probabilities(node_probabilities)

Return the probability that each minimal path set fully works.

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

Parameters:

Name Type Description Default
node_probabilities dict

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

required

Returns:

Type Description
ndarray

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

Raises:

Type Description
ValueError

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

Examples:

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

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

system_probability

system_probability(node_probabilities, method='p')

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

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

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

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

Parameters:

Name Type Description Default
node_probabilities Dict

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

required
method str

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

'p'

Returns:

Type Description
ndarray

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

Raises:

Type Description
ValueError

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

KeyError

If an intermediate node has no entry in node_probabilities.

Examples:

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

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

Arrays give one system probability per element:

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

system_capacity

system_capacity(node_probabilities)

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

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

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

Parameters:

Name Type Description Default
node_probabilities Dict

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

required

Returns:

Type Description
CapacityDistribution

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

Raises:

Type Description
ValueError

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

KeyError

If an intermediate node has no entry in node_probabilities.

Examples:

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

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

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

improvement_allocation

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

Improve the node probabilities just enough to meet a target.

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

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

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

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

Parameters:

Name Type Description Default
target float

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

required
node_probabilities Dict

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

required
fixed list

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

None
weights dict

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

None

Returns:

Type Description
dict

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

Raises:

Type Description
ValueError

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

KeyError

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

Examples:

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

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

Every unreliability was cut by the same factor:

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

equal_allocation

equal_allocation(target)

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

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

Parameters:

Name Type Description Default
target float

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

required

Returns:

Type Description
dict

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

Raises:

Type Description
ValueError

If target is above 1 or below 0.

Examples:

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

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

simple_allocation

simple_allocation(target, weights=None)

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

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

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

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

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

Parameters:

Name Type Description Default
target float

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

required
weights dict

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

None

Returns:

Type Description
dict

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

Raises:

Type Description
ValueError

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

KeyError

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

Examples:

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

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

minimum_effort_allocation

minimum_effort_allocation(target, node_probabilities)

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

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

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

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

Parameters:

Name Type Description Default
target float

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

required
node_probabilities Dict

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

required

Returns:

Type Description
dict

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

Raises:

Type Description
ValueError

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

References

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

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

Examples:

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

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

cost_based_allocation

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

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

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

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

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

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

Parameters:

Name Type Description Default
target float

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

required
node_probabilities Dict

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

required
max_probabilities dict

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

None
feasibility dict

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

None

Returns:

Type Description
dict

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

Raises:

Type Description
ValueError

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

Warns:

Type Description
UserWarning

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

References

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

Examples:

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

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

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

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

node_names

node_names()

Return the names of the intermediate (component) nodes.

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

Returns:

Type Description
list[Hashable]

The intermediate node names, as a new list.

Examples:

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

structural_importance

structural_importance(working_nodes=None, broken_nodes=None)

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

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

Parameters:

Name Type Description Default
working_nodes Iterable[Hashable]

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

None
broken_nodes Iterable[Hashable]

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

None

Returns:

Type Description
dict[Any, float]

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

Raises:

Type Description
ValueError

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

Examples:

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

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

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

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

to_dict

to_dict()

Serialise the RBD to a JSON-friendly dict.

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

Returns:

Type Description
dict

The serialised RBD.

Raises:

Type Description
NotImplementedError

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

AttributeError

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

Examples:

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

to_json

to_json(fp=None, **json_kwargs)

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

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

Parameters:

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

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

None
**json_kwargs

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

{}

Returns:

Type Description
str or None

The JSON document, or None once written to fp.

Raises:

Type Description
NotImplementedError

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

AttributeError

If called on a bare RBD.

TypeError

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

Examples:

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

from_dict classmethod

from_dict(d)

Reconstruct an RBD from the output of to_dict.

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

Parameters:

Name Type Description Default
d dict

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

required

Returns:

Type Description
RBD

The reconstructed NonRepairableRBD or RepairableRBD.

Raises:

Type Description
ValueError

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

KeyError

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

Examples:

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

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

from_json classmethod

from_json(s)

Reconstruct an RBD from a JSON document made by to_json.

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

Parameters:

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

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

required

Returns:

Type Description
RBD

The reconstructed NonRepairableRBD or RepairableRBD.

Raises:

Type Description
ValueError

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

FileNotFoundError

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

KeyError

If a required entry is missing (see from_dict).

Examples:

RBD.from_json returns whichever RBD type the document names:

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

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 is_fixed). A time before 0 gives the reliability at 0 (1 for lifetimes), as the nodes' lifetime distributions do; a RepairableRBD, whose history starts at 0, refuses one.

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" or "paths" (the default) works out the probability that the system works; "c" or "cuts" the probability that it fails, and returns its complement. Both use the same decomposition, are exact and give the same result.

'p'

Returns:

Type Description
float or ndarray

The system reliability: a float for scalar x, an array for array x.

Raises:

Type Description
ValueError

If x is omitted for a time-varying RBD, or a working/broken node is unknown, is the input or output node, is in both sets, or is a repeat of another node.

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" or "c", as for sf; both give the same result, computed the same way.

'p'

Returns:

Type Description
float or ndarray

The system unreliability: a float for scalar x, an array for array x.

Raises:

Type Description
ValueError

As for sf.

NotImplementedError

As for sf.

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 x, and one row per level and one column per time for an array. Its meets(demand) is the probability that the capacity meets a demand, and mean() the expected capacity.

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, x is omitted for a time-varying RBD, or a working/broken node is invalid (as for sf).

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's covariance()), 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's lfp_p or a zero-inflated fit's f0; 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 with qf or ppf, 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

{node or tuple of nodes: uncertainty} for the uncertain nodes, and {CCFGroup: uncertainty} for an uncertain common-cause model (see above). The other nodes and groups keep their models. By default, every node whose model is a surpyval fit with a parameter covariance is drawn as "fit", the nodes that share one model object (or a common-cause group) together. Nodes with separate but equal fits are drawn apart, which understates the uncertainty if they are one estimate: give them together then.

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" (the default) draws with pseudo-random numbers; "sobol" from the points of a scrambled Sobol sequence, which cover the parameters more evenly and shrink the error of the summaries for the same number of draws.

'random'

Returns:

Type Description
UncertaintyResult

The system reliability of every draw (samples), the nominal value with the nodes' own models, and summaries: mean, median, std, percentile and interval (see UncertaintyResult).

Raises:

Type Description
ValueError

If uncertainty is empty, names an unknown, input, output or repeated node, or names a node twice; if a tuple's nodes have different models; if an uncertainty cannot be drawn (e.g. "fit" for a model with no fitted covariance, or an unknown parameter); if a common-cause group's members are not given together, or its model's uncertainty is not one of the above; if n_draws is not a positive integer; or if x is left out for time-varying models.

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

{node or tuple of nodes: uncertainty} for the uncertain nodes, as for sf_uncertainty: "fit", {parameter name: distribution} or a list of models; nodes given together in a tuple share their draws. A common-cause group's model may be uncertain too, as for sf_uncertainty. By default, every fitted node, as for sf_uncertainty.

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" (the default) draws with pseudo-random numbers; "sobol" from the points of a scrambled Sobol sequence, which cover the parameters more evenly and shrink the error of the summaries for the same number of draws.

'random'

Returns:

Type Description
UncertaintyResult

The MTTF of every draw (samples; inf for a draw in which the system may never fail), the exact MTTF with the nodes' own models (nominal), and summaries such as interval.

Raises:

Type Description
ValueError

As for sf_uncertainty, or if every node is fixed-probability (the system has no lifetimes).

NotImplementedError

If a common-cause group splits a failure probability (the default basis), as for mean: its model assumes a small one, while over a lifetime it runs to 1.

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

{node or tuple of nodes: uncertainty} for the uncertain nodes, as for sf_uncertainty (by default, every fitted node).

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" (the default) draws with pseudo-random numbers; "sobol" from the points of a scrambled Sobol sequence, which cover the parameters more evenly and shrink the error of the summaries for the same number of draws.

'random'
upper_bound float

An upper bound for each draw's search, at which its reliability is below target; found by doubling if None.

None

Returns:

Type Description
UncertaintyResult

The time of every draw (samples), the time with the nodes' own models (nominal), and summaries such as interval.

Raises:

Type Description
ValueError

If target is not in (0, 1), the RBD is fixed-probability, a draw's reliability starts below target or cannot be bracketed, or as for sf_uncertainty.

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

{node or tuple of nodes: uncertainty} for the uncertain nodes, as for sf_uncertainty (by default, every fitted node).

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" (the default) draws with pseudo-random numbers; "sobol" from the points of a scrambled Sobol sequence, which cover the parameters more evenly and shrink the error of the summaries for the same number of draws.

'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 x is not in (0, 100), or as for time_to_reliability_uncertainty.

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 "sf", the percentage for "bx_life", the reliability for "time_to_reliability"; none for "mean".

None
uncertainty dict

{node or tuple of nodes: uncertainty}, and {CCFGroup: uncertainty}, as for sf_uncertainty: by default, every fitted node.

None
of str

"sf" (the default), "mean", "bx_life" or "time_to_reliability".

'sf'
method str

"delta" (the default) or "sobol".

'delta'
n_draws int

With method="sobol", the draws of each set, by default 1000.

1000
seed int

With method="sobol", the seed of the draws.

None
sampling str

With method="sobol", "random" (the default) or "sobol": the draws' two sets from the points of a scrambled Sobol sequence, each from dimensions of its own, which shrinks the indices' sampling error for the same number of draws.

'random'
rel_step float

With method="delta", the parameters' step, as a fraction of each (one-sided at a bound of its range), by default 1e-4.

0.0001
upper_bound float

For "bx_life" and "time_to_reliability", as for time_to_reliability.

None

Returns:

Type Description
UncertaintyImportance

The method, the quantity's variance, and each input's first_order and total shares, by the key it was given under: floats, or arrays for an array of times.

Raises:

Type Description
ValueError

For an unknown of or method, an x the quantity does not take, a list of models with the delta method, or as sf_uncertainty and the quantity's own method do.

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 the budget, 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

{node: what one copy uses} for the nodes that may be duplicated: a number (its cost) or, for several resources, a dict of resource → amount, naming the same resources for every node. Or, for a choice of component types, a list of ComponentOption (each giving what one copy of it uses in the same way); the node's own model is then not used. Every copy is counted, including the original, so the cheapest allocation (one of each) uses the sum of the amounts. A number must be finite and positive; the amounts in a dict finite and non-negative.

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 target, reliability is maximised within it, and it must afford one of each costed node. With a target it adds limits to the minimisation.

None
target float

Minimise the total of one resource subject to system reliability

= target, in (0, 1).

None
minimise Hashable

The resource a target minimises. Needed only with several resources, none of them called "cost" (which is minimised by default).

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" (the default): all copies operate, and the node works while required of them do; "cold": required copies operate and the others wait unpowered as cold spares, switched in, in the order of the node's options, as operating copies fail (a StandbyModel: exact for identical Exponential units, a numerical convolution for one unit required, and otherwise simulated from 10 000 lifetimes, seeded, so reproducible); "choose": whichever of the two is better, chosen for each node by the optimiser. Cold standby needs lifetime models, not fixed probabilities.

'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" (the default) returns a proven optimum, by dynamic programming for costed nodes in series with the rest (giving up with an explanatory error beyond 2,000,000 partial allocations) and otherwise by an exhaustive search (giving up beyond 500,000 allocations examined, which is a handful of costed nodes). "greedy" repeatedly takes the step (one more copy, or a change of type) with the best log-reliability gain per unit of what it uses (its cost, the minimised resource with a target, or with several limits its total share of them): fast for any size, usually optimal or close, but not guaranteed optimal.

'exact'

Returns:

Type Description
RedundancyAllocation

The chosen units per costed node (copies of every type), with the resulting reliability, the total cost (of the resource minimised, "cost", or the first resource), the total of every resource in resources, the method used, each node's strategy and, for nodes with options, how many of each type in mix (see RedundancyAllocation).

Raises:

Type Description
ValueError

If neither budget nor target is given, or on any other invalid input: an unknown method; empty costs; a costed node that is not a component node or is a repeated node; a cost that is not finite and positive (an amount in a dict, not finite and non-negative); nodes naming different resources, or a mixture of numbers and dicts; invalid options (an empty list, duplicate names, or a reliability that is not a model or a probability); a budget naming an unknown resource, or a number with several resources; a node nothing limits (see max_units); an invalid max_units, minimise, mixing, required (or one above max_units), strategy or switching_probability; cold standby of a fixed probability; t missing for a time-varying RBD or not a single number; a budget that cannot afford the required copies of each costed node; a target outside (0, 1) or not reachable (within max_units and the budget); or an exact search that is too large.

NotImplementedError

If a costed node is in a common-cause group whose model is an MGL or AlphaFactor (its parameters are for its group's size), or has options or cold spares (see above).

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 allocate_redundancy: a number, a dict of resources, or a list of ComponentOption.

required
budget float or dict

Limits on the resources, as for allocate_redundancy. The budget or max_units must bound every node's copies.

None
t float

The mission time, as for allocate_redundancy.

None
max_units int or dict

The most copies of each node, as for allocate_redundancy.

None
required int or dict

The copies of each node that must work, as for allocate_redundancy.

None
strategy str or dict

"active", "cold" or "choose", as for allocate_redundancy.

'active'
switching_probability float or dict

For cold standby, as for allocate_redundancy.

1.0
mixing bool

Whether a node's copies may mix types, as for allocate_redundancy.

True

Returns:

Type Description
list of RedundancyAllocation

One per non-dominated design, by increasing cost (the total of "cost", or of the first resource), the most reliable first among equals. With one resource the reliability rises along the list.

Raises:

Type Description
ValueError

On invalid input, as for allocate_redundancy, if a node's copies are not bounded by the budget or max_units, or if there are too many designs to evaluate.

NotImplementedError

As for allocate_redundancy.

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

{node: function}: function(r, n) gives what n copies of component reliability r use, a number (their cost) or a dict of resource -> amount, naming the same resources for every node. It must not decrease as r or n grows.

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, (low, high) in [0, 1], for every node (a tuple) or for each node (a dict naming every node in uses). The lowest must be affordable.

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 units and component_reliability of each node, the system reliability, and the total of each resource used (resources; cost is the total of "cost", or of the first resource).

Raises:

Type Description
ValueError

On invalid input: empty uses, a node that is not a component node or is a repeated node, a use that is not a function or returns something other than finite, non-negative amounts of the same resources for every node, a budget naming an unknown resource, invalid bounds or max_units, t missing for a time-varying RBD, a budget that cannot afford one copy of each node at its lowest reliability, a node whose copies nothing bounds, or more than 500,000 copy vectors to consider.

NotImplementedError

If a node in uses is in a common-cause group: its component reliability is the group's.

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 ff (working_nodes, broken_nodes, method).

()
**kwargs

Keyword arguments of ff.

{}

Returns:

Type Description
float or ndarray

The system unreliability: a float for scalar x, an array for array x.

Raises:

Type Description
ValueError

As for sf.

NotImplementedError

As for sf.

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 sf (working_nodes, broken_nodes, method).

()
**kwargs

Keyword arguments of sf.

{}

Returns:

Type Description
float or ndarray

The system reliability: a float for scalar x, an array for array x.

Raises:

Type Description
ValueError

As for sf.

NotImplementedError

As for sf.

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 sf (working_nodes, broken_nodes, method).

()
**kwargs

Keyword arguments of sf.

{}

Returns:

Type Description
float or ndarray

The conditional survival probability: a float if both x and X are scalars, otherwise an array.

Raises:

Type Description
ValueError

As for sf.

NotImplementedError

As for sf.

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 sf).

None
broken_nodes Collection[Hashable]

Nodes to treat as failed, by default none (as for sf).

None
method str

"p" or "c", as for sf.

'p'

Returns:

Type Description
float or ndarray

The cumulative hazard: a float for scalar x, an array for array x.

Raises:

Type Description
ValueError

As for sf.

NotImplementedError

As for sf.

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 sf).

None
broken_nodes Collection[Hashable]

Nodes to treat as failed, by default none (as for sf).

None
method str

"p" or "c", as for sf.

'p'

Returns:

Type Description
float or ndarray

The failure density: a float for scalar x, an array for array x.

Raises:

Type Description
ValueError

As for sf.

NotImplementedError

As for sf.

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 df, by default 1e-6.

1e-06
**kwargs

Keyword arguments of sf (working_nodes, broken_nodes, method).

{}

Returns:

Type Description
float or ndarray

The hazard rate: a float for scalar x, an array for array x.

Raises:

Type Description
ValueError

As for sf.

NotImplementedError

As for sf.

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 working_nodes/broken_nodes do not apply here.

()
**kwargs

Accepted but ignored.

{}

Returns:

Type Description
dict[Any, float | ndarray]

{node: reliability}: floats for scalar x, arrays for array x.

Raises:

Type Description
ValueError

If x is omitted for a time-varying RBD.

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 working_nodes/broken_nodes do not apply here.

()
**kwargs

Accepted but ignored.

{}

Returns:

Type Description
dict[Any, float | ndarray]

{node: unreliability}: floats for scalar x, arrays for array x.

Raises:

Type Description
ValueError

If x is omitted for a time-varying RBD.

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]

{node: type name of its model} for every such node, e.g. {"a": "StandbyModel"}. Empty if there is none.

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 (get_non_analytic_nodes lists which).

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 route ("exact", "numerical", "simulated" or "refused"), the reason, and the nodes that decide it (see AnalysisRoute).

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 .random uses the global RNG); the caller's RNG state is restored afterwards. By default None (non-reproducible).

None
antithetic bool

Draw the lifetimes in antithetic pairs, by default False: the second of each pair (samples 2i and 2i + 1) is drawn from 1 - u for every uniform u the first drew. Each lifetime is still a correct draw, but the two of a pair are negatively correlated (for a system whose lifetime rises with its nodes'), so their mean varies less than two independent draws'. The pairs, not the lifetimes, are independent: estimate a standard error from the pairs' means. size must be even, and every node's draws must be replayable (as for the batched sampling above), else NotImplementedError.

False
n_jobs int

Draw in parallel, in blocks of RANDOM_BLOCK (10_000) lifetimes, each seeded in turn from seed, over n_jobs processes (-1: one per CPU). The blocks are the same however many processes draw them, so the lifetimes do not depend on n_jobs (any number, 1 included), but they differ from a draw without it. By default None: one draw in this process.

None

Returns:

Type Description
ndarray

size system lifetimes; inf in a sample where the system never fails (e.g. through a perfectly reliable path).

Raises:

Type Description
ValueError

If size is not a positive integer (even, with antithetic), or n_jobs is not a positive integer or -1.

NotImplementedError

If antithetic is asked for and a node's draws cannot be replayed from uniforms (a node model sampled its own way).

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 random does with antithetic=True, by default False.

False

Returns:

Type Description
ndarray

The block's RANDOM_BLOCK lifetimes.

Raises:

Type Description
ValueError

If block is not a whole number of at least 0, or seed is None.

NotImplementedError

As for random with antithetic.

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 method="simulate": the number of system lifetimes to simulate, by default 100_000.

None
seed int or None

With method="simulate": the seed for reproducibility (see random), by default None.

None
method (exact, simulate)

How to find the MTTF, by default "exact".

"exact"
tolerance float

With method="simulate": simulate until the MTTF is known to within tolerance (in the lifetimes' units) either side, at confidence. After the first mc_samples lifetimes, and each further mc_samples, the run stops once the half-width of the confidence interval of the mean is at most tolerance, or max_samples have been drawn (then with a RuntimeWarning). By default None: exactly mc_samples. A run that stops at m lifetimes gives the result of a run of m from the start (without n_jobs).

None
confidence float

With method="simulate": the confidence level tolerance is judged at, by default 0.95.

None
max_samples int

With method="simulate": the most lifetimes a run to tolerance draws, by default 100 times mc_samples.

None
antithetic bool

With method="simulate": draw the lifetimes in antithetic pairs (see random), by default False. mc_samples must then be even.

False
n_jobs int

With method="simulate": draw in parallel over n_jobs processes (see random), by default None.

None

Returns:

Type Description
float

The MTTF (inf if the system may never fail), or with method="simulate" its estimate.

Raises:

Type Description
ValueError

If method is neither "exact" nor "simulate"; for the exact MTTF, if every node is fixed-probability, so the system has no lifetimes; or if a simulation option is invalid (see random; max_samples without a tolerance, or smaller than mc_samples).

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 CCFGroup), while over a whole lifetime it runs to 1. A group splitting the failure rate (basis="rate") is included. With method="simulate", if a group splitting the failure rate cannot be drawn (see random).

TypeError

If a simulation option is given without method="simulate".

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 method="simulate": the number of system lifetimes to simulate, by default 100_000.

None
seed int or None

With method="simulate": the seed for reproducibility (see random), by default None.

None
method (exact, simulate)

How to find the MTTF, by default "exact".

"exact"
tolerance float

With method="simulate": simulate until the MTTF is known to within tolerance either side (see mean), by default None.

None
confidence float

With method="simulate": the confidence level tolerance is judged at, by default 0.95.

None
max_samples int

With method="simulate": the most lifetimes a run to tolerance draws, by default 100 times mc_samples.

None
antithetic bool

With method="simulate": draw the lifetimes in antithetic pairs (see random), by default False.

False
n_jobs int

With method="simulate": draw in parallel over n_jobs processes (see random), by default None.

None

Returns:

Type Description
float

The MTTF, or its estimate.

Raises:

Type Description
(ValueError, NotImplementedError, TypeError)

As for mean.

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 probability p it takes about z**2 * (1 - p) / (p * relative_tolerance**2) lifetimes: some 4e10 for p = 1e-8 and 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: p as 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", "plain", "latin_hypercube", "sobol", "cross_entropy" or "subset", by default "auto".

'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 (n_samples), and the method used (method).

Raises:

Type Description
ValueError

If x is not a number of at least 0, method is unknown, or relative_tolerance, confidence or max_samples is out of range.

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 random), by default None.

None
tolerance float

Simulate until the interval is at most tolerance either side of the estimate: after the first mc_samples lifetimes, and each further mc_samples, the run stops once it is, or once max_samples have been drawn (then with a RuntimeWarning). By default None: exactly mc_samples. Without n_jobs, a run that stops at m lifetimes gives the result of a run of m from the start.

None
max_samples int

The most lifetimes a run to tolerance draws, by default 100 times mc_samples.

None
antithetic bool

Draw the lifetimes in antithetic pairs (see random), by default False: a narrower interval for the same number of lifetimes, when the system's lifetime rises with its nodes' (as a coherent system's does). mc_samples must then be even.

False
n_jobs int

Draw in parallel over n_jobs processes (-1: one per CPU; see random), by default None. The result does not depend on the number of processes.

None

Returns:

Type Description
ConfidenceInterval

The estimate, bounds, confidence level, standard error and sample count (see ConfidenceInterval).

Raises:

Type Description
ValueError

If confidence is not in (0, 1), mc_samples is fewer than 2 (an interval needs two lifetimes; two pairs, with antithetic), or another option is invalid (see mean).

NotImplementedError

With antithetic, if a node's draws cannot be replayed from uniforms.

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 mean works out both MTTFs, else simulated. "exact": the exact difference, refused where a mean is. "simulate": the simulated difference, whatever.

None

Returns:

Type Description
ConfidenceInterval

The difference in MTTF (this system's minus other's): exact (method="exact"), or the mean difference of the simulated lifetimes ("simulated"), with its standard error and a normal confidence interval (not clipped: the difference may be negative). Common-cause groups that split a probability are left out, as by random; a group splitting the failure rate draws each member's own causes from the member's own random numbers, and each shared cause from its own.

Raises:

Type Description
ValueError

If mc_samples, confidence or method is invalid.

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 method="exact", as for mean.

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 target; found automatically (by doubling) if None.

None
**kwargs

Keyword arguments of sf (working_nodes, broken_nodes, method).

{}

Returns:

Type Description
float or ndarray

The time at which R(t) == target: an array of one for each target, if several are given.

Raises:

Type Description
ValueError

If target is not in (0, 1); if the RBD is fixed-probability (reliability is constant in time); if target exceeds the system reliability at t = 0 (so it is never reached); if no upper bound is found (after 1000 doublings); or if the reliability at upper_bound is still above target. Also as for sf.

NotImplementedError

As for sf.

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 time_to_reliability (upper_bound) and sf (working_nodes, broken_nodes, method).

{}

Returns:

Type Description
float or ndarray

The time at which x percent of systems have failed (one for each x, if several are given).

Raises:

Type Description
ValueError

If x is not in (0, 100), or as for time_to_reliability (e.g. for a fixed-probability RBD).

NotImplementedError

As for sf.

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 x = 0 is the present). May be omitted only for a fixed-probability RBD.

None
state dict[Hashable, NodeState]

{node: NodeState}, the current state of some or all of the component nodes. By default empty, which reproduces sf(x).

None
method str

"p" or "paths" (the default) works out the probability that the system works, "c" or "cuts" that it fails (and returns its complement); both are exact.

'p'

Returns:

Type Description
float or ndarray

System reliability given the state: a float for scalar x, an array for array x.

Raises:

Type Description
TypeError

If state is not a dict, or one of its values is neither a NodeState nor a number (the node's age).

ValueError

If x is omitted for a time-varying RBD, or state names the input or output node, an unknown node (including a repeated node), or a node whose model is a standby, load-sharing or repeated node or a nested RBD.

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]

{node: NodeState}, the current state of some or all of the component nodes (see sf_given_state). By default empty (all nodes new).

None
upper_bound float

An upper bound for the search, at which the reliability must be below target; found automatically (by doubling) if None.

None

Returns:

Type Description
float or ndarray

The remaining time until R_sys(t | state) == target (one for each target, if several are given).

Raises:

Type Description
ValueError

If target is not in (0, 1); if the RBD is fixed-probability; if target exceeds the current system reliability R_sys(0 | state) (e.g. because the state has already failed the system); if no upper bound is found; if the reliability at upper_bound is still above target; or if state is invalid (as for sf_given_state).

TypeError

If state is not a dict of NodeState values (or numbers, the nodes' ages).

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]

{node: NodeState}, the current state of some or all of the component nodes (see sf_given_state). By default empty (all nodes new).

None

Returns:

Type Description
float

The mean remaining life: inf if some systems never fail.

Raises:

Type Description
TypeError

As for sf_given_state: state is not a dict of NodeState values (or numbers, the nodes' ages).

ValueError

If the RBD is fixed-probability (its reliability does not change with time), or as for sf_given_state.

NotImplementedError

As for sf_given_state (common-cause groups).

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]

{node: NodeState}, the current state of some or all of the component nodes (see sf_given_state). By default empty (all nodes new).

None
kind str

The criticality's form: "failure" (the default) or "success", as for criticality_importance.

'failure'

Returns:

Type Description
dict[str, dict[Any, float | ndarray]]

{"birnbaum": {node: value}, "criticality": {node: value}} over the component nodes. Values are floats for scalar x and arrays for array x.

Raises:

Type Description
TypeError

If state is not a dict of NodeState values.

ValueError

If x is omitted for a time-varying RBD, state is invalid (as for sf_given_state), or kind is neither "failure" nor "success".

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 NonRepairableRBD or a RepeatedNode: the area under its reliability (see mean);
  • a StandbyModel or LoadSharingModel: its mean(), 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's mean() 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]

{node: MTTF} over the component nodes (not the input or output node, nor a junction).

Raises:

Type Description
AttributeError

If a node's model has no mean() method.

NotImplementedError

If a nested RBD has common-cause groups that split a probability (see mean).

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]

{node: importance} over the component nodes: floats for scalar x, arrays for array x.

Raises:

Type Description
ValueError

If x is omitted for a time-varying RBD, or a working/broken node is invalid (as for sf).

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]

{node: improvement potential} over the component nodes: floats for scalar x, arrays for array x.

Raises:

Type Description
ValueError

If x is omitted for a time-varying RBD, or a working/broken node is invalid (as for sf).

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]

{node: RAW} over the component nodes: floats for scalar x, arrays for array x.

Raises:

Type Description
ValueError

If x is omitted for a time-varying RBD, or a working/broken node is invalid (as for sf).

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]

{node: RRW} over the component nodes: floats for scalar x, arrays for array x.

Raises:

Type Description
ValueError

If x is omitted for a time-varying RBD, or a working/broken node is invalid (as for sf).

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 node i has failed and is critical, given that the system has failed -- the share of system failures that node i accounts 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 in 1 - R_sys however reliable the system is. It is nan where the system cannot fail (e.g. at x = 0).
  • kind="success" gives the success-oriented form, CI_i = B_i * R_i / R_sys: the probability that node i is 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 is nan where 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" (the default) or "success".

'failure'

Returns:

Type Description
dict[Any, float | ndarray]

{node: criticality importance} over the component nodes: floats for scalar x, arrays for array x.

Raises:

Type Description
ValueError

If x is omitted for a time-varying RBD, a working/broken node is invalid (as for sf), or kind is neither "failure" nor "success".

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" (the default) for the minimal cut sets and "p" for the minimal path sets.

'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" (the default): the probability of the union of the sets' failures; or "rare_event": the sum of their probabilities.

'exact'

Returns:

Type Description
dict[Any, float | ndarray]

{node: Fussell-Vesely importance} over the component nodes: floats for scalar x, arrays for array x.

Raises:

Type Description
ValueError

If fv_type is not "c" or "p", method is not "exact" or "rare_event", x is omitted for a time-varying RBD, or a working/broken node is invalid (as for sf).

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-5.

1e-05

Returns:

Type Description
dict[Any, dict[str, float | ndarray]]

{node: {parameter_name: sensitivity}}, with surpyval's parameter names (e.g. "alpha" and "beta" for a Weibull; "param0", "param1", ... if it gives none), and a common-cause group's under the tuple of its members (see above). Sensitivities are floats for scalar x and arrays for array x.

Raises:

Type Description
ValueError

If x is omitted for a time-varying RBD, or a working/broken node is invalid (as for sf).

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 parameter_sensitivity's order.

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 Lever (from levers()) or by its (key, name), as parameter_sensitivity reports it.

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 bounds).

TypeError

If values is not a dict, a lever is neither a Lever nor a (key, name), or a value is not a number.

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 birnbaum_importance (needed unless every node's probability is fixed).

None
working_nodes Collection[Hashable]

Nodes held working, as for birnbaum_importance.

None
broken_nodes Collection[Hashable]

Nodes held failed, likewise.

None
over str

"components" (the default) or "parameters".

'components'
change str

"uniform" (the default) or "proportional".

'uniform'
kind str

Over the nodes, with a proportional change, which of each node's probabilities moves in proportion: "failure" (the default), of failing, or "success", of working.

'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

{name: keys}: each group's share, the sum of its keys' (nodes, or (node, parameter) pairs), instead of each key's.

None
rel_step float

The parameters' finite-difference step, as for parameter_sensitivity.

1e-05

Returns:

Type Description
dict

The shares, by node, (node, parameter) or group name: floats for a single time, else arrays (NaN where the total is 0).

Raises:

Type Description
ValueError

For an unknown over, change or kind, a group naming an unknown key, or as birnbaum_importance and parameter_sensitivity do.

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 birnbaum_importance.

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

{(i, j): JRI} for each pair once, its nodes' names in order as text, and found either way round (the measure is symmetric): floats for a single time, else arrays.

Raises:

Type Description
ValueError

As birnbaum_importance does.

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 rate of change (-df: a float for a number x, else an array) and each node's (and group's) part in it, node_rate; no jumps.

Raises:

Type Description
ValueError

As birnbaum_importance does.

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 x or the whole life, else arrays. NaN where the system cannot have failed.

Raises:

Type Description
ValueError

For a negative time, if every node is a fixed probability (the system then has no lifetimes), or as birnbaum_importance does.

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}