Source code for pylife.strength.fkm_load_distribution

# Copyright (c) 2019-2026 - for information on the respective copyright owner
# see the NOTICE file and/or the repository
# https://github.com/boschresearch/pylife
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

r"""Scale a load sequence for FKM nonlinear load distribution safety.

The accessors in this module multiply a stress or load sequence by the FKM
nonlinear load factor :math:`\gamma_L`.  The factor accounts for the assumed
load distribution, the assessment failure probability :math:`P_A`, and the
load probability :math:`P_L` of either ``2.5`` percent or ``50`` percent.

The FKM nonlinear guideline defines three possible methods to consider the
statistical distribution of the load:

* Normal distribution with standard deviation :math:`s_L`.
* Lognormal distribution with logarithmic standard deviation :math:`LSD_s`.
* Unknown distribution, using :math:`\gamma_L = 1.1` for
  :math:`P_L = 2.5\%`.

The corresponding accessors are ``fkm_safety_normal_from_stddev``,
``fkm_safety_lognormal_from_stddev``, and ``fkm_safety_blanket``.

The resulting scaling factor can be retrieved with
``.gamma_L(input_parameters)``, the scaled load series can be obtained with
``.scaled_load_sequence(input_parameters)``.

Notes
-----
The statistical load factors implement FKM nonlinear guideline clause 2.3.2.

Examples
--------
>>> input_parameters = pd.Series({"P_A": 1e-5, "P_L": 50, "s_L": 10, "LSD_s": 1e-2,})
>>> load_sequence = pd.Series([100.0, 150.0, 200.0], name="load")
>>> # uses input_parameters.s_L, input_parameters.P_L, input_parameters.P_A
>>> load_sequence.fkm_safety_normal_from_stddev.scaled_load_sequence(input_parameters)
0    114.9450
1    172.4175
2    229.8900
Name: load, dtype: float64

>>> # uses input_parameters.s_L, input_parameters.P_L, input_parameters.P_A
>>> load_sequence.fkm_safety_lognormal_from_stddev.scaled_load_sequence(input_parameters)
0    107.124794
1    160.687191
2    214.249588
Name: load, dtype: float64

>>> # uses input_parameters.P_L
>>> load_sequence.fkm_safety_blanket.scaled_load_sequence(input_parameters)
0    100.0
1    150.0
2    200.0
Name: load, dtype: float64
"""

__author__ = "Benjamin Maier"
__maintainer__ = __author__

import numpy as np
import pandas as pd
from pylife import PylifeSignal

[docs] @pd.api.extensions.register_dataframe_accessor("fkm_load_sequence") @pd.api.extensions.register_series_accessor("fkm_load_sequence") class FKMLoadSequence(PylifeSignal): r"""Scale load data by a constant FKM load factor. This accessor is available as ``.fkm_load_sequence`` on :class:`pandas.Series` and :class:`pandas.DataFrame` objects. It is the shared base for the FKM statistical load distribution accessors and can also be used directly when a factor :math:`\gamma_L` is already known. Signal contract: * A :class:`pandas.Series` represents one scalar load value per load step. * A :class:`pandas.DataFrame` with one column represents one scalar load value per index row. * A :class:`pandas.DataFrame` with multiple columns stores the load in the first column; additional columns, such as a stress gradient, are copied unchanged by :meth:`scaled_by_constant`. * The object must not be empty. Parameters ---------- pandas_obj : pandas.Series or pandas.DataFrame Load sequence, stress sequence, or mesh-indexed load table to scale. Load or stress values are interpreted in the units used by the subsequent assessment, typically MPa for stresses. Notes ----- Mesh-like data are commonly indexed by ``load_step`` and ``node_id``. The scaling methods preserve the original index and all non-load columns. """
[docs] def scaled_by_constant(self, gamma_L): """Scale the load sequence by the given constant ``gamma_L``. This method basically computes ``gamma_L * self._obj``. The data in ``self._obj`` is either a :class:`pandas.Series`, a :class:`pandas.DataFrame` with a single column or a pandas.DataFrame with multiple columns (usually two for stress and stress gradient). In the case of a Series or only one column, it simply scales all values by the factor ``gamma_L``. In the case of a DataFrame with multiple columns, it only scales the first column by the factor gamma_L and keeps the other columns unchanged. Returns a scaled copy of the data. Parameters ---------- gamma_L : float Scaling factor for the load sequence, dimensionless. Returns ------- pandas.Series or pandas.DataFrame Scaled copy of the load sequence. For a multi-column :class:`pandas.DataFrame`, only the first column is multiplied by ``gamma_L``. """ # `self._obj` can be either a pd.Series or a pd.DataFrame. A gradient can only be included if we have a pd.DataFrame if isinstance(self._obj, pd.DataFrame): # if the number of columns in the DataFrame is at least two (for the stress and the gradient) if len(self._obj.columns) >= 2: # only scale the first column result = self._obj.copy().astype(np.float64) result.iloc[:, 0] = result.iloc[:, 0] * gamma_L return result return self._obj * gamma_L
[docs] def maximum_absolute_load(self, max_load_independently_for_nodes=False): """Get the maximum absolute load over all nodes and load steps. This is implemented for pd.Series (where the index is just the index of the load step), pd.DataFrame with one column (where the index is a MultiIndex of load_step and node_id), and pd.DataFrame with multiple columns with the load is given in the first column. Parameters ---------- max_load_independently_for_nodes : bool, optional Flag indicating whether to compute the maximum absolute load separately for every node. If set to ``False``, a single maximum value is computed over all nodes. Default is ``False``. Returns ------- float Maximum absolute load. The unit is the same as the load sequence, typically MPa for stress values. """ # if the load sequence is a pd.Series if len(self._obj.index.names) == 1: L_max = max(abs(self._obj)) # if the load sequence is a pd.DataFrame else: # if we have a multi-indexed DataFrame with (load_step, node_id) if "node_id" in self._obj.index.names: L_max = self._obj.abs().groupby("node_id").max() else: levels = list(range(self._obj.index.nlevels - 1)) # all but the last level L_max = self._obj.abs().groupby(level=levels).max() # if there are multiple columns, select the first one if isinstance(L_max, pd.DataFrame) and len(L_max.columns) > 1: L_max = L_max.iloc[:,0] if max_load_independently_for_nodes: return L_max # take maximum over all load steps L_max = L_max.max() if isinstance(L_max, pd.DataFrame) or isinstance(L_max, pd.Series): L_max = L_max.squeeze() return float(L_max)
def _validate(self): if len(self._obj) == 0: raise AttributeError("Load series is empty.") def _validate_parameters(self, input_parameters, required_parameters): for required_parameter in required_parameters: if required_parameter not in input_parameters: raise ValueError(f"Given parameters have to include \"{required_parameter}\".") def _get_beta(self, input_parameters): r"""Compute the reliability index for the assessment failure probability. For details, refer to the FKM nonlinear document. The beta factors are also described in "A. Fischer. Bestimmung modifizierter Teilsicherheitsbeiwerte zur semiprobabilistischen Bemessung von Stahlbetonkonstruktionen im Bestand. TU Kaiserslautern, 2010" Parameters ---------- input_parameters : pd.Series Assessment parameters. The series must contain ``P_A``, the assessment failure probability as a dimensionless value. Supported values are ``1e-7``, ``1e-6``, ``1e-5``, ``7.2e-5``, ``1e-3``, ``2.3e-1``, and ``0.5``. Returns ------- float Reliability index :math:`\beta`, dimensionless. Raises ------ ValueError If ``P_A`` is not one of the tabulated probabilities. """ # list of predefined P_A and beta values P_A_beta_list = [(1e-7, 5.20), (1e-6, 4.75), (1e-5, 4.27), (7.2e-5, 3.8), (1e-3, 3.09), (2.3e-1, 0.739), (0.5, 0)] # check if given P_A values is close to any of the tabulated predefined values for P_A, beta in P_A_beta_list: if np.isclose(input_parameters.P_A, P_A): return beta # raise error if the given P_A value is not among the known ones P_A_list = [str(P_A) for P_A,_ in P_A_beta_list] raise ValueError(f"P_A={input_parameters.P_A} has to be one of "+"{"+", ".join(P_A_list)+"}.")
[docs] @pd.api.extensions.register_dataframe_accessor("fkm_safety_normal_from_stddev") @pd.api.extensions.register_series_accessor("fkm_safety_normal_from_stddev") class FKMLoadDistributionNormal(FKMLoadSequence): r"""Scale a load sequence assuming normally distributed loads. Use this accessor when the load values are normally distributed and the standard deviation :math:`s_L` is known in the same unit as the load sequence. It converts a load sequence from the reference load probability to the requested FKM load probability and assessment failure probability. Signal contract: * The accessor accepts the same non-empty :class:`pandas.Series` or :class:`pandas.DataFrame` objects as :class:`FKMLoadSequence`. * In a multi-column :class:`pandas.DataFrame`, the first column contains the load or stress values to scale; further columns are copied unchanged. Parameters ---------- pandas_obj : pandas.Series or pandas.DataFrame Load sequence or stress sequence. Stress values are typically given in MPa. See Also -------- FKMLoadDistributionLognormal : Scale loads with a lognormal distribution. FKMLoadDistributionBlanket : Scale loads when the distribution is unknown. Notes ----- Implement FKM nonlinear guideline clause 2.3.2.1. Prefer this accessor over :class:`FKMLoadDistributionLognormal` when additive scatter in load or stress is the appropriate model. """
[docs] def gamma_L(self, input_parameters): r"""Compute the scaling factor :math:`\gamma_L = (L_\text{max} + \alpha_L) / L_\text{max}`. Note that for load sequences on multiple nodes (i.e. on a full mesh), :math:`L_\text{max}` is the maximum load over all nodes and load steps, not different for different nodes. Parameters ---------- input_parameters : pandas Series The parameters to specify the upscaling method. * ``input_parameters.s_L``: standard deviation of the normal distribution * ``input_parameters.P_L``: probability in [%] of the load for which to do the assessment, one of {2.5, 50} * ``input_parameters.P_A``: probability in [%], one of {1e-7, 1e-6, 1e-5, 7.2e-5, 1e-3, 2.3e-1, 0.5} (de: Ausfallwahrscheinlichkeit) * ``input_parameters.max_load_independently_for_nodes``: optional, whether the scaling should be performed independently at every node (True), or uniformly over all nodes (False). The default value is False. Returns ------- float Load scaling factor :math:`\gamma_L`, dimensionless. Raises ------ ValueError If a required parameter is missing or ``P_A`` is not supported. """ self._validate_parameters(input_parameters, required_parameters=["P_L", "s_L", "P_A"]) beta = self._get_beta(input_parameters) # eq. 2.3-4 if np.isclose(input_parameters.P_L, 2.5): alpha_L = (0.7 * beta - 2) * input_parameters.s_L else: alpha_L = 0.7 * beta * input_parameters.s_L # eq. 2.3-5 if "max_load_independently_for_nodes" not in input_parameters: input_parameters["max_load_independently_for_nodes"] = False L_max = self.maximum_absolute_load(input_parameters.max_load_independently_for_nodes) gamma_L = (L_max + alpha_L) / L_max return gamma_L
[docs] def scaled_load_sequence(self, input_parameters): r"""Scale the load sequence with the normal-distribution factor. The following parameters are used: s_L, P_L, P_A. Parameters ---------- input_parameters : pandas Series The parameters to specify the upscaling method. * ``input_parameters.s_L``: standard deviation of the normal distribution * ``input_parameters.P_L``: probability in [%] of the load for which to do the assessment, one of {2.5, 50} * ``input_parameters.P_A``: probability in [%], one of {1e-7, 1e-6, 1e-5, 7.2e-5, 1e-3, 2.3e-1, 0.5} (de: Ausfallwahrscheinlichkeit) Returns ------- pandas.Series or pandas.DataFrame Scaled copy of the input object. The load column is multiplied by :math:`\gamma_L` according to FKM nonlinear guideline clause 2.3.2.1. Raises ------ ValueError If a required parameter is missing or ``P_A`` is not supported. """ gamma_L = self.gamma_L(input_parameters) return self.scaled_by_constant(gamma_L)
[docs] @pd.api.extensions.register_dataframe_accessor("fkm_safety_lognormal_from_stddev") @pd.api.extensions.register_series_accessor("fkm_safety_lognormal_from_stddev") class FKMLoadDistributionLognormal(FKMLoadSequence): r"""Scale a load sequence assuming lognormally distributed loads. Use this accessor when the logarithm of the load values is normally distributed and the logarithmic standard deviation :math:`LSD_s` is known. It is suited to multiplicative scatter, where the safety factor is independent of the absolute load level. Signal contract: * The accessor accepts the same non-empty :class:`pandas.Series` or :class:`pandas.DataFrame` objects as :class:`FKMLoadSequence`. * In a multi-column :class:`pandas.DataFrame`, the first column contains the load or stress values to scale; further columns are copied unchanged. Parameters ---------- pandas_obj : pandas.Series or pandas.DataFrame Load sequence or stress sequence. Stress values are typically given in MPa. See Also -------- FKMLoadDistributionNormal : Scale loads with a normal distribution. FKMLoadDistributionBlanket : Scale loads when the distribution is unknown. Notes ----- Implement FKM nonlinear guideline clause 2.3.2.2. Prefer this accessor over :class:`FKMLoadDistributionNormal` when scatter acts as a relative factor on the load level. """
[docs] def gamma_L(self, input_parameters): r"""Compute the scaling factor :math:`\gamma_L`. Parameters ---------- input_parameters : pandas Series The parameters to specify the upscaling method. * ``input_parameters.LSD_s``: standard deviation of the lognormal distribution * ``input_parameters.P_L``: probability in [%] of the load for which to do the assessment, one of {2.5, 50} * ``input_parameters.P_A``: probability in [%], one of {1e-7, 1e-6, 1e-5, 7.2e-5, 1e-3, 2.3e-1, 0.5} (de: Ausfallwahrscheinlichkeit) Returns ------- float Load scaling factor :math:`\gamma_L`, dimensionless. Raises ------ ValueError If a required parameter is missing or ``P_A`` is not supported. """ self._validate_parameters(input_parameters, required_parameters=["P_L", "LSD_s", "P_A"]) beta = self._get_beta(input_parameters) # eq. 2.3-6 if np.isclose(input_parameters.P_L, 2.5): alpha_LSD = (0.7 * beta - 2) * input_parameters.LSD_s else: alpha_LSD = 0.7 * beta * input_parameters.LSD_s # eq. 2.3-7 gamma_L = max(1, 10 ** alpha_LSD) return gamma_L
[docs] def scaled_load_sequence(self, input_parameters): r"""Scale the load sequence with the lognormal-distribution factor. The following parameters are used: LSD_s, P_L, P_A. Parameters ---------- input_parameters : pandas Series The parameters to specify the upscaling method. * ``input_parameters.LSD_s``: standard deviation of the lognormal distribution * ``input_parameters.P_L``: probability in [%] of the load for which to do the assessment, one of {2.5, 50} * ``input_parameters.P_A``: probability in [%], one of {1e-7, 1e-6, 1e-5, 7.2e-5, 1e-3, 2.3e-1, 0.5} (de: Ausfallwahrscheinlichkeit) Returns ------- pandas.Series or pandas.DataFrame Scaled copy of the input object. The load column is multiplied by :math:`\gamma_L` according to FKM nonlinear guideline clause 2.3.2.2. Raises ------ ValueError If a required parameter is missing or ``P_A`` is not supported. """ gamma_L = self.gamma_L(input_parameters) return self.scaled_by_constant(gamma_L)
[docs] @pd.api.extensions.register_dataframe_accessor("fkm_safety_blanket") @pd.api.extensions.register_series_accessor("fkm_safety_blanket") class FKMLoadDistributionBlanket(FKMLoadSequence): r"""Scale a load sequence with the FKM blanket load factor. Use this accessor when the statistical load distribution is unknown. For :math:`P_L = 2.5\%` it applies the constant blanket factor :math:`\gamma_L = 1.1`; for :math:`P_L = 50\%` it leaves the load unchanged. Signal contract: * The accessor accepts the same non-empty :class:`pandas.Series` or :class:`pandas.DataFrame` objects as :class:`FKMLoadSequence`. * In a multi-column :class:`pandas.DataFrame`, the first column contains the load or stress values to scale; further columns are copied unchanged. Parameters ---------- pandas_obj : pandas.Series or pandas.DataFrame Load sequence or stress sequence. Stress values are typically given in MPa. See Also -------- FKMLoadDistributionNormal : Scale loads with a normal distribution. FKMLoadDistributionLognormal : Scale loads with a lognormal distribution. Notes ----- Implement FKM nonlinear guideline clause 2.3.2.3. Prefer this accessor when no reliable statistical distribution parameters are available. """
[docs] def gamma_L(self, input_parameters): r"""Compute the scaling factor :math:`\gamma_L`. Parameters ---------- input_parameters : pandas Series The parameters to specify the upscaling method. * ``input_parameters.P_L``: probability in [%] of the load for which to do the assessment, has to be on of {2.5, 50}. (de: Ausfallwahrscheinlichkeit) Returns ------- float Load scaling factor :math:`\gamma_L`, dimensionless. Raises ------ ValueError If ``P_L`` is missing or is neither ``2.5`` nor ``50`` percent. """ self._validate_parameters(input_parameters, required_parameters=["P_L"]) if np.isclose(input_parameters.P_L, 2.5): # eq. 2.3-8 gamma_L = 1.1 elif np.isclose(input_parameters.P_L, 50): gamma_L = 1.0 else: raise ValueError( "fkm_safety_blanket is only possible for P_L=2.5 % " f"or P_L=50 %, not P_L={input_parameters.P_L} %" ) return gamma_L
[docs] def scaled_load_sequence(self, input_parameters): r"""Scale the load sequence with the blanket load factor. The only required input parameter is ``P_L``. Parameters ---------- input_parameters : pandas Series The parameters to specify the upscaling method. * ``input_parameters.P_L``: probability in [%] of the load for which to do the assessment, one of {2.5, 50} Returns ------- pandas.Series or pandas.DataFrame Scaled copy of the input object. The load column is multiplied by :math:`\gamma_L` according to FKM nonlinear guideline clause 2.3.2.3. Raises ------ ValueError If ``P_L`` is missing or is neither ``2.5`` nor ``50`` percent. """ gamma_L = self.gamma_L(input_parameters) # eq. 2.3-5 return self.scaled_by_constant(gamma_L)