Skip to content

Tutorial: from a system to a decision

This walkthrough takes a small but realistic system and answers the questions a reliability engineer actually asks of it — how reliable is it, when should we service it, what is the weak link, and what does its remaining life look like once it is in the field. Every capability in RePyability shows up here in the order you would reach for it.

We will model a pumping skid:

                ┌── pump1 ──┐            ┌── filterA ──┐
   inlet ───────┤           ├── ctrl ───┤             ├─────── outlet
                └── pump2 ──┘            └── filterB ──┘

Two redundant pumps feed a single controller, which feeds two redundant filters. The pumps and filters are each a parallel pair (either one carries the duty), but the controller stands alone — a single point of failure. Keep an eye on it; the analysis will keep pointing back to it.

1. Get the component models

RePyability consumes already-fitted lifetime models. Fitting failure data to a distribution is surpyval's job — you would normally do:

import surpyval as surv

# From observed failure/censoring data (illustrative):
pump_model = surv.Weibull.fit(failures=pump_hours, censored=still_running)

Here we will just assume the fits have been done and write the parameters directly. Times are in operating hours.

import surpyval as surv

reliabilities = {
    "pump1":   surv.Weibull.from_params([12000, 1.8]),
    "pump2":   surv.Weibull.from_params([12000, 1.8]),
    "ctrl":    surv.Weibull.from_params([40000, 1.2]),
    "filterA": surv.Weibull.from_params([9000, 2.5]),
    "filterB": surv.Weibull.from_params([9000, 2.5]),
}

The controller has a long characteristic life (α = 40000) but the filters wear in fast once they are near their life (β = 2.5).

2. Build the RBD

An RBD is its edges (a directed graph from one input to one output) plus a model per intermediate node. The input/output nodes are inferred as the unique source and sink.

from repyability import NonRepairableRBD

edges = [
    ("inlet", "pump1"), ("inlet", "pump2"),
    ("pump1", "ctrl"),  ("pump2", "ctrl"),
    ("ctrl", "filterA"), ("ctrl", "filterB"),
    ("filterA", "outlet"), ("filterB", "outlet"),
]
rbd = NonRepairableRBD(edges, reliabilities)

3. How reliable is it?

rbd.sf(4000)      # -> 0.9091   reliability at 4000 h
rbd.ff(4000)      # -> 0.0909   the complement, unreliability
rbd.sf([2000, 4000, 8000])      # an array in, an array out

A scalar time returns a float; an array returns a numpy array. Reliability is computed exactly (not by simulation) via a memoised decomposition over the diagram, so these calls are cheap and repeatable.

Mean time to failure is a simulated quantity — seed it for reproducibility:

rbd.mean_time_to_failure(seed=0)   # ~ 8250 h (Monte-Carlo)

4. When should we service it?

Invert the reliability to turn a target into a time:

rbd.time_to_reliability(0.95)   # -> 2884 h  until reliability falls to 0.95
rbd.bx_life(10)                 # -> 4188 h  the B10 life (10% failed)

If you want the 95%-reliability interval as a maintenance trigger, service the skid by ~2900 h.

Two questions, two tools.

At design time, before you trust any data, use structural importance — it sets every component reliability to ½ and measures how often each node is pivotal, so it depends only on the diagram:

rbd.structural_importance()
# -> {'ctrl': 0.562, 'pump1': 0.188, 'pump2': 0.188,
#     'filterA': 0.188, 'filterB': 0.188}

The controller scores 3× its neighbours purely because it is unredundant.

With the models in hand, use Birnbaum importance — how much a small change in each node's reliability moves the system:

rbd.birnbaum_importance(4000)
# -> {'ctrl': 0.968, 'pump1': 0.120, 'pump2': 0.120,
#     'filterA': 0.114, 'filterB': 0.114}

Same verdict, sharper: at 4000 h the system's reliability is almost entirely hostage to the controller. If you are going to add redundancy anywhere, add it there.

6. Which measurement should we invest in?

Suppose you can afford to pin down one fitted parameter with more data. Which one most changes the answer? Parameter sensitivity chains the Birnbaum importance with each parameter's effect on its node:

rbd.parameter_sensitivity(4000)["ctrl"]
# -> {'alpha': 2e-06, 'beta': 0.132}

The controller's shape β dwarfs its scale α: system reliability barely moves with the controller's characteristic life but is sensitive to how its hazard grows. Spend the test budget resolving the shape.

7. Going live: a condition-based "digital twin"

Everything above assumed brand-new components. In service, each unit has run for a while and telemetry tells you how long. Feed that in and every answer re-computes for this skid, not the fleet average.

Say pump1 is well-worn, pump2 is nearly new, and the controller has a lot of hours on it:

from repyability import NodeState

state = {
    "pump1": NodeState(age=9000),
    "pump2": NodeState(age=1000),
    "ctrl":  NodeState(age=30000),
}

rbd.sf_given_state(2000, state)   # -> 0.9293  reliability of the next 2000 h
rbd.sf(2000)                      # -> 0.9709  what a new skid would give

The remaining useful life to a 0.9 reliability target has dropped by more than a third against a fresh skid:

rbd.remaining_life(0.9, state)    # -> 2603 h
rbd.remaining_life(0.9, {})       # -> 4188 h  (fresh)

And the live importance ranking has shifted — with pump1 worn, the system now leans on the healthy pump2:

rbd.importances_given_state(2000, state)["birnbaum"]
# -> {'ctrl': 0.984, 'pump2': 0.216, 'pump1': 0.065, ...}

Stream a new state each time fresh readings arrive; every update is a cheap, exact re-evaluation. (Only lifetime distributions age; standby/composite nodes are out of scope for the condition-based methods in this release.)

8. If the skid is repairable: availability

If a failed component is repaired rather than replaced, give each one a repairability (time-to-repair) distribution and ask about availability instead of reliability.

import surpyval as surv
from repyability import RepairableRBD

components = {
    "pump1": {
        "reliability":   surv.Weibull.from_params([12000, 1.8]),
        "repairability": surv.Exponential.from_params([1 / 48]),  # ~48 h MTTR
    },
    # ... same for the other components ...
}
rep = RepairableRBD(edges, components)

rep.mean_availability()                       # closed-form long-run availability
result = rep.availability(t_simulation=20000, N=10_000, seed=0)
result.availability                           # availability at each event time

Long-run availability is exact and needs no simulation; the time-resolved availability curve and its criticality measures come from a seeded discrete-event simulation.

9. Persist the model

The structure and its fitted models round-trip to plain JSON, so you can build the RBD once and reload it wherever the analysis runs (a dashboard, a scheduled job, a notebook):

rbd.to_json()                       # -> a JSON string
NonRepairableRBD.from_json(saved)   # reconstruct it

Node state is not serialised — the structure is the durable asset; state is transient input you supply at evaluation time.

Where to next

  • Concepts — the theory behind these numbers: path/cut sets, the full importance-measure family and when to use each, and how conditioning works.
  • User guide — the reference for every method, argument, and return contract.
  • API reference — the generated signatures and docstrings.