Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ other release, fixes included, the minor.

## [Unreleased]

### Added

- A unit of a surpyval joint model of readings and failures, given its
readings, is a block of a `NonRepairableRBD`: a `JointNode`, or the
unit's `DynamicPrediction` (`joint_model.predict(Z, markers, given)`)
given as the node. Its reliability is from now, `pred.sf(age + t)`; its
lifetimes `pred.random() - age`; its mean the unit's mean life left. It
takes part in the reliability, MTTF, B-lives, importance measures,
states, simulations and saving. Its age is its prediction's: a
`NodeState` with another age is refused, as it would condition the unit
again. A `RepairableRBD` and a `NonRepairable` refuse a joint model,
whose units after the first are new and without its readings. See the
condition-based evaluation guide.

### Changed

- surpyval 0.25 is required (for its joint models).

## [0.14] - 2026-10-16

Fitted models in, every kind of model brought up to the diagram, and
Expand Down
9 changes: 5 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ Setup, branches, the checks a change needs, where tests go, pull requests,
deprecations and releases are in [CONTRIBUTING.md](CONTRIBUTING.md); read it
first. This file adds the internals and conventions it does not cover.

**Versions (17 Oct 2026):** 0.13 is the latest release (PyPI, `v0.13`). 0.14
is prepared on `dev` (`_version.py`, CHANGELOG dated 2026-10-16) and waits on
the release pull request from `dev` into `main`. `dev` needs surpyval 0.24
**Versions (17 Oct 2026):** 0.14 is the latest release (PyPI, `v0.14`). 0.15
is prepared on `dev` (`_version.py`, CHANGELOG dated 2026-10-17) and waits on
the release pull request from `dev` into `main`. `dev` needs surpyval 0.25
or later.

## Architecture & scope
Expand Down Expand Up @@ -483,7 +483,8 @@ CONTRIBUTING.md. What it leaves to the maintainer's say:

## surpyval workarounds to remove

surpyval 0.24 (the minimum) gives a mixture a precise tail (`sf`, #671) and
surpyval 0.25 (the minimum) has the joint models a `JointNode` takes; 0.24
gives a mixture a precise tail (`sf`, #671) and
has `conditional_gaps` read a limited failure population's `lfp_p`, not a
mixture's `p` (#626); 0.23 pickles its fits (#573), names the
limited-failure proportion `lfp_p` (#608) and takes `success_run`'s
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pip install -e .[dev] # add ,fast for numba, ,docs for mkdocs
pre-commit install # run the formatters and linters on commit
```

RePyability needs Python 3.11 or later and surpyval 0.24 or later.
RePyability needs Python 3.11 or later and surpyval 0.25 or later.

## Branches and the one automated test run

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ How each family of analyses is worked out on a diagram with each kind of node or
| A probability per demand | E | – | – | E | E | N | S | E N | – | S |
| A mixture (`MixtureModel`) | E | N | N | E N | E N | N | S | E N | – | S |
| A regression node (covariates) | E | N | N | E N | E N | N | S | E N | – | S |
| A joint model's unit, given its readings | N | N | N | N | N | N | S | N | – | S |
| A degrading node | N | N | N | N | N | N | S | N | N | S |
| A fitted degradation process | E | N | N | E N | E N | N | S | E N | – | S |
| Cold standby (convolved) | N | N | N | N | N | N | S | N | – | S |
Expand Down Expand Up @@ -303,8 +304,9 @@ places, nested diagrams):
- **components:** surpyval lifetime distributions, degradation processes
(Wiener and gamma, as the time to their threshold) and fixed
probabilities (a diagram refuses a non-parametric fit, such as
Kaplan–Meier: fit a parametric distribution in surpyval); repeated
nodes; cold standby with one or
Kaplan–Meier: fit a parametric distribution in surpyval); a unit of a
surpyval joint model of readings and failures, given its readings
(`JointNode`, from now); repeated nodes; cold standby with one or
two units operating (any units) or more (identical units); warm standby
with one unit operating, and hot standby (any units); load sharing of
identical units; common-cause groups, for the system's reliability,
Expand Down
2 changes: 2 additions & 0 deletions docs/api/nodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ models provided here (see [Building an RBD](../guide/building.md) and

::: repyability.RegressionNode

::: repyability.JointNode

::: repyability.PerfectReliability

::: repyability.PerfectUnreliability
Expand Down
89 changes: 89 additions & 0 deletions docs/guide/condition-based.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ and the valve works.

- Parametric distributions and `RegressionNode`s age.
For them, `NodeState(age=0)` is the same as leaving the node out.
- A [`JointNode`][repyability.JointNode] (a unit of a joint model, given
its readings) takes its age from its prediction: see
[below](#a-unit-with-readings-joint-models).
- A fixed-probability component given any state in which it is alive
contributes 1 from now on: its chance of failing has been resolved by
observing it working. Left out of the state, it keeps its usual
Expand Down Expand Up @@ -228,3 +231,89 @@ ramped.sf_given_state(1000, {"m": NodeState(age=2500)}) # -> 0.7036
semi-parametric Cox model has no defined mean and says so.
- The node saves with the RBD; the fitted model round-trips through
`surpyval.from_dict`.

## A unit with readings: joint models

Some components are watched: a vibration level, a wear measurement or an
oil analysis is read every so often, and a rising reading warns of a
failure to come. A **joint model** of the readings and the failures,
fitted in surpyval (`surpyval.WeibullJointPH`, or any
`surpyval.JointPH(distribution, path_model)`), says how. Its `predict`
gives one unit's life given its covariates, its readings so far and that
it is still running at its age now, and that prediction can be a node:

```python
from surpyval import ItemLifetime, SurpyvalData, WeibullJointPH
from repyability import JointNode

# A fleet of 60 pumps, each with its vibration read every 10 hours.
rng = np.random.default_rng(3)
pumps = []
for k in range(60):
start, rate = rng.normal(2.0, 0.4), rng.normal(0.02, 0.005)
life = 100 * rng.weibull(2.0) * np.exp(-0.9 * (start - 2))
end = min(life, 120.0)
ages = np.arange(0.0, end, 10.0)
vibration = start + rate * ages + rng.normal(0, 0.2, ages.size)
outcome = "observed" if life < 120 else "right_censored"
pumps.append(
ItemLifetime(k, **{outcome: end}, markers=dict(zip(ages, vibration)))
)
joint = WeibullJointPH.fit(SurpyvalData.from_items(pumps))

# Our pump: 40 hours old, its vibration rising faster than most.
pump = joint.predict(markers={0: 2.1, 20: 2.5, 40: 3.0}, given=40)
valve = surv.Weibull.from_params([80, 1.5])
line = NonRepairableRBD(
[("in", "pump"), ("pump", "valve"), ("valve", "out")],
{"pump": pump, "valve": valve},
)
line.sf(20) # -> 0.6953 pump.sf(60) * valve.sf(20)
```

The diagram's time is **time from now**, while a prediction's functions
take ages since new. The node is the prediction moved to now: at mission
time `t` the pump's reliability is `pump.sf(40 + t)`, which surpyval has
already conditioned on the pump reaching 40 hours, its lifetimes are
`pump.random() - 40` and its mean is its mean life left. A prediction given
as a node becomes a [`JointNode`][repyability.JointNode];
`JointNode(joint, Z=..., markers=..., age=...)` makes the same node from the
fitted model and the unit's covariates, readings and age.

It takes part in everything an ordinary node does: the system's
reliability, MTTF and B-lives, the importance measures and the
simulations.

```python
line.mean() # -> 35.50
line.birnbaum_importance(20)["valve"] # -> 0.7879 the pump's sf(60)
line.sf_given_state(20, {"valve": NodeState(age=30)}) # -> 0.6048
```

- **Its age is the prediction's.** In a state, the pump may be given
`alive=False`, or its prediction's age (`NodeState(age=40)`, the same as
leaving it out); any other age raises `ValueError`, as it would condition
the pump a second time. For the pump later on, predict again with
`given=` its new age and its readings since.
- **Its mean can be infinite.** With a straight-line marker path whose
slope varies from unit to unit, a few units' paths fall away and they
never fail; the model leaves even this pump a chance of that (about
5e-9), so `JointNode(pump).mean()` is `inf`, as the mean of such a
model is. The reliability, the B-lives and a system whose other parts
wear out (`line` above) are not affected.
- **Parameter uncertainty** (`sf_uncertainty` and the others) holds the
joint model at its fit, unless the pump is given a list of predictions
to draw from (from joint models refitted to bootstrap resamples, say);
surpyval's `pump.cb(...)` bounds the pump's own reliability for its
model's.
- **A new unit** is `JointNode(joint, Z=...)`, with no readings: the
model's life over the fleet's marker paths.
- **Not in a repairable diagram.** A [`RepairableRBD`][repyability.RepairableRBD]
(and a `NonRepairable`) refuses a joint model: the prediction is of the
unit in service now, given its readings, but the units that replace it
are new and have none, while such a component draws every one of its
lives from one surpyval distribution. Use a `NonRepairableRBD` for the
mission from now.
- The node saves with the RBD: the joint model in surpyval's format
(`JointModel.to_dict()`, a built-in path model) and the unit's
covariates, readings and age.
14 changes: 8 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,14 @@ dependencies = [
# (sf/ff/qf/random/mean, extras/with_params, regression sf/sf_tvc,
# to_dict/from_dict). A surpyval release reaches every fresh install at
# once, so run the tests against surpyval's development branch before it
# is released, to show a break first. CI also tests this minimum. 0.24
# gives a mixture a precise tail (sf) and has conditional_gaps read a
# limited failure population's lfp_p, not a mixture's p; 0.23 named the
# proportion lfp_p, takes success_run's level as alpha_ci, and pickles
# its fits (for n_jobs' worker processes), as RePyability relies on.
"surpyval>=0.24",
# is released, to show a break first. CI also tests this minimum. 0.25
# has the joint models (surpyval.univariate.joint) a JointNode takes;
# 0.24 gives a mixture a precise tail (sf) and has conditional_gaps read
# a limited failure population's lfp_p, not a mixture's p; 0.23 named
# the proportion lfp_p, takes success_run's level as alpha_ci, and
# pickles its fits (for n_jobs' worker processes), as RePyability
# relies on.
"surpyval>=0.25",
"tqdm>=4.64",
]

Expand Down
2 changes: 2 additions & 0 deletions repyability/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
PerfectReliability,
PerfectUnreliability,
)
from repyability.rbd.joint_node import JointNode
from repyability.rbd.load_sharing_node import LoadSharingModel
from repyability.rbd.node_state import NodeState
from repyability.rbd.non_repairable_rbd import NonRepairableRBD
Expand Down Expand Up @@ -130,6 +131,7 @@
"PerfectUnreliability",
"NodeState",
"RegressionNode",
"JointNode",
"BetaFactor",
"MGL",
"AlphaFactor",
Expand Down
3 changes: 3 additions & 0 deletions repyability/non_repairable.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
never_fails,
)
from repyability.rbd.helper_classes import PerfectReliability, perfect_class
from repyability.rbd.joint_node import NOT_REPAIRABLE, is_joint
from repyability.rbd.results import FailureForecast
from repyability.rbd.standby_node import StandbyModel
from repyability.utils.checks import is_number, no_distribution, real_array
Expand Down Expand Up @@ -235,6 +236,8 @@ def __init__(
# is only simulated, and its sf refuses.
self.model_parameterization = "standby"
self.reliability_function = _scalar_sf(reliability)
elif is_joint(reliability):
raise ValueError(NOT_REPAIRABLE)
elif reliability is PerfectReliability:
raise ValueError(
"A NonRepairable's life must end, and PerfectReliability "
Expand Down
3 changes: 3 additions & 0 deletions repyability/rbd/_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ def _validate_groups(
def _unknown_component(node, component) -> str:
"""Why ``component`` cannot be node ``node``: it is none of the
kinds a node can be."""
from repyability.rbd.joint_node import NOT_REPAIRABLE, is_joint
from repyability.repairable import Repairable

kinds = (
Expand All @@ -369,6 +370,8 @@ def _unknown_component(node, component) -> str:
"that never fails, such as a k-out-of-n vote point, "
"PerfectReliability)."
)
if is_joint(component):
return f"Component {node!r}: {NOT_REPAIRABLE}"
if isinstance(component, Repairable):
return (
f"Component {node!r} is a Repairable, which models the "
Expand Down
Loading
Loading