Skip to content

Node models

Composite and helper nodes

Anything exposing sf/ff can be a node; these are the composite and helper models provided here (see Building an RBD and Redundancy models).

StandbyModel

StandbyModel(reliabilities, k=1, *, switching_probability=1.0, dormancy_factor=0.0)

A k-out-of-n standby arrangement, from cold through warm to hot.

n = len(reliabilities) units with k operating at a time: the first k units in the list start operating, spares are promoted in list order as operating units fail, and the arrangement fails when fewer than k units survive. dormancy_factor sets how fast a dormant spare ages relative to an operating unit (the cumulative-exposure model, the same virtual-age machinery as LoadSharingModel):

  • 0 — cold standby (the default): spares do not age while dormant. With k = 1 the lifetime is the sum of the units' lifetimes.
  • 0 < dormancy_factor < 1 — warm standby: a dormant spare ages at that fraction of the operating rate, so it can fail latent (dead before it is ever switched in; it is skipped at promotion).
  • 1 — hot standby: spares age as fast as operating units, which is exactly an ordinary k-out-of-n parallel arrangement.

The survival function is set up once, at construction, by the first of these methods that applies:

  1. Exact closed form, for identical Exponential units (one common rate) with perfect switching. Cold, the lifetime is Erlang(n - k + 1, k * rate) for any k; warm or hot, it is hypoexponential with stage rates rate * (k + (j - k) * dormancy_factor) for j = n, ..., k units alive (unless the stage rates are too close for surpyval to separate, as with a tiny dormancy_factor).
  2. Exact k-out-of-n, for hot standby of any units: the arrangement works while at least k of its units do, each independently.
  3. Numerical, deterministic and accurate to about 1e-5 of a probability or better:

  4. cold standby with k = 1: the lifetime is the sum of the units' lifetimes (under imperfect switching, a mixture of partial sums), convolved on a time grid;

  5. cold standby of identical units with k >= 2: each operating position runs a renewal process of the units' lives, and the arrangement fails at the n - k + 1-th failure in all;
  6. cold standby of different units with k = 2: a recursion over the switch-ins on the time and the other operating unit's start (about 1e-4, 1e-3 for lives with a steep start, such as a Weibull of shape below 1);
  7. warm standby with k = 1: a recursion over the spares' switch-ins on a time grid (a spare switched in at tau has aged dormancy_factor * tau).
  8. None otherwise (cold standby of different units with k >= 3, and warm standby with k >= 2): sf, ff, cs and mean() refuse, and is_simulated is True. Its lifetimes are still drawn by random, so a diagram with it is simulated: random, mean(method="simulate") and unreliability_interval of a NonRepairableRBD, and availability and cost of a RepairableRBD. mean(method="simulate", mc_samples=..., seed=...) estimates its own mean from new draws. (Until 0.12 its sf was a Kaplan-Meier fit to simulated lifetimes.)

As an RBD node, sf/ff give its reliability, random its lifetimes for Monte-Carlo system simulation and mean its MTTF.

Parameters:

Name Type Description Default
reliabilities sequence of lifetime models

The units' lifetime distributions (e.g. surpyval distributions), in promotion order. The same model object may be listed several times; each entry is an independent unit.

required
k int

The number of units that must operate for the arrangement to work, from 1 to len(reliabilities), by default 1.

1
switching_probability float or sequence of float

The probability, in [0, 1], that switching onto the next spare succeeds: a scalar for every switch, or one value per spare (length len(reliabilities) - k). A failed switch ends the arrangement's life when the unit it should replace fails. By default 1.0 (perfect switching). Imperfect switching is supported for cold standby only. Given by name, as dormancy_factor is.

1.0
dormancy_factor float

The dormant-to-operating aging ratio, in [0, 1]: 0 is cold, 1 is hot and anything between is warm. By default 0.0.

0.0

Attributes:

Name Type Description
N int

The number of units, len(reliabilities).

Raises:

Type Description
ValueError

If k exceeds len(reliabilities), dormancy_factor is outside [0, 1], or (cold, k = 1) a switching probability is outside [0, 1] or a sequence of them has the wrong length.

NotImplementedError

If switching_probability is not 1 while dormancy_factor > 0.

Examples:

One operating unit and one cold spare, both Exponential with mean life 100: the lifetimes add, and the exact Erlang form applies.

>>> import surpyval as surv
>>> from repyability import StandbyModel
>>> unit = surv.Exponential.from_params([0.01])
>>> cold = StandbyModel([unit, unit])
>>> round(float(cold.mean()), 4)
200.0
>>> round(float(cold.sf(100.0)), 4)  # exp(-1) * (1 + 1)
0.7358

A warm spare aging at half the operating rate, and a hot spare (an ordinary parallel pair):

>>> warm = StandbyModel([unit, unit], dormancy_factor=0.5)
>>> round(float(warm.mean()), 4)  # 1 / (1.5 * 0.01) + 1 / 0.01
166.6667
>>> hot = StandbyModel([unit, unit], dormancy_factor=1.0)
>>> round(float(hot.mean()), 4)  # 1 / (2 * 0.01) + 1 / 0.01
150.0

Two of three identical Weibull units must operate: each operating position renews its unit from the spares, and the reliability is numerical, with no simulation:

>>> w = surv.Weibull.from_params([100.0, 2.0])
>>> two = StandbyModel([w, w, w], k=2)
>>> two.is_simulated
False
>>> round(float(two.sf(50.0)), 4)
0.9364

A warm spare, aging at a third of the operating rate:

>>> warm = StandbyModel([w, w], dormancy_factor=0.3)
>>> round(float(warm.sf(150.0)), 4)
0.4815

As a node of an RBD:

>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
...     [("s", "pumps"), ("pumps", "t")], {"pumps": cold}
... )
>>> round(rbd.sf(100.0), 4)
0.7358

is_simulated property

is_simulated

Whether only simulations take the arrangement: no exact or numerical method gives its reliability, so sf, ff, cs and mean() refuse, and a diagram with it is simulated.

random

random(size, seed=None)

Draw random lifetimes of the arrangement.

Each draw samples every unit's lifetime and plays the arrangement out. Cold with k = 1, the lifetimes are summed; under imperfect switching a spare adds its lifetime only if every switch up to and including its own succeeds (one uniform draw per switch). Cold with k >= 2, each spare in turn extends whichever of the k operating positions ends first, and the arrangement fails at the first end after the spares run out. Warm or hot, a virtual-age loop runs: operating units age at rate 1 and dormant spares at dormancy_factor, and a spare whose dormant aging reaches its failure age dies latent and is skipped. This is the sampler behind the simulated sf and an RBD's Monte-Carlo random/mean.

Parameters:

Name Type Description Default
size int

The number of lifetimes to draw.

required
seed int or None

If given, numpy's global RNG is seeded for the draw and its previous state restored afterwards, so the result is reproducible. By default None (draw from the current state).

None

Returns:

Type Description
ndarray

The size lifetimes, shape (size,).

Examples:

>>> import numpy as np
>>> import surpyval as surv
>>> from repyability import StandbyModel
>>> unit = surv.Exponential.from_params([0.01])
>>> standby = StandbyModel([unit, unit])
>>> standby.random(5, seed=1).shape
(5,)
>>> a = standby.random(5, seed=1)
>>> bool(np.array_equal(a, standby.random(5, seed=1)))
True

mean

mean(mc_samples=None, seed=None, *, method=None)

Mean lifetime (MTTF) of the arrangement.

Exact for the Erlang and hypoexponential closed forms, and deterministic for the numerical methods (the integral of the survival function). An arrangement with neither (see is_simulated) has no exact mean, and refuses, unless asked for an estimate: the mean of mc_samples new draws of random. method="simulate" estimates it so whatever the arrangement, to check the exact mean against draws, say.

Parameters:

Name Type Description Default
mc_samples int

The number of draws to estimate the mean from, 10_000 by default: with method="simulate", or for an arrangement with no exact mean. An exact mean draws nothing, and refuses it.

None
seed int or None

Seed for those draws (see random), by default None. Refused as mc_samples is.

None
method (None, exact, simulate)

None (the default): the exact or numerical mean, or for an arrangement with neither, the draws' estimate when mc_samples or seed is given. "exact": the exact or numerical mean, refused where there is none. "simulate": the draws' estimate, whatever.

None

Returns:

Type Description
float

The mean lifetime, or its estimate.

Raises:

Type Description
NotImplementedError

If the arrangement has no exact mean and neither mc_samples nor seed is given (or method="exact").

ValueError

If method is not one of these, or mc_samples or seed is given to an exact mean.

Examples:

>>> import surpyval as surv
>>> from repyability import StandbyModel
>>> unit = surv.Exponential.from_params([0.01])
>>> round(float(StandbyModel([unit] * 3).mean()), 4)
300.0
>>> w = surv.Weibull.from_params([100.0, 2.0])
>>> round(float(StandbyModel([w, w, w], k=2).mean()), 2)
104.44

Warm standby with two operating has no exact mean, which draws estimate:

>>> sim = StandbyModel([w, w, w], k=2, dormancy_factor=0.5)
>>> sim.is_simulated
True
>>> round(sim.mean(method="simulate", mc_samples=20_000, seed=1), 1)
95.3

The exact mean, checked against draws:

>>> exact = StandbyModel([w, w, w], k=2)
>>> drawn = exact.mean(method="simulate", mc_samples=20_000, seed=1)
>>> round(drawn, 1)
104.5

sf

sf(x, *args, **kwargs)

Survival function (reliability) of the arrangement.

Evaluates the survival function set up at construction: the exact closed form or the numerical method (interpolated on its grid). An RBD calls this for the node's reliability.

Parameters:

Name Type Description Default
x array_like

The time(s).

required
*args

Passed on, with x, to the underlying survival model.

()
**kwargs

Passed on, with x, to the underlying survival model.

()

Returns:

Type Description
float or ndarray

The probability of surviving beyond x: an array for an array x, and a numpy float for a scalar x.

Raises:

Type Description
NotImplementedError

If no exact or numerical method gives it (see is_simulated).

Examples:

>>> import surpyval as surv
>>> from repyability import StandbyModel
>>> unit = surv.Exponential.from_params([0.01])
>>> standby = StandbyModel([unit, unit])
>>> [round(float(r), 4) for r in standby.sf([50.0, 100.0])]
[0.9098, 0.7358]

ff

ff(x, *args, **kwargs)

Cumulative failure probability, 1 - sf(x).

Evaluated from the same survival function as sf.

Parameters:

Name Type Description Default
x array_like

The time(s).

required
*args

Passed on, with x, to the underlying survival model.

()
**kwargs

Passed on, with x, to the underlying survival model.

()

Returns:

Type Description
float or ndarray

The probability of failing by x, shaped as for sf.

Raises:

Type Description
NotImplementedError

As for sf.

cs

cs(x, X)

Conditional survival R(x | X) = sf(X + x) / sf(X).

The probability of surviving a further x given the arrangement has already survived to age X, computed from sf.

Parameters:

Name Type Description Default
x float or array_like

The further time(s) to survive.

required
X float or array_like

The age(s) already survived.

required

Returns:

Type Description
float or ndarray

The conditional survival, clipped to [0, 1] and 0 where sf(X) is 0: a float if x and X are both scalars, otherwise an array.

Examples:

>>> import surpyval as surv
>>> from repyability import StandbyModel
>>> unit = surv.Exponential.from_params([0.01])
>>> round(StandbyModel([unit, unit]).cs(50.0, 100.0), 4)
0.7582

DegradingNode

DegradingNode(stages)

Bases: StandbyModel

A component that degrades through stages before it fails.

It runs in its first stage, at the first capacity, for a time drawn from the first stage's model; then in the second stage, at the second capacity, for a time drawn from the second's; and so on. It has failed once its last stage ends. A pump that runs at full output until it wears, then at half output until it fails, has two stages. The stages' times are independent, and each is a lifetime model, such as one fitted by surpyval to the times units spent in that stage.

As an RBD node its reliability is the probability that its last stage has not ended: its lifetime is the sum of its stages' times, as a cold standby arrangement's is, so it is a StandbyModel of the stage models and can be used wherever one can (a node of a NonRepairableRBD, or the reliability of a RepairableRBD component). The capacity analysis (capacity_distribution) uses its stages' capacities, weighted by the probability of being in each stage, unless the RBD gives the node a capacity of its own.

Parameters:

Name Type Description Default
stages sequence of (float, model)

The stages in order, each a (capacity, model) pair: the capacity the component has in the stage, a positive number (inf for no limit), and a lifetime model for the time it spends there, exposing sf, df, random and mean (e.g. a surpyval distribution).

required

Attributes:

Name Type Description
stages tuple of (float, model)

The stages, with the capacities as floats.

capacities tuple of float

The stages' capacities, in order.

Raises:

Type Description
ValueError

If there are no stages, a stage is not a (capacity, model) pair, or a capacity is not a positive number.

Examples:

A pump at full output (100) for a Weibull time, then at half output for an exponential one:

>>> import surpyval as surv
>>> from repyability import DegradingNode
>>> pump = DegradingNode([
...     (100, surv.Weibull.from_params([1000, 2])),
...     (50, surv.Exponential.from_params([1 / 500])),
... ])
>>> round(pump.mean(), 1)  # 886.2 + 500
1386.2
>>> capacity = pump.capacity_distribution(1000)
>>> capacity.levels.tolist()
[0.0, 50.0, 100.0]
>>> capacity.probabilities.round(4).tolist()
[0.3152, 0.3169, 0.3679]
>>> round(float(pump.sf(1000)), 4)  # in either stage
0.6848

is_simulated property

is_simulated

Whether only simulations take the arrangement: no exact or numerical method gives its reliability, so sf, ff, cs and mean() refuse, and a diagram with it is simulated.

random

random(size, seed=None)

Draw random lifetimes of the arrangement.

Each draw samples every unit's lifetime and plays the arrangement out. Cold with k = 1, the lifetimes are summed; under imperfect switching a spare adds its lifetime only if every switch up to and including its own succeeds (one uniform draw per switch). Cold with k >= 2, each spare in turn extends whichever of the k operating positions ends first, and the arrangement fails at the first end after the spares run out. Warm or hot, a virtual-age loop runs: operating units age at rate 1 and dormant spares at dormancy_factor, and a spare whose dormant aging reaches its failure age dies latent and is skipped. This is the sampler behind the simulated sf and an RBD's Monte-Carlo random/mean.

Parameters:

Name Type Description Default
size int

The number of lifetimes to draw.

required
seed int or None

If given, numpy's global RNG is seeded for the draw and its previous state restored afterwards, so the result is reproducible. By default None (draw from the current state).

None

Returns:

Type Description
ndarray

The size lifetimes, shape (size,).

Examples:

>>> import numpy as np
>>> import surpyval as surv
>>> from repyability import StandbyModel
>>> unit = surv.Exponential.from_params([0.01])
>>> standby = StandbyModel([unit, unit])
>>> standby.random(5, seed=1).shape
(5,)
>>> a = standby.random(5, seed=1)
>>> bool(np.array_equal(a, standby.random(5, seed=1)))
True

cs

cs(x, X)

Conditional survival R(x | X) = sf(X + x) / sf(X).

The probability of surviving a further x given the arrangement has already survived to age X, computed from sf.

Parameters:

Name Type Description Default
x float or array_like

The further time(s) to survive.

required
X float or array_like

The age(s) already survived.

required

Returns:

Type Description
float or ndarray

The conditional survival, clipped to [0, 1] and 0 where sf(X) is 0: a float if x and X are both scalars, otherwise an array.

Examples:

>>> import surpyval as surv
>>> from repyability import StandbyModel
>>> unit = surv.Exponential.from_params([0.01])
>>> round(StandbyModel([unit, unit]).cs(50.0, 100.0), 4)
0.7582

stage_probabilities

stage_probabilities(x)

The probability that the component is in each stage at time/s x: one row per stage, in order (one value per stage for a scalar x). The rest of the probability, ff(x), is that it has failed.

Being in stage j at x means the first j - 1 stages have ended by x and the j-th has not, so its probability is the survival function of the sum of the first j stages' times less that of the first j - 1. Those come from the same (exact or numerical) convolution as sf, so the rows add up to sf(x). A time before 0 is taken as 0: a new component is in its first stage.

Parameters:

Name Type Description Default
x float or array_like

The time(s).

required

Returns:

Type Description
ndarray

Shape (n_stages,) for a scalar x, else (n_stages, len(x)).

sf

sf(x, *args, **kwargs)

Survival function (reliability): the probability that the last stage has not ended by time/s x.

With one stage, the stage model's own sf; with several, that of the sum of the stages' times (see StandbyModel.sf).

Parameters:

Name Type Description Default
x array_like

The time(s).

required
*args

Passed on, with x, to the underlying survival model.

()
**kwargs

Passed on, with x, to the underlying survival model.

()

Returns:

Type Description
float or ndarray

The probability of surviving beyond x.

ff

ff(x, *args, **kwargs)

Cumulative failure probability, 1 - sf(x).

Parameters:

Name Type Description Default
x array_like

The time(s).

required
*args

Passed on, with x, to the underlying survival model.

()
**kwargs

Passed on, with x, to the underlying survival model.

()

Returns:

Type Description
float or ndarray

The probability that the last stage has ended by x.

mean

mean(mc_samples=None, seed=None, *, method=None)

Mean lifetime (MTTF): the sum of the stages' mean times, exactly; infinite if a stage may never end. method="simulate" estimates it from mc_samples draws of random instead.

Parameters:

Name Type Description Default
mc_samples int

With method="simulate", the number of draws, 10_000 by default. The exact mean draws nothing, and refuses it.

None
seed int or None

Seed for those draws, refused as mc_samples is.

None
method (None, exact, simulate)

As for StandbyModel.mean: None and "exact" give the exact mean.

None

Returns:

Type Description
float

The mean lifetime, or its estimate.

Raises:

Type Description
ValueError

If mc_samples or seed is given to the exact mean.

capacity_distribution

capacity_distribution(x)

The distribution of the component's capacity at time/s x: the capacity of the stage it is in, or 0 once it has failed.

Parameters:

Name Type Description Default
x float or array_like

The time(s).

required

Returns:

Type Description
CapacityDistribution

The capacities it can have (0 and each stage's, equal ones merged) and their probabilities: one per level for a scalar x, and one row per level and one column per time for an array.

stage_fractions

stage_fractions()

The long-run fraction of its working time the component spends in each stage, when it is renewed after each failure.

Each renewal runs through the stages again, so by the renewal-reward theorem the fraction in stage j is its mean time over the mean lifetime. A stage that may never end (a model with units that never fail) catches the component for good sooner or later, and the fractions are then the probabilities of being caught in each stage.

Returns:

Type Description
ndarray

One fraction per stage; they sum to 1.

RepeatedNode

RepeatedNode(model, repeats, kind)

Independent identical copies of one component, in series or parallel.

One RBD node standing for repeats independent copies of model, arranged in series (the node fails at the first copy failure) or in parallel (at the last). Its survival function is exact: R(x) ** repeats in series and 1 - F(x) ** repeats in parallel, with R and F the model's sf and ff, and so is its mean, the area under sf.

As an RBD node, sf/ff give its reliability, random its lifetimes for Monte-Carlo system simulation and mean its MTTF. The copies fail independently. This differs from a repeated node of an RBD (a reliabilities entry that names another node), which puts one component in several places of the diagram, failing in all of them at once.

Parameters:

Name Type Description Default
model lifetime model

The component's lifetime distribution, e.g. a surpyval distribution. Sampling calls its random with a (size, repeats) shape, which surpyval distributions accept.

required
repeats int

The number of copies, at least 1.

required
kind (series, parallel)

How the copies are arranged.

'series'

Raises:

Type Description
ValueError

If kind is neither 'series' nor 'parallel', or repeats is not a whole number of at least 1.

Examples:

>>> import surpyval as surv
>>> from repyability import RepeatedNode
>>> unit = surv.Weibull.from_params([100.0, 2.0])
>>> series = RepeatedNode(unit, 3, "series")
>>> round(float(series.sf(50.0)), 4)  # unit.sf(50) ** 3
0.4724
>>> parallel = RepeatedNode(unit, 3, "parallel")
>>> round(float(parallel.sf(50.0)), 4)  # 1 - unit.ff(50) ** 3
0.9892

random

random(size, seed=None)

Draw random lifetimes of the node.

Draws repeats independent lifetimes of model per sample and takes their minimum (series) or maximum (parallel).

Parameters:

Name Type Description Default
size int

The number of lifetimes to draw.

required
seed int or None

If given, numpy's global RNG is seeded for the draw and its previous state restored afterwards, so the result is reproducible. By default None (draw from the current state).

None

Returns:

Type Description
ndarray

The size lifetimes, shape (size,).

mean

mean(mc_samples=None, seed=None, *, method='exact')

Mean lifetime (MTTF) of the node.

Exact by default: the area under sf, integrated by adaptive Gauss-Legendre quadrature to about 1e-10, relative, and inf if the node may never fail (its model has units that never fail, and the node needs only them). method="simulate" estimates it instead, as the mean of mc_samples draws of random.

Parameters:

Name Type Description Default
mc_samples int

With method="simulate": the number of draws, by default 1_000_000.

None
seed int or None

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

None
method (exact, simulate)

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

"exact"

Returns:

Type Description
float

The mean lifetime, or with method="simulate" its estimate.

Raises:

Type Description
ValueError

If method is neither "exact" nor "simulate".

TypeError

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

Examples:

>>> import surpyval as surv
>>> from repyability import RepeatedNode
>>> unit = surv.Weibull.from_params([100.0, 2.0])
>>> node = RepeatedNode(unit, 3, "series")
>>> round(node.mean(), 4)  # 100 * Gamma(1.5) / sqrt(3)
51.1663
>>> round(node.mean(method="simulate", mc_samples=10_000, seed=1), 1)
51.1

sf

sf(x)

Survival function (reliability) of the node, exactly.

model.sf(x) ** repeats in series and 1 - model.ff(x) ** repeats in parallel. An RBD calls this for the node's reliability.

Parameters:

Name Type Description Default
x float or array_like

The time(s) at which to evaluate.

required

Returns:

Type Description
float or ndarray

The probability of surviving beyond x, as a scalar or an array following model.sf.

ff

ff(x)

Cumulative failure probability of the node, 1 - sf(x).

1 - model.sf(x) ** repeats in series and model.ff(x) ** repeats in parallel, each worked out from the model's ff so that a small one keeps its precision (in series, as -expm1(repeats * log1p(-model.ff(x)))).

Parameters:

Name Type Description Default
x float or array_like

The time(s) at which to evaluate.

required

Returns:

Type Description
float or ndarray

The probability of failing by x, as a scalar or an array following model.ff.

cs

cs(x, X)

Conditional survival R(x | X) = sf(X + x) / sf(X).

The probability that the node survives a further x given it has already survived to age X, computed from sf.

Parameters:

Name Type Description Default
x float or array_like

The further time(s) to survive.

required
X float or array_like

The age(s) already survived.

required

Returns:

Type Description
float or ndarray

The conditional survival, clipped to [0, 1] and 0 where sf(X) is 0: a float if x and X are both scalars, otherwise an array.

RepeatedStandbyNode

RepeatedStandbyNode(model, repeats, *, switching_probability=1.0)

Cold standby of identical copies of one component, as one node.

repeats independent copies of model in cold standby with one operating at a time: the first copy runs and, each time the running copy fails, the next is switched in. Spares do not age while dormant, so with perfect switching the lifetime is the sum of repeats independent lifetimes. With imperfect switching it is a mixture of partial sums, since a failed switch ends the node's life when the copy it should replace fails. The survival function is computed at construction by numerical convolution of the copies' densities on a time grid: deterministic (no Monte-Carlo sampling), and linearly interpolated when evaluated.

For different units, k >= 2 or warm and hot spares, use StandbyModel. Unlike this node, which always convolves numerically, StandbyModel([model] * repeats) uses the exact Erlang form for identical Exponential units with perfect switching.

As an RBD node, sf/ff give its reliability, random its lifetimes for Monte-Carlo system simulation and mean its MTTF.

Parameters:

Name Type Description Default
model lifetime model

The copies' lifetime distribution, e.g. a surpyval distribution. It must expose df, sf and mean (for the convolution) and random (for sampling).

required
repeats int

The number of copies, at least 1.

required
switching_probability float or sequence of float

The probability, in [0, 1], that switching onto the next copy succeeds: a scalar for every switch, or one value per switch (length repeats - 1). By default 1.0 (perfect switching).

1.0

Raises:

Type Description
ValueError

If repeats < 1, a switching probability is outside [0, 1], or a sequence of them does not have length repeats - 1.

Examples:

Three copies of a Weibull unit with mean life 88.62; the lifetimes add, so the mean is close to 3 * 88.62 = 265.87:

>>> import surpyval as surv
>>> from repyability import RepeatedStandbyNode
>>> unit = surv.Weibull.from_params([100.0, 2.0])
>>> node = RepeatedStandbyNode(unit, 3)
>>> round(float(node.mean()), 1)
265.9
>>> round(float(node.sf(200.0)), 4)
0.786

If each switch works with probability 0.9, the mean falls to about 88.62 * (1 + 0.9 + 0.81):

>>> flaky = RepeatedStandbyNode(unit, 3, switching_probability=0.9)
>>> round(float(flaky.mean()), 1)
240.2

random

random(size, seed=None)

Draw random lifetimes of the node.

Each lifetime is the sum of repeats independent draws of model. Under imperfect switching a spare adds its lifetime only if every switch up to and including its own succeeds (one uniform draw per switch).

Parameters:

Name Type Description Default
size int

The number of lifetimes to draw.

required
seed int or None

If given, numpy's global RNG is seeded for the draw and its previous state restored afterwards, so the result is reproducible. By default None (draw from the current state).

None

Returns:

Type Description
ndarray

The size lifetimes, shape (size,).

mean

mean(*args, **kwargs)

Mean lifetime (MTTF) of the node, from the convolution.

E[T], the integral of the survival function over its grid (trapezoidal rule): deterministic, with no sampling.

Parameters:

Name Type Description Default
*args

Ignored; accepted so mean can be called like the other node models' (e.g. with a sample count).

()
**kwargs

Ignored; accepted so mean can be called like the other node models' (e.g. with a sample count).

()

Returns:

Type Description
float

The mean lifetime.

sf

sf(*args, **kwargs)

Survival function (reliability) of the node.

The numerically convolved survival function, linearly interpolated on its grid: 1 for x below 0 and 0 beyond the grid's end (placed where the survival is negligible). An RBD calls this for the node's reliability.

Parameters:

Name Type Description Default
*args array_like

The time(s) x, as in sf(x).

()
**kwargs

Passed on with x; the only keyword accepted is x itself, as in sf(x=t).

{}

Returns:

Type Description
float or ndarray

The probability of surviving beyond x: a numpy float for a scalar x, an array for an array x.

ff

ff(*args, **kwargs)

Cumulative failure probability of the node, 1 - sf(x).

Parameters:

Name Type Description Default
*args array_like

The time(s) x, as in ff(x).

()
**kwargs

Passed on with x; the only keyword accepted is x itself, as in ff(x=t).

{}

Returns:

Type Description
float or ndarray

The probability of failing by x: a numpy float for a scalar x, an array for an array x.

cs

cs(x, X)

Conditional survival R(x | X) = sf(X + x) / sf(X).

The probability that the node survives a further x given it has already survived to age X, computed from sf.

Parameters:

Name Type Description Default
x float or array_like

The further time(s) to survive.

required
X float or array_like

The age(s) already survived.

required

Returns:

Type Description
float or ndarray

The conditional survival, clipped to [0, 1] and 0 where sf(X) is 0: a float if x and X are both scalars, otherwise an array.

LoadSharingModel

LoadSharingModel(models, load, k=1)

A load-sharing arrangement of coupled AFT units as one RBD node.

N = len(models) units share a total load L: while s of them survive, each carries L / s and ages on its baseline clock at its accelerated-failure-time rate phi(L / s), so when a unit fails the survivors pick up its share and age faster (dependent failure). The group works while at least k units survive and fails at the (N - k + 1)-th unit failure. Because each unit's aging depends on how many siblings are alive, the group is one node; it cannot be modelled as N independent nodes.

The survival function is set up once, at construction:

  • Exact, when every unit's baseline is Exponential with one common rate lambda and the units' phi agree at every stage: the lifetime is hypoexponential with stage rates s * lambda * phi(L / s) for s = N, ..., k survivors.
  • Numerical, for other identical units (one baseline and one phi): every survivor ages alike, so the units fail in the order of their exposures to failure, and a recursion over the failures on a grid of exposure and time gives the lifetime's distribution, to about 1e-4 of a probability.
  • None for different units: sf, ff, cs and mean() refuse, and is_simulated is True. Its lifetimes are still drawn by random, so a diagram with it is simulated: random, mean(method="simulate") and unreliability_interval of a NonRepairableRBD, and availability and cost of a RepairableRBD. mean(method="simulate", mc_samples=..., seed=...) estimates its own mean from new draws. (Until 0.12 its sf was a Kaplan-Meier fit to simulated lifetimes.)

As an RBD node, sf/ff give its reliability, random its lifetimes for Monte-Carlo system simulation and mean its MTTF.

Parameters:

Name Type Description Default
models sequence of fitted surpyval AFT models

The units: fitted accelerated-failure-time regression models (e.g. from surpyval.ExponentialAFT.fit or surpyval.WeibullAFT.fit) with the load as their single covariate. A unit's aging rate under load l is its phi(l), and its baseline is its fitted distribution at phi = 1. For identical units, pass the same model several times.

required
load float

The total load L shared by the surviving units. Each of s survivors carries L / s.

required
k int

The minimum number of surviving units for the group to work, from 1 to len(models), by default 1.

1

Attributes:

Name Type Description
N int

The number of units, len(models).

Raises:

Type Description
ValueError

If models is empty, k < 1, k > len(models), or a unit is not a fitted accelerated-failure-time model.

Notes

phi with no load effect (phi == 1) reproduces the ordinary k-out-of-n parallel result exactly, since the units then neither share stress nor age faster as siblings fail.

Examples:

Fit an Exponential AFT model with load as its covariate in surpyval. With these data the life at load 1 is 100 and at load 2 is 25:

>>> import numpy as np
>>> import surpyval as surv
>>> from repyability import LoadSharingModel
>>> x = np.array([50.0, 100.0, 150.0, 12.5, 25.0, 37.5])
>>> load = np.array([[1.0], [1.0], [1.0], [2.0], [2.0], [2.0]])
>>> pump = surv.ExponentialAFT.fit(x, Z=load)

Two such pumps share a total load of 2. Each carries 1 while both run (rate 0.01 each); the survivor carries 2 (rate 0.04). The closed form applies:

>>> pumps = LoadSharingModel([pump, pump], load=2.0, k=1)
>>> pumps.is_simulated
False
>>> round(float(pumps.mean()), 1)  # 1 / (2 * 0.01) + 1 / 0.04
75.0
>>> round(float(pumps.sf(50.0)), 4)
0.6004

A Weibull baseline has no closed form, so the reliability of two such pumps is numerical:

>>> w = surv.WeibullAFT.fit(x, Z=load)
>>> numerical = LoadSharingModel([w, w], load=2.0)
>>> numerical.is_simulated
False
>>> round(float(numerical.sf(50.0)), 2)
0.92

is_simulated property

is_simulated

Whether only simulations take the group: its units are different, so no exact or numerical method gives its reliability, sf, ff, cs and mean() refuse, and a diagram with it is simulated.

random

random(size, seed=None)

Simulate group lifetimes with the cumulative-exposure event loop.

Each unit draws the baseline exposure at which it fails (its virtual age at failure, from its baseline distribution). The loop then steps from one unit failure to the next: while s units survive each accrues exposure at phi(L / s), the unit that reaches its threshold first fails, and the survivors carry on at the new share. The group lifetime is the time at which fewer than k units survive. This is the sampler behind the simulated sf and an RBD's Monte-Carlo random/mean; it is used even when sf is exact.

Parameters:

Name Type Description Default
size int

The number of lifetimes to draw.

required
seed int or None

If given, numpy's global RNG is seeded for the draw and its previous state restored afterwards, so the result is reproducible. By default None (draw from the current state).

None

Returns:

Type Description
ndarray

The size group lifetimes, shape (size,).

mean

mean(mc_samples=None, seed=None, *, method=None)

Mean lifetime (MTTF) of the group.

Exact (the hypoexponential mean, the sum of the stage means) when the closed form applies, and numerical for identical units (the integral of the survival function). A group of different units has neither (see is_simulated), and refuses, unless asked for an estimate: the mean of mc_samples new draws of random. method="simulate" estimates it so whatever the group.

Parameters:

Name Type Description Default
mc_samples int

The number of draws to estimate the mean from, 10_000 by default: with method="simulate", or for a group with no exact mean. An exact mean draws nothing, and refuses it.

None
seed int or None

Seed for those draws (see random), by default None. Refused as mc_samples is.

None
method (None, exact, simulate)

As for StandbyModel.mean.

None

Returns:

Type Description
float

The mean lifetime, or its estimate.

Raises:

Type Description
NotImplementedError

If the group has no exact mean and neither mc_samples nor seed is given (or method="exact").

ValueError

If method is not one of these, or mc_samples or seed is given to an exact mean.

sf

sf(x, *args, **kwargs)

Survival function (reliability) of the group.

Evaluates the exact hypoexponential closed form, or the numerical recursion for identical units. An RBD calls this for the node's reliability.

Parameters:

Name Type Description Default
x array_like

The time(s).

required
*args

Passed on, with x, to the underlying survival model.

()
**kwargs

Passed on, with x, to the underlying survival model.

()

Returns:

Type Description
float or ndarray

The probability that the group survives beyond x: an array for an array x, and a numpy float for a scalar x.

Raises:

Type Description
NotImplementedError

For different units (see is_simulated).

ff

ff(x, *args, **kwargs)

Cumulative failure probability, 1 - sf(x).

Evaluated from the same survival function as sf.

Parameters:

Name Type Description Default
x array_like

The time(s).

required
*args

Passed on, with x, to the underlying survival model.

()
**kwargs

Passed on, with x, to the underlying survival model.

()

Returns:

Type Description
float or ndarray

The probability that the group has failed by x, shaped as for sf.

Raises:

Type Description
NotImplementedError

As for sf.

cs

cs(x, X)

Conditional survival R(x | X) = sf(X + x) / sf(X).

The probability that the group survives a further x given it has already survived to age X, computed from sf.

Parameters:

Name Type Description Default
x float or array_like

The further time(s) to survive.

required
X float or array_like

The age(s) already survived.

required

Returns:

Type Description
float or ndarray

The conditional survival, clipped to [0, 1] and 0 where sf(X) is 0: a float if x and X are both scalars, otherwise an array.

RegressionNode

RegressionNode(model, covariates=None, schedule=None)

An RBD node backed by a fitted surpyval regression model.

Pairs a fitted regression model (accelerated failure time, proportional hazards, proportional odds, ...) with this component's covariates, giving an ordinary univariate node. Provide exactly one of:

  • covariates, a fixed operating point Z: the reliability is R(x) = model.sf(x, Z), for any regression family; or
  • schedule, a time-varying covariate path Z(t): the reliability is R(x) = model.sf_tvc(x, schedule), the survival along that path (accelerated-failure-time, proportional- and additive-hazards, and proportional-odds models).

sf and ff evaluate that curve directly, so the node takes part in system reliability, importance measures and the condition-based (age) methods with no special handling. mean integrates the curve to a relative 1e-10, and random inverts it exactly (the model's quantile at fixed covariates; the cumulative hazard along a schedule), which needs a proper parametric curve: a semiparametric model such as surpyval.CoxPH supports sf but not mean or random. Do the regression fit in surpyval and pass the fitted model in.

Parameters:

Name Type Description Default
model surpyval regression model

A fitted regression model, e.g. surpyval.WeibullAFT.fit(...) or surpyval.CoxPH.fit(...). Fixed covariates use its sf(x, Z); a schedule uses its sf_tvc(x, schedule).

required
covariates array_like

The component's fixed covariate vector Z (its operating conditions), matching the covariates the model was fitted with.

None
schedule surpyval StepSchedule

A piecewise-constant covariate path Z(t) -- the load the component runs under over its life (build with surpyval.StepSchedule.from_changepoints / from_intervals).

None

Raises:

Type Description
ValueError

If not exactly one of covariates and schedule is given, or if a trial evaluation of the survival at x = 1 fails or is not finite: e.g. model is not a fitted regression model, covariates has the wrong width, or in schedule mode the model's sf_tvc cannot evaluate it.

Examples:

Fit an Exponential AFT model with load as its covariate in surpyval. With these data the life at load 1 is 100 and at load 2 is 25:

>>> import numpy as np
>>> import surpyval as surv
>>> from repyability import RegressionNode
>>> x = np.array([50.0, 100.0, 150.0, 12.5, 25.0, 37.5])
>>> load = np.array([[1.0], [1.0], [1.0], [2.0], [2.0], [2.0]])
>>> model = surv.ExponentialAFT.fit(x, Z=load)

A component always run at load 1:

>>> node = RegressionNode(model, covariates=[1.0])
>>> round(float(node.sf(100.0)), 4)  # exp(-100 / 100)
0.3679
>>> round(node.mean(), 1)
100.0

One run at load 1 for 50 time units and at load 2 after that:

>>> schedule = surv.StepSchedule.from_changepoints(
...     [0, 50], [[1.0], [2.0]]
... )
>>> ramped = RegressionNode(model, schedule=schedule)
>>> round(float(ramped.sf(100.0)), 4)  # exp(-50 / 100 - 50 / 25)
0.0821

sf

sf(x)

Reliability at the stored covariates or along the schedule.

model.sf(x, Z) at the fixed covariates Z, or model.sf_tvc(x, schedule) along the covariate schedule. An RBD calls this for the node's reliability.

Parameters:

Name Type Description Default
x array_like

The time(s) at which to evaluate.

required

Returns:

Type Description
float or ndarray

The probability of surviving beyond each x: a float for a scalar x, as a surpyval model gives it, and an array for an array.

ff

ff(x)

Unreliability: 1 - sf(x).

model.ff(x, Z) at the fixed covariates, or -expm1(-model.Hf_tvc(x, schedule)) along the schedule: worked out in its own right, so that a small one keeps its precision (for a proportional-odds model along a schedule too, from surpyval 0.22: surpyval #528).

Parameters:

Name Type Description Default
x array_like

The time(s) at which to evaluate.

required

Returns:

Type Description
float or ndarray

The probability of failing by each x: a float for a scalar x, as for sf.

mean

mean()

Mean time to failure at the stored covariates / along the schedule.

For a non-negative lifetime E[T] = integral of R(t), integrated to a relative accuracy of 1e-10 on pieces split at the model's quantiles (fixed covariates) or the schedule's change points, as an RBD's MTTF is: inf if some lifetimes never end.

Returns:

Type Description
float

The mean time to failure.

Raises:

Type Description
ValueError

If the model is semiparametric (a Cox model: its baseline is defined on the observed range only), or if the survival curve is improper (R(0+) <= 0.99).

random

random(size)

Draw size failure times at the stored covariates / schedule.

Inverse-transform sampling, exact: each uniform u maps to the time at which the distribution reaches u, the model's quantile qf(u, Z) at fixed covariates, and along a schedule the time at which the cumulative hazard reaches -log(1 - u) (found by bisection, to the last bits). There is no seed argument: this uses numpy's global RNG, so seed it (np.random.seed) or wrap the call in repyability.utils.wrappers.numpy_seed to reproduce the draws. An RBD's seeded random and mean do this for you.

Parameters:

Name Type Description Default
size int

The number of failure times to draw.

required

Returns:

Type Description
ndarray

The size failure times, shape (size,).

Raises:

Type Description
ValueError

If the survival curve is improper, as for mean.

to_dict

to_dict()

Serialise the node to a JSON-friendly dict.

The fitted model is stored with its own to_dict(), plus either "covariates" (a list of floats) or "schedule" (the schedule's finite segment edges as "times" and its covariate rows as "values"). from_dict rebuilds the node, and an RBD's to_dict/to_json use this for a regression node.

A schedule is rebuilt with StepSchedule.from_changepoints, so only one whose last segment is open-ended round-trips (e.g. from from_changepoints). One whose last edge is finite (e.g. from from_intervals) is written, but from_dict then raises ValueError.

Returns:

Type Description
dict

{"model": ..., "covariates": [...]} or {"model": ..., "schedule": {"times": [...], "values": [...]}}.

Raises:

Type Description
NotImplementedError

If the schedule is cyclic (has a period).

from_dict classmethod

from_dict(d)

Reconstruct a node from the output of to_dict.

The fitted model round-trips through surpyval.from_dict, and a schedule is rebuilt with StepSchedule.from_changepoints.

Parameters:

Name Type Description Default
d dict

A dict produced by to_dict, e.g. after a JSON round trip.

required

Returns:

Type Description
RegressionNode

The reconstructed node.

Raises:

Type Description
KeyError

If d has no "model", or neither "schedule" nor "covariates".

ValueError

If the schedule cannot be rebuilt, or the node fails the constructor's checks.

Examples:

>>> import json
>>> import numpy as np
>>> import surpyval as surv
>>> from repyability import RegressionNode
>>> x = np.array([50.0, 100.0, 150.0, 12.5, 25.0, 37.5])
>>> load = np.array([[1.0], [1.0], [1.0], [2.0], [2.0], [2.0]])
>>> model = surv.ExponentialAFT.fit(x, Z=load)
>>> node = RegressionNode(model, covariates=[1.0])
>>> text = json.dumps(node.to_dict())
>>> restored = RegressionNode.from_dict(json.loads(text))
>>> round(float(restored.sf(100.0)), 4)
0.3679

JointNode

JointNode(model, Z=None, markers=None, age=None)

One unit of a fitted joint model, given its readings, as a node.

surpyval's joint models (surpyval.WeibullJointPH) model a marker read repeatedly on each unit together with the unit's life; a fitted model's predict(Z, markers, given) is one unit's failure-age distribution given its covariates, its readings so far and that it is still running at age given. This node is that unit in a NonRepairableRBD, from now: at time t from now its reliability is pred.sf(age + t) (already conditioned on the unit reaching age), its lifetimes are pred.random() - age and its mean is the unit's mean residual life. A DynamicPrediction given as a diagram's node is taken as the JointNode of its unit.

The node's age is the prediction's: a NodeState for it in sf_given_state and the other condition-based methods may say that it has failed, or give that same age, but not another, which would condition it again (predict again, with given the new age and the readings since). A RepairableRBD refuses it: the units that replace this one are new, without its readings. See the guide's joint-model page.

Parameters:

Name Type Description Default
model surpyval DynamicPrediction or JointModel

The unit's prediction (joint_model.predict(...)), or the fitted joint model, with the unit's Z, markers and age.

required
Z dict, array_like or surpyval ItemLifetime

With a JointModel: the unit's covariates ({name: value}, or a row in the order of the model's feature_names), needed where the model has covariates; or an ItemLifetime still running, which holds the unit's covariates, readings and age.

None
markers dict

With a JointModel: the unit's readings so far, {age: value}.

None
age float

With a JointModel: the unit's age now (surpyval's given), at or after its last reading; 0 (new) by default, when it has no readings.

None

Raises:

Type Description
TypeError

If model is neither, or Z, markers or age is given with a prediction, which holds them.

ValueError

If the model's checks refuse the unit's inputs, or a covariate is missing (nan).

Examples:

Fit a joint model to a small fleet in surpyval, and put a pump whose marker has risen into a diagram, 40 hours old:

>>> import numpy as np
>>> from surpyval import ItemLifetime, SurpyvalData, WeibullJointPH
>>> from repyability import JointNode, NonRepairableRBD
>>> rng = np.random.default_rng(3)
>>> items = []
>>> 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)
...     y = start + rate * ages + rng.normal(0, 0.2, ages.size)
...     outcome = "observed" if life < 120 else "right_censored"
...     items.append(
...         ItemLifetime(k, **{outcome: end},
...                      markers=dict(zip(ages, y)))
...     )
>>> model = WeibullJointPH.fit(SurpyvalData.from_items(items))
>>> pred = model.predict(markers={0: 2.1, 20: 2.5, 40: 3.0}, given=40)
>>> pump = JointNode(pred)
>>> bool(pump.sf(20.0) == pred.sf(60.0))
True
>>> rbd = NonRepairableRBD([("in", "pump"), ("pump", "out")],
...                        {"pump": pump})
>>> bool(rbd.sf(20.0) == pred.sf(60.0))
True

sf

sf(x)

The probability that the unit survives a further x: pred.sf(age + x).

Parameters:

Name Type Description Default
x array_like

The time(s) from now.

required

Returns:

Type Description
float or ndarray

With the shape of x.

ff

ff(x)

The probability that the unit fails within a further x: pred.ff(age + x), worked out in its own right.

Parameters:

Name Type Description Default
x array_like

The time(s) from now.

required

Hf

Hf(x)

The cumulative hazard over the next x, -log sf(x).

Parameters:

Name Type Description Default
x array_like

The time(s) from now.

required

hf

hf(x)

The unit's hazard x from now, given its readings.

Parameters:

Name Type Description Default
x array_like

The time(s) from now.

required

df

df(x)

The density of the unit's life left at x from now.

Parameters:

Name Type Description Default
x array_like

The time(s) from now.

required

qf

qf(p)

The life left that the unit outlives with probability 1 - p: pred.qf(p) - age (inf where the survival never falls to 1 - p).

Parameters:

Name Type Description Default
p array_like

The probabilities of failing.

required

mean

mean()

The unit's mean residual life: the integral of sf from now, to a relative 1e-10 on pieces split at its quantiles; inf where its survival stays above 0 for ever (a marker path that falls away can leave a chance of never failing).

Returns:

Type Description
float

The mean life left.

random

random(size)

Draw size lives left, from now: pred.qf(u) - age of uniforms u from numpy's global RNG, as pred.random draws failure ages. Seed it (np.random.seed) to reproduce the draws; an RBD's seeded methods do.

Parameters:

Name Type Description Default
size int

The number of draws.

required

Returns:

Type Description
ndarray

The size lives left, shape (size,).

to_dict

to_dict()

The node as a JSON-friendly dict: the joint model's own to_dict() and the unit's inputs, its covariates (in the order of the model's feature_names), its readings as [age, value] pairs and its age. from_dict rebuilds it, and an RBD's to_dict/to_json save a joint node so. A joint model on a path model of one's own does not save (surpyval raises NotImplementedError).

Returns:

Type Description
dict

{"model": ..., "covariates": [...], "markers": [[age, value], ...], "age": ...}.

from_dict classmethod

from_dict(d)

Rebuild a node from :meth:to_dict's dict.

Parameters:

Name Type Description Default
d dict

A dict to_dict made, e.g. after a JSON round trip.

required

Returns:

Type Description
JointNode

The node, its prediction made again from the saved model and the unit's inputs.

Examples:

>>> import json
>>> import numpy as np
>>> from surpyval import ItemLifetime, SurpyvalData, WeibullJointPH
>>> from repyability import JointNode
>>> rng = np.random.default_rng(3)
>>> items = []
>>> 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)
...     y = start + rate * ages + rng.normal(0, 0.2, ages.size)
...     outcome = "observed" if life < 120 else "right_censored"
...     items.append(
...         ItemLifetime(k, **{outcome: end},
...                      markers=dict(zip(ages, y)))
...     )
>>> model = WeibullJointPH.fit(SurpyvalData.from_items(items))
>>> node = JointNode(model, markers={0: 2.1, 40: 3.0}, age=40)
>>> back = JointNode.from_dict(json.loads(json.dumps(node.to_dict())))
>>> bool(back.sf(20.0) == node.sf(20.0))
True

PerfectReliability

A node model that never fails.

Its survival function is 1 at every time, its failure probability 0, and a simulated failure time is infinite. A NonRepairableRBD uses it for the input and output nodes and for nodes forced working (working_nodes), and it can be given as a node's model, e.g. for a connection that cannot fail. Its methods are class methods, and the RBD recognises it (as time-invariant, and when serialising) by the class: an instance, PerfectReliability(), is taken as the class itself.

Examples:

A perfectly reliable node in series changes nothing:

>>> from surpyval import FixedEventProbability
>>> from repyability import NonRepairableRBD, PerfectReliability
>>> rbd = NonRepairableRBD(
...     [("s", "a"), ("a", "b"), ("b", "t")],
...     {
...         "a": FixedEventProbability.from_params(0.1),
...         "b": PerfectReliability,
...     },
... )
>>> round(rbd.sf(), 4)
0.9
>>> PerfectReliability.sf([10.0, 20.0]).tolist()
[1.0, 1.0]

sf classmethod

sf(x)

Return the survival function (reliability): 1 at every time.

Parameters:

Name Type Description Default
x array_like

Time(s); only the shape is used.

required

Returns:

Type Description
ndarray

Ones (floats) with the shape of x: a 0-d array for a scalar x.

ff classmethod

ff(x)

Return the failure probability (unreliability): 0 at every time.

Parameters:

Name Type Description Default
x array_like

Time(s); only the shape is used.

required

Returns:

Type Description
ndarray

Zeros (floats) with the shape of x: a 0-d array for a scalar x.

cs classmethod

cs(x, X)

Return the conditional survival: 1 for any further time.

The probability of surviving a further x given survival to age X, which is 1 because the node never fails.

Parameters:

Name Type Description Default
x array_like

The further time(s); only the shape is used.

required
X array_like

The age(s) survived to; not used.

required

Returns:

Type Description
ndarray

Ones (floats) with the shape of np.atleast_1d(x).

mean classmethod

mean()

Return the mean life: infinite, as the node never fails.

Returns:

Type Description
float

inf.

random classmethod

random(size)

Draw failure times: all infinite, as the node never fails.

Parameters:

Name Type Description Default
size int or tuple of int

The shape of the output.

required

Returns:

Type Description
ndarray

An array of inf with shape size.

PerfectUnreliability

A node model that has always failed.

Its survival function is 0 at every time, its failure probability 1, and a simulated failure time is 0. A NonRepairableRBD uses it for nodes forced failed (broken_nodes) and for failed components in condition-based evaluation (a NodeState with alive=False), and it can be given as a node's model, e.g. to study the system with a component missing. Its methods are class methods, and the RBD recognises it (as time-invariant, and when serialising) by the class: an instance is taken as the class itself.

Examples:

With one of two parallel nodes failed, the other carries the system:

>>> from surpyval import FixedEventProbability
>>> from repyability import NonRepairableRBD, PerfectUnreliability
>>> rbd = NonRepairableRBD(
...     [("s", "a"), ("s", "b"), ("a", "t"), ("b", "t")],
...     {
...         "a": FixedEventProbability.from_params(0.1),
...         "b": PerfectUnreliability,
...     },
... )
>>> round(rbd.sf(), 4)
0.9
>>> PerfectUnreliability.sf([10.0, 20.0]).tolist()
[0.0, 0.0]

sf classmethod

sf(x)

Return the survival function (reliability): 0 at every time.

Parameters:

Name Type Description Default
x array_like

Time(s); only the shape is used.

required

Returns:

Type Description
ndarray

Zeros (floats) with the shape of x: a 0-d array for a scalar x.

ff classmethod

ff(x)

Return the failure probability (unreliability): 1 at every time.

Parameters:

Name Type Description Default
x array_like

Time(s); only the shape is used.

required

Returns:

Type Description
ndarray

Ones (floats) with the shape of x: a 0-d array for a scalar x.

cs classmethod

cs(x, X)

Return the conditional survival: 0 for any further time.

Parameters:

Name Type Description Default
x array_like

The further time(s); only the shape is used.

required
X array_like

The age(s) survived to; not used.

required

Returns:

Type Description
ndarray

Zeros (floats) with the shape of np.atleast_1d(x).

mean classmethod

mean()

Return the mean life: 0, as the node has always failed.

Returns:

Type Description
float

0.0.

random classmethod

random(size)

Draw failure times: all 0, as the node has always failed.

Parameters:

Name Type Description Default
size int or tuple of int

The shape of the output.

required

Returns:

Type Description
ndarray

An array of zeros with shape size.

Design inputs

ComponentOption dataclass

ComponentOption(name, reliability, cost)

A candidate component type for a node, in redundancy allocation.

Give a node a list of these in the costs of allocate_redundancy to choose its copies among several types, e.g. a standard pump and a premium one, each with its own reliability and cost. The node's own model in the RBD is then not used: list it as an option too if it is a candidate.

Parameters:

Name Type Description Default
name Hashable

What the result's mix calls this type. Distinct within a node.

required
reliability model or float

The reliability of one copy: a model with an sf method (such as a fitted surpyval distribution), evaluated at the mission time, or a fixed probability of working, in [0, 1].

required
cost float or dict

What one copy uses: a number (its cost) or a dict of resources, as for a node without options.

required

Examples:

>>> import surpyval as surv
>>> from repyability import ComponentOption
>>> pumps = [
...     ComponentOption(
...         "standard", surv.Weibull.from_params([8000, 1.8]), cost=4000
...     ),
...     ComponentOption(
...         "premium", surv.Weibull.from_params([15000, 2.2]), cost=9000
...     ),
... ]
>>> pumps[1].name, pumps[1].cost
('premium', 9000)

Common-cause failures

CCFGroup

CCFGroup(members, model)

A common-cause group: member nodes coupled by a shared failure cause.

Pass groups to NonRepairableRBD as ccf_groups=[...]. At each time the RBD takes the members' failure probability Q (1 - sf(t), or for the RBD's ff their model's own ff(t), which keeps a small one's precision), splits it with the model's decompose into independent failures and mutually exclusive shocks, and computes the exact system reliability by conditioning on every group's shock outcome. The groups enter the RBD's sf and ff and the methods built on them (sf raises NotImplementedError if a member is forced through working_nodes/broken_nodes). A model that splits the failure probability (the default) is meant for a small Q: the RBD warns, once per group, when a member's Q passes VALIDITY (0.1), and its exact mean refuses the group, while its Monte-Carlo random and mean(method="simulate") leave the group out. A model that splits the failure rate (basis="rate") holds over the whole life: the exact mean includes the group, and the simulations draw its shared shocks. The RBD's importance measures condition a member on its state through the shock outcomes, its parameter sensitivity and uncertainty take a group's members together (with the model's own parameters), and its redundancy allocation lets a BetaFactor member's copies join the group; its condition-based methods raise NotImplementedError.

Parameters:

Name Type Description Default
members collection of node names

The RBD nodes that share the common cause (at least two, distinct). Standard CCF theory is for symmetric groups, so the members must carry identical component models.

required
model (BetaFactor, MGL or AlphaFactor)

The common-cause model coupling the members. An MGL model fixes the group size (m - 1 letters for m members), as an AlphaFactor model does (m alpha factors).

required

Raises:

Type Description
ValueError

If there are fewer than two members, a member is listed twice, model is not a BetaFactor, MGL or AlphaFactor, or an MGL or AlphaFactor model's group size differs from the number of members.

Notes

The RBD validates the groups when it is built. It rejects a member that is not a component node of the RBD (an unknown node, the input or output node, or a repeated node), a node in more than one group, and a group whose members carry different models (compared through their serialised form; members that cannot be serialised are not compared).

Examples:

Two filters in parallel, each failing with probability 0.1, where 10% of their failures come from a shared cause:

>>> from surpyval import FixedEventProbability
>>> from repyability import BetaFactor, CCFGroup, NonRepairableRBD
>>> edges = [("s", "a"), ("s", "b"), ("a", "t"), ("b", "t")]
>>> nodes = {n: FixedEventProbability.from_params(0.1) for n in "ab"}
>>> round(NonRepairableRBD(edges, nodes).sf(), 4)  # independent
0.99
>>> group = CCFGroup(["a", "b"], BetaFactor(0.1))
>>> rbd = NonRepairableRBD(edges, nodes, ccf_groups=[group])
>>> round(rbd.sf(), 4)  # (1 - 0.01) * (1 - 0.09 ** 2)
0.982

BetaFactor

BetaFactor(beta, basis='probability')

Bases: _Model

The beta-factor common-cause model (all-or-nothing).

A fraction beta of each component's failures come from a cause shared across the whole group (which fails every member simultaneously); the rest are independent. It applies to a group of any size. Use it through a CCFGroup; models with equal beta and basis compare equal.

basis says what beta is a fraction of. By default, "probability", of each member's probability of failing Q: the shared cause fails the group with beta * Q, and each member fails on its own with (1 - beta) * Q. That is the PRA basic-event model, for a small Q, as over a mission or a proof-test interval; over a whole life it stops describing a lifetime (each member would only ever fail with 1 - beta * (1 - beta)), and the RBD warns once a member's Q passes 0.1. With "rate", of each member's failure rate: the shared cause is a shock that has not struck by t with probability R(t) ** beta, and each member survives its own causes with R(t) ** (1 - beta), R the members' reliability. Every member keeps its own life distribution, the model holds over the whole life, and the RBD's MTTF and simulations include the group. The two agree to first order in Q.

Parameters:

Name Type Description Default
beta float

The common-cause fraction, in [0, 1]. beta = 0 is ordinary independence; beta = 1 makes the group fail entirely in unison.

required
basis (probability, rate)

What beta is a fraction of, by default "probability".

"probability"

Raises:

Type Description
ValueError

If beta is outside [0, 1], or basis is neither "probability" nor "rate".

Examples:

With a failure probability Q = 0.2 per member and beta = 0.1, each member fails independently with 0.9 * 0.2 and the shared cause fails both with 0.1 * 0.2:

>>> from repyability import BetaFactor
>>> q_independent, shocks = BetaFactor(0.1).decompose(["a", "b"], 0.2)
>>> round(float(q_independent[0]), 4)
0.18
>>> [(sorted(members), round(float(q[0]), 4)) for members, q in shocks]
[(['a', 'b'], 0.02)]

Split by rate, the shock strikes with 1 - 0.8 ** 0.1 and each member fails on its own with 1 - 0.8 ** 0.9: close to the above at this Q, and a lifetime model at any:

>>> rate = BetaFactor(0.1, basis="rate")
>>> q_independent, shocks = rate.decompose(["a", "b"], 0.2)
>>> round(float(q_independent[0]), 4)
0.1819
>>> [(sorted(members), round(float(q[0]), 4)) for members, q in shocks]
[(['a', 'b'], 0.0221)]

shocks property

shocks

How the shared causes combine: "independent" events (by rate, always), or "exclusive" ones (the probability split's default; see MGL).

required_group_size

required_group_size()

The group size this model requires: None, meaning any size.

CCFGroup checks a model's required size against its members; a beta-factor model fits a group of any size (two or more members).

Returns:

Type Description
None

Always None.

decompose

decompose(members, Q)

Split the failure probability into independent and shared parts.

Each member's failure probability Q splits into an independent part and one whole-group shock. The RBD calls this at each evaluation time with Q, the probability that the group's first member has failed by then.

Parameters:

Name Type Description Default
members collection of hashable

The group's member node names.

required
Q float or array_like

Each member's total failure probability (the same for every member of a symmetric group), at one or more times.

required

Returns:

Name Type Description
q_independent ndarray

Each member's independent failure probability, at least 1-d: (1 - beta) * Q, or by rate 1 - (1 - Q) ** (1 - beta).

shocks list of (frozenset, numpy.ndarray)

One shock, frozenset(members) with the probability that the shared cause fails every member: beta * Q, or by rate 1 - (1 - Q) ** beta.

MGL

MGL(*letters, basis='probability', shocks=None)

Bases: _SpecificSets

The Multiple Greek Letter common-cause model.

The parameters are the conditional probabilities of a common-cause failure escalating to the next level: beta = P(shared by >= 2 | failed), gamma = P(>= 3 | >= 2), delta = P(>= 4 | >= 3), and so on. The number of parameters fixes the group size: n letters describe a group of n + 1 members. MGL(beta) is exactly BetaFactor on a two-member group.

The probability that a common cause fails a specific set of k of the m members is the standard MGL basic-event probability

Q_k = [1 / C(m-1, k-1)] * (rho_1 * ... * rho_k) * (1 - rho_{k+1}) * Q

with rho_1 = 1, rho_2 = beta, rho_3 = gamma, ..., and rho_{m+1} = 0. Those of the sets holding a component sum to its total failure probability Q; its own failure and the shared causes being separate events, it fails with probability Q to first order (Q - beta (1 - beta) Q**2 for one shared cause), as in PRA's basic events. The AlphaFactor model gives the same Q_k from the parameters event counts estimate.

That is the default basis, "probability": the PRA basic-event model, for a small Q (see BetaFactor). With basis="rate" the same fractions split each member's failure rate instead: each specific set of k members has a cause of its own, a shock that has not struck by t with probability R(t) ** (Q_k / Q), independently of the others, and the members it strikes fail together. Every member keeps its own life distribution, and the model holds over the whole life.

How the shocks combine, splitting the probability. By default (shocks="exclusive") the outcomes are mutually exclusive: the group is struck by one shared cause at most, set S with probability Q_k, so each member fails with probability Q to first order. PRA codes (SAPHIRE, CAFTA, RiskSpectrum) instead take each Q_k as a basic event of its own, independent of the others, so that several may strike: shocks="independent". The two differ at second order in Q (a 2-out-of-3 group of MGL(0.2, 0.3) at Q = 0.0311 fails with probability 0.010219 one way and 0.010192 the other), so match the tool you check against. A BetaFactor, or an MGL model with one shared cause, has one shock, and both agree. By rate, the causes strike independently.

Use it through a CCFGroup; models with equal letters, basis and shocks compare equal.

Parameters:

Name Type Description Default
*letters float

beta, gamma, delta, ..., each in [0, 1]; at least one. A group of m members needs m - 1 letters.

()
basis (probability, rate)

What the letters split, by default "probability" (keyword only).

"probability"
shocks (exclusive, independent)

How the shared causes combine when the letters split the probability (keyword only): by default "exclusive", one at most; "independent", as independent basic events. By rate they are always independent.

"exclusive"

Raises:

Type Description
ValueError

If no letters are given, any is outside [0, 1], basis is neither "probability" nor "rate", or shocks is neither "exclusive" nor "independent" ("exclusive" by rate).

Examples:

A three-member group with beta = 0.1 and gamma = 0.3, each member failing with probability Q = 0.1:

>>> from repyability import MGL
>>> mgl = MGL(0.1, 0.3)
>>> mgl.group_size
3
>>> q_independent, shocks = mgl.decompose(["a", "b", "c"], 0.1)
>>> round(float(q_independent[0]), 4)  # (1 - beta) * Q
0.09
>>> for members, q in shocks:
...     print(sorted(members), round(float(q[0]), 4))
['a', 'b'] 0.0035
['a', 'c'] 0.0035
['b', 'c'] 0.0035
['a', 'b', 'c'] 0.003

shocks property

shocks

How the shared causes combine: "exclusive" (one at most, the probability split's default) or "independent" (as independent basic events; by rate, always).

group_size property

group_size

The number of members this model describes, len(letters) + 1.

required_group_size

required_group_size()

The group size this model requires, group_size.

CCFGroup rejects a group whose number of members differs.

Returns:

Type Description
int

group_size: an MGL model's len(letters) + 1, an AlphaFactor model's len(alphas).

decompose

decompose(members, Q)

Split the failure probability into independent and shared parts.

With m = len(members), each member fails alone with Q_1 and each specific set of k >= 2 members fails together with the basic-event probability Q_k given in the class description. The RBD calls this at each evaluation time with Q, the probability that the group's first member has failed by then.

By rate, or with shocks="independent", the shocks are the sets of members the causes that have struck fail between them: several causes may strike, and a member fails if any of its causes has, so these are the distinct unions, mutually exclusive as the RBD needs, each with its probability.

Parameters:

Name Type Description Default
members collection of hashable

The group's member node names: group_size of them, as CCFGroup ensures.

required
Q float or array_like

Each member's total failure probability (the same for every member of a symmetric group), at one or more times.

required

Returns:

Name Type Description
q_independent ndarray

Q_1, each member's independent failure probability, at least 1-d (by rate, 1 - (1 - Q) ** (Q_1 / Q)).

shocks list of (frozenset, numpy.ndarray)

(subset, Q_k) for every subset of k = 2, ..., m members, smaller subsets first; by rate, (subset, probability) for every set the struck causes can fail together.

AlphaFactor

AlphaFactor(alphas, *, testing='staggered', basis='probability', shocks=None)

Bases: _SpecificSets

The alpha-factor common-cause model.

The parameters are the shares of a group's failure events by how many members each failed: alpha_k is the fraction of the events that failed exactly k of the m members, so alpha_1, ..., alpha_m sum to 1. Each is estimated directly from event counts, which is why common-cause data sources (NUREG/CR-5485, the NRC's CCF parameter estimates) publish this model's parameters. The multiplicities are the MGL model's: the probability that a common cause fails a specific set of k of the m members is

Q_k = [1 / C(m-1, k-1)] * alpha_k * Q

for a group whose members are tested one after another, in a staggered scheme (testing="staggered"), and

Q_k = [k / C(m-1, k-1)] * (alpha_k / alpha_t) * Q

with alpha_t = sum of k * alpha_k, for one tested all at once, in a non-staggered scheme (testing="non-staggered"): the schemes count a multiple failure differently (NUREG/CR-5485), so use the one the estimates were published for. Either way the Q_k of the sets holding a component sum to its total failure probability Q, and everything else is as for MGL (the basis, the shocks, and what the diagrams do with the group): each AlphaFactor model is an MGL model with the letters rho_k = sum over j >= k of C(m-1, j-1) Q_j / sum over j >= k-1.

The levers of the model (its parameters, for levers(), parameter_sensitivity and the parameter uncertainty) are alpha_2, ..., alpha_m, named ccf_alpha_2, ...; alpha_1 is the remainder, so a change that would take their sum past 1 is refused.

Use it through a CCFGroup; models with equal alpha factors, testing, basis and shocks compare equal.

Parameters:

Name Type Description Default
alphas sequence of float

alpha_1, ..., alpha_m, each in [0, 1], summing to 1 (a rounded table's, summing to within 1% of 1, are scaled to 1); at least two. A group of m members needs m alpha factors.

required
testing (staggered, non - staggered)

The testing scheme the alpha factors were estimated under, by default "staggered" (keyword only).

"staggered"
basis (probability, rate)

What the alpha factors split, by default "probability" (keyword only; see MGL).

"probability"
shocks (exclusive, independent)

How the shared causes combine when the alpha factors split the probability (keyword only): by default "exclusive", one at most; "independent", as independent basic events. By rate they are always independent.

"exclusive"

Raises:

Type Description
ValueError

If fewer than two alpha factors are given, any is not a number in [0, 1], they do not sum to 1, testing is neither "staggered" nor "non-staggered", basis is neither "probability" nor "rate", or shocks is neither "exclusive" nor "independent" ("exclusive" by rate).

Examples:

A three-member group in which 95% of the failure events failed one member, 4% two and 1% all three, each member failing with probability Q = 0.1, its tests staggered:

>>> from repyability import AlphaFactor
>>> model = AlphaFactor([0.95, 0.04, 0.01])
>>> model.group_size
3
>>> q_independent, shocks = model.decompose(["a", "b", "c"], 0.1)
>>> round(float(q_independent[0]), 4)  # alpha_1 * Q
0.095
>>> for members, q in shocks:
...     print(sorted(members), round(float(q[0]), 4))
['a', 'b'] 0.002
['a', 'c'] 0.002
['b', 'c'] 0.002
['a', 'b', 'c'] 0.001

The same alpha factors estimated under non-staggered testing give more weight to the multiple failures (alpha_t = 1.06):

>>> model = AlphaFactor([0.95, 0.04, 0.01], testing="non-staggered")
>>> q_independent, shocks = model.decompose(["a", "b", "c"], 0.1)
>>> round(float(q_independent[0]), 4)  # 0.95 / 1.06 * Q
0.0896
>>> round(float(shocks[0][1][0]), 4)  # 2 / 2 * 0.04 / 1.06 * Q
0.0038
>>> round(float(shocks[-1][1][0]), 4)  # 3 * 0.01 / 1.06 * Q
0.0028

shocks property

shocks

How the shared causes combine: "exclusive" (one at most, the probability split's default) or "independent" (as independent basic events; by rate, always).

group_size property

group_size

The number of members this model describes, len(alphas).

required_group_size

required_group_size()

The group size this model requires, group_size.

CCFGroup rejects a group whose number of members differs.

Returns:

Type Description
int

group_size: an MGL model's len(letters) + 1, an AlphaFactor model's len(alphas).

decompose

decompose(members, Q)

Split the failure probability into independent and shared parts.

With m = len(members), each member fails alone with Q_1 and each specific set of k >= 2 members fails together with the basic-event probability Q_k given in the class description. The RBD calls this at each evaluation time with Q, the probability that the group's first member has failed by then.

By rate, or with shocks="independent", the shocks are the sets of members the causes that have struck fail between them: several causes may strike, and a member fails if any of its causes has, so these are the distinct unions, mutually exclusive as the RBD needs, each with its probability.

Parameters:

Name Type Description Default
members collection of hashable

The group's member node names: group_size of them, as CCFGroup ensures.

required
Q float or array_like

Each member's total failure probability (the same for every member of a symmetric group), at one or more times.

required

Returns:

Name Type Description
q_independent ndarray

Q_1, each member's independent failure probability, at least 1-d (by rate, 1 - (1 - Q) ** (Q_1 / Q)).

shocks list of (frozenset, numpy.ndarray)

(subset, Q_k) for every subset of k = 2, ..., m members, smaller subsets first; by rate, (subset, probability) for every set the struck causes can fail together.

Condition-based evaluation

NodeState dataclass

NodeState(age=0.0, alive=True, down_for=0.0, maintenance=False, since_scheduled=None, stationary=False, virtual_age=None)

The current condition of a single RBD node.

Passed, as a dict {node: NodeState}, to the condition-based methods of NonRepairableRBD (sf_given_state, remaining_life and importances_given_state). A working component's forward reliability is conditioned on the life it has already survived: R_i(x | age) = R_i(age + x) / R_i(age). A failed component (alive=False) contributes zero reliability regardless of its age. A node left out of the dict is treated as new and unconditioned. The state is immutable (a frozen dataclass).

A RepairableRBD takes the same states, with more fields, to start its analyses from now (state= of point_availability, availability and the others): a component up is age since it was last put into service as new; one down has been down for down_for, in a repair, or (maintenance=True) in its preventive maintenance; one on a calendar (block replacement, or tests of hidden failures) is since_scheduled past its last scheduled replacement or test; one repaired imperfectly has the virtual_age it had at its last repair; and stationary=True puts it in its long-run state instead, for a component long in service whose state is not known.

Parameters:

Name Type Description Default
age float

The component's current age / accumulated operating time (>= 0), by default 0.0. Age 0 is the same as no conditioning whenever the component's reliability at time 0 is 1, as for ordinary lifetime distributions. In a RepairableRBD it is on the diagram's clock: for a component that operates part of the time (a spec's "duty"), the calendar time since it was put into service, its operating age being duty * age.

0.0
alive bool

Whether the component is currently working, by default True.

True
down_for float

For a repairable component that is down: how long it has been down so far, by default 0.0 (it has just gone down). What is left of its repair (or maintenance) is conditioned on it.

0.0
maintenance bool

For a repairable component that is down: whether it is down for its preventive maintenance rather than for a repair, by default False.

False
since_scheduled float

For a repairable component on a calendar: the time since its last block replacement, or its last test (of hidden failures, at which it was found working; with an age below it, it was put into service since, and has not been tested), by default None: 0, as from new.

None
stationary bool

For a repairable component: start it in its long-run state, by default False. Its since_scheduled still places it on its calendar; the other fields must be left at their defaults.

False
virtual_age float

For a repairable component repaired imperfectly (a spec's "repair", or a fitted GeneralizedRenewal or Poisson process): its virtual age at its last repair, by default None (as new); age is then its operating time since. One down is at this virtual age once its repair is over. For a unit of a fitted surpyval renewal model, unit_states() gives its virtual_age now and since_failure: give age= since_failure and virtual_age=virtual_age - since_failure. The simulations take it.

None

Raises:

Type Description
ValueError

If age, down_for, since_scheduled or virtual_age is negative or not finite, down_for or maintenance is given for a component that is alive, or another field is given with stationary.

Notes

Only lifetime (time-varying) distributions age. A fixed-probability component given a state with alive=True contributes reliability 1 whatever its age: observing it working resolves its per-demand uncertainty. Leave it out of the dict to use its unconditioned probability instead. Covariate/load dependence is a property of the component, not its transient state: a RegressionNode stores the covariates, so age keeps a single meaning (operating time) for every node type.

Examples:

>>> from repyability import NodeState
>>> NodeState(age=1200)
NodeState(age=1200, alive=True)
>>> NodeState(alive=False)
NodeState(age=0.0, alive=False)

Two Weibull components in parallel: one 50 hours old and one already failed, so the system's reliability over the next 20 hours is that of the survivor, R(70) / R(50):

>>> import surpyval as surv
>>> from repyability import NonRepairableRBD
>>> rbd = NonRepairableRBD(
...     [("s", "a"), ("s", "b"), ("a", "t"), ("b", "t")],
...     {
...         "a": surv.Weibull.from_params([100, 2]),
...         "b": surv.Weibull.from_params([100, 2]),
...     },
... )
>>> state = {"a": NodeState(age=50), "b": NodeState(alive=False)}
>>> round(rbd.sf_given_state(20, state), 4)
0.7866

A repairable pump down for 2 hours of a repair, one 300 hours old, and one tested weekly, last tested 100 hours ago:

>>> NodeState(alive=False, down_for=2.0)
NodeState(age=0.0, alive=False, down_for=2.0)
>>> NodeState(age=300.0)
NodeState(age=300.0, alive=True)
>>> NodeState(age=300.0, since_scheduled=100.0)
NodeState(age=300.0, alive=True, since_scheduled=100.0)

new property

new

Whether the state is that of a component new now: up, at age 0, at the start of its calendar.