Coverage for moptipy / evaluation / plot_ert.py: 71%
170 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +0000
1"""
2Plot a set of :class:`~moptipy.evaluation.ert.Ert` objects into one figure.
4The (empirically estimated) Expected Running Time (ERT, see
5:mod:`~moptipy.evaluation.ert`) is a function that tries to give an estimate
6how long a given algorithm setup will need (y-axis) to achieve given solution
7qualities (x-axis). It uses a set of runs of the algorithm on the problem to
8make this estimate under the assumption of independent restarts.
101. Kenneth V. Price. Differential Evolution vs. The Functions of the 2nd ICEO.
11 In Russ Eberhart, Peter Angeline, Thomas Back, Zbigniew Michalewicz, and
12 Xin Yao, editors, *IEEE International Conference on Evolutionary
13 Computation,* April 13-16, 1997, Indianapolis, IN, USA, pages 153-157.
14 IEEE Computational Intelligence Society. ISBN: 0-7803-3949-5.
15 doi: https://doi.org/10.1109/ICEC.1997.592287
162. Nikolaus Hansen, Anne Auger, Steffen Finck, Raymond Ros. *Real-Parameter
17 Black-Box Optimization Benchmarking 2010: Experimental Setup.*
18 Research Report RR-7215, INRIA. 2010. inria-00462481.
19 https://hal.inria.fr/inria-00462481/document/
20"""
21from typing import Any, Callable, Final, Iterable, cast
23import numpy as np
24from matplotlib.artist import Artist # type: ignore
25from matplotlib.axes import Axes # type: ignore
26from matplotlib.figure import Figure # type: ignore
27from pycommons.types import type_error
29import moptipy.utils.plot_defaults as pd
30import moptipy.utils.plot_utils as pu
31from moptipy.evaluation.axis_ranger import AxisRanger
32from moptipy.evaluation.base import get_algorithm, get_instance, sort_key
33from moptipy.evaluation.ert import Ert
34from moptipy.evaluation.styler import Styler
35from moptipy.utils.lang import Lang
38def plot_ert(erts: Iterable[Ert],
39 figure: Axes | Figure,
40 x_axis: AxisRanger | Callable[[str], AxisRanger]
41 = AxisRanger.for_axis,
42 y_axis: AxisRanger | Callable[[str], AxisRanger]
43 = AxisRanger.for_axis,
44 legend: bool = True,
45 distinct_colors_func: Callable[[int], Any] =
46 pd.distinct_colors,
47 distinct_line_dashes_func: Callable[[int], Any] =
48 pd.distinct_line_dashes,
49 importance_to_line_width_func: Callable[[int], float] =
50 pd.importance_to_line_width,
51 importance_to_alpha_func: Callable[[int], float] =
52 pd.importance_to_alpha,
53 importance_to_font_size_func: Callable[[int], float] =
54 pd.importance_to_font_size,
55 x_grid: bool = True,
56 y_grid: bool = True,
57 x_label: str | Callable[[str], str] | None = Lang.translate,
58 x_label_inside: bool = True,
59 y_label: str | Callable[[str], str] | None =
60 Lang.translate_func("ERT"),
61 y_label_inside: bool = True,
62 instance_sort_key: Callable[[str], Any] = lambda x: x,
63 algorithm_sort_key: Callable[[str], Any] = lambda x: x,
64 instance_namer: Callable[[str], str] = lambda x: x,
65 algorithm_namer: Callable[[str], str] = lambda x: x,
66 instance_priority: float = 0.666,
67 algorithm_priority: float = 0.333) -> Axes:
68 """
69 Plot a set of Ert functions into one chart.
71 :param erts: the iterable of Ert functions
72 :param figure: the figure to plot in
73 :param x_axis: the x_axis ranger
74 :param y_axis: the y_axis ranger
75 :param legend: should we plot the legend?
76 :param distinct_colors_func: the function returning the palette
77 :param distinct_line_dashes_func: the function returning the line styles
78 :param importance_to_line_width_func: the function converting importance
79 values to line widths
80 :param importance_to_alpha_func: the function converting importance
81 values to alphas
82 :param importance_to_font_size_func: the function converting importance
83 values to font sizes
84 :param x_grid: should we have a grid along the x-axis?
85 :param y_grid: should we have a grid along the y-axis?
86 :param x_label: a callable returning the label for the x-axis, a label
87 string, or `None` if no label should be put
88 :param x_label_inside: put the x-axis label inside the plot (so that
89 it does not consume additional vertical space)
90 :param y_label: a callable returning the label for the y-axis, a label
91 string, or `None` if no label should be put
92 :param y_label_inside: put the y-axis label inside the plot (so that
93 it does not consume additional horizontal space)
94 :param instance_sort_key: the sort key function for instances
95 :param algorithm_sort_key: the sort key function for algorithms
96 :param instance_namer: the name function for instances receives an
97 instance ID and returns an instance name; default=identity function
98 :param algorithm_namer: the name function for algorithms receives an
99 algorithm ID and returns an instance name; default=identity function
100 :param instance_priority: the style priority for instances
101 :param algorithm_priority: the style priority for algorithms
102 :returns: the axes object to allow you to add further plot elements
103 """
104 # Before doing anything, let's do some type checking on the parameters.
105 # I want to ensure that this function is called correctly before we begin
106 # to actually process the data. It is better to fail early than to deliver
107 # some incorrect results.
108 if not isinstance(erts, Iterable):
109 raise type_error(erts, "erts", Iterable)
110 if not isinstance(figure, Axes | Figure):
111 raise type_error(figure, "figure", (Axes, Figure))
112 if not isinstance(legend, bool):
113 raise type_error(legend, "legend", bool)
114 if not callable(distinct_colors_func):
115 raise type_error(
116 distinct_colors_func, "distinct_colors_func", call=True)
117 if not callable(distinct_line_dashes_func):
118 raise type_error(
119 distinct_line_dashes_func, "distinct_line_dashes_func", call=True)
120 if not callable(distinct_line_dashes_func):
121 raise type_error(importance_to_line_width_func,
122 "importance_to_line_width_func", call=True)
123 if not callable(importance_to_alpha_func):
124 raise type_error(
125 importance_to_alpha_func, "importance_to_alpha_func", call=True)
126 if not callable(importance_to_font_size_func):
127 raise type_error(importance_to_font_size_func,
128 "importance_to_font_size_func", call=True)
129 if not isinstance(x_grid, bool):
130 raise type_error(x_grid, "x_grid", bool)
131 if not isinstance(y_grid, bool):
132 raise type_error(y_grid, "y_grid", bool)
133 if not ((x_label is None) or callable(x_label)
134 or isinstance(x_label, str)):
135 raise type_error(x_label, "x_label", (str, None), call=True)
136 if not isinstance(x_label_inside, bool):
137 raise type_error(x_label_inside, "x_label_inside", bool)
138 if not ((y_label is None) or callable(y_label)
139 or isinstance(y_label, str)):
140 raise type_error(y_label, "y_label", (str, None), call=True)
141 if not isinstance(y_label_inside, bool):
142 raise type_error(y_label_inside, "y_label_inside", bool)
143 if not callable(instance_sort_key):
144 raise type_error(instance_sort_key, "instance_sort_key", call=True)
145 if not callable(algorithm_sort_key):
146 raise type_error(algorithm_sort_key, "algorithm_sort_key", call=True)
147 if not isinstance(instance_priority, float):
148 raise type_error(instance_priority, "instance_priority", float)
149 if not isinstance(algorithm_priority, float):
150 raise type_error(algorithm_priority, "algorithm_priority", float)
151 if not callable(instance_namer):
152 raise type_error(instance_namer, "instance_namer", call=True)
153 if not callable(algorithm_namer):
154 raise type_error(algorithm_namer, "algorithm_namer", call=True)
156 # First, we try to find groups of data to plot together in the same
157 # color/style.
158 instances: Final[Styler] = Styler(
159 key_func=get_instance,
160 namer=instance_namer,
161 none_name=Lang.translate("all_insts"),
162 priority=instance_priority,
163 name_sort_function=instance_sort_key)
164 algorithms: Final[Styler] = Styler(
165 key_func=get_algorithm,
166 namer=algorithm_namer,
167 none_name=Lang.translate("all_algos"),
168 priority=algorithm_priority,
169 name_sort_function=algorithm_sort_key)
170 x_dim: str | None = None
171 y_dim: str | None = None
172 source: list[Ert] = cast("list[Ert]", erts) if isinstance(erts, list) \
173 else list(erts)
174 del erts
176 # First pass: find out the instances and algorithms
177 for ert in source:
178 if not isinstance(ert, Ert):
179 raise type_error(ert, "ert data source", Ert)
180 instances.add(ert)
181 algorithms.add(ert)
183 # Validate that we have consistent time and objective units.
184 if x_dim is None:
185 x_dim = ert.f_name
186 elif x_dim != ert.f_name:
187 raise ValueError(
188 f"F-units {x_dim} and {ert.f_name} do not fit!")
190 if y_dim is None:
191 y_dim = ert.time_unit
192 elif y_dim != ert.time_unit:
193 raise ValueError(
194 f"Time units {y_dim} and {ert.time_unit} do not fit!")
196 if (x_dim is None) or (y_dim is None) or (len(source) <= 0):
197 raise ValueError("Illegal state?")
199 # determine the style groups
200 groups: list[Styler] = []
201 instances.finalize()
202 algorithms.finalize()
204 sf: Callable[[Ert], Any] = sort_key
205 if (instances.count > 1) and (algorithms.count == 1):
206 def __x1(r: Ert, ssf=instance_sort_key) -> Any:
207 return ssf(r.instance)
208 sf = __x1
209 elif (instances.count == 1) and (algorithms.count > 1):
210 def __x2(r: Ert, ssf=algorithm_sort_key) -> Any:
211 return ssf(r.algorithm)
212 sf = __x2
213 elif (instances.count > 1) and (algorithms.count > 1):
214 def __x3(r: Ert, sas=algorithm_sort_key,
215 ias=instance_sort_key,
216 ag=algorithm_priority > instance_priority) \
217 -> tuple[Any, Any]:
218 k1 = ias(r.instance)
219 k2 = sas(r.algorithm)
220 return (k2, k1) if ag else (k1, k2)
221 sf = __x3
222 source.sort(key=sf)
224 def __set_importance(st: Styler) -> None:
225 none = 1
226 not_none = 0
227 none_lw = importance_to_line_width_func(none)
228 not_none_lw = importance_to_line_width_func(not_none)
229 st.set_line_width(lambda p: [none_lw if i <= 0 else not_none_lw
230 for i in range(p)])
231 none_a = importance_to_alpha_func(none)
232 not_none_a = importance_to_alpha_func(not_none)
233 st.set_line_alpha(lambda p: [none_a if i <= 0 else not_none_a
234 for i in range(p)])
236 if instances.count > 1:
237 groups.append(instances)
238 if algorithms.count > 1:
239 groups.append(algorithms)
241 if len(groups) > 0:
242 groups.sort()
243 groups[0].set_line_color(distinct_colors_func)
245 if len(groups) > 1:
246 groups[1].set_line_dash(distinct_line_dashes_func)
248 # If we only have <= 2 groups, we can mark None and not-None values with
249 # different importance.
250 if instances.has_none and (instances.count > 1):
251 __set_importance(instances)
252 elif algorithms.has_none and (algorithms.count > 1):
253 __set_importance(algorithms)
255 # we will collect all lines to plot in plot_list
256 plot_list: list[dict] = []
258 # set up the axis rangers
259 if callable(x_axis):
260 x_axis = x_axis(x_dim)
261 if not isinstance(x_axis, AxisRanger):
262 raise type_error(x_axis, "x_axis", AxisRanger)
264 if callable(y_axis):
265 y_axis = y_axis(y_dim)
266 if not isinstance(y_axis, AxisRanger):
267 raise type_error(y_axis, "y_axis", AxisRanger)
269 # first we collect all progress object
270 for ert in source:
271 style = pd.create_line_style()
272 for g in groups:
273 g.add_line_style(ert, style)
274 x = ert.ert[:, 0]
275 style["x"] = x
276 x_axis.register_array(x)
277 y = ert.ert[:, 1]
278 y_axis.register_array(y)
279 style["y"] = y
280 plot_list.append(style)
281 del source
283 font_size_0: Final[float] = importance_to_font_size_func(0)
285 # set up the graphics area
286 axes: Final[Axes] = pu.get_axes(figure)
287 axes.tick_params(axis="x", labelsize=font_size_0)
288 axes.tick_params(axis="y", labelsize=font_size_0)
290 # draw the grid
291 if x_grid or y_grid:
292 grid_lwd = importance_to_line_width_func(-1)
293 if x_grid:
294 axes.grid(axis="x", color=pd.GRID_COLOR, linewidth=grid_lwd)
295 if y_grid:
296 axes.grid(axis="y", color=pd.GRID_COLOR, linewidth=grid_lwd)
298 max_y = y_axis.get_pinf_replacement()
300 # plot the lines
301 for line in plot_list:
302 y = line["y"]
303 if np.isposinf(y[0]):
304 y = y.copy()
305 y[0] = max_y
306 line["y"] = y
307 axes.step(where="post", **line)
308 del plot_list
310 x_axis.apply(axes, "x")
311 y_axis.apply(axes, "y")
313 if legend:
314 handles: list[Artist] = []
316 for g in groups:
317 g.add_to_legend(handles.append)
318 g.has_style = False
320 if instances.has_style:
321 instances.add_to_legend(handles.append)
322 if algorithms.has_style:
323 algorithms.add_to_legend(handles.append)
325 axes.legend(loc="upper right",
326 handles=handles,
327 labelcolor=[art.color if hasattr(art, "color")
328 else pd.COLOR_BLACK for art in handles],
329 fontsize=font_size_0)
331 pu.label_axes(axes=axes,
332 x_label=x_label(x_dim) if callable(x_label) else x_label,
333 x_label_inside=x_label_inside,
334 x_label_location=1,
335 y_label=y_label(y_dim) if callable(y_label) else y_label,
336 y_label_inside=y_label_inside,
337 y_label_location=0,
338 font_size=font_size_0)
339 return axes