Source code for gemseo.algos.doe.doe_quality

# 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.
"""DOE assessor."""

from __future__ import annotations

from collections import namedtuple
from operator import ge
from operator import gt
from operator import le
from operator import lt
from operator import neg
from typing import TYPE_CHECKING
from typing import Any
from typing import Final
from typing import Literal

from scipy.spatial import distance
from scipy.stats import qmc

if TYPE_CHECKING:
    from collections.abc import Callable
    from numbers import Real

    from gemseo.typing import RealArray


__EUCLIDEAN: Final[str] = "euclidean"
_DEFAULT_DISCREPANCY_TYPE_NAME: Final[str] = "CD"
_DEFAULT_POWER: Final[int] = 50


DOEMeasures = namedtuple("DOEMeasures", ["discrepancy", "mindist", "phip"])
r"""The quality measures of a DOE.

Namely :math:`\phi^p`, minimum-distance and discrepancy measures,
accessible with the attributes ``discrepancy``, ``mindist`` and ``phip``.

The smaller the quality measures, the better,
except for the minimum-distance criterion for which the larger it is the better.
"""

_measure_transformations: tuple[Callable[[float], float]] = (
    lambda x: x,
    neg,
    lambda x: x,
)
"""Transformations of quality measures into quantities to minimize."""

DiscrepancyTypeNameType = Literal["CD", "WD", "MD", "L2-star"]


[docs] class DOEQuality: """The quality of a DOE.""" measures: DOEMeasures """The quality measures of the DOE.""" def __init__( self, samples: RealArray, power: int = _DEFAULT_POWER, discrepancy_type_name: DiscrepancyTypeNameType = _DEFAULT_DISCREPANCY_TYPE_NAME, **discrepancy_options: Any, ) -> None: r""" Args: samples: The input samples of the DOE. power: The power :math:`p` of the :math:`\phi^p` criterion. discrepancy_type_name: The type of discrepancy. **discrepancy_options: The options passed to ``scipy.stats.qmc.discrepancy``. """ # noqa: D205, D212, D415 self.measures = DOEMeasures( compute_discrepancy( samples, type_name=discrepancy_type_name, **discrepancy_options, ), compute_mindist_criterion(samples), compute_phip_criterion(samples, power), ) def __repr__(self) -> str: return repr(self.measures) def __eq__(self, other_doe_quality: DOEQuality) -> bool: return all(x == y for x, y in zip(self.measures, other_doe_quality.measures)) def __lt__(self, other_doe_quality: DOEQuality) -> bool: return self.__compare(gt, other_doe_quality) def __le__(self, other_doe_quality: DOEQuality) -> bool: return self.__compare(ge, other_doe_quality) def __gt__(self, other_doe_quality: DOEQuality) -> bool: return self.__compare(lt, other_doe_quality) def __ge__(self, other_doe_quality: DOEQuality) -> bool: return self.__compare(le, other_doe_quality) def __compare( self, operator: Callable[[Real, Real], bool], other_doe_quality: DOEQuality ) -> bool: """Compare a DOE quality with another one. Args: operator: The logical operator. other_doe_quality: The other DOE quality. Returns: Whether the comparison is true. """ return ( sum( operator(transformation(field), transformation(other_field)) for field, other_field, transformation in zip( self.measures, other_doe_quality.measures, _measure_transformations, ) ) / len(self.measures) >= 0.5 )
[docs] def compute_mindist_criterion(samples: RealArray) -> float: """Compute the minimum-distance criterion of a sample set (the higher, the better). This criterion is also called *mindist*. Args: samples: The data samples. Returns: The minimum-distance criterion. """ return min(distance.pdist(samples, __EUCLIDEAN))
[docs] def compute_discrepancy( samples: RealArray, type_name: DiscrepancyTypeNameType = _DEFAULT_DISCREPANCY_TYPE_NAME, **options: Any, ) -> float: """Compute the discrepancy of a sample set (the smaller, the better). Args: samples: The data samples. type_name: The type of discrepancy. **options: The options passed to :func:`scipy.stats.qmc.discrepancy`. Returns: The discrepancy. """ return qmc.discrepancy(samples, method=type_name, **options)
[docs] def compute_phip_criterion(samples: RealArray, power: float = _DEFAULT_POWER) -> float: r"""Compute the math:`\phi^p` criterion of a sample set (the smaller, the better). See :cite:`morris1995`. Args: samples: The data samples. power: The power :math:`p` of the :math:`\phi^p` criterion. Returns: The math:`\phi^p` criterion. """ return sum(distance.pdist(samples, __EUCLIDEAN) ** (-power)) ** (1.0 / power)