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

1""" 

2Plot a set of :class:`~moptipy.evaluation.ert.Ert` objects into one figure. 

3 

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. 

9 

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 

22 

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 

28 

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 

36 

37 

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. 

70 

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) 

155 

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 

175 

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) 

182 

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!") 

189 

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!") 

195 

196 if (x_dim is None) or (y_dim is None) or (len(source) <= 0): 

197 raise ValueError("Illegal state?") 

198 

199 # determine the style groups 

200 groups: list[Styler] = [] 

201 instances.finalize() 

202 algorithms.finalize() 

203 

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) 

223 

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)]) 

235 

236 if instances.count > 1: 

237 groups.append(instances) 

238 if algorithms.count > 1: 

239 groups.append(algorithms) 

240 

241 if len(groups) > 0: 

242 groups.sort() 

243 groups[0].set_line_color(distinct_colors_func) 

244 

245 if len(groups) > 1: 

246 groups[1].set_line_dash(distinct_line_dashes_func) 

247 

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) 

254 

255 # we will collect all lines to plot in plot_list 

256 plot_list: list[dict] = [] 

257 

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) 

263 

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) 

268 

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 

282 

283 font_size_0: Final[float] = importance_to_font_size_func(0) 

284 

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) 

289 

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) 

297 

298 max_y = y_axis.get_pinf_replacement() 

299 

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 

309 

310 x_axis.apply(axes, "x") 

311 y_axis.apply(axes, "y") 

312 

313 if legend: 

314 handles: list[Artist] = [] 

315 

316 for g in groups: 

317 g.add_to_legend(handles.append) 

318 g.has_style = False 

319 

320 if instances.has_style: 

321 instances.add_to_legend(handles.append) 

322 if algorithms.has_style: 

323 algorithms.add_to_legend(handles.append) 

324 

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) 

330 

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