Lesson 7: What it costs¶
In this lesson
You will learn:
- the three things a repairable system makes you pay for, and how to price each in RePyability;
- the renewal-reward idea: a long-run cost per hour is the cost of a cycle divided by its length;
- to work out a system's long-run cost rate by hand, check it with
expected_cost_rate(), and see where the money goes; - to simulate a year's cost with
cost()and budget with its percentiles; - to tell how much a year's cost varies from how precisely its mean is known, and what uncertain prices change;
- to add the price of buying the system, and to find how much redundancy gives the lowest total cost of ownership.
Before you start: Lesson 6. About 35 minutes.
The question¶
You look after the repairable plant from Lesson 6: two pumps in parallel feeding a valve, each part repaired whenever it fails. You know it is up about 95% of the time. At the budget meeting nobody asks for a percentage. The plant manager wants two numbers: what does running the plant cost in a year, on average? And how much should be set aside so that a bad year does not overrun the budget?
Availability cannot answer either question, because an hour of lost production, a repair callout and a spare part all cost different amounts. This lesson puts prices on the events Lesson 6 counted, turns them into an exact cost per hour, and then simulates how much one year's cost can vary.
What you pay for¶
A repairable component costs money in two ways: at each failure (someone is called out, a part is fitted), and for as long as it is down. The system adds a third: while the whole system is down, production is lost. RePyability has a key for each:
| You pay for | Charged | Key |
|---|---|---|
| A repair: labour, a callout | at every failure of the component | repair_cost, in the component's dict |
| A replacement part | at every failure of the component | replace_cost, in the component's dict |
| The component being down: a hired stand-in, a penalty | per hour that component is down, even while the system runs | downtime_cost, in the component's dict |
| Lost production | per hour the system is down | downtime_cost_rate=, an argument of the RBD |
repair_cost and replace_cost fall due together; keeping them apart shows
labour and parts separately in the results. The two downtime prices differ in
whose downtime counts, which matters with redundancy: while one pump is
down, the other keeps the plant running.
Here is the plant, priced. Prices are plain numbers in whatever currency you use, and times are in hours:
import numpy as np
import surpyval as surv
from repyability import RepairableRBD
def unit(mttf, mttr, **prices):
"""An exponential component with the given MTTF and MTTR, and its prices."""
return {
"reliability": surv.Exponential.from_params([1 / mttf]),
"repairability": surv.Exponential.from_params([1 / mttr]),
**prices,
}
edges = [("s", "pump1"), ("s", "pump2"), ("pump1", "valve"),
("pump2", "valve"), ("valve", "t")]
plant = RepairableRBD(
edges,
{
"pump1": unit(10, 1, repair_cost=200),
"pump2": unit(10, 1, repair_cost=200),
"valve": unit(50, 2, repair_cost=500, replace_cost=1500),
},
downtime_cost_rate=1000, # lost production per hour the plant is down
)
plant.has_costs # True
flowchart LR
s((in)) --> p1["pump 1<br/>MTTF 10 h, MTTR 1 h<br/>200 per repair"]
s --> p2["pump 2<br/>MTTF 10 h, MTTR 1 h<br/>200 per repair"]
p1 & p2 --> v["valve<br/>MTTF 50 h, MTTR 2 h<br/>500 per repair, 1500 per part"]
v --> t((out))
MTTF and MTTR are the mean times to failure and to repair (an exponential
model takes the rate, one over the mean). Every price defaults to 0, so you
price only what you know; has_costs says whether anything is priced. A
misspelt key such as repair_costs raises a ValueError rather than pricing
nothing.
One component: the cost of a cycle¶
Start smaller, with one pump working alone, so that its downtime is the system's downtime. It has an MTTF of 10 h and an MTTR of 1 h, a repair costs 100, and each hour it is down loses 50 of production.
Its life is a string of cycles: up for a while (10 h on average), then it fails (pay 100), is down under repair (1 h on average, paying 50 an hour), and comes back as good as new. Because each repair makes it new, every cycle is a fresh, independent copy of the same random experiment.
flowchart LR
up["up<br/>mean 10 h"] -->|"fails: pay 100"| down["under repair<br/>mean 1 h, pay 50 per hour"]
down -->|"repaired, as good as new"| up
A cycle lasts 10 + 1 = 11 h on average and costs 100 + 50 × 1 = 150 on average. Over a long time \(t\) the pump completes about \(t/11\) cycles, costing about \(150\,t/11\) in all: 150/11 = 13.64 per hour. This is the renewal-reward theorem: when a process repeats in independent cycles, its long-run cost per unit time is the expected cost of a cycle divided by the expected length of a cycle. For one component,
where \(c_{\text{fail}}\) is the price of a failure and \(c_{\text{down}}\) the price of an hour down. Divide the two means: the average of each cycle's own cost per hour is a different number, usually larger, because short cycles have a huge cost per hour.
Split the fraction, and two quantities from Lesson 6 appear:
The pump fails \(\omega = 1/(\text{MTTF} + \text{MTTR}) = 1/11\) times per hour, and each failure costs 100. It is down a fraction \(1 - A = \text{MTTR}/(\text{MTTF} + \text{MTTR}) = 1/11\) of the hours, and each such hour costs 50. Every cost term has this shape: a rate times a price.
one = RepairableRBD(
[("s", "pump"), ("pump", "t")],
{"pump": unit(10, 1, repair_cost=100)},
downtime_cost_rate=50,
)
one.expected_cost_rate() # -> 13.64 per hour: (100 + 50 × 1) / 11
Only the means entered: the long-run rate depends on the lifetime and
repair-time distributions only through MTTF and MTTR (their shapes matter for
the spread). A repair fast enough to ignore can be modelled with
"repairability": "instant": the part still fails and pays its per-failure
prices, but is never down.
The whole plant: rates times prices¶
In a system each price is charged at its own rate, and because the expected value of a sum is the sum of the expected values, the expected costs add. RePyability's long-run cost rate is
where \(c_{\text{sys}}\) is downtime_cost_rate and \(A_{\text{sys}}\) the
system's long-run availability; for each component \(i\), \(A_i\) is its
availability, \(\omega_i\) its failure frequency, and \(c^{\text{repair}}_i\),
\(c^{\text{replace}}_i\) and \(c^{\text{down}}_i\) its three prices. Each term is
still a rate times a price.
From Lesson 6, \(A_{\text{pump}} = 10/11 = 0.9091\) and \(A_{\text{valve}} = 50/52 = 0.9615\). The plant is up when the valve is up and at least one pump is:
| Term | Rate | Price | Per hour |
|---|---|---|---|
| Lost production | 0.04641 of the hours | 1000 | 46.41 |
| Pump 1 repairs | 1/11 = 0.0909 failures per hour | 200 | 18.18 |
| Pump 2 repairs | 1/11 = 0.0909 failures per hour | 200 | 18.18 |
| Valve repairs and parts | 1/52 = 0.0192 failures per hour | 500 + 1500 | 38.46 |
| Component downtime | none priced | 0 | |
| Total | 121.23 |
A penalty of 30 per hour while a pump is in the workshop
(downtime_cost=30) would add \(2 \times (1/11) \times 30 = 5.45\) per
hour through the third term.
plant.expected_cost_rate() # -> 121.23 per hour, in the long run
1000 * (1 - plant.mean_availability()) # -> 46.41 lost production
200 / (10 + 1) # -> 18.18 each pump's repairs
(500 + 1500) / (50 + 2) # -> 38.46 the valve's repairs and parts
plant.expected_cost_rate() * 8760 # -> 1062003.8 a year of running
The plant costs 121.23 per hour, about 1.06 million over a year of continuous running (8,760 hours). That answers the manager's first question, exactly and without simulation.
The terms also show where the money goes: 38% lost production, 32% the valve's bills, 30% the pumps'. Split the lost production by cause: the plant stops when the valve is down (2/52 = 0.0385 of the hours: 38.46 per hour) or when both pumps are down while the valve is up (\((1/11)^2 \times 0.9615 = 0.0079\): 7.95 per hour). Each pump fails almost five times as often as the valve, yet the pumps cause only a sixth of the lost production: their redundancy absorbs their failures, so they cost mostly repair bills. The valve, a single point of failure, costs both.
What would a perfect part be worth?¶
expected_cost_rate accepts working_nodes: components held working, which
never fail and so cost no repairs and no downtime. The difference from the
normal rate is what a perfect version of the part would be worth:
plant.expected_cost_rate(working_nodes=["pump1"]) # -> 95.1
plant.expected_cost_rate(working_nodes=["valve"]) # -> 44.63
A pump that never failed would save 121.23 − 95.10 = 26.13 per hour: its own 18.18 of repairs, plus the 7.95 of lost production when both pumps are down. A valve that never failed would save 121.23 − 44.63 = 76.61 per hour: its 38.46 of bills plus 38.14 of lost production, about 671,000 a year.
No real improvement makes a part perfect, so these are ceilings: the
improvement potential of Lesson 4, in money. A better
valve, condition monitoring or a spare on the shelf can save at most 76.61
per hour, and is not worth buying for more. broken_nodes asks the opposite
question, what an hour costs while a part is out of action:
plant.expected_cost_rate(broken_nodes=["pump1"]) # -> 182.52 pump 1 away for overhaul
Each hour pump 1 is away costs 61.29 more than a normal hour.
A year is not an average year¶
The exact rate is a long-run average, but a budget covers one year, in which the number of failures and the length of each outage are random. The manager's second question needs the distribution of a year's cost.
cost(t_simulation, mc_samples=..., seed=...) simulates mc_samples
independent windows of
t_simulation hours, each starting with every component new and working. It
charges every price as it falls due and returns a
CostResult holding one total cost per window in
samples. Take N = 400 years. (The plant's expected cost over a window
is exact too, plant.expected_cost(8760.0), and by default a run's
mean_interval is that cost, with no error; this lesson is about the
simulation's own error, so its runs pass control_variate=False, which
keeps the simulations' own.)
year = plant.cost(t_simulation=8760.0, mc_samples=400, seed=0,
control_variate=False)
year.mean # -> 1064784.4 mean cost of a year
year.cost_rate # -> 121.55 the mean per hour, against the exact 121.23
year.std # -> 55340.5 how much one year's cost varies
year.percentile(90) # -> 1138386.7 nine years in ten cost less than this
The 90th percentile is the cost that 90% of the simulated years stay below. Grouping the 400 years into bins 50,000 wide shows the shape:
counts, bins = np.histogram(year.samples, bins=np.linspace(0.85e6, 1.25e6, 9))
counts # array([ 1, 9, 34, 109, 143, 78, 26, 0])
counts.sum() # -> 400 every simulated year falls in a bin
xychart-beta
title "Cost of a year, 400 simulated years"
x-axis "cost of the year, millions (bin centre)" ["0.875", "0.925", "0.975", "1.025", "1.075", "1.125", "1.175", "1.225"]
y-axis "number of years" 0 --> 140
bar [1, 9, 34, 109, 143, 78, 26, 0]
The mean, 1.065 million, agrees with the exact 1.062 million within the simulation's error (next section). The spread is the new information: a typical year lands within about 55,000 of the mean, and one year in ten costs more than 1.138 million. That is the budget to set if you can accept an overrun one year in ten; a budget equal to the mean is overrun about every other year.
The result also breaks the mean down:
year.by_category
# {'repair': 402661.75, 'replace': 253743.75, 'preventive': 0.0,
# 'inspection': 0.0, 'component_downtime': 0.0,
# 'system_downtime': 408378.88, 'setup': 0.0}
year.by_component
# {'pump1': 158803.5, 'pump2': 159277.0, 'valve': 338325.0}
sum(year.by_category.values()) # -> 1064784.4 the categories add up to the mean
The categories agree, within simulation error, with the exact terms times
8,760 (lost production: 46.41 × 8,760 ≈ 406,500). by_component holds each
component's own charges; lost production belongs to the system and is not
split among components. (plant.availability(...) runs the same simulation
and carries this result as its cost.)
Two different uncertainties¶
The simulation holds two uncertainties that are easy to confuse:
- How much a year's cost varies:
stdandpercentile. This is a property of the plant, since years genuinely differ. More simulations estimate it better but do not make it smaller. - How precisely the mean is known:
mean_se, the standard error \(\text{std}/\sqrt{N}\), andmean_interval(confidence), the range mean ± 1.96 ×mean_sefor 95% confidence. This comes from simulating only \(N\) years, and shrinks like \(1/\sqrt{N}\).
Run a quick 100 years with the same seed, which are the first 100 of the 400, and compare them with the 400:
quick = plant.cost(t_simulation=8760.0, mc_samples=100, seed=0,
control_variate=False)
quick.std # -> 58535.9
quick.percentile(90) # -> 1140300.5
quick.mean_se # -> 5853.6
year.mean_se # -> 2767.0
interval = year.mean_interval(0.95)
interval.lower # -> 1059361.1
interval.upper # -> 1070207.6
interval.lower < plant.expected_cost_rate() * 8760 < interval.upper # True
| N = 100 | N = 400 | Shrinks as N grows? | |
|---|---|---|---|
std |
58,536 | 55,340 | no |
percentile(90) |
1,140,301 | 1,138,387 | no |
mean_se |
5,854 | 2,767 | yes, like \(1/\sqrt{N}\) |
mean_interval(0.95) |
1,047,830 to 1,070,776 | 1,059,361 to 1,070,208 | yes |
Four times the simulations halved the standard error and the width of the
interval, and left the spread where it was. The exact cost of an average
year, 1,062,004, lies inside both intervals, as a 95% interval does about
nineteen times in twenty. Check mean_interval before
quoting a simulated mean; quote a percentile when the question is about one
year.
Short windows start new¶
Two things separate a simulated mean from rate × window. One is simulation error, above. The other is the start: every simulated window begins with all components new and working, not in the long-run mix of up and down.
The start pulls two ways. No outage is under way at time 0, so lost production builds up only as the first failures arrive. Meanwhile failures come slightly faster than on average: a pump that is up fails at 1/10 per hour, whereas in the long run, counting its time under repair, it fails 1/11 per hour. For this plant the first effect is larger, so a short window costs less per hour. An 8-hour shift shows it:
shift = plant.cost(t_simulation=8.0, mc_samples=10_000, seed=0,
control_variate=False)
shift.cost_rate # -> 112.2 per hour, against 121.23
shift.by_category["system_downtime"] / 8 # -> 35.5 lost production per hour, against 46.41
shift_interval = shift.mean_interval(0.95)
shift_interval.upper / 8 # -> 115.83 per hour, below 121.23
The whole 95% interval lies below 121.23, so the gap is real. Within a few repair times (about ten hours here) the plant forgets how it started and costs accrue at the long-run rate, so the start-up saving is a fixed sum (about (121.23 − 112.2) × 8 ≈ 72 in the shift) that does not grow with the window. Over a year it is lost in the noise: the year's mean came out 2,800 above rate × window, where the start-up saving alone would put it 72 below, so that gap is simulation error (1.0 standard errors).
Short windows also have lopsided distributions, in which the mean is not a typical value:
shift.percentile(50) # -> 400.0 the median shift: two pump repairs
shift.mean # -> 897.70
shift.percentile(90) # -> 3030.1
Prices that vary¶
Real invoices vary: one pump repair takes an hour, another needs a day and parts. A per-failure price can therefore be a distribution instead of a number, typically fitted to past invoices with surpyval (fitting belongs there, not in RePyability). Here a LogNormal stands in for such a fit. The logarithm of an invoice is Normal with mean \(\mu\) and standard deviation 0.5, so the invoice's mean is \(e^{\mu + 0.5^2/2}\), and \(\mu = \ln 200 - 0.125\) makes it 200:
invoices = surv.LogNormal.from_params([np.log(200) - 0.5**2 / 2, 0.5])
invoices.mean() # -> 200.0
invoices.qf(0.05) # -> 77.5 one invoice in twenty is below this
invoices.qf(0.95) # -> 401.7 and one in twenty above this
variable = RepairableRBD(
edges,
{
"pump1": unit(10, 1, repair_cost=invoices),
"pump2": unit(10, 1, repair_cost=invoices),
"valve": unit(50, 2, repair_cost=500, replace_cost=1500),
},
downtime_cost_rate=1000,
)
variable.expected_cost_rate() # -> 121.23 unchanged
Three things follow:
- The exact rate uses only the price's mean (expected values add, again), so it is unchanged at 121.23.
- The simulation draws a fresh price at every failure, which widens the spread.
- The prices come from a random stream of their own, so with the same seed the failures and outages are exactly those simulated before. Only the prices differ, which makes the comparison fair.
priced = variable.cost(t_simulation=8760.0, mc_samples=100, seed=0,
control_variate=False)
priced.by_category["system_downtime"] == quick.by_category["system_downtime"] # True
priced.std # -> 58231.4 against 58535.9 at a fixed 200
The spread barely moved. A year holds about 2 × 8,760/11 ≈ 1,600 pump
repairs, and their price variations largely cancel. Price uncertainty matters
more for rare, expensive events, where there are too few charges to average
out. Only the per-failure prices may be distributions: downtime_cost and
downtime_cost_rate must be numbers, since they are rates and the outage
lengths already make them random.
Buying redundancy¶
So far the plant was already built. When you design one, the question is how much redundancy to buy, and the answer depends on how long you will own it. A redundant unit is bought once, and then it runs: it fails, is repaired and costs money for as long as you own it, while it saves the lost production of the outages it covers. The total cost of ownership over a horizon \(H\) counts both:
with \(a_i\) each component's price, its acquisition_cost. Take a transfer
pump with an MTTF of 1000 h and an MTTR of 10 h, 500 per repair, bought for
20,000, where an hour without pumping loses 100. Is a second pump, in
parallel, worth buying?
One pump is down \(U = 10/1010 = 0.0099\) of the hours and fails \(\omega = 1/1010\) times per hour. It costs \(500/1010 = 0.495\) per hour in repairs and \(100 \times 0.0099 = 0.990\) per hour in lost production: 1.485 per hour. Over ten years (87,600 h) that is \(20{,}000 + 87{,}600 \times 1.485 = 150{,}099\).
Two pumps fail and are repaired independently, so both are down \(U^2 = 9.8 \times 10^{-5}\) of the hours: lost production falls to \(100 \times U^2 = 0.0098\) per hour, while the repairs double to 0.990. Over ten years: \(40{,}000 + 87{,}600 \times 0.9999 = 127{,}591\). The second pump saves 22,508.
Per hour it saves \(100 \times (U - U^2) = 0.980\) of lost production and costs 0.495 of repairs, a net 0.485, which repays its 20,000 after \(20{,}000 / 0.485 = 41{,}200\) hours, about 4.7 years. Over one year it does not pay.
A third pump would cover only the times both others are down: it saves at most \(100 \times U^2 (1 - U) = 0.0097\) per hour, less than the 0.495 its own repairs cost. It never pays, whatever its price and however long you own the plant.
pump = {
"reliability": surv.Exponential.from_params([1 / 1000]),
"repairability": surv.Exponential.from_params([1 / 10]),
"repair_cost": 500,
"acquisition_cost": 20000, # paid once
}
line = RepairableRBD([("s", "pump"), ("pump", "t")], {"pump": pump},
downtime_cost_rate=100)
line.expected_cost_rate() # -> 1.4851 per hour: buying is not a running cost
line.total_cost(87600) # -> 150099.0 one pump, ten years
best = line.allocate_redundancy(87600)
best.units # {'pump': 2}
best.total_cost # -> 127591.4
best.acquisition_cost # -> 40000.0
line.allocate_redundancy(8760).units # {'pump': 1} one year
allocate_redundancy weighs every design this way: for each component that
has an acquisition_cost, how many copies give the lowest total over the
horizon. Each design is scored exactly, as you just did by hand, so it works
on any diagram.
The third pump shows why this is a harder search than the one in Lesson 9. There, a more reliable design was always better, and only the budget stopped the copies. Here each copy costs as much as the one before and saves a fraction \(U\) of what the one before saved, so the total falls and then rises. The rule you used for the third pump bounds the search: the \(k{+}1\)-th copy of a unit down a fraction \(U\) of the time saves at most \(H \times c_{\text{sys}} \times U^k (1 - U)\) (what it would save were everything else perfect), so once that is less than a copy's price and running cost, no further copy is worth trying. Within those bounds the search is exhaustive, and its answer a proven optimum.
A contract may instead demand a minimum availability. Then the cheapest design that meets it may need copies that do not pay for themselves:
line.allocate_redundancy(87600, min_availability=0.99999).units # {'pump': 3}
Two cautions. The copies are assumed to fail independently: a common cause
(Lesson 5) sets a floor that no number of copies gets below, so price it in
before trusting a design with many copies. And unless you give a
discount_rate, money spent in ten years counts the same as money spent now
(see the pitfall below).
Pitfalls¶
Costs are not discounted unless you ask
By default a cost next year counts the same as one today. That is fine
for a year's budget; to compare designs over a 20-year life, give
total_cost and allocate_redundancy a discount_rate: a continuous
rate per unit time of the models, math.log(1.07) / 8760 for 7% a year
with lives in hours. It can change which design wins, as a copy bought
now saves money later. The rest (expected_cost, the cost rates, the
simulated costs) stays undiscounted.
A mean alone hides risk
Designs with the same expected_cost_rate() can have very different bad
years, and over a short window the mean is not even a typical value.
Budget with a percentile from cost().
Price lost production on the system, not on the parts
Lost production is often the largest cost, and how much of it a part
causes depends on the structure. Pricing it on the pumps
(downtime_cost=1000 on each) would charge \(1000/11 = 90.91\) per hour
per pump, 181.82 in all, for outages that, thanks to the redundancy, lose
7.95 per hour of production. Use downtime_cost_rate, and let the
structure decide when production stops.
Simulated numbers carry error
cost() estimates what expected_cost_rate() computes exactly. Check
mean_interval before quoting a simulated mean, and remember that every
simulated window starts new.
Summary¶
Key ideas
- A repairable system costs money at each failure (
repair_cost,replace_cost), per hour a component is down (downtime_cost), and per hour the system is down (downtime_cost_rate). - Renewal-reward: in the long run, cost per hour = expected cost of a cycle / expected length of a cycle. One component costs \((c_{\text{fail}} + c_{\text{down}} \cdot \text{MTTR}) / (\text{MTTF} + \text{MTTR})\) per hour.
- A system's long-run cost rate is a sum of rates times prices,
\(c_{\text{sys}}(1 - A_{\text{sys}}) + \sum_i \omega_i (c^{\text{repair}}_i + c^{\text{replace}}_i) + \sum_i (1 - A_i)\,c^{\text{down}}_i\).
expected_cost_rate()computes it exactly; its terms show where the money goes, andworking_nodesprices a perfect part. - One year's cost is random:
cost()simulates its distribution. Budget with a percentile, not the mean. stdandpercentiledescribe the plant and do not shrink with \(N\);mean_seandmean_intervaldescribe the simulation and shrink like \(1/\sqrt{N}\).- An uncertain price enters the exact rate through its mean; in a simulation it widens the spread without changing the failures.
- The total cost of ownership over \(H\) hours is the price of buying
the components (
acquisition_cost) plus \(H\) times the cost rate:total_cost(H). A redundant copy pays when the lost production it saves over the horizon exceeds its price and its own running cost;allocate_redundancy(H)finds the design with the lowest total.
Exercises¶
1. A conveyor drive runs alone: MTTF 200 h and MTTR 5 h, both exponential. Each failure costs 300 in labour and 700 in parts, and each hour it is down loses 400 of production. (a) What does it cost per hour in the long run, and per year? (b) A spare drive kept on site would make each swap so quick that you can treat repair as instant. What is the spare worth per year?
Answer
(a) A cycle lasts 205 h on average and costs 300 + 700 + 400 × 5 = 3,000, so the rate is 3000/205 = 14.63 per hour, about 128,200 a year; two-thirds of it is lost production.
(b) With instant repair the drive is never down, but it still fails every 200 h on average: 1000/200 = 5.00 per hour. The spare saves 9.63 per hour, about 84,400 a year; holding it pays if it costs less.
drive = RepairableRBD(
[("s", "drive"), ("drive", "t")],
{"drive": unit(200, 5, repair_cost=300, replace_cost=700)},
downtime_cost_rate=400,
)
drive.expected_cost_rate() # -> 14.63
drive.expected_cost_rate() * 8760 # -> 128195.1
swap = RepairableRBD(
[("s", "drive"), ("drive", "t")],
{"drive": {"reliability": surv.Exponential.from_params([1 / 200]),
"repairability": "instant",
"repair_cost": 300, "replace_cost": 700}},
downtime_cost_rate=400,
)
swap.expected_cost_rate() # -> 5.0
(drive.expected_cost_rate() - swap.expected_cost_rate()) * 8760 # -> 84395.1
2. For the plant, year.percentile(90) is about 1.138 million. What does
this number mean for the plant manager? How often would a budget equal to
the mean be overrun, and is 1.138 million the worst case?
Answer
Nine simulated years in ten cost less than 1.138 million, so a budget of that size is overrun about one year in ten. A year's cost is spread almost symmetrically about its mean, so a budget of the mean would be overrun about one year in two. It is not a worst case: one year in ten costs more, and the costliest simulated year reached 1.195 million. It is also an estimate from 400 years; another seed would move it by a few thousand.
np.mean(year.samples > year.percentile(90)) # -> 0.1
np.mean(year.samples > year.mean) # -> 0.5
year.samples.max() # -> 1195381.3
3. How many simulated years would you need to know the plant's expected yearly cost to within ±1,000, with 95% confidence? What would you do instead?
Answer
The interval's half-width is 1.96 × std/√N, so you need
N ≥ (1.96 × 55,340/1,000)² ≈ 11,800 years, about thirty times the 400
simulated. Use the exact rate instead: plant.expected_cost_rate() * 8760
gives 1,062,003.8 with no simulation error at all. Simulate for the
spread, which the exact rate cannot give.
round((1.96 * year.std / 1000) ** 2) # -> 11765
4. Would a second valve, in parallel with the first, pay for itself in this plant? At what price of lost production would it start to?
Answer
With two valves the plant stops only when both pumps or both valves are down: \(1 - A_{\text{sys}} = 1 - (1 - (1/11)^2)(1 - (2/52)^2) = 0.00973\). Lost production falls from 46.41 to 9.73 per hour, a saving of 36.68, but the second valve fails as often as the first and adds 38.46 per hour of bills. Net, the plant costs 1.79 per hour more (123.02 against 121.23), before paying for the valve. Availability rises from 0.954 to 0.990, yet so does the running cost. The valve pays once an hour of lost production costs more than \(38.46 / (0.04641 - 0.00973) \approx 1049\).
edges2 = [("s", "pump1"), ("s", "pump2"),
("pump1", "valve1"), ("pump1", "valve2"),
("pump2", "valve1"), ("pump2", "valve2"),
("valve1", "t"), ("valve2", "t")]
valve = unit(50, 2, repair_cost=500, replace_cost=1500)
two_valves = RepairableRBD(
edges2,
{"pump1": unit(10, 1, repair_cost=200),
"pump2": unit(10, 1, repair_cost=200),
"valve1": valve, "valve2": valve},
downtime_cost_rate=1000,
)
two_valves.mean_availability() # -> 0.9903
two_valves.expected_cost_rate() # -> 123.02
(2000 / 52) / (plant.mean_unavailability() - two_valves.mean_unavailability()) # -> 1048.7
5. The plant's valve (MTTF 50 h, MTTR 2 h, 500 + 1500 per failure) costs 30,000 to buy. Over ten years (87,600 h), is a second valve in parallel worth buying (a) as priced, at 1000 per hour of lost production; (b) at 5000? (c) From what price of lost production does it pay?
Answer
With both pumps as they are, a second valve lowers the plant's unavailability by \(A_{\text{pumps}} (U_v - U_v^2) = 0.99174 \times (1/26 - 1/26^2) = 0.03668\), and adds \(2000/52 = 38.46\) per hour of bills. Over \(H\) hours it pays when \(c_{\text{sys}} \times 0.03668 \times H\) exceeds \(30{,}000 + 38.46\,H\): for ten years, when \(c_{\text{sys}} > (30{,}000/87{,}600 + 38.46) / 0.03668 = 1058\).
(a) At 1000 per hour, no: it saves 36.68 per hour and costs 38.46 before its price (Exercise 4). (b) At 5000, yes: it saves 183.38 per hour, and pays for itself in about 207 hours. (c) From about 1058 per hour, only a little above Exercise 4's 1049: over ten years its price adds only 0.34 per hour. A third valve would save at most \(5000 \times (1/26)^2 (25/26) = 7.11\) per hour, less than its bills, so even at 5000 two valves are best.
def plant_at(price_of_an_hour):
return RepairableRBD(
edges,
{"pump1": unit(10, 1, repair_cost=200),
"pump2": unit(10, 1, repair_cost=200),
"valve": unit(50, 2, repair_cost=500, replace_cost=1500,
acquisition_cost=30000)},
downtime_cost_rate=price_of_an_hour,
)
plant_at(1000).allocate_redundancy(87600).units # {'valve': 1}
plant_at(5000).allocate_redundancy(87600).units # {'valve': 2}
plant_at(1057).allocate_redundancy(87600).units # {'valve': 1}
plant_at(1059).allocate_redundancy(87600).units # {'valve': 2}
Where next¶
- Lesson 8: Maintaining on purpose. So far a part is touched only when it fails. Replacing it before it fails adds a fourth kind of cost, the planned replacement, and changes the other three: fewer failures, but planned stops. Lesson 8 prices that trade with the same two tools.
- Costs in the user guide: every pricing option, costs drawn from distributions, instant repair, preventive maintenance and the total cost of ownership.
- Concepts: Costs for the theory in brief, and
Repairable systems for the simulation that
cost()runs.