# 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 typing import Any
from docstring_inheritance import GoogleDocstringInheritanceMeta
from numpy import array
from numpy import inf
from numpy import ndarray
from gemseo.core.factory import Factory
from gemseo.utils.base_enum import BaseEnum
LOGGER = logging.getLogger(__name__)
ToleranceIntervalSide = BaseEnum("ToleranceIntervalSide", "LOWER UPPER BOTH")
[docs]class ToleranceInterval(metaclass=GoogleDocstringInheritanceMeta):
"""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.
"""
def __init__(
self,
size: int,
) -> None:
""".. # noqa: D205 D212 D415
Args:
size: The number of samples.
"""
self.__size = size
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.
"""
raise NotImplementedError
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.
"""
raise NotImplementedError
def _compute(
self,
coverage: float,
alpha: float,
size: int,
side: ToleranceIntervalSide,
) -> tuple[ndarray, ndarray]:
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 == ToleranceIntervalSide.LOWER:
lower = self._compute_lower_bound(coverage, alpha, size)
upper = inf
elif side == ToleranceIntervalSide.UPPER:
lower = -inf
upper = self._compute_upper_bound(coverage, alpha, size)
elif side == ToleranceIntervalSide.BOTH:
coverage = (coverage + 1.0) / 2.0
alpha = alpha / 2.0
lower = self._compute_lower_bound(coverage, alpha, size)
upper = self._compute_upper_bound(coverage, alpha, size)
else:
raise ValueError("The type of tolerance interval is incorrect.")
return array([lower]), array([upper])
[docs] def compute(
self,
coverage: float,
confidence: float = 0.95,
side: ToleranceIntervalSide = ToleranceIntervalSide.BOTH,
) -> tuple[ndarray, ndarray]:
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:
"""A factory of :class:`.ToleranceInterval`."""
def __init__(self) -> None: # noqa: D107
self.__factory = Factory(
ToleranceInterval, ("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.error(
"Failed to create class %s with arguments %s", class_name, args
)
raise TypeError(
"Cannot create {}ToleranceInterval with arguments {}".format(
class_name, args
)
)
[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 self.__factory.get_class(f"{name}ToleranceInterval")