Source code for gemseo_benchmark.results.performance_histories
# 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.
"""A class to implement a collection of performance histories."""
from __future__ import annotations
import collections.abc
import statistics
from typing import TYPE_CHECKING
from typing import Callable
import numpy
from gemseo_benchmark.results.performance_history import PerformanceHistory
if TYPE_CHECKING:
from collections.abc import Iterable
from collections.abc import Sequence
from matplotlib.axes import Axes
from gemseo_benchmark import MarkeveryType
from gemseo_benchmark.results.history_item import HistoryItem
[docs]
class PerformanceHistories(collections.abc.MutableSequence):
"""A collection of performance histories."""
__histories: list[PerformanceHistory]
"""The performance histories of the collection."""
def __init__(self, *histories: PerformanceHistory) -> None:
"""
Args:
*histories: The performance histories.
""" # noqa: D205, D212, D415
self.__histories = list(histories)
def __getitem__(self, index: int) -> PerformanceHistory:
return self.__histories[index]
def __setitem__(self, index: int, history: PerformanceHistory) -> None:
self.__histories[index] = history
def __delitem__(self, index: int) -> None:
del self.__histories[index]
def __len__(self) -> int:
return len(self.__histories)
[docs]
def insert(self, index: int, history: PerformanceHistory) -> None:
"""Insert a performance history in the collection.
Args:
index: The index where to insert the performance history.
history: The performance history.
"""
self.__histories.insert(index, history)
def __get_equal_size_histories(self) -> PerformanceHistories:
"""Return the histories extended to the maximum size."""
return PerformanceHistories(*[
history.extend(self.__maximum_size) for history in self
])
@property
def __maximum_size(self) -> int:
"""The maximum size of a history."""
return max(len(history) for history in self)
[docs]
def compute_minimum(self) -> PerformanceHistory:
"""Return the itemwise minimum history of the collection.
Returns:
The itemwise minimum history of the collection.
"""
return self.__compute_itemwise_statistic(min)
[docs]
def compute_maximum(self) -> PerformanceHistory:
"""Return the itemwise maximum history of the collection.
Returns:
The itemwise maximum history of the collection.
"""
return self.__compute_itemwise_statistic(max)
[docs]
def compute_median(self, compute_low_median: bool = True) -> PerformanceHistory:
"""Return the itemwise median history of the collection.
Args:
compute_low_median: Whether to compute the low median
(rather than the high median).
Returns:
The itemwise median history of the collection.
"""
if compute_low_median:
return self.__compute_itemwise_statistic(statistics.median_low)
return self.__compute_itemwise_statistic(statistics.median_high)
def __compute_itemwise_statistic(
self,
statistic_computer: Callable[[tuple[HistoryItem]], HistoryItem],
) -> PerformanceHistory:
"""Return the history of an itemwise statistic of the collection.
The histories are extended to the same length before being split.
Args:
statistic_computer: The computer of the statistic.
Returns:
The history of the itemwise statistic.
"""
history = PerformanceHistory()
history.items = [
statistic_computer(items)
for items in zip(*[
history.items for history in self.__get_equal_size_histories()
])
]
return history
[docs]
def cumulate_minimum(self) -> PerformanceHistories:
"""Return the histories of the minimum."""
return PerformanceHistories(*[
history.compute_cumulated_minimum() for history in self
])
[docs]
def plot_algorithm_histories(
self,
axes: Axes,
algorithm_name: str,
max_feasible_objective: float,
plot_all: bool,
color: str,
marker: str,
alpha: float,
markevery: MarkeveryType,
) -> float | None:
"""Plot the histories associated with an algorithm.
Args:
axes: The axes on which to plot the performance histories.
algorithm_name: The name of the algorithm.
max_feasible_objective: The ordinate for infeasible history items.
plot_all: Whether to plot all the performance histories.
color: The color of the plot.
marker: The marker type of the plot.
alpha: The opacity level for overlapping areas.
Refer to the Matplotlib documentation.
markevery: The sampling parameter for the markers of the plot.
Refer to the Matplotlib documentation.
Returns:
The minimum feasible objective value of the median history
or ``None`` if the median history has no feasible item.
"""
# Plot all the performance histories
if plot_all:
for history in self:
history.plot(axes, only_feasible=True, color=color, alpha=alpha)
# Get the minimum history, starting from its first feasible item
abscissas, minimum_items = self.compute_minimum().get_plot_data(feasible=True)
minimum_ordinates = [item.objective_value for item in minimum_items]
# Get the maximum history for the same abscissas as the minimum history
maximum_items = self.compute_maximum().items
# Replace the infeasible objective values with the maximum value
# N.B. Axes.fill_between requires finite values, that is why the infeasible
# objective values are replaced with a finite value rather than with infinity.
maximum_ordinates = self.__get_penalized_objective_values(
maximum_items, abscissas, max_feasible_objective
)
# Plot the area between the minimum and maximum histories.
axes.fill_between(abscissas, minimum_ordinates, maximum_ordinates, alpha=alpha)
axes.plot(abscissas, minimum_ordinates, color=color, alpha=alpha)
# Replace the infeasible objective values with infinity
maximum_ordinates = self.__get_penalized_objective_values(
maximum_items, abscissas, numpy.inf
)
axes.plot(abscissas, maximum_ordinates, color=color, alpha=alpha)
# Plot the median history
median = self.compute_median()
median.plot(
axes,
only_feasible=True,
label=algorithm_name,
color=color,
marker=marker,
markevery=markevery,
)
# Return the smallest objective value of the median
_, history_items = median.get_plot_data(feasible=True)
if history_items:
return min(history_items).objective_value
return None
@staticmethod
def __get_penalized_objective_values(
history_items: Sequence[HistoryItem], indexes: Iterable[int], value: float
) -> list[float]:
"""Return the objectives of history items, replacing the infeasible ones.
Args:
history_items: The history items.
indexes: The 1-based indexes of the history items.
value: The replacement for infeasible objective values.
Returns:
The objective values.
"""
return [
history_items[index - 1].objective_value
if history_items[index - 1].is_feasible
else value
for index in indexes
]