Download notebook · Download runnable bundle
Dependencies: PySTRA core.
Calibrating target-reliability tables#
Calculate target reliabilities from economic and LQI assumptions, compare representative code classes, and examine reference periods and jurisdictional inputs.
Before you start: failure probabilities, reliability indices and discounted costs. This example uses PySTRA’s core dependencies. The steel-bar decision study provides the single-structure context.
[1]:
import pandas as pd
import pystra as ra
From a decision to a target table#
A code jurisdiction asks the same question many times under representative assumptions: how do target reliabilities change with the relative cost of safety, failure consequences, and uncertainty model?
PySTRA exposes this as target-reliability calibration. First, the LQI target can be calculated from the marginal model
instead of only being looked up from the rounded Fischer/Barnardo/Faber table.
[2]:
lqi_rows = []
for label, k1 in [("large", 1e-3), ("medium", 1e-4), ("small", 1e-5)]:
published = ra.decision.LQI.lookup_target(k1)
calculated = ra.decision.LQI.derive_target(k1, resistance_cov=0.4, load_cov=0.4)
lqi_rows.append(
{
"class": label,
"K1": f"{k1:.0e}",
"published beta": f"{published.beta:.1f}",
"calculated beta": f"{calculated.beta:.2f}",
"published pf": f"{published.pf:.0e}",
"calculated pf": f"{calculated.pf:.1e}",
}
)
lqi_targets = pd.DataFrame(lqi_rows)
lqi_targets
[2]:
| class | K1 | published beta | calculated beta | published pf | calculated pf | |
|---|---|---|---|---|---|---|
| 0 | large | 1e-03 | 3.1 | 3.13 | 1e-03 | 8.8e-04 |
| 1 | medium | 1e-04 | 3.7 | 3.71 | 1e-04 | 1.0e-04 |
| 2 | small | 1e-05 | 4.2 | 4.22 | 1e-05 | 1.2e-05 |
Rackwitz’s code-making formulation, restated by Steenbergen, Rózsás, and Vrouwenvelder (2018), uses a normalized life-cycle objective:
Here p = E[R] / E[S], C(p) = C0 + C1 p, and the ULS and SLS probabilities come from a lognormal resistance-demand model. A single RackwitzTargetModel therefore behaves like the linked steel-bar decision study: maximize an objective, then read off the reliability at the optimum.
[3]:
normal_moderate = ra.decision.RackwitzTargetModel(
safety_cost_ratio=0.03,
failure_cost_ratio=2.5,
)
normal_moderate_result = normal_moderate.calibrate()
# Teaching columns only; full model provenance is in .to_dict() / .metadata.
pd.DataFrame([normal_moderate_result.to_dict()])[
["method", "design", "pf", "beta", "objective", "converged"]
]
[3]:
| method | design | pf | beta | objective | converged | |
|---|---|---|---|---|---|---|
| 0 | Rackwitz/Steenbergen | 4.837457 | 0.000073 | 3.797091 | -1.941943 | True |
Before a jurisdiction publishes a target, it must also decide the reference period. Steenbergen et al. emphasize annual failure rates because they keep this choice explicit. The same annual pf maps to a different equivalent lifetime reliability depending on the dependence model; TargetReliability.for_period() compounds the annual pf to perform that conversion.
[4]:
# Each route returns a TargetReliability; .for_period() compounds the annual
# failure probability over a reference period under a chosen dependence model.
# (The fully-dependent column is a single renewal, i.e. the annual target.)
# These are LQI source-table targets (their numeric values resemble, but are
# not, the rounded JCSS benchmark classes).
targets = {
"LQI small class (pf 1e-5)": ra.decision.LQI.lookup_target(1e-5),
"LQI medium class (pf 1e-4)": ra.decision.LQI.lookup_target(1e-4),
}
pd.DataFrame(
{
"target": label,
"annual pf": t.pf,
"β 50 yr, independent": t.for_period(50).beta,
"β 50 yr, 10-yr dependence": t.for_period(50, dependence_interval=10).beta,
"β 50 yr, fully dependent": t.for_period(50, dependence_interval=50).beta,
}
for label, t in targets.items()
)
[4]:
| target | annual pf | β 50 yr, independent | β 50 yr, 10-yr dependence | β 50 yr, fully dependent | |
|---|---|---|---|---|---|
| 0 | LQI small class (pf 1e-5) | 0.00001 | 3.290596 | 3.890597 | 4.264891 |
| 1 | LQI medium class (pf 1e-4) | 0.00010 | 2.576676 | 3.290583 | 3.719016 |
A target table is just a sweep of these calibrations across representative cost and consequence classes. The calculated Rackwitz/Steenbergen table is shown beside the published rounded benchmark from the JCSS Probabilistic Model Code (related consequence/reliability classes appear in ISO 2394 and EN 1990, though the matrices are not identical). These are representative calibrations, not an exact reproduction: the published matrix is rounded to one decimal and each class reflects its own modeling judgment (consequence model, COVs, reference period), whereas this table applies one consistent objective and varies only the two cost ratios. It therefore reproduces the trend — β rises as safety gets cheaper or consequences grow — rather than each rounded cell; matching specific published values would mean fitting the per-class ratios and COVs. This is the code-jurisdiction version of the single-structure decision workflow.
Because this objective is normalized by the base construction cost, the table depends only on the relative cost and consequence ratios — it is the same for every country. Changing those ratios (or the COVs, interest, and obsolescence rates) recalibrates the table for new settings. The jurisdiction’s economy enters elsewhere — through the SWTP — which the next section uses to retarget for a new country.
[5]:
rackwitz_targets = ra.decision.RackwitzTargetModel.table()
rackwitz_beta = rackwitz_targets.pivot(
index="relative_safety_cost",
columns="failure_consequence",
values="beta",
).loc[["large", "normal", "small"], ["minor", "moderate", "large"]]
published_jcss_beta = pd.DataFrame(
{
"minor": [3.1, 3.7, 4.2],
"moderate": [3.3, 4.2, 4.4],
"large": [3.7, 4.4, 4.7],
},
index=pd.Index(["large", "normal", "small"], name="relative_safety_cost"),
)
pd.concat(
{
"calculated beta": rackwitz_beta.round(2),
"published beta": published_jcss_beta,
},
axis=1,
)
[5]:
| calculated beta | published beta | |||||
|---|---|---|---|---|---|---|
| failure_consequence | minor | moderate | large | minor | moderate | large |
| relative_safety_cost | ||||||
| large | 3.03 | 3.13 | 3.25 | 3.1 | 3.3 | 3.7 |
| normal | 3.74 | 3.80 | 3.88 | 3.7 | 4.2 | 4.4 |
| small | 4.37 | 4.40 | 4.45 | 4.2 | 4.4 | 4.7 |
Re-target for a new jurisdiction or new settings#
The two routes above split cleanly:
New settings → the normalized Rackwitz/Steenbergen objective. Cost ratios, COVs, and rates are structural and economic assumptions, not a country. Change them to recalibrate the whole table.
New country → the LQI route. A jurisdiction enters through exactly one quantity: its SWTP. Everything else (marginal safety cost, expected fatalities) is the decision scenario.
First, new settings — recalibrate the normalized table with tighter resistance variability and a higher discount rate (again a representative calibration, not exact JCSS/ISO values):
[6]:
ra.decision.RackwitzTargetModel.table(resistance_cov=0.2, load_cov=0.35, interest_rate=0.05)[
["relative_safety_cost", "failure_consequence", "beta"]
]
[6]:
| relative_safety_cost | failure_consequence | beta | |
|---|---|---|---|
| 0 | large | minor | 3.022741 |
| 1 | large | moderate | 3.117148 |
| 2 | large | large | 3.235785 |
| 3 | normal | minor | 3.755065 |
| 4 | normal | moderate | 3.807678 |
| 5 | normal | large | 3.883094 |
| 6 | small | minor | 4.399291 |
| 7 | small | moderate | 4.426015 |
| 8 | small | large | 4.469441 |
The COVs and rates above are one kind of setting. The cost classes themselves are also inputs: every cost in the model is a ratio to the base construction cost \(C_0\), so safety_costs are the \(C_1/C_0\) values (marginal safety cost) and failure_costs are the \(H/C_0\) values (failure consequence). Pass your own dictionaries to recalibrate the table for different cost and consequence assumptions — this is how you change “those numbers”:
[7]:
# safety_costs are C1/C0, failure_costs are H/C0 — set your own classes.
ra.decision.RackwitzTargetModel.table(
safety_costs={"cheap safety": 0.005, "costly safety": 0.1},
failure_costs={"low": 1.0, "high": 8.0},
)[
[
"relative_safety_cost",
"failure_consequence",
"safety_cost_ratio",
"failure_cost_ratio",
"beta",
]
]
[7]:
| relative_safety_cost | failure_consequence | safety_cost_ratio | failure_cost_ratio | beta | |
|---|---|---|---|---|---|
| 0 | cheap safety | low | 0.005 | 1.0 | 4.247738 |
| 1 | cheap safety | high | 0.005 | 8.0 | 4.346548 |
| 2 | costly safety | low | 0.100 | 1.0 | 3.396431 |
| 3 | costly safety | high | 0.100 | 8.0 | 3.583340 |
Now a new country. The helper below takes a country code and a decision scenario and returns the LQI target reliability; the only country-dependent line is the SWTP lookup. Swapping "CH" for any other built-in country (or flipping indexed) retargets it — that is the entire change needed to rebuild targets for a different jurisdiction.
Holding the scenario fixed (marginal safety cost 5000, 12 expected fatalities), a wealthier country with a higher SWTP requires a higher reliability for the same engineering decision. The rounded table β is coarse — CH/US/NO all land in one class — while the calculated β varies smoothly with the SWTP:
[8]:
def lqi_target_for_country(
country, marginal_safety_cost, expected_fatalities, *, indexed=True
):
"""Country-specific LQI target reliability.
The only country-dependent input is the SWTP; everything else describes the
decision scenario. Swap ``country`` (or ``indexed``) to retarget.
"""
lqi = ra.decision.LQI.from_country(
country,
indexed=indexed,
expected_fatalities_given_failure=expected_fatalities,
marginal_safety_cost=marginal_safety_cost,
)
calculated = ra.decision.LQI.derive_target(lqi.k1) # calculated from the marginal model
return {
"country": country,
"SWTP [10^6]": lqi.swtp.value_per_life / 1e6,
"K1": lqi.k1,
"cost class": lqi.target.cost_class, # rounded LQI source-table class
"rounded table beta": lqi.target.beta,
"calculated beta": round(calculated.beta, 2),
}
scenario = dict(marginal_safety_cost=5000.0, expected_fatalities=12.0)
country_targets = pd.DataFrame(
lqi_target_for_country(code, **scenario) for code in ["CH", "US", "NO", "JP", "CZ"]
)
country_targets
[8]:
| country | SWTP [10^6] | K1 | cost class | rounded table beta | calculated beta | |
|---|---|---|---|---|---|---|
| 0 | CH | 4.999502 | 0.000083 | small | 4.2 | 3.75 |
| 1 | US | 5.220884 | 0.000080 | small | 4.2 | 3.76 |
| 2 | NO | 6.007365 | 0.000069 | small | 4.2 | 3.79 |
| 3 | JP | 2.628652 | 0.000159 | medium | 3.7 | 3.60 |
| 4 | CZ | 1.996526 | 0.000209 | medium | 3.7 | 3.53 |
Interpretation#
Calculated targets depend on the selected uncertainty, cost, consequence and temporal-dependence models. The normalized table reproduces trends in published rounded classes; it is not an exact reproduction of every published target. SWTP records carry dated source assumptions, and jurisdictional suitability requires review for the intended study.
Use TargetReliability to retain the probability, index and provenance, and state the dependence assumption when converting reference periods. See the decision guide, API and theory.
The target calculations in this tutorial remain separate from achieved-reliability evaluation. When applying a target to a design study, retain each analysis status and verify the selected designs for the same failure event and reference period.
Continue: User guide · API reference · Theory