Source code for gemseo.uncertainty.statistics.tolerance_interval.distribution

# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
# Contributors:
#    INITIAL AUTHORS - initial API and implementation and/or initial
#                           documentation
#        :author: Matthias De Lozzo
#    OTHER AUTHORS   - MACROSCOPIC CHANGES
"""Computation of tolerance intervals from a data-fitted probability distribution."""

from __future__ import annotations

import logging
from abc import abstractmethod
from typing import TYPE_CHECKING
from typing import Any
from typing import NamedTuple

from numpy import array
from numpy import inf
from strenum import LowercaseStrEnum

from gemseo.core.base_factory import BaseFactory
from gemseo.utils.metaclasses import ABCGoogleDocstringInheritanceMeta

if TYPE_CHECKING:
    from numpy.typing import NDArray

LOGGER = logging.getLogger(__name__)


[docs] class ToleranceInterval(metaclass=ABCGoogleDocstringInheritanceMeta): """Computation of tolerance intervals from a data-fitted probability distribution. A :class:`.ToleranceInterval` (TI) is initialized from the number of samples used to estimate the parameters of the probability distribution and from the estimations of these parameters. A :class:`.ToleranceInterval` can be evaluated from: - a coverage defining the minimum percentage of belonging to the TI, e.g. 0.90, - a level of confidence in [0,1], e.g. 0.95, - a type of interval, either 'lower' for lower-sided TI, 'upper' for upper-sided TI or 'both' for both-sided TI. .. note:: Lower-sided tolerance intervals are used to analyse the strength of materials. They are also known as *basis tolerance limits*. In particular, the *B-value* is the lower bound of the lower-sided tolerance interval with 90%-coverage and 95%-confidence while the *A-value* is the lower bound of the lower-sided tolerance interval with 95%-coverage and 95%-confidence. """
[docs] class ToleranceIntervalSide(LowercaseStrEnum): """The side of the tolerance interval.""" LOWER = "lower" UPPER = "upper" BOTH = "both"
[docs] class Bounds(NamedTuple): """The component-wise bounds of a vector.""" lower: NDArray[float] upper: NDArray[float]
def __init__( self, size: int, ) -> None: """ Args: size: The number of samples. """ # noqa: D205 D212 D415 self.__size = size @abstractmethod def _compute_lower_bound( self, coverage: float, alpha: float, size: int, ) -> float: """Compute the lower bound of the tolerance interval. Args: coverage: A minimum percentage of belonging to the TI. alpha: ``1-alpha`` is the level of confidence in [0,1]. size: The number of samples. Returns: The lower bound of the tolerance interval. """ @abstractmethod def _compute_upper_bound( self, coverage: float, alpha: float, size: int, ) -> float: """Compute the upper bound of the tolerance interval. Args: coverage: A minimum percentage of belonging to the TI. alpha: ``1-alpha`` is the level of confidence in [0,1]. size: The number of samples. Returns: The upper bound of the tolerance interval. """ def _compute_bounds( self, coverage: float, alpha: float, size: int, ) -> tuple[float, float]: """Compute the lower and upper bounds of a both-sided tolerance interval. Args: coverage: A minimum percentage of belonging to the TI. alpha: ``1-alpha`` is the level of confidence in [0,1]. size: The number of samples. Returns: The lower and upper bounds of the both-sided tolerance interval. """ coverage = (coverage + 1.0) / 2.0 alpha /= 2.0 return ( self._compute_lower_bound(coverage, alpha, size), self._compute_upper_bound(coverage, alpha, size), ) def _compute( self, coverage: float, alpha: float, size: int, side: ToleranceIntervalSide, ) -> Bounds: r"""Compute the bounds of the tolerance interval. Args: coverage: A minimum percentage of belonging to the TI. alpha: ``1-alpha`` is the level of confidence in [0,1]. size: The number of samples. side: The type of the tolerance interval characterized by its *sides* of interest, either a lower-sided tolerance interval :math:`[a, +\infty[`, an upper-sided tolerance interval :math:`]-\infty, b]`, or a two-sided tolerance interval :math:`[c, d]`. Returns: The bounds of the tolerance interval. Raises: ValueError: If the type of tolerance interval is incorrect. """ if side == self.ToleranceIntervalSide.LOWER: lower = self._compute_lower_bound(coverage, alpha, size) upper = inf elif side == self.ToleranceIntervalSide.UPPER: lower = -inf upper = self._compute_upper_bound(coverage, alpha, size) elif side == self.ToleranceIntervalSide.BOTH: lower, upper = self._compute_bounds(coverage, alpha, size) else: msg = "The type of tolerance interval is incorrect." raise ValueError(msg) return self.Bounds(array([lower]), array([upper]))
[docs] def compute( self, coverage: float, confidence: float = 0.95, side: ToleranceIntervalSide = ToleranceIntervalSide.BOTH, ) -> Bounds: r"""Compute a tolerance interval. Args: coverage: A minimum percentage of belonging to the TI. confidence: A level of confidence in [0,1]. side: The type of the tolerance interval characterized by its *sides* of interest, either a lower-sided tolerance interval :math:`[a, +\infty[`, an upper-sided tolerance interval :math:`]-\infty, b]`, or a two-sided tolerance interval :math:`[c, d]`. Returns: The tolerance bounds. """ return self._compute(coverage, 1 - confidence, self.__size, side)
[docs] class ToleranceIntervalFactory(BaseFactory): """A factory of :class:`.ToleranceInterval`.""" _CLASS = ToleranceInterval _MODULE_NAMES = ("gemseo.uncertainty.statistics.tolerance_interval",)
[docs] def create( self, class_name: str, size: int, *args: float, ) -> ToleranceInterval: """Return an instance of :class:`.ToleranceInterval`. Args: size: The number of samples used to estimate the parameters of the probability distribution. *args: The arguments of the probability distribution. Returns: The instance of the class. Raises: TypeError: If the class cannot be instantiated. """ cls = self.get_class(class_name) try: return cls(size, *args) except TypeError: LOGGER.exception( "Failed to create class %s with arguments %s", class_name, args ) msg = f"Cannot create {class_name}ToleranceInterval with arguments {args}" raise RuntimeError(msg) from None
[docs] def get_class(self, name: str) -> type[Any]: """Return a class from its name. Args: name: The name of the class. Returns: The class. """ return super().get_class(f"{name}ToleranceInterval")