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

\[\min_p\;K_1p + P_f(p), \qquad K_1 = -\frac{dP_f(p)}{dp},\]

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:

\[Z(p)=B-C(p)-U\frac{\lambda}{\gamma}P_{f,SLS}(p)-(C(p)+A)\frac{\omega}{\gamma}-(C(p)+H)\frac{\lambda}{\gamma}P_{f,ULS}(p).\]

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