Skip to content

Maintenance policies

Learning this for the first time?

This page is the reference. The ideas behind it are taught step by step, with worked examples and exercises, in Lesson 8 (maintaining on purpose).

Two component-level classes price preventive maintenance. They sit at the two ends of how effective a repair is:

  • NonRepairable: the unit is replaced on failure, as good as new, so every cycle is a statistical renewal. It is also the component model a RepairableRBD uses internally.
  • Repairable: failures are repaired in a way that restores the unit only partly (imperfect repair) or not at all (minimal repair, "as bad as old"), and the unit is periodically overhauled or replaced to as good as new. A Repairable is a standalone tool, not an RBD node; in a RepairableRBD a component's "repair" simulates the same imperfect repair (see Repairable systems).

Fitting the lifetime or recurrence models is surpyval's job; these classes take fitted models. Theory: Concepts.

Age replacement (NonRepairable)

Replace the unit at age t (a planned replacement costing cp) or on failure (unplanned, costing cu > cp), whichever comes first. The long-run cost rate is the expected cost of a cycle over its expected length,

cost_rate(t) = (cp · R(t) + cu · F(t)) / ∫₀ᵗ R(u) du

and find_optimal_replacement() minimises it:

import surpyval as surv
from repyability import NonRepairable

bearing = NonRepairable(surv.Weibull.from_params([1000, 2.5]))
bearing.set_costs_planned_and_unplanned(1, 5)   # cp, cu

bearing.find_optimal_replacement()      # -> 493.05   replace at age 493
policy = bearing.optimal_replacement_policy()
policy.interval                         # -> 493.05
policy.cost_rate                        # -> 0.003462   cost per unit time
bearing.avg_replacement_time(493.05)    # -> 470.15  ∫₀ᵗ R(u) du, the mean cycle length

For an exponential lifetime, or a Weibull with shape 1 or less, the unit does not wear out and preventive replacement never pays: the interval is inf and the cost rate is the run-to-failure rate cu / MTTF. The same goes for any lifetime whose best replacement age is no cheaper than running to failure (a Gamma with shape below 1, say, or a Weibull of shape 1 or less with zero-inflation or an offset). When some units never fail (a limited failure population), running to failure costs nothing in the long run, since those units are kept, so the interval is inf at a cost rate of 0.

An offset (a failure-free period) can make replacement pay even without wear-out: no unit fails before the offset, so replacing just as it ends avoids the failures that follow.

dormant = NonRepairable(surv.Weibull.from_params([10, 0.5], gamma=1000))
dormant.set_costs_planned_and_unplanned(1, 5)
dormant.find_optimal_replacement()                 # -> 1000.0   at the offset
dormant.optimal_replacement_policy().cost_rate     # -> 0.001    cp / 1000
steady = NonRepairable(surv.Exponential.from_params([0.001]))
steady.set_costs_planned_and_unplanned(1, 5)
steady.optimal_replacement_policy().cost_rate   # -> 0.005   = 5 / 1000

set_costs_planned_and_unplanned raises ValueError unless both costs are finite, cp is not negative and cp < cu. Set the costs before asking for a cost rate or a policy: without them cost_rate, find_optimal_replacement() and optimal_replacement_policy() raise ValueError (except that find_optimal_replacement() returns inf without them in the cases above).

A non-parametric lifetime (a Kaplan–Meier fit, say) works too, for a unit on its own (a diagram refuses one: see Building an RBD). Its survival function is taken as linear between the estimate's time points, from 1 at age 0, and held at its last value beyond them; the cost rate is then exact, and the search covers ages up to the last time point, since the estimate says nothing about later ones.

As a repairable component

The second argument of NonRepairable is the time to replace (repair) the unit; it defaults to zero (an instant swap). With it, the unit has a long-run availability and failure frequency, which is how a RepairableRBD uses it:

pump = NonRepairable(
    surv.Weibull.from_params([1000, 2.5]),
    surv.Exponential.from_params([1 / 24]),   # mean 24 h to replace
)
pump.mean_availability()     # -> 0.9737   MTTF / (MTTF + MTTR)
pump.mean_unavailability()   # -> 0.02634
pump.failure_frequency()     # -> 0.001097   1 / (MTTF + MTTR)

reset() and next_event() are the event interface a RepairableRBD simulation calls: next_event() alternates between drawing a time to failure from reliability (returned with False) and a time to repair from time_to_replace (returned with True), and reset() starts again with a failure.

To price an age- or block-replacement policy for a component in its system (where a planned stop may or may not halt production), give the component a "preventive" schedule in a RepairableRBD: see Costs.

Overhaul under minimal repair (Repairable)

Each failure is minimally repaired at cost cr; every t the unit is overhauled to as good as new at cost co > cr. With Λ(t) the expected number of failures by t (the cumulative intensity), the long-run cost rate is the Barlow–Hunter policy,

cost_rate(t) = (cr · Λ(t) + co) / t

For a power-law (Crow–AMSAA) process Λ(t) is analytic:

from surpyval.recurrent import CrowAMSAA
from repyability import Repairable

gearbox = Repairable(CrowAMSAA.from_params([1500, 1.5]))
gearbox.set_repair_and_overhaul_costs(100, 10_000)   # cr, co

gearbox.is_simulated                    # False: E[N(t)] is analytic
gearbox.find_optimal_overhaul_interval()   # -> 51299.3
gearbox.optimal_overhaul_policy().cost_rate   # -> 0.5848
gearbox.cost(5000)                      # -> 10608.6   the cost of one 5000-hour cycle
gearbox.cost_rate(5000)                 # -> 2.122     overhauling every 5000 hours instead

A finite optimum needs wear-out (Crow–AMSAA beta > 1). With beta <= 1 repairs never become more frequent, overhauls never pay, and the interval is inf with the cost rate of the repairs alone:

steady_gearbox = Repairable(CrowAMSAA.from_params([1500, 0.9]))
steady_gearbox.set_repair_and_overhaul_costs(100, 10_000)
steady_gearbox.optimal_overhaul_policy().interval   # inf

Imperfect repair (Repairable)

Most repairs sit between the two extremes: each one turns the unit's virtual age back part of the way. Give Repairable a surpyval GeneralizedRenewal model (Kijima I or II, restoration factor q between 0 for perfect repair and 1 for minimal repair) and the same policies apply. E[N(t)] then has no closed form and is estimated by simulation, so pass a seed for a reproducible answer and mc_samples to trade speed for precision (default 1000). The simulated numbers below (marked ~>) vary a little with the seed and with surpyval's simulator; the cost rate is flat near its minimum, so the interval found varies more than the cost rate:

from surpyval.recurrent import GeneralizedRenewal

model = GeneralizedRenewal.fit_from_parameters(
    [1500, 2.0], q=0.4, kijima="i", dist=surv.Weibull
)
compressor = Repairable(model)
compressor.set_repair_and_overhaul_costs(100, 1000)

compressor.is_simulated   # True
overhaul = compressor.optimal_overhaul_policy(seed=0, mc_samples=200)
overhaul.interval         # ~> 7600   between 7,400 and 7,900
overhaul.cost_rate        # ~> 0.294

max_interval bounds the search (by default 15 times the mean of the baseline lifetime). If surpyval's simulator reports that it stopped resolving failures before the horizon (as it can close to minimal repair, q near 1, where the unit's virtual age reaches ages at which the survival underflows double precision), the search shortens its horizon to what the simulation can resolve, so near-minimal repair needs no special handling. The simulated search never returns inf: if the cost rate is still falling at the horizon (overhauls never pay, or the optimum lies beyond it), it returns the horizon with a warning that says whether a larger max_interval could help. The optimum grows as repairs become more effective.

Replace at the N-th failure

Instead of renewing at a fixed age, repair each failure and replace the unit at its N-th failure. The cost rate is (cr · (N − 1) + co) / E[T_N], with E[T_N] the expected time to the N-th failure:

limit = compressor.optimal_failure_limit_policy(seed=0, mc_samples=500)
limit.failure_count   # ~> 14   replace at the 13th or 14th failure
limit.cost_rate       # ~> 0.285
compressor.find_optimal_replacement_failure_count(seed=0, mc_samples=500)   # ~> 14
compressor.expected_time_to_nth_failure(5, seed=0, mc_samples=500)   # ~> 4360

The result is a FailureLimitPolicy. max_failures bounds the search (by default 30); if the optimum equals the bound, raise it. The cost rate is usually flat near its minimum, so the optimal count can move by several failures with the seed or mc_samples while the cost rate barely changes: read it as a region, and compare cost rates. These methods need a simulation-backed (imperfect-repair) model and raise ValueError otherwise.

For the minimal-repair limit (q = 1, a power-law process), E[T_n] is exact and needs no simulation:

from repyability import minimal_repair_time_to_nth_failure

minimal_repair_time_to_nth_failure(alpha=1500, beta=2.0, n=5)   # -> 3271.4

It is alpha · Γ(n + 1/beta) / Γ(n).

Results

Both policy types are small typed results: MaintenancePolicy (interval, cost_rate) and FailureLimitPolicy (failure_count, cost_rate). Costs are per event and undiscounted.