Skip to content

Components and maintenance

NonRepairable

NonRepairable(reliability, time_to_replace=None, *, planned_cost=None, unplanned_cost=None)

A component renewed by replacement ("as good as new").

Pairs a lifetime (reliability) model with a time-to-replace model for a unit that cannot be repaired in place: every failure or planned replacement fits a new unit, so each cycle is a statistical renewal.

It plays two roles:

  • Standalone, it prices the classic age-replacement policy: replace preventively at age t (planned, cost cp) or on failure (unplanned, cost cu > cp), whichever comes first, via cost_rate, find_optimal_replacement() and optimal_replacement_policy(). The cost calculations treat replacement as instantaneous (they do not use time_to_replace).
  • Inside a RepairableRBD, it is the per-component model (the RBD builds one from each {"reliability": ..., "repairability": ...} component spec): the unit alternates between a time to failure drawn from reliability and a replacement time drawn from time_to_replace (see next_event()), and mean_availability() and failure_frequency() give its exact long-run behaviour. The same object can be given for several nodes: each node gets its own copy.

Contrast with Repairable, which models minimal ("as bad as old") or imperfect repair and the overhaul policy.

Parameters:

Name Type Description Default
reliability surpyval model or StandbyModel

The unit's lifetime distribution, one of:

  • a surpyval parametric distribution, fitted or built with from_params;
  • a surpyval MixtureModel (a population of several modes, such as infant mortality and wear-out), which has no quantile function: the simulations invert its distribution function;
  • a surpyval non-parametric estimate (e.g. a KaplanMeier fit). For the cost calculations its survival function is taken as linear between its time points, starting from 1 at age 0, and held at its last value beyond the last one; the availability methods reject it, and its draws in next_event() cannot be seeded;
  • a StandbyModel, in any of its forms (a closed form, a convolution or a simulation), through its survival function sf.
required
time_to_replace surpyval model or "instant"

The distribution of the time taken to replace the unit after a failure, used by the availability and event methods. By default None, or "instant" as a RepairableRBD spec takes it: instantaneous replacement (ExactEventTime.from_params(0)).

None
planned_cost float

What a planned (preventive) replacement costs, at least 0, by name; as set_costs_planned_and_unplanned sets it (cp). By default None: not given.

None
unplanned_cost float

What a replacement at a failure costs, at least 0, by name (cu): with planned_cost, more than it. Either may be given alone: failures needs only this one. By default None: not given.

None

Attributes:

Name Type Description
reliability object

The lifetime model given.

time_to_replace object

The time-to-replace model given.

cost_rate callable

cost_rate(t), the long-run expected cost per unit time of replacing at age t or on failure: (cp * R(t) + cu * (1 - R(t))) / avg_replacement_time(t), with R the survival function of reliability. Vectorised over t; returns a numpy array (0-d for a scalar t). Needs the costs set by set_costs_planned_and_unplanned() (without them it raises ValueError).

Raises:

Type Description
ValueError

If reliability is not a surpyval parametric, mixture or non-parametric model or a StandbyModel, or time_to_replace is text other than "instant".

TypeError

If either is a surpyval distribution itself (surv.Weibull) rather than a model of it, or time_to_replace is not a model.

Examples:

Age replacement of a wearing-out unit:

>>> import surpyval as surv
>>> from repyability import NonRepairable
>>> unit = NonRepairable(surv.Weibull.from_params([1000, 2.5]))
>>> unit.set_costs_planned_and_unplanned(cp=1, cu=5)
>>> round(float(unit.find_optimal_replacement()), 1)
493.0
>>> [round(float(g), 5) for g in unit.cost_rate([250, 493, 1000])]
[0.00453, 0.00346, 0.00452]

As an RBD component with a mean time to failure of 100 and a mean time to replace of 2:

>>> from repyability import RepairableRBD
>>> pump = NonRepairable(
...     surv.Exponential.from_params([0.01]),
...     surv.Exponential.from_params([0.5]),
... )
>>> round(pump.mean_availability(), 4)
0.9804
>>> rbd = RepairableRBD([("s", "pump"), ("pump", "t")], {"pump": pump})
>>> round(rbd.mean_availability(), 4)
0.9804

failures

failures(t, age=0.0)

The failures over a window t from now, with the current unit age old and working, each failure replaced by a new unit.

The chance the current unit fails in the window is its life conditioned on the age reached, 1 - R(age + t) / R(age). The expected number of failures counts its replacements' too: the renewal count of the part as a RepairableRBD's component started from that age (its expected_events, exact, with no simulation), each replacement taking its time_to_replace.

Parameters:

Name Type Description Default
t float or array_like

The window's length (or lengths), at least 0, in the models' time unit.

required
age float

The current unit's age, at least 0, by default 0 (new).

0.0

Returns:

Type Description
FailureForecast

The chance the current unit fails, the expected failures, and their expected cost (with an unplanned_cost).

Examples:

A bearing with a Weibull life (scale 365 days, shape 2), 224 days old, over the next 82 days, at 400 a failure:

>>> import surpyval as surv
>>> from repyability import NonRepairable
>>> bearing = NonRepairable(
...     surv.Weibull.from_params([365.0, 2.0]), unplanned_cost=400.0
... )
>>> forecast = bearing.failures(82.0, age=224.0)
>>> round(forecast.probability, 4)
0.2784
>>> round(forecast.expected_failures, 4)
0.283
>>> round(forecast.expected_cost, 2)
113.2

set_costs_planned_and_unplanned

set_costs_planned_and_unplanned(cp, cu)

Set the planned and unplanned replacement costs.

Required before any cost calculation (cost_rate, find_optimal_replacement(), optimal_replacement_policy()). The costs are stored as the cp and cu attributes.

Parameters:

Name Type Description Default
cp float

Cost of a planned (preventive) replacement, made when the unit reaches the replacement age without failing.

required
cu float

Cost of an unplanned (corrective) replacement, made on failure. Must exceed cp, otherwise preventive replacement never pays.

required

Raises:

Type Description
ValueError

If a cost is negative or not finite, or if cp >= cu.

avg_replacement_time

avg_replacement_time(t)

Expected time between replacements when replacing at age t.

The expected cycle length of the age-replacement policy, E[min(T, t)] = integral_0^t R(u) du: a cycle ends at failure or at age t, whichever comes first. It is the denominator of cost_rate.

For a parametric model the integral is computed by quadrature, and for a StandbyModel by the trapezoidal rule on 4,001 ages (its survival function may be a step function). For a non-parametric model it is exact for the survival function described in the class docstring (linear between the estimate's time points, from 1 at age 0, and held beyond the last one).

Parameters:

Name Type Description Default
t float

The replacement age (a scalar).

required

Returns:

Type Description
float

The expected cycle length, in the lifetime model's time units.

Examples:

For an exponential lifetime with rate 0.01 the integral is (1 - exp(-0.01 * t)) / 0.01:

>>> import surpyval as surv
>>> from repyability import NonRepairable
>>> unit = NonRepairable(surv.Exponential.from_params([0.01]))
>>> round(unit.avg_replacement_time(100), 2)
63.21

mean_unavailability

mean_unavailability()

Long-run unavailability, 1 - mean_availability().

MTTR / (MTTF + MTTR), worked out in its own right (not as one less the availability), so that a small one keeps its precision; with units that never fail or replacements that never finish, the probability that the unit ends down for good (see mean_availability).

Returns:

Type Description
float

The long-run fraction of time the unit is down (being replaced).

Raises:

Type Description
ValueError

If reliability is a non-parametric model.

Examples:

>>> import surpyval as surv
>>> from repyability import NonRepairable
>>> unit = NonRepairable(
...     surv.Exponential.from_params([0.01]),
...     surv.Exponential.from_params([0.5]),
... )
>>> round(unit.mean_unavailability(), 4)
0.0196

mean_availability

mean_availability()

Long-run availability, MTTF / (MTTF + MTTR).

The steady-state fraction of time the unit is up when it is run to failure and then replaced, so up and down times alternate (an alternating renewal process). MTTF is the mean of reliability and MTTR the mean of time_to_replace. For a simulated StandbyModel lifetime the MTTF is the mean of the lifetimes simulated when it was built (see its mean), so it is the same on every call.

If some units never fail (a limited-failure-population model, p < 1), sooner or later a replacement is one of them, and the unit is then up for good: the long-run availability is 1. Likewise, if some replacements never finish, the unit ends down for good. With both, it ends up for good with probability u / (u + (1 - u) * d), for u the fraction of units that never fail and d the fraction of replacements that never finish, and that is its long-run availability.

Returns:

Type Description
float

The long-run availability, between 0 and 1.

Raises:

Type Description
ValueError

If reliability is a non-parametric model.

Examples:

A unit with MTTF 100 and a mean time to replace of 2:

>>> import surpyval as surv
>>> from repyability import NonRepairable
>>> unit = NonRepairable(
...     surv.Exponential.from_params([0.01]),
...     surv.Exponential.from_params([0.5]),
... )
>>> round(unit.mean_availability(), 4)
0.9804

One in ten units never fails (lfp_p, the share that ever fails, is 0.9), so in the long run the unit is up.

>>> cured = NonRepairable(
...     surv.Exponential.from_params([0.01], lfp_p=0.9),
...     surv.Exponential.from_params([0.5]),
... )
>>> cured.mean_availability()
1.0
>>> cured.failure_frequency()
0.0

failure_frequency

failure_frequency()

Long-run failure frequency (failures per unit time).

For an alternating renewal process (fail, replace, fail, ...) this is 1 / (MTTF + MTTR): one failure per mean up-down cycle, with MTTF the mean of reliability and MTTR the mean of time_to_replace. It is 0 if some units never fail or some replacements never finish: the unit then fails only finitely often (see mean_availability).

Returns:

Type Description
float

The expected number of failures per unit time in the long run.

Raises:

Type Description
ValueError

If reliability is a non-parametric model.

Examples:

One failure per 100 + 2 time units:

>>> import surpyval as surv
>>> from repyability import NonRepairable
>>> unit = NonRepairable(
...     surv.Exponential.from_params([0.01]),
...     surv.Exponential.from_params([0.5]),
... )
>>> round(unit.failure_frequency(), 5)
0.0098

find_optimal_replacement

find_optimal_replacement()

The replacement age that minimises the long-run cost rate.

Minimises the age-replacement cost rate (see cost_rate) (cp * R(t) + cu * (1 - R(t))) / integral_0^t R(u) du over the replacement age t:

  • Parametric model: an exponential lifetime, or a Weibull with shape beta <= 1, does not wear out, so preventive replacement never pays and inf is returned at once (unless the model has an offset gamma or zero-inflation f0). So does a limited failure population (p < 1), whatever its shape: running to failure then costs nothing in the long run (see below). Otherwise scipy.optimize.minimize (BFGS) searches over log(t), starting from the mean life, and its result is kept in the optimisation_results attribute. With an offset gamma no unit can fail before that age, so the cost rate falls as cp / t up to it and replacing at gamma itself is also considered. If the best age found is no cheaper than running to failure (see below), inf is returned.
  • Non-parametric model: the cost rate is evaluated exactly at 10,001 evenly spaced ages from 0 to the estimate's last time point, and at its time points, and the cheapest age is returned (the estimate says nothing about later ages).
  • StandbyModel: its survival function may be a step function (a simulated arrangement), so the cost rate is evaluated on a grid of 2,001 ages up to where survival falls to 1e-6, and the cheapest grid age is refined with a bounded search between its neighbours. If survival never falls that far, inf is returned.

Preventive replacement must beat running to failure, whose long-run cost rate is cu / MTTF -- or 0 when some units never fail (a limited failure population): the units that never fail are then kept. Without wear-out (e.g. a Weibull or Gamma with shape below 1, with or without an offset, zero-inflation or a limited failure population) the cost rate only falls towards that rate as the age grows, so inf is returned rather than wherever the search stopped on the flattening curve. A replacement age must save more than one part in a million to count.

Returns:

Type Description
float

The optimal replacement age, in the lifetime model's time units, or inf if preventive replacement never pays.

Raises:

Type Description
ValueError

If the costs have not been set with set_costs_planned_and_unplanned() (the parametric inf cases above return without them).

Examples:

>>> import surpyval as surv
>>> from repyability import NonRepairable
>>> unit = NonRepairable(surv.Weibull.from_params([1000, 2.5]))
>>> unit.set_costs_planned_and_unplanned(cp=1, cu=5)
>>> round(float(unit.find_optimal_replacement()), 1)
493.0

A memoryless (exponential) unit is never worth replacing early:

>>> unit = NonRepairable(surv.Exponential.from_params([0.001]))
>>> unit.set_costs_planned_and_unplanned(cp=1, cu=5)
>>> unit.find_optimal_replacement()
inf

optimal_replacement_policy

optimal_replacement_policy()

The optimal age-replacement policy as a typed result.

Finds the replacement age with find_optimal_replacement() (see there for the search and its limits) and evaluates cost_rate at it. When preventive replacement never pays (no wear-out, e.g. an exponential lifetime or a Weibull with shape <= 1) the interval is inf and the cost rate is the run-to-failure rate cu / MTTF, or 0 when some units never fail.

Returns:

Type Description
MaintenancePolicy

interval is the cost-optimal replacement age and cost_rate the long-run cost per unit time under it.

Raises:

Type Description
ValueError

If the costs have not been set (see set_costs_planned_and_unplanned()).

Examples:

>>> import surpyval as surv
>>> from repyability import NonRepairable
>>> unit = NonRepairable(surv.Weibull.from_params([1000, 2.5]))
>>> unit.set_costs_planned_and_unplanned(cp=1, cu=5)
>>> policy = unit.optimal_replacement_policy()
>>> round(policy.interval, 1), round(policy.cost_rate, 6)
(493.0, 0.003462)

Run to failure: cu / MTTF = 5 / 100.

>>> unit = NonRepairable(surv.Exponential.from_params([0.01]))
>>> unit.set_costs_planned_and_unplanned(cp=1, cu=5)
>>> policy = unit.optimal_replacement_policy()
>>> policy.interval, round(policy.cost_rate, 4)
(inf, 0.05)

optimal_next_replacement

optimal_next_replacement(age, *, now_cost=None, scheduled_outages=(), policy=None)

When to replace the part in service next: now, at one of the scheduled outages, at a planned replacement between them, or at its failure.

optimal_replacement_policy is the rule for a part in general; this is the next replacement of the one in service, from its age and the outages scheduled (this package's use case UC-12). Each choice's cost is its expected cost until the part is replaced (at the failure price if it fails first), less the time the part serves until then, valued at the baseline: the long-run cost a unit of time of how the position is run, policy.cost_rate. By default that is the age replacement's (optimal_replacement_policy().cost_rate); where cheap shuts keep coming, give the calendar's policy (optimal_calendar_policy), whose rate is lower, so a part is kept longer. The cheapest choice is the best. With no outages and the default baseline, replacing now is the best from the optimal replacement age.

A planned replacement is one later than now, at its best time. Where no later time beats replacing at once (a part past its optimal age), replacing at once is replacing now, which costs now_cost: the planned choice is then replacing now (time 0, at now_cost), now is True exactly when the best choice is at time 0, and replace_from_age follows. A planned replacement with a lead time is a scheduled outage at that time.

Parameters:

Name Type Description Default
age float

The part's age now, at least 0, in the life's time unit.

required
now_cost float

What replacing it now costs, by default the planned cost.

None
scheduled_outages list of (float, float)

The scheduled outages (shuts, turnarounds) as (time from now, price) pairs: when each is, in the life's time unit, more than 0, and what replacing the part at it costs, at least 0.

()
policy CalendarPolicy or MaintenancePolicy

The policy whose cost_rate is the baseline, by default optimal_replacement_policy().

None

Returns:

Type Description
NextReplacement

Every choice with its cost and break-even age (the youngest at which replacing now costs no more), the best, and the age from which replacing now is the best.

Raises:

Type Description
ValueError

If the costs have not been set, or an argument is out of its range.

TypeError

If policy is not a policy (its cost rate alone is not one).

NotImplementedError

For a life with no survival function in closed form (a non-parametric estimate or a StandbyModel).

Examples:

A bearing with a Weibull life (scale 365 days, shape 2), 200 days old, $150 for a planned replacement, $400 at a failure, and outages scheduled in 150 and 330 days, at which replacing it costs $100:

>>> import surpyval as surv
>>> from repyability import NonRepairable
>>> bearing = NonRepairable(
...     surv.Weibull.from_params([365.0, 2.0]),
...     planned_cost=150.0,
...     unplanned_cost=400.0,
... )
>>> found = bearing.optimal_next_replacement(
...     200.0, scheduled_outages=[(150.0, 100.0), (330.0, 100.0)]
... )
>>> found.now, found.best.kind, found.best.time
(False, 'outage', 150.0)
>>> round(found.replace_from_age, 1)
297.6

With shuts every 180 days at $100 kept coming, the calendar's policy values a day of service at 0.990 rather than 1.117, and replacing now beats the outage in 150 days from a younger age:

>>> from repyability import Calendar, Opportunity
>>> shuts = bearing.optimal_calendar_policy(
...     Calendar([Opportunity("shut", 100.0, period=180.0)])
... )
>>> found = bearing.optimal_next_replacement(
...     200.0,
...     scheduled_outages=[(150.0, 100.0), (330.0, 100.0)],
...     policy=shuts,
... )
>>> outage = found.choices[1]
>>> round(found.cost_rate, 3), round(outage.break_even_age, 1)
(0.99, 256.1)

skip_next_replacement

skip_next_replacement(every, *, resets, time_to_next=None, discount_rate=0.0)

Whether to skip the next planned replacement, right after a failure (a new unit just in): the present-value cost of doing it against that of skipping it, and the NPV of skipping.

The part is replaced when it fails (at its unplanned_cost) and on a plan (at its planned_cost), in no time either way (this package's use case UC-6). The plan either resets at each replacement, planned or not (age replacement: the next planned replacement is every after the failure), or keeps to fixed times (block replacement: the next is time_to_next after the failure, and the following one every later). Both costs are exact, with no simulation; what each covers is in SkipValue. optimal_next_replacement is the undiscounted, long-run question of a part in service with no plan in force.

Parameters:

Name Type Description Default
every float

The plan's interval, more than 0, in the life's time unit.

required
resets bool

Whether the plan resets at each replacement: True, the next planned replacement is every after the last replacement, planned or not; False, planned replacements fall on fixed times, every apart, whatever happens.

required
time_to_next float

For a plan that does not reset: the time from the failure to the next planned replacement, more than 0 and at most every. A plan that resets has its next planned replacement every after the failure, and takes none.

None
discount_rate float

The continuous discount rate per unit time of the life, by default 0 (undiscounted): for an annual rate of 8% with a life in days, math.log(1.08) / 365.25.

0.0

Returns:

Type Description
SkipValue

The present-value cost of doing the replacement (replace) and of skipping it (skip), their difference (npv: positive, skipping pays), the best of the two, and what they were worked out for. On a plan that resets each is the whole future's, endless (inf) with no discounting, where only npv is finite. A discount rate that discounts the costs away (a rate a year given with a life in days) is warned of.

Raises:

Type Description
ValueError

If the costs have not been set, or an argument is out of its range, or time_to_next is given with a plan that resets or missing from one that does not.

NotImplementedError

For a life with no survival function in closed form (a non-parametric estimate or a StandbyModel).

Examples:

A bearing with a Weibull life (scale 365 days, shape 2), replaced every 180 days for $150, and at a failure for $400; right after a failure, at 8% a year, skipping the next planned replacement saves:

>>> import math
>>> import surpyval as surv
>>> from repyability import NonRepairable
>>> bearing = NonRepairable(
...     surv.Weibull.from_params([365.0, 2.0]),
...     planned_cost=150.0,
...     unplanned_cost=400.0,
... )
>>> a_day = math.log(1.08) / 365.25
>>> value = bearing.skip_next_replacement(
...     180.0, resets=True, discount_rate=a_day
... )
>>> round(value.npv, 2), value.best
(26.56, 'skip')

On fixed times, it depends on how soon the next planned replacement is:

>>> value = bearing.skip_next_replacement(
...     180.0, resets=False, time_to_next=30.0, discount_rate=a_day
... )
>>> round(value.npv, 2)
123.08

optimal_calendar_policy

optimal_calendar_policy(calendar, *, max_age=None, method='auto', tail='repeat', mc_samples=64, seed=None)

The long-run optimal replacement policy over a calendar of opportunities: shuts at their own prices, a planned replacement on any day at a dearer one (or none), and a failure at the dearest.

Age replacement (optimal_replacement_policy) has one planned price at every time, so the state is the age alone. Here the price depends on the time, so the best policy is a threshold age for each time, a*(t): replace if the part is at least that old. It minimises the long-run average cost a unit of time (opportunity-based age replacement, Dekker et al.). The methods (this package's use case UC-13):

  • "dp": dynamic programming on a grid, its step sized from the life (about the mean life over 1,000) and put on the opportunities' own times. Over a periodic calendar whose cycle has at most 20,000 steps, by relative value iteration: the cost rate exact for the grid, which is within about the square of the step of deciding at any moment. Over a longer one, by a long finite horizon, the cost rate differenced.
  • "integration": between opportunities in continuous time, on quadrature nodes; only with no replacement on any day.
  • "sampling": for a cycle too long to sweep (co-prime periods), the cost rate is the mean over mc_samples sampled times of the cycle of a window's differenced rate, with its standard error (cost_rate_se); each window is an exact solve that may take a second or more, so mc_samples sets the cost. The rates by kind (replacement_rates, cost_rates) and the schedule's chances are then estimates from a new unit run in, with no standard error, and move with the seed; the threshold at a time is a solve over a window from it (deterministic).
  • "auto": "dp", or "sampling" for a periodic calendar whose cycle is too long to sweep, with a warning that says so.

The grid's step is not an argument: it is a numerical setting, sized from the models (never in absolute time), not a choice of when decisions are made; a calendar says that, by its opportunities. It is a whole fraction of the periods, so a periodic calendar keeps its period on the grid whatever its offsets; an offset or date off the grid (a few hours on a period of days, as dates turned into model time give) is laid at the grid time nearest it, with a warning.

A calendar with given dates (a plan) is extended past its last date by tail: "repeat" repeats the plan (from 0 to its last date and its mean gap), "anytime" leaves only replacement on any day. Both are solved, and the policy says the first time they differ (determined_until); its cost rate is the repeated plan's.

Parameters:

Name Type Description Default
calendar Calendar

The opportunities and prices; its "part" prices are this part's planned and unplanned costs.

required
max_age float

The oldest age on the grid, by default the age a unit outlives with a chance of 1e-9.

None
method str

"auto" (the default), "dp", "integration" or "sampling".

'auto'
tail str

How a plan of given dates goes on after its last date: "repeat" (the default) or "anytime".

'repeat'
mc_samples int

The times sampled by "sampling", by default 64; the other methods take none.

64
seed int

Seeds the sampled times; the other methods draw nothing.

None

Returns:

Type Description
CalendarPolicy

The long-run cost rate and what it is made of by kind, each shut's threshold and chance of a replacement (schedule()), a unit's next replacement (next_replacement()), how it was worked out, and the life it was worked out for.

Raises:

Type Description
ValueError

If a price the calendar takes from the part is not set, the calendar allows no planned replacement at all (no opportunities, and none on any day), or an argument is out of its range.

NotImplementedError

For method="integration" with replacement on any day, or method="sampling" with given dates.

Examples:

With no opportunities and a planned replacement on any day at the planned cost, it is age replacement:

>>> import surpyval as surv
>>> from repyability import Calendar, NonRepairable
>>> unit = NonRepairable(
...     surv.Weibull.from_params([1000, 2.5]),
...     planned_cost=1.0,
...     unplanned_cost=5.0,
... )
>>> policy = unit.optimal_calendar_policy(Calendar())
>>> round(policy.cost_rate, 6)
0.003462
>>> round(policy.next_replacement(age=0.0).time, 1)
493.3
>>> round(unit.optimal_replacement_policy().interval, 1)
493.0

calendar_policy_cost

calendar_policy_cost(calendar, rule, *, max_age=None)

The long-run average cost a unit of time of a given replacement rule over a calendar of opportunities: to price simple policies against optimal_calendar_policy.

The rule gives, for each kind of replacement it uses (an opportunity's name, or "anytime" for any day), the age from which it replaces the part; the kinds it leaves out never replace. Where kinds coincide, the one with the youngest age replaces (the cheapest, at a tie). Worked out as the optimal policy's cost rate is, on the same grid, with the decisions fixed (a plan of given dates repeated past its last date).

Parameters:

Name Type Description Default
calendar Calendar

The opportunities and prices.

required
rule dict

The age, at least 0 (inf for never), from which each kind replaces: {"annual": 0.0} replaces at every annual shut, {"anytime": 493.0} at age 493 on any day, {} runs to failure.

required
max_age float

The oldest age on the grid, as for optimal_calendar_policy.

None

Returns:

Type Description
float

The rule's long-run cost a unit of time.

Examples:

>>> import surpyval as surv
>>> from repyability import Calendar, NonRepairable
>>> unit = NonRepairable(
...     surv.Weibull.from_params([1000, 2.5]),
...     planned_cost=1.0,
...     unplanned_cost=5.0,
... )
>>> rate = unit.calendar_policy_cost(Calendar(), {"anytime": 493.0})
>>> round(rate, 6)
0.003462

reset

reset()

Restart the event sequence with a working, as-new unit.

The next next_event() call then draws a time to failure. A RepairableRBD simulation resets each component this way at the start of every simulated history.

next_event

next_event()

Draw the time to the unit's next state change.

The event API a RepairableRBD simulation steps each component through. Calls alternate, starting (after construction or reset()) with a failure:

  • a time to failure drawn from reliability, returned as (time, False): the unit is now down;
  • a replacement time drawn from time_to_replace, returned as (time, True): the unit is working again, as new.

Each time is a duration measured from the previous event, not an absolute time. Each draw is one random(1) call on the model. surpyval parametric models draw from numpy's global RNG, so seed it (e.g. with np.random.seed, which RepairableRBD.availability(seed=...) does) for reproducible draws; surpyval non-parametric models cannot be seeded this way. (For most parametric models the RBD simulation replays these same draws in batches instead of calling this method, with identical results.)

Returns:

Type Description
tuple of (float, bool)

The time to the event, and whether the unit is working after it.

Examples:

With fixed times, a failure 10 time units after each replacement, and replacements that take 2:

>>> import surpyval as surv
>>> from repyability import NonRepairable
>>> unit = NonRepairable(
...     surv.ExactEventTime.from_params(10),
...     surv.ExactEventTime.from_params(2),
... )
>>> unit.next_event()
(10.0, False)
>>> unit.next_event()
(2.0, True)
>>> unit.next_event()
(10.0, False)
>>> unit.reset()
>>> unit.next_event()
(10.0, False)

Repairable

Repairable(model)

Repairable component with optimal overhaul and failure-limit policies.

Prices the repair-versus-renew trade-off for a unit whose repairs are minimal ("as bad as old") or imperfect (partial rejuvenation). Each failure is repaired at cost cr; an overhaul/replacement costs co > cr and renews the unit to as good as new. Two policies are optimised:

  • periodic overhaul: renew every t time units, with long-run cost rate g(t) = (cr * E[N(t)] + co) / t (the Barlow-Hunter policy), where E[N(t)] is the expected number of failures by age t; see optimal_overhaul_policy();
  • failure limit: repair the first n - 1 failures and replace at the n-th, with cost rate (cr * (n - 1) + co) / E[T_n], where E[T_n] is the expected time to the n-th failure; see optimal_failure_limit_policy() (simulation-backed models only).

Repairs and overhauls take no time and costs are per event, so cost rates are per unit of the model's time. Set the costs with set_repair_and_overhaul_costs() before any cost calculation.

This is a standalone component-level tool, not an RBD node model: RBD components are assumed to renew on repair, as a NonRepairable does.

Parameters:

Name Type Description Default
model object

A recurrent-event model exposing E[N(t)] as either:

  • cif(t): an analytic cumulative intensity (minimal repair), e.g. a surpyval CrowAMSAA or Duane model (fitted, or built with from_params) or a fitted HPP; or
  • mcf(t, items=..., random_state=...): a simulation-estimated mean cumulative function (imperfect repair), e.g. a surpyval GeneralizedRenewal (Kijima I/II) model, fitted or built with fit_from_parameters.

If both are present cif is used (analytic, exact). The failure-count methods (expected_time_to_nth_failure() and the failure-limit policy) also call the model's count_terminated_simulation, which surpyval's simulation-backed models provide.

required

Attributes:

Name Type Description
model object

The recurrent-event model given.

Raises:

Type Description
ValueError

If model exposes neither cif nor mcf.

Examples:

Minimal repair with a power-law (Crow-AMSAA) intensity, where E[N(t)] = (t / 100) ** 1.5 is exact:

>>> from surpyval.recurrent import CrowAMSAA
>>> from repyability import Repairable
>>> unit = Repairable(CrowAMSAA.from_params([100.0, 1.5]))
>>> unit.set_repair_and_overhaul_costs(cr=10.0, co=1000.0)
>>> policy = unit.optimal_overhaul_policy()
>>> round(policy.interval, 1), round(policy.cost_rate, 4)
(3420.0, 0.8772)

Imperfect repair (Kijima I with restoration factor q = 0.5) has no closed-form E[N(t)], so it is simulated; pass a seed for a reproducible result. The cost rate is flat near its minimum, so the interval found varies from run to run far more than the cost rate:

>>> import surpyval as surv
>>> from surpyval.recurrent import GeneralizedRenewal
>>> grp = GeneralizedRenewal.fit_from_parameters(
...     [100.0, 2.0], 0.5, kijima="i", dist=surv.Weibull
... )
>>> unit = Repairable(grp)
>>> unit.set_repair_and_overhaul_costs(cr=1.0, co=5.0)
>>> policy = unit.optimal_overhaul_policy(
...     seed=1, mc_samples=100, max_interval=600.0
... )
>>> bool(250 < policy.interval < 400)
True
>>> bool(0.034 < policy.cost_rate < 0.037)
True

is_simulated property

is_simulated

Whether E[N(t)] is estimated by simulation.

True for an mcf-only (imperfect-repair) model, whose methods honour seed, mc_samples and max_interval; False for an analytic cif model, whose methods ignore them.

Examples:

>>> from surpyval.recurrent import CrowAMSAA
>>> from repyability import Repairable
>>> Repairable(CrowAMSAA.from_params([100.0, 1.5])).is_simulated
False

set_repair_and_overhaul_costs

set_repair_and_overhaul_costs(cr, co)

Set the repair cost cr and overhaul/replacement cost co.

Required before any cost or policy calculation. The costs are stored as the cr and co attributes.

Parameters:

Name Type Description Default
cr float

Cost of each (minimal or imperfect) repair. Must be positive.

required
co float

Cost of an overhaul/replacement, which renews the unit. Must exceed cr: otherwise one would simply renew at every failure.

required

Raises:

Type Description
ValueError

If a cost is not finite, or if cr <= 0 or cr >= co.

cost

cost(t, seed=None, mc_samples=None)

Expected cost of one overhaul/replacement cycle of length t.

cr * E[N(t)] + co: the repairs expected before the overhaul at age t, plus the overhaul itself.

Parameters:

Name Type Description Default
t float or array_like

The cycle length(s), in the model's time units.

required
seed int

Seed for the simulation of a simulation-backed model (ignored for an analytic one). None (the default) draws from numpy's global RNG.

None
mc_samples int

Number of simulated histories used to estimate E[N(t)] (default 1000; ignored for an analytic model).

None

Returns:

Type Description
float or ndarray

The expected cycle cost: a float for a scalar t, an array for an array t.

Raises:

Type Description
ValueError

If the costs have not been set.

Examples:

With E[N(250)] = (250 / 100) ** 1.5 = 3.95 expected repairs:

>>> from surpyval.recurrent import CrowAMSAA
>>> from repyability import Repairable
>>> unit = Repairable(CrowAMSAA.from_params([100.0, 1.5]))
>>> unit.set_repair_and_overhaul_costs(cr=10.0, co=1000.0)
>>> round(unit.cost(250.0), 2)
1039.53

cost_rate

cost_rate(t, seed=None, mc_samples=None)

Long-run cost per unit time when overhauling every t.

g(t) = (cr * E[N(t)] + co) / t, the cost of one cycle over its length (renewal-reward); t = 0 gives inf. This is the cost rate the overhaul policy minimises.

For a simulation-backed model a single simulation, run to the largest t, serves every age in the call, so pass all the ages to be compared in one call: with a fixed seed, the estimate at an age depends on the largest age requested.

Parameters:

Name Type Description Default
t float or array_like

The overhaul interval(s), in the model's time units.

required
seed int

Seed for the simulation of a simulation-backed model (ignored for an analytic one). None (the default) draws from numpy's global RNG.

None
mc_samples int

Number of simulated histories used to estimate E[N(t)] (default 1000; ignored for an analytic model).

None

Returns:

Type Description
float or ndarray

The cost rate: a float for a scalar t, an array for an array t.

Raises:

Type Description
ValueError

If the costs have not been set.

Examples:

>>> from surpyval.recurrent import CrowAMSAA
>>> from repyability import Repairable
>>> unit = Repairable(CrowAMSAA.from_params([100.0, 1.5]))
>>> unit.set_repair_and_overhaul_costs(cr=10.0, co=1000.0)
>>> round(unit.cost_rate(250.0), 4)
4.1581
>>> [round(float(g), 4) for g in unit.cost_rate([1000.0, 3420.0])]
[1.3162, 0.8772]

find_optimal_overhaul_interval

find_optimal_overhaul_interval(seed=None, mc_samples=None, max_interval=None)

The overhaul/replacement interval minimising the long-run cost rate.

Minimises g(t) = (cr * E[N(t)] + co) / t (see cost_rate).

For an analytic (cif) model the search is deterministic: it brackets, to within a factor of 2, the age at which cr * E[N(t)] reaches co, scans a 1601-point log-spaced grid from 1e-4 to 1e4 times that age (moving the grid when the minimum falls on an edge), and refines the best grid point with a bounded scalar minimiser. It returns inf when renewal never pays: the unit does not wear out (E[N(t)] grows at most linearly, e.g. an HPP, or Crow-AMSAA with beta <= 1), so the cost rate keeps falling as the interval grows. seed, mc_samples and max_interval are ignored.

For a simulation-backed (imperfect-repair) model, E[N(t)] is estimated by a single simulation over 250 log-spaced ages from horizon / 10**2.5 to the search horizon, and the cheapest of those ages is returned (no refinement). The horizon is max_interval, shortened if need be to what surpyval's simulator can resolve: close to minimal repair (q near 1) the unit's virtual age can reach ages where the baseline survival function underflows double precision, surpyval then ends those simulated histories early, and the simulated E[N(t)] would stop growing and drag the optimum out to the horizon. So while the simulation is cut short, the horizon is halved, down to no less than the age at which the baseline survival is 1e-10 (below which, for Kijima I or II with q <= 1, it resolves every failure). The result is never inf: if the cost rate is still falling at the horizon (the optimum lies beyond it, or there is none), the horizon itself is returned with a warning.

Parameters:

Name Type Description Default
seed int

Seed for a reproducible simulation (simulation-backed models only). None (the default) draws from numpy's global RNG.

None
mc_samples int

Number of simulated histories used to estimate E[N(t)] (default 1000; simulation-backed models only).

None
max_interval float

The search horizon (simulation-backed models only), shortened if the simulation cannot resolve it (see above). By default 15 times the mean of the model's baseline lifetime distribution (model.model.mean()), or 15 if there is none.

None

Returns:

Type Description
float

The optimal interval, in the model's time units; inf (for an analytic model only) when overhauls never pay.

Raises:

Type Description
ValueError

If the costs have not been set.

Warns:

Type Description
UserWarning

For a simulation-backed model, if the cost rate is still falling at the search horizon, so the horizon returned is not a true optimum; the message says whether a larger max_interval could help, or the horizon was already shortened to what the simulation can resolve.

Examples:

A unit with a constant failure intensity (no wear-out) is never worth overhauling:

>>> from surpyval.recurrent import CrowAMSAA
>>> from repyability import Repairable
>>> unit = Repairable(CrowAMSAA.from_params([500.0, 1.0]))
>>> unit.set_repair_and_overhaul_costs(cr=10.0, co=1000.0)
>>> unit.find_optimal_overhaul_interval()
inf

Simulated minimal repair (q = 1) lands near the closed-form optimum alpha * (co / (cr * (beta - 1))) ** (1 / beta), here 100 * 5 ** 0.5 = 223.6:

>>> import surpyval as surv
>>> from surpyval.recurrent import GeneralizedRenewal
>>> grp = GeneralizedRenewal.fit_from_parameters(
...     [100.0, 2.0], 1.0, kijima="i", dist=surv.Weibull
... )
>>> unit = Repairable(grp)
>>> unit.set_repair_and_overhaul_costs(cr=1.0, co=5.0)
>>> t = unit.find_optimal_overhaul_interval(seed=1, mc_samples=100)
>>> bool(abs(t - 223.6) < 25)  # a flat minimum: within about 10%
True

optimal_overhaul_policy

optimal_overhaul_policy(seed=None, mc_samples=None, max_interval=None)

The optimal overhaul/replacement policy as a typed result.

Runs the search of find_optimal_overhaul_interval() (see there for the method, its limits and the parameters) and also reports the long-run cost rate at the optimum.

Parameters:

Name Type Description Default
seed int

Seed for a reproducible simulation (simulation-backed models only). None (the default) draws from numpy's global RNG.

None
mc_samples int

Number of simulated histories used to estimate E[N(t)] (default 1000; simulation-backed models only).

None
max_interval float

The search horizon (simulation-backed models only), shortened if the simulation cannot resolve it; see find_optimal_overhaul_interval().

None

Returns:

Type Description
MaintenancePolicy

interval is the optimal overhaul interval and cost_rate the long-run cost per unit time under it. When the interval is inf (never overhaul), cost_rate is the cost rate at the far end of the search grid, which approximates the limiting rate of repairs alone, cr * E[N(t)] / t as t grows. For a simulation-backed model, cost_rate is the simulated estimate at the chosen interval.

Raises:

Type Description
ValueError

If the costs have not been set.

Warns:

Type Description
UserWarning

As find_optimal_overhaul_interval(): the cost rate is still falling at a simulated search's horizon.

Examples:

>>> from surpyval.recurrent import CrowAMSAA
>>> from repyability import Repairable
>>> unit = Repairable(CrowAMSAA.from_params([100.0, 1.5]))
>>> unit.set_repair_and_overhaul_costs(cr=10.0, co=1000.0)
>>> policy = unit.optimal_overhaul_policy()
>>> round(policy.interval, 1), round(policy.cost_rate, 4)
(3420.0, 0.8772)

Without wear-out the unit is never overhauled and only repairs are paid for, here cr / alpha = 10 / 500 per unit time:

>>> unit = Repairable(CrowAMSAA.from_params([500.0, 1.0]))
>>> unit.set_repair_and_overhaul_costs(cr=10.0, co=1000.0)
>>> policy = unit.optimal_overhaul_policy()
>>> policy.interval, round(policy.cost_rate, 3)
(inf, 0.02)

expected_time_to_nth_failure

expected_time_to_nth_failure(n, seed=None, mc_samples=None)

Expected time to the n-th failure, E[T_n].

Estimated from a single count-terminated simulation of mc_samples failure histories (the model's count_terminated_simulation), as the mean of their n-th failure times. It needs a simulation-backed (imperfect-repair) model; for a power-law minimal-repair process the closed form minimal_repair_time_to_nth_failure is exact and cheaper.

If the simulation fails at a high failure count (surpyval's simulator can, for near-minimal repair), it is retried with the count halved until it succeeds, with a warning; ValueError is then raised, as n failures were not reached.

Parameters:

Name Type Description Default
n int

The failure number, 1 for the first failure.

required
seed int

Seed for a reproducible simulation. None (the default) draws from numpy's global RNG.

None
mc_samples int

Number of simulated histories (default 1000).

None

Returns:

Type Description
float

The expected time to the n-th failure, in the model's time units.

Raises:

Type Description
ValueError

If n < 1, if the model is analytic (cif), or if the simulation cannot reach n failures.

Examples:

At q = 1 (minimal repair) the estimate approaches the closed form:

>>> import surpyval as surv
>>> from surpyval.recurrent import GeneralizedRenewal
>>> from repyability import Repairable
>>> from repyability import minimal_repair_time_to_nth_failure
>>> grp = GeneralizedRenewal.fit_from_parameters(
...     [100.0, 2.0], 1.0, kijima="i", dist=surv.Weibull
... )
>>> unit = Repairable(grp)
>>> t3 = unit.expected_time_to_nth_failure(
...     3, seed=1, mc_samples=500
... )
>>> round(t3)
165
>>> round(minimal_repair_time_to_nth_failure(100.0, 2.0, 3), 1)
166.2

find_optimal_replacement_failure_count

find_optimal_replacement_failure_count(seed=None, mc_samples=None, max_failures=_DEFAULT_MAX_FAILURES)

The failure count at which to replace, minimising the cost rate.

Under a replace-at-n-th-failure policy the unit is repaired (cost cr) at each of its first n - 1 failures and replaced (cost co, renewing it) at the n-th, so the long-run cost rate is (cr * (n - 1) + co) / E[T_n]. E[T_1], ..., E[T_max_failures] come from a single count-terminated simulation (see expected_time_to_nth_failure()), and the cheapest n is returned. It needs a simulation-backed (imperfect-repair) model.

n = 1 means replace at every failure. If the simulation fails at a high failure count (as surpyval's simulator can for near-minimal repair), the count is halved until it succeeds and the search is truncated there, with a warning. A result equal to the largest count searched may mean the optimum lies beyond it: raise max_failures if the search was not truncated. The cost rate is often flat near its minimum, so the chosen count can shift with the seed, mc_samples and max_failures.

Parameters:

Name Type Description Default
seed int

Seed for a reproducible simulation. None (the default) draws from numpy's global RNG.

None
mc_samples int

Number of simulated histories (default 1000).

None
max_failures int

The largest failure count searched (default 30).

_DEFAULT_MAX_FAILURES

Returns:

Type Description
int

The optimal number of failures per replacement cycle.

Raises:

Type Description
ValueError

If the costs have not been set, if the model is analytic (cif), or if the failure process cannot be simulated at all.

Examples:

>>> import surpyval as surv
>>> from surpyval.recurrent import GeneralizedRenewal
>>> from repyability import Repairable
>>> grp = GeneralizedRenewal.fit_from_parameters(
...     [100.0, 2.0], 0.4, kijima="i", dist=surv.Weibull
... )
>>> unit = Repairable(grp)
>>> unit.set_repair_and_overhaul_costs(cr=1.0, co=5.0)
>>> unit.find_optimal_replacement_failure_count(
...     seed=1, mc_samples=200, max_failures=15
... )
7

optimal_failure_limit_policy

optimal_failure_limit_policy(seed=None, mc_samples=None, max_failures=_DEFAULT_MAX_FAILURES)

The optimal replace-at-N-th-failure policy as a typed result.

Repair on each failure (cost cr) and replace on the failure_count-th (cost co); the long-run cost rate (cr * (n - 1) + co) / E[T_n] is minimised over n by the search of find_optimal_replacement_failure_count() (see there for the method and its limits).

Parameters:

Name Type Description Default
seed int

Seed for a reproducible simulation. None (the default) draws from numpy's global RNG.

None
mc_samples int

Number of simulated histories (default 1000).

None
max_failures int

The largest failure count searched (default 30).

_DEFAULT_MAX_FAILURES

Returns:

Type Description
FailureLimitPolicy

failure_count is the optimal number of failures per replacement cycle and cost_rate the long-run cost per unit time under it.

Raises:

Type Description
ValueError

If the costs have not been set, if the model is analytic (cif), or if the failure process cannot be simulated at all.

Examples:

>>> import surpyval as surv
>>> from surpyval.recurrent import GeneralizedRenewal
>>> from repyability import Repairable
>>> grp = GeneralizedRenewal.fit_from_parameters(
...     [100.0, 2.0], 0.4, kijima="i", dist=surv.Weibull
... )
>>> unit = Repairable(grp)
>>> unit.set_repair_and_overhaul_costs(cr=1.0, co=5.0)
>>> policy = unit.optimal_failure_limit_policy(
...     seed=1, mc_samples=200, max_failures=15
... )
>>> policy.failure_count, round(policy.cost_rate, 3)
(7, 0.031)

minimal_repair_time_to_nth_failure

minimal_repair_time_to_nth_failure(alpha, beta, n)

Expected time to the n-th failure under power-law minimal repair.

For a minimal-repair process with expected number of failures E[N(t)] = (t / alpha) ** beta (a Weibull(alpha, beta) unit repaired "as bad as old": surpyval's CrowAMSAA.from_params([alpha, beta]), or a GeneralizedRenewal with that Weibull baseline and q = 1), the n-th failure time is alpha * S ** (1 / beta) with S ~ Gamma(n, 1), so E[T_n] = alpha * Gamma(n + 1 / beta) / Gamma(n).

The closed form is exact and cheap: use it for minimal repair in place of the simulated Repairable.expected_time_to_nth_failure (which needs an imperfect-repair model), or to check that estimate at q = 1. n = 1 gives the Weibull mean, alpha * Gamma(1 + 1 / beta).

Parameters:

Name Type Description Default
alpha float

Scale of the power law (the Weibull baseline's scale), in time units.

required
beta float

Shape of the power law (the Weibull baseline's shape).

required
n int

The failure number, 1 for the first failure.

required

Returns:

Type Description
float

The expected time to the n-th failure, in the units of alpha.

Raises:

Type Description
ValueError

If n < 1.

Examples:

>>> from repyability import minimal_repair_time_to_nth_failure
>>> round(minimal_repair_time_to_nth_failure(100.0, 2.0, 1), 2)
88.62
>>> round(minimal_repair_time_to_nth_failure(100.0, 2.0, 3), 2)
166.17

Opportunity dataclass

Opportunity(name, cost, period=None, offset=0.0, dates=None)

A kind of scheduled opportunity to replace a part: a shut (weekly, quarterly, annual, ...) at which replacing it costs cost.

Its times are either every period from offset (on and on, both ways: offset + k * period for every whole k), or the given dates. Times are in the life model's unit, from 0.

Parameters:

Name Type Description Default
name str

The opportunity's name, as the policy's schedule lists it (not "anytime", which names a replacement on any day).

required
cost float

What replacing the part at it costs, at least 0.

required
period float

The time from one to the next, more than 0.

None
offset float

The time of one of them, by default 0. Only with period.

0.0
dates array_like

Its times, instead of period: finite numbers, put in order.

None

Examples:

>>> from repyability import Opportunity
>>> weekly = Opportunity("weekly", 3.0, period=7.0)
>>> annual = Opportunity("annual", 1.0, period=364.0, offset=182.0)
>>> turnarounds = Opportunity("turnaround", 0.5, dates=[400.0, 1130.0])
>>> weekly.times(0.0, 30.0).tolist()
[0.0, 7.0, 14.0, 21.0, 28.0]

periodic property

periodic

Whether its times are on a period (not given dates).

times

times(start, end)

Its times from start up to end.

Parameters:

Name Type Description Default
start float

The first time to give, if one falls there.

required
end float

The time to give them up to (not including).

required

Returns:

Type Description
ndarray

Its times, in order.

Calendar

Calendar(opportunities=(), failure=PART, anytime=PART)

The prices of replacing a part over time: its scheduled opportunities, a planned replacement on any day, and a failure.

Parameters:

Name Type Description Default
opportunities list of Opportunity

The scheduled opportunities, each with its own name.

()
failure float or part

What a replacement at a failure costs. By default "part": the part's unplanned_cost.

PART
anytime (float, part or None)

What a planned replacement on any day costs. By default "part": the part's planned_cost, if it has one (else no replacement on any day). None: no replacement but at the opportunities (and at failures).

PART

Examples:

>>> from repyability import Calendar, Opportunity
>>> calendar = Calendar(
...     [
...         Opportunity("weekly", 3.0, period=7.0),
...         Opportunity("annual", 1.0, period=364.0),
...     ],
...     failure=40.0,
...     anytime=4.0,
... )
>>> calendar.periodic, calendar.period
(True, 364.0)
>>> calendar.planned_cost([0.0, 3.0, 7.0]).tolist()
[1.0, 4.0, 3.0]

periodic property

periodic

Whether every opportunity is on a period (no given dates), so that the calendar repeats.

period property

period

The time after which a periodic calendar repeats: the least common multiple of its periods (each read as a fraction), which may be very long; None if it has dates, or no opportunities.

plan_end property

plan_end

The last of the given dates; None if it has none.

planned_cost

planned_cost(times, step=None)

The cheapest planned replacement at each of times.

Parameters:

Name Type Description Default
times float or array_like

The times, in the life's unit.

required
step float

A grid's step: an opportunity then counts at the grid time nearest it, as the policy lays it. Without one, only at its time itself.

None

Returns:

Type Description
ndarray

The price of the cheapest planned replacement open at each time (an opportunity there, or the calendar's own anytime price), inf where none is.