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 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 numpy import ndarray

__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,
    lambda x: -x,
    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: ndarray, 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: ndarray) -> 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: ndarray, 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: ndarray, 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)