Safety Factor Calibration#

Pystra includes the class Calibration which can be used to obtain calibrated load safety factors, \(\gamma\), resistance safety factor, \(\phi\), and load combination factors, \(\psi\). This example demonstrates running safety and combination factor calibration problems using Pystra.

Example#

This demonstration problem is adopted from Example 4 pp 190-1 from Sorensen, J.D. (2004), Notes in Structural Reliability Theory And Risk Analysis, with the following error-fixes:

  1. The loads described as \(Q_1\) and \(Q_2\), are not the point-in-time loads, but the annual maxima distributions, from which the point-in-time loads are to be inferred.

  2. There is an error in the textbook, and the wind load \(Q_2\) is not taken as occurring \(r_2 = 360\) times per year as stated, but instead \(r_2=2\) per year.

With these adaptations, the results of the calibration here match the results specified in Sorensen (2004).

Import Libraries#

[1]:
import pystra as ra
import numpy as np

Define the limit state function for calibration#

The LSF to be supplied in LoadCombination can be any valid Pystra LSF with one additional argument: a scalar design parameter for a random variable (generally, the resistance). Note that: 1. The design parameter must be specified as a Pystra constant with any arbitrary default value, such as 1.0. 2. Only a single design parameter for the resistance random variable can be calibrated at a time.

Keeping with the LoadCombination example, the design parameter for resistance, denoted \(z\), is added to the LSF:

[2]:
def lsf(z, R, G, Q1, Q2, cg):
    return z*R - (cg*G + 0.6*Q1 + 0.3*Q2)

Define the Load and Resistance distributions#

Next follow the creation of load combination objects, as explained for the LoadCombination object.

Define the annual max distributions

[3]:
Q1max = ra.Gumbel("Q1", 1, 0.2)  # Imposed Load
Q2max = ra.Gumbel("Q2", 1, 0.4)  # Wind Load

Next specifcy the parameters of inferred point-in-time parent distributions

[4]:
Q1pit = ra.Gumbel("Q1", 0.89, 0.2)  # Imposed Load
Q2pit = ra.Gumbel("Q2", 0.77, 0.4)  # Wind Load
Q_dict = {'Q1': {'max': Q1max, 'pit': Q1pit},
          'Q2': {'max': Q2max, 'pit': Q2pit}}

And define any constants to be passed through

[5]:
cg = ra.Constant("cg", 0.4)
z = ra.Constant("z", 1)  # Design parameter for resistance with arbitrary default value

Finally, define any other random variables in the problem

[6]:
Rdist = ra.Lognormal("R", 1.0, 0.15)  # Resistance
Gdist = ra.Normal("G", 1, 0.1)  # Permanent Load (other load variable)

Instantiate LoadCombination object#

Class Calibration requires specification of a LoadCombination object to run reliability analyses for different load cases.

Setup load combination cases#

For this problem, we’re interested in investigating the reliabilities for two cases: 1. Q1_max: Q1 is maximum and Q2 is the point-in-time distribution 2. Q2_max: Q1 is the point-in-time distribution and Q2 is maximum

[7]:
loadcombinations = {'Q1_max':['Q1'], 'Q2_max':['Q2']}
[8]:
lc = ra.LoadCombination(lsf=lsf,
                        dict_dist_comb=Q_dict,
                        list_dist_resist=[Rdist],
                        list_dist_other=[Gdist],
                        list_const=[z, cg],
                        dict_comb_cases=loadcombinations)

Note that Pystra categorizes the variables for the calibration problem as follows: 1. Combination load variables: time variant load effects with a point-in-time distribution and maximum distribution 2. Resistance variables 3. Other load variables (optional): time invariant load effects 4. Constants: including the calibration parameter

Specify Nominal (or characteristic) values#

In calibration problems, the safety and combination factors are calibrated with respect to a given nominal value of load and resistance distributions. Generally, these nominal values correspond to the characteristic value of the load or resistance, e.g. 95-percentile or 5-percentile value.

Class Calibration requires specifying the nominal values as Python dictionary.

For this problem, the combination loads use a 98-percentile value for nominal, and a 5-percentile for resistance; the permanent load nominal value is considered at its mean. These are found using the ppf function in each Pystra distribution object:

[9]:
Qk = np.array([Q1max.ppf(0.98), Q2max.ppf(0.98)])
Gk = np.array([Gdist.ppf(0.5)])
Rk = np.array([Rdist.ppf(0.05)])
rvs_all = ['R', 'G', 'Q1', 'Q2', 'Q3']
dict_nom = dict(zip(rvs_all, np.concatenate([Rk, Gk, Qk])))

Specify target reliability index, \(\beta_T\)#

In calibration problems, the safety and combination factors are calibrated to obtain a lower bound safety indicated by a target reliablity index, \(\beta_T\).

Class Calibration requires specifying the target reliability index as a floating point integer.

[10]:
betaT = 4.3

This corresponds to a probability of failure of:

[11]:
ra.StdNormal.cdf(-betaT)
[11]:
8.539905471005582e-06

Calibrate safety and combination factors#

Class Calibration structures the calibration problem into three parts: 1. Calibration: this part involves projecting the design point of the LSF to correspond to the specified \(\beta_T\). 2. Estimation: this part involves estimating and calibrating the safety and combination factors, \(\phi\), \(\gamma\), and \(\psi\) using the projected design point. These factors correspond to \(\beta_T\) when utilized in structural design code equation, \(z\phi R_n \geq \sum_i \psi_i \gamma_i S_{ni}\), where \(S_{ni}\) is the nominal value of the load effect \(S_i\). 3. Design check: this part involves utilizing the estimates of \(\phi\), \(\gamma\), and \(\psi\) for the given load effects to obtain the maximum required design resistance, i.e. \(z \cdot R_n\). Then the reliabilities (or safety) corresponding to the maximum required design resistance are checked for all load cases to ensure that they are greater than \(\beta_T\). This design check is explained in more detail later in this tutorial.

Calibration algorithms#

Class Calibration implements two algorithms for calibration: optimize (default) and alpha.

Algorithm optimize (default)#

The optimize algorithm calibrates the resistance design parameter, \(z\), to obtain the design point on the LSF corresponding to \(\beta_T\) by utilizing SciPy’s inbuilt optimization algorithms.

Algorithm alpha#

The alpha algorithm calibrates the resistance design parameter, \(z\), by projecting the design point using the FORM \(\alpha\) estimates to obtain the design point on the LSF corresponding to \(\beta_T\).

Estimation algorithms#

Class Calibration implements two algorithms for estimation of safety and combination factors: coeff and matrix (default).

Algorithm matrix (default)#

The matrix algorithm utilizes the calibrated design points, \(X^*_{\beta_T}\), to estimate \(\phi\), \(\gamma\), and \(\psi\) by formulating a set of simultaneous equations using the structural design code equation, \(z\phi R_k \geq \sum_i \psi_i \gamma_i S_{ki}\), where \(S_{ki}\).

For more details, see Caprani, C. and Khan, M. S., Determination of load combination factors for the assessment of existing bridges, ICASP14 (July 2023).

The estimates of \(\phi\) and \(\gamma\) are obtained by dividing \(X^*_{\beta_T}\) with the corresponding nominal values as outlined earlier. To estimate \(\psi\), the structural design code equation is rearranged to obtain a system of simultaneous equation at the limiting condition,

\[\begin{split} \begin{bmatrix} 0 & \gamma_2 ~Q_{k2} \\ \gamma_1 ~Q_{k1} & 0 \\ \end{bmatrix} \cdot \begin{bmatrix} \psi_1 \\ \psi_2 \\ \end{bmatrix} = \begin{bmatrix} z_1\phi_1 R_k - \gamma_{G_1}~G_k - \gamma_1~ Q_{k1} \\ z_2\phi_2 R_k - \gamma_{G_1}~G_k - \gamma_2~ Q_{k2} \\ \end{bmatrix}\end{split}\]

Using numpy libraries to solve the system of simultaenous equations, estimates of \(\psi\) are obtained.

This matrix estimation algorithm provides unique values of \(\gamma\) and \(\psi\), while the \(\phi\) values can differ per load case. Calibration.run() takes an optional argument set_max (default value is False). If set_max=True, then the coeff algorithm sets the resulting dataframe of \(\phi\) to their maximum value per load case.

Algorithm coeff#

The coeff algorithm utilizes the calibrated design points, \(X^*_{\beta_T}\), to estimate \(\phi\), \(\gamma\), and \(\psi\) by comparing the coefficients. In this comparison, the design points values are mapped to the corresponding factors and nominal values, and the values of the corresponding factors are estimated, as demonstrated below. For more details refer to pp 136-42 of Sorensen, J.D. (2004), Notes in Structural Reliability Theory And Risk Analysis.

For the example problem, the design points recovered from the projection step for the two load cases are:

\[\begin{split}X^*_{\beta_T} = \begin{bmatrix} z_1 & \phi_1 R_k & \gamma_{G_1}~G_k & \gamma_1~ Q_{k1} & \psi_{1,2}~~ \gamma_2 ~Q_{k2} \\ z_2 & \phi_2 R_k & \gamma_{G_1}~G_k & \psi_{2,1}~~ \gamma_1 ~Q_{k1} & \gamma_2~ Q_{k2} \\ \end{bmatrix}\end{split}\]

Dividing by the corresponding nominal values and considering only the random variables, we get,

\[\begin{split}\begin{bmatrix} \phi_1 & \gamma_G & \gamma_1 & \psi_{1,2}~~ \gamma_2 \\ \phi_2 & \gamma_G & \psi_{2,1}~~ \gamma_1 & \gamma_2 \\ \end{bmatrix}\end{split}\]

By comparing the coefficients in the above matrix, estimates of \(\phi\), \(\gamma\), and \(\psi\) are obtained. This estimation algorithm provides unique values of \(\gamma\), while the \(\phi\) and \(\psi\) values can differ per load case. Calibration.run() takes an optional argument set_max (default value is False). If set_max=True, then the coeff algorithm sets the resulting dataframe of \(\psi\) or \(\phi\) to their maximum value per load case.

Design Check#

Calibration implements methods to check whether the a design conducted on the basis of the estimated safety factors, actually achieves a minimum safety corresponding to \(\beta_T\) for all load cases. This check is implemented as follows. The calibrated estimates of \(\phi\), \(\gamma\), and \(\psi\) can be checked to ensure that a design based on them actually achieves a lower bound safety corresponding to \(\beta_T\) as follows: 1. Using \(\phi\), \(\gamma\), \(\psi\), and the specified nominal values, estimate the design multiplier for resistance, \(z\) for each load case, \(j\) using the LSF. 2. Using the design multiplier, i.e. maximum \(z\), and corresponding load and resistance distributions, do forward reliability analysis to obtain the reliability index for each load case, \(\beta_j\). 3. Check \(\beta_j \geq \beta_T\).

Pystra implements two methods for the design check: calibration.get_design_param_factor() and calibration.calc_beta_design_param() for above step 1 and step 2, respectively.

Reliability Calibration (using coeff and optimize)#

Perform Calibration#

This configuration mirrors that of Sorensen’s (2004) solution noted above.

[12]:
calib = ra.Calibration(lc, target_beta=betaT, dict_nom_vals=dict_nom, calib_var='z',
                        est_method="coeff", calib_method="optimize",
                        print_output=False)
calib.run()
calib.print_detailed_output()


======================================================
X* =
            R     G    Q1    Q2     z
Q1_max  0.66  1.04  1.62  2.02  3.04
Q2_max  0.66  1.04  1.51  2.25  3.05

phi =
            R
Q1_max  0.85
Q2_max  0.85

gamma =
            G    Q1   Q2
Q1_max  1.04  1.07  1.1
Q2_max  1.04  1.07  1.1

psi =
           G    Q1   Q2
Q1_max  1.0  1.00  0.9
Q2_max  1.0  0.93  1.0
======================================================

The above results are in agreement with those presented in Example 4 in Sorensen, J.D. (2004), Notes in Structural Reliability Theory And Risk Analysis.

Design Check#

Here we confirm that the reliability target is met, as explained above.

[13]:
design_z = calib.get_design_param_factor()
design_beta = calib.calc_beta_design_param(np.max(design_z))
print(f"Design reliabilities = {design_beta.round(2)}")
print(f"Design Check = {design_beta.round(2)>=betaT}")
Design reliabilities = [4.31 4.3 ]
Design Check = [ True  True]

Similarly, the calibration can be done using other algorithms

Reliability Calibration (using matrix and optimize)#

[14]:
calib = ra.Calibration(lc, target_beta=betaT, dict_nom_vals=dict_nom, calib_var='z',
                        est_method="matrix", calib_method="optimize")
calib.run()
calib.print_detailed_output()


======================================================
X* =
            R     G    Q1    Q2     z
Q1_max  0.66  1.04  1.62  2.02  3.04
Q2_max  0.66  1.04  1.51  2.25  3.05

phi =
            R
Q1_max  0.85
Q2_max  0.85

gamma =
            G    Q1   Q2
Q1_max  1.04  1.07  1.1
Q2_max  1.04  1.07  1.1

psi =
           G    Q1   Q2
Q1_max  1.0  1.00  0.9
Q2_max  1.0  0.93  1.0
======================================================
[15]:
design_z = calib.get_design_param_factor()
design_beta = calib.calc_beta_design_param(np.max(design_z))
print(f"Design reliabilities = {design_beta.round(2)}")
print(f"Design Check = {design_beta.round(2)>=betaT}")
Design reliabilities = [4.31 4.3 ]
Design Check = [ True  True]

Reliability Calibration (using matrix and alpha)#

[16]:
calib = ra.Calibration(lc, target_beta=betaT, dict_nom_vals=dict_nom, calib_var='z',
                        est_method="matrix", calib_method="alpha")
calib.run()
calib.print_detailed_output()


======================================================
X* =
            R     G    Q1    Q2     z
Q1_max  0.66  1.04  1.62  2.02  3.04
Q2_max  0.66  1.04  1.51  2.25  3.05

phi =
            R
Q1_max  0.85
Q2_max  0.85

gamma =
            G    Q1   Q2
Q1_max  1.04  1.07  1.1
Q2_max  1.04  1.07  1.1

psi =
           G    Q1   Q2
Q1_max  1.0  1.00  0.9
Q2_max  1.0  0.93  1.0
======================================================
[17]:
design_z = calib.get_design_param_factor()
design_beta = calib.calc_beta_design_param(np.max(design_z))
print(f"Design reliabilities = {design_beta.round(2)}")
print(f"Design Check = {design_beta.round(2)>=betaT}")
Design reliabilities = [4.31 4.3 ]
Design Check = [ True  True]

Safety Factor Calibration for Non Linear Limit State Functions (LSFs)#

This tutorial demonstrates how Pystra can calibrate safety and combination factors for Non-Linear limit state functions.

Import Libraries#

[18]:
import pystra as ra
import numpy as np

Define the limit state function for calibration#

The LSF to be supplied in LoadCombination can be any valid Pystra LSF with one additional argument: a scalar design parameter for a random variable (generally, the resistance). Note that: 1. The design parameter must be specified as a Pystra constant with any arbitrary default value, such as 1.0. 2. Only a single design parameter for the resistance random variable can be calibrated at a time.

Keeping with the LoadCombination example, the design parameter for resistance, denoted \(z\), is added to the LSF. The LSF contains additional random variables wR and wS to account for the uncertainties within the resistance and the load effects, respectively. The non-linearity of the LSF is evident as the degree of random variables in each term of the LSF is two.

[19]:
def lsf(z, wR, wS, R, Q1, Q2):
    gX = z * wR * R - wS * (Q1 + Q2)
    return gX

Define the Load and Resistance distributions#

Next follow the creation of load combination object, as explained for the LoadCombination class.

[20]:
wR = ra.Lognormal("wR", 1.0, 0.05)
wS = ra.Lognormal("wS", 1.0, 0.10)
R = ra.Normal("R", 60, 6)  # [units]
Q1_max = ra.Normal("Q1", 30, 3)  # [units]
Q2_max = ra.Normal("Q2", 20, 2)  # [units]
Q1_pit = ra.Normal("Q1", 15, 3)  # [units]
Q2_pit = ra.Normal("Q2", 10, 2)  # [units]

z = ra.Constant("z", 1)

Set up Load Combinations#

Specify nominal values & combinations#

[21]:
rvs_all = ['wR', 'wS', 'R', 'Q1', 'Q2']
dict_nom = dict(zip(rvs_all, np.array([1.0, 1.0, R.ppf(0.05),
                                             Q1_max.ppf(0.95),
                                             Q2_max.ppf(0.95)])))

Q_dict = {'Q1': {'max': Q1_max, 'pit': Q1_pit},
      'Q2': {'max': Q2_max, 'pit': Q2_pit}}
[22]:
loadcombinations = {'Q1_max':['Q1'], 'Q2_max':['Q2']}

Instantiate LoadCombination object#

[23]:
lc = ra.LoadCombination(lsf,
                        dict_dist_comb=Q_dict,
                        list_dist_other=[wS],
                        list_dist_resist=[R, wR],
                        list_const = [z],
                        dict_comb_cases=loadcombinations)

Note that since wR is associated with the resistance, it is specified in list_dist_resist; while wS is specified in list_dist_other as it is associated with the load effects but it’s not a load combination variable.

Safety Factor calibration#

[24]:
betaT = 3.7
calib1 = ra.Calibration(lc, target_beta=betaT, dict_nom_vals=
                                dict_nom, calib_var='z',
                                est_method="matrix", calib_method="optimize")
calib1.run()

calib1.print_detailed_output()


======================================================
X* =
             R    wR   wS     Q1     Q2     z
Q1_max  44.40  0.95  1.2  33.81  11.69  1.30
Q2_max  44.76  0.95  1.2  19.16  21.85  1.16

phi =
            R    wR
Q1_max  0.89  0.95
Q2_max  0.89  0.95

gamma =
          wS    Q1    Q2
Q1_max  1.2  0.97  0.94
Q2_max  1.2  0.97  0.94

psi =
          wS    Q1    Q2
Q1_max  1.0  1.00  0.54
Q2_max  1.0  0.57  1.00
======================================================

Design Check#

[25]:
design_z1 = calib1.get_design_param_factor()
design_beta1 = calib1.calc_beta_design_param(np.max(design_z1))
print(f"Design reliabilities = {design_beta1.round(2)}")
print(f"Design Check = {design_beta1.round(2)>=betaT}")
Design reliabilities = [3.7  4.29]
Design Check = [ True  True]