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, costcp) or on failure (unplanned, costcu > cp), whichever comes first, viacost_rate,find_optimal_replacement()andoptimal_replacement_policy(). The cost calculations treat replacement as instantaneous (they do not usetime_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 fromreliabilityand a replacement time drawn fromtime_to_replace(seenext_event()), andmean_availability()andfailure_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:
|
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 |
None
|
planned_cost
|
float
|
What a planned (preventive) replacement costs, at least 0, by name;
as |
None
|
unplanned_cost
|
float
|
What a replacement at a failure costs, at least 0, by name ( |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
reliability |
object
|
The lifetime model given. |
time_to_replace |
object
|
The time-to-replace model given. |
cost_rate |
callable
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
TypeError
|
If either is a surpyval distribution itself ( |
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 |
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 |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a cost is negative or not finite, or if |
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 |
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 |
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 |
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 andinfis returned at once (unless the model has an offsetgammaor zero-inflationf0). So does a limited failure population (p < 1), whatever its shape: running to failure then costs nothing in the long run (see below). Otherwisescipy.optimize.minimize(BFGS) searches overlog(t), starting from the mean life, and its result is kept in theoptimisation_resultsattribute. With an offsetgammano unit can fail before that age, so the cost rate falls ascp / tup to it and replacing atgammaitself is also considered. If the best age found is no cheaper than running to failure (see below),infis 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,infis 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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the costs have not been set with
|
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
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the costs have not been set (see
|
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 |
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 |
NotImplementedError
|
For a life with no survival function in closed form (a
non-parametric estimate or a |
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 |
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
|
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, |
0.0
|
Returns:
| Type | Description |
|---|---|
SkipValue
|
The present-value cost of doing the replacement ( |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the costs have not been set, or an argument is out of its
range, or |
NotImplementedError
|
For a life with no survival function in closed form (a
non-parametric estimate or a |
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 overmc_samplessampled 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, somc_samplessets 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 |
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'
|
tail
|
str
|
How a plan of given dates goes on after its last date:
|
'repeat'
|
mc_samples
|
int
|
The times sampled by |
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 ( |
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 |
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 ( |
required |
max_age
|
float
|
The oldest age on the grid, as for |
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
ttime units, with long-run cost rateg(t) = (cr * E[N(t)] + co) / t(the Barlow-Hunter policy), whereE[N(t)]is the expected number of failures by aget; seeoptimal_overhaul_policy(); - failure limit: repair the first
n - 1failures and replace at then-th, with cost rate(cr * (n - 1) + co) / E[T_n], whereE[T_n]is the expected time to then-th failure; seeoptimal_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
If both are present |
required |
Attributes:
| Name | Type | Description |
|---|---|---|
model |
object
|
The recurrent-event model given. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a cost is not finite, or if |
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
|
mc_samples
|
int
|
Number of simulated histories used to estimate |
None
|
Returns:
| Type | Description |
|---|---|
float or ndarray
|
The expected cycle cost: a float for a scalar |
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
|
mc_samples
|
int
|
Number of simulated histories used to estimate |
None
|
Returns:
| Type | Description |
|---|---|
float or ndarray
|
The cost rate: a float for a scalar |
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
|
mc_samples
|
int
|
Number of simulated histories used to estimate |
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
( |
None
|
Returns:
| Type | Description |
|---|---|
float
|
The optimal interval, in the model's time units; |
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 |
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
|
mc_samples
|
int
|
Number of simulated histories used to estimate |
None
|
max_interval
|
float
|
The search horizon (simulation-backed models only), shortened if
the simulation cannot resolve it; see
|
None
|
Returns:
| Type | Description |
|---|---|
MaintenancePolicy
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the costs have not been set. |
Warns:
| Type | Description |
|---|---|
UserWarning
|
As |
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
|
mc_samples
|
int
|
Number of simulated histories (default 1000). |
None
|
Returns:
| Type | Description |
|---|---|
float
|
The expected time to the |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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
|
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
( |
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
|
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
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the costs have not been set, if the model is analytic
( |
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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
|
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 |
0.0
|
dates
|
array_like
|
Its times, instead of |
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]
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
|
anytime
|
(float, part or None)
|
What a planned replacement on any day costs. By default
|
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.
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 |