Coverage for moptipy / examples / jssp / plots.py: 11%

236 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-24 08:49 +0000

1"""The JSSP-example specific plots.""" 

2 

3from math import inf 

4from statistics import median 

5from typing import Any, Callable, Final, Iterable 

6 

7from pycommons.io.console import logger 

8from pycommons.io.path import Path, directory_path 

9from pycommons.types import type_error 

10 

11import moptipy.utils.plot_utils as pu 

12from moptipy.evaluation.axis_ranger import AxisRanger 

13from moptipy.evaluation.base import F_NAME_RAW, F_NAME_SCALED, TIME_UNIT_MILLIS 

14from moptipy.evaluation.end_results import EndResult 

15from moptipy.evaluation.end_statistics import EndStatistics, from_end_results 

16from moptipy.evaluation.plot_end_results import plot_end_results 

17from moptipy.evaluation.plot_end_statistics_over_parameter import ( 

18 plot_end_statistics_over_param, 

19) 

20from moptipy.evaluation.plot_progress import plot_progress 

21from moptipy.evaluation.progress import Progress 

22from moptipy.evaluation.progress import from_logs as pr_from_logs 

23from moptipy.evaluation.stat_run import STAT_MEAN_ARITH, StatRun 

24from moptipy.evaluation.stat_run import from_progress as sr_from_progress 

25from moptipy.examples.jssp.plot_gantt_chart import plot_gantt_chart 

26from moptipy.utils.lang import Lang 

27from moptipy.utils.plot_defaults import importance_to_font_size 

28 

29 

30def plot_end_makespans(end_results: Iterable[EndResult], 

31 name_base: str, 

32 dest_dir: str, 

33 instance_sort_key: Callable = lambda x: x, 

34 algorithm_sort_key: Callable = lambda x: x, 

35 algorithm_namer: Callable[[str], str] = lambda x: x, 

36 x_label_location: float = 1.0) \ 

37 -> list[Path]: 

38 """ 

39 Plot a set of end result boxes/violins functions into one chart. 

40 

41 :param end_results: the iterable of end results 

42 :param name_base: the basic name 

43 :param dest_dir: the destination directory 

44 :param instance_sort_key: the sort key function for instances 

45 :param algorithm_sort_key: the sort key function for algorithms 

46 :param algorithm_namer: the name function for algorithms receives an 

47 algorithm ID and returns an instance name; default=identity function 

48 :param x_label_location: the location of the label of the x-axis 

49 :returns: the list of generated files 

50 """ 

51 logger(f"beginning to plot chart {name_base}.") 

52 if not isinstance(end_results, Iterable): 

53 raise type_error(end_results, "end_results", Iterable) 

54 if not isinstance(name_base, str): 

55 raise type_error(name_base, "name_base", str) 

56 if not isinstance(dest_dir, str): 

57 raise type_error(dest_dir, "dest_dir", str) 

58 if not callable(instance_sort_key): 

59 raise type_error(instance_sort_key, "instance_sort_key", call=True) 

60 if not callable(algorithm_sort_key): 

61 raise type_error(algorithm_sort_key, "algorithm_sort_key", call=True) 

62 

63 algorithms: set[str] = set() 

64 instances: set[str] = set() 

65 pairs: set[str] = set() 

66 for er in end_results: 

67 algorithms.add(er.algorithm) 

68 instances.add(er.instance) 

69 pairs.add(f"{er.algorithm}+{er.instance}") 

70 

71 n_algos: Final[int] = len(algorithms) 

72 n_insts: Final[int] = len(instances) 

73 n_pairs: Final[int] = len(pairs) 

74 if n_pairs != (n_algos * n_insts): 

75 raise ValueError( 

76 f"found {n_algos} algorithms and {n_insts} instances, " 

77 f"but only {n_pairs} algorithm-instance pairs!") 

78 

79 if n_algos >= 16: 

80 raise ValueError(f"{n_algos} are just too many algorithms...") 

81 max_insts: Final[int] = 16 // n_algos 

82 insts: Final[list[str]] = sorted(instances, key=instance_sort_key) 

83 result: Final[list[Path]] = [] 

84 

85 for lang in Lang.all_langs(): 

86 lang.set_current() 

87 figure, plots = pu.create_figure_with_subplots( 

88 items=n_insts, max_items_per_plot=max_insts, max_rows=5, 

89 max_cols=1, max_width=8.6, default_height_per_row=2.5) 

90 

91 for plot, start_inst, end_inst, _, _, _ in plots: 

92 instances.clear() 

93 instances.update(insts[start_inst:end_inst]) 

94 plot_end_results( 

95 end_results=[er for er in end_results 

96 if er.instance in instances], 

97 figure=plot, 

98 dimension=F_NAME_SCALED, 

99 instance_sort_key=instance_sort_key, 

100 algorithm_sort_key=algorithm_sort_key, 

101 y_label_location=1.0, 

102 x_label_location=x_label_location, 

103 algorithm_namer=algorithm_namer) 

104 

105 result.extend(pu.save_figure(fig=figure, 

106 file_name=lang.filename(name_base), 

107 dir_name=dest_dir)) 

108 del figure 

109 

110 logger(f"finished plotting chart {name_base}.") 

111 result.sort() 

112 return result 

113 

114 

115def plot_stat_gantt_charts( 

116 end_results: Iterable[EndResult], 

117 results_dir: str, 

118 name_base: str, 

119 dest_dir: str, 

120 instance_sort_key: Callable = lambda x: x, 

121 statistic: Callable[[Iterable[int | float]], 

122 int | float] = median) -> list[Path]: 

123 """ 

124 Plot a set of Gantt charts following a specific statistics. 

125 

126 :param end_results: the iterable of end results 

127 :param results_dir: the result directory 

128 :param name_base: the basic name 

129 :param dest_dir: the destination directory 

130 :param instance_sort_key: the sort key function for instances 

131 :param statistic: the statistic to use 

132 :returns: the list of generated files 

133 """ 

134 logger(f"beginning to plot stat chart {name_base}.") 

135 if not isinstance(end_results, Iterable): 

136 raise type_error(end_results, "end_results", Iterable) 

137 if not isinstance(results_dir, str): 

138 raise type_error(results_dir, "results_dir", str) 

139 if not isinstance(name_base, str): 

140 raise type_error(name_base, "name_base", str) 

141 if not isinstance(dest_dir, str): 

142 raise type_error(dest_dir, "dest_dir", str) 

143 if not callable(instance_sort_key): 

144 raise type_error(instance_sort_key, "instance_sort_key", call=True) 

145 if not callable(statistic): 

146 raise type_error(statistic, "statistic", call=True) 

147 

148 results: Final[list[Path]] = [] # the list of generated files 

149 

150 # gather all the data 

151 data: Final[dict[str, list[EndResult]]] = {} 

152 algorithm: str | None = None 

153 for er in end_results: 

154 if algorithm is None: 

155 algorithm = er.algorithm 

156 elif algorithm != er.algorithm: 

157 raise ValueError( 

158 f"found two algorithms: {algorithm} and {er.algorithm}!") 

159 if er.instance in data: 

160 data[er.instance].append(er) 

161 else: 

162 data[er.instance] = [er] 

163 if algorithm is None: 

164 raise ValueError("Did not encounter any algorithm?") 

165 instances: Final[list[str]] = sorted(data.keys(), key=instance_sort_key) 

166 del end_results 

167 

168 # get the median runs 

169 stat_runs: list[Path] = [] 

170 results_dir = directory_path(results_dir) 

171 for instance in instances: 

172 runs: list[EndResult] = data[instance] 

173 runs.sort() 

174 med = statistic([er.best_f for er in runs]) 

175 best: int | float = inf 

176 solution: EndResult | None = None 

177 for er in runs: 

178 current = abs(er.best_f - med) 

179 if current < best: 

180 best = current 

181 solution = er 

182 if solution is None: 

183 raise ValueError( 

184 f"found no {name_base} end result for instance {instance}.") 

185 path: Path = solution.path_to_file(results_dir) 

186 path.enforce_file() 

187 stat_runs.append(path) 

188 del data 

189 del instances 

190 if len(stat_runs) < 0: 

191 raise ValueError("empty set of runs?") 

192 

193 # plot the gantt charts 

194 for lang in Lang.all_langs(): 

195 lang.set_current() 

196 

197 figure, plots = pu.create_figure_with_subplots( 

198 items=len(stat_runs), max_items_per_plot=1, max_cols=2, 

199 max_rows=4, max_width=8.6, max_height=11.5) 

200 

201 for plot, start, end, _, _, _ in plots: 

202 if start != (end - 1): 

203 raise ValueError(f"{start} != {end} - 1") 

204 

205 args: dict[str, Any] = { 

206 "gantt": stat_runs[start], 

207 "figure": plot, 

208 } 

209 if statistic is min: 

210 args["markers"] = None 

211 else: 

212 args["info"] = lambda gantt: \ 

213 Lang.current().format_str("gantt_info_short", gantt=gantt) 

214 if len(plots) > 2: 

215 args["importance_to_font_size_func"] = lambda i: \ 

216 0.9 * importance_to_font_size(i) 

217 

218 plot_gantt_chart(**args) 

219 

220 results.extend(pu.save_figure(fig=figure, 

221 file_name=lang.filename(name_base), 

222 dir_name=dest_dir)) 

223 

224 logger("done plotting stat gantt charts.") 

225 return results 

226 

227 

228def plot_progresses(results_dir: str, 

229 algorithms: Iterable[str], 

230 name_base: str, 

231 dest_dir: str, 

232 time_unit: str = TIME_UNIT_MILLIS, 

233 log_time: bool = True, 

234 instance_sort_key: Callable = lambda x: x, 

235 algorithm_sort_key: Callable = lambda x: x, 

236 x_label_location: float = 0.0, 

237 include_runs: bool = False, 

238 algorithm_namer: Callable[[str], str] = lambda x: x) \ 

239 -> list[Path]: 

240 """ 

241 Plot a set of end result boxes/violins functions into one chart. 

242 

243 :param results_dir: the directory with the log files 

244 :param algorithms: the set of algorithms to plot together 

245 :param name_base: the basic name 

246 :param dest_dir: the destination directory 

247 :param time_unit: the time unit to plot 

248 :param log_time: should the time axis be scaled logarithmically? 

249 :param instance_sort_key: the sort key function for instances 

250 :param algorithm_sort_key: the sort key function for algorithms 

251 :param x_label_location: the location of the x-labels 

252 :param include_runs: should we include the pure runs as well? 

253 :param algorithm_namer: the name function for algorithms receives an 

254 algorithm ID and returns an instance name; default=identity function 

255 :returns: the list of generated files 

256 """ 

257 logger(f"beginning to plot chart {name_base}.") 

258 if not isinstance(results_dir, str): 

259 raise type_error(results_dir, "results_dir", str) 

260 if not isinstance(algorithms, Iterable): 

261 raise type_error(algorithms, "algorithms", Iterable) 

262 if not isinstance(name_base, str): 

263 raise type_error(name_base, "name_base", str) 

264 if not isinstance(dest_dir, str): 

265 raise type_error(dest_dir, "dest_dir", str) 

266 if not isinstance(time_unit, str): 

267 raise type_error(time_unit, "time_unit", str) 

268 if not isinstance(log_time, bool): 

269 raise type_error(log_time, "log_time", bool) 

270 if not callable(instance_sort_key): 

271 raise type_error(instance_sort_key, "instance_sort_key", call=True) 

272 if not callable(algorithm_sort_key): 

273 raise type_error(algorithm_sort_key, "algorithm_sort_key", call=True) 

274 if not isinstance(x_label_location, float): 

275 raise type_error(x_label_location, "x_label_location", float) 

276 if not isinstance(include_runs, bool): 

277 raise type_error(include_runs, "include_runs", bool) 

278 if not callable(algorithm_namer): 

279 raise type_error(algorithm_namer, "algorithm_namer", call=True) 

280 

281 # get the data 

282 spath: Final[Path] = directory_path(results_dir) 

283 progresses: Final[list[Progress]] = [] 

284 for algorithm in sorted(algorithms, key=algorithm_sort_key): 

285 progresses.extend(pr_from_logs(spath.resolve_inside( 

286 algorithm), time_unit=time_unit, f_name=F_NAME_RAW)) 

287 if len(progresses) <= 0: 

288 raise ValueError(f"did not find log files in dir {results_dir!r}.") 

289 

290 stat_runs: Final[list[Progress | StatRun]] = [] 

291 sr_from_progress(progresses, STAT_MEAN_ARITH, 

292 stat_runs.append, False, False) 

293 if len(stat_runs) <= 0: 

294 raise ValueError( 

295 f"failed to compile stat runs from dir {results_dir!r}.") 

296 if include_runs: 

297 stat_runs.extend(progresses) 

298 del progresses 

299 instances: Final[list[str]] = sorted({sr.instance for sr in stat_runs}, 

300 key=instance_sort_key) 

301 if len(instances) <= 0: 

302 raise ValueError(f"no instances in dir {results_dir!r}.") 

303 algos: Final[list[str]] = sorted({sr.algorithm for sr in stat_runs}, 

304 key=algorithm_sort_key) 

305 if len(set(algorithms).difference(algos)) > 0: 

306 raise ValueError( 

307 f"found the {len(algos)} algorithms {algos}, but expected " 

308 f"algorithms {algorithms}.") 

309 

310 results: Final[list[Path]] = [] # the list of generated files 

311 

312 # plot the progress charts 

313 for lang in Lang.all_langs(): 

314 lang.set_current() 

315 figure, plots = pu.create_figure_with_subplots( 

316 items=len(instances), max_items_per_plot=1, max_cols=2, 

317 max_rows=4, max_width=8.6, max_height=11.5) 

318 

319 for plot, start, end, _, _, _ in plots: 

320 if start != (end - 1): 

321 raise ValueError(f"{start} != {end} - 1") 

322 inst = instances[start] 

323 plot_progress( 

324 progresses=[sr for sr in stat_runs if sr.instance == inst], 

325 figure=plot, 

326 x_axis=AxisRanger.for_axis_func(log_scale=log_time), 

327 importance_to_font_size_func=lambda i: 

328 0.9 * importance_to_font_size(i), 

329 algorithm_sort_key=algorithm_sort_key, 

330 instance_sort_key=instance_sort_key, 

331 x_label_location=x_label_location, 

332 algorithm_namer=algorithm_namer) 

333 axes = pu.get_axes(plot) 

334 pu.label_box(axes, inst, x=0.5, y=1) 

335 

336 results.extend(pu.save_figure(fig=figure, 

337 file_name=lang.filename(name_base), 

338 dir_name=dest_dir)) 

339 

340 logger(f"finished plotting chart {name_base!r}.") 

341 return results 

342 

343 

344def plot_end_makespans_over_param( 

345 end_results: Iterable[EndResult], 

346 name_base: str, 

347 dest_dir: str, 

348 x_getter: Callable[[EndStatistics], int | float], 

349 algorithm_getter: Callable[[EndStatistics], str | None] = 

350 lambda es: es.algorithm, 

351 instance_sort_key: Callable = lambda x: x, 

352 algorithm_sort_key: Callable = lambda x: x, 

353 title: str | None = None, 

354 x_axis: AxisRanger | Callable[[], AxisRanger] = AxisRanger, 

355 x_label: str | None = None, 

356 x_label_location: float = 1.0, 

357 plot_single_instances: bool = True, 

358 plot_instance_summary: bool = True, 

359 legend_pos: str = "upper right", 

360 title_x: float = 0.5, 

361 y_label_location: float = 1.0) \ 

362 -> list[Path]: 

363 """ 

364 Plot the performance over a parameter. 

365 

366 :param end_results: the iterable of end results 

367 :param name_base: the basic name 

368 :param dest_dir: the destination directory 

369 :param title: the optional title 

370 :param x_getter: the function computing the x-value for each statistics 

371 object 

372 :param algorithm_getter: the algorithm getter 

373 :param instance_sort_key: the sort key function for instances 

374 :param algorithm_sort_key: the sort key function for algorithms 

375 :param x_axis: the x_axis ranger 

376 :param x_label: the x-label 

377 :param x_label_location: the location of the x-labels 

378 :param plot_single_instances: shall we plot the values for each single 

379 instance? 

380 :param plot_instance_summary: shall we plot the value over all instances? 

381 :param legend_pos: the legend position 

382 :param title_x: the title position 

383 :param y_label_location: the location of the y label 

384 :returns: the list of generated files 

385 """ 

386 logger(f"beginning to plot chart {name_base}.") 

387 if not isinstance(end_results, Iterable): 

388 raise type_error(end_results, "end_results", Iterable) 

389 if not isinstance(name_base, str): 

390 raise type_error(name_base, "name_base", str) 

391 if not isinstance(dest_dir, str): 

392 raise type_error(dest_dir, "dest_dir", str) 

393 if not callable(x_getter): 

394 raise type_error(x_getter, "x_getter", call=True) 

395 if not callable(algorithm_getter): 

396 raise type_error(algorithm_getter, "algorithm_getter", call=True) 

397 if not callable(instance_sort_key): 

398 raise type_error(instance_sort_key, "instance_sort_key", call=True) 

399 if not callable(algorithm_sort_key): 

400 raise type_error(algorithm_sort_key, "algorithm_sort_key", call=True) 

401 if not isinstance(x_label_location, float): 

402 raise type_error(x_label_location, "x_label_location", float) 

403 if not isinstance(y_label_location, float): 

404 raise type_error(y_label_location, "y_label_location", float) 

405 if (title is not None) and (not isinstance(title, str)): 

406 raise type_error(title, "title", (str, None)) 

407 if not isinstance(plot_single_instances, bool): 

408 raise type_error(plot_single_instances, "plot_single_instances", bool) 

409 if not isinstance(plot_instance_summary, bool): 

410 raise type_error(plot_instance_summary, "plot_instance_summary", bool) 

411 if not (plot_single_instances or plot_instance_summary): 

412 raise ValueError("plot_instance_summary and plot_single_instances " 

413 "cannot both be False") 

414 if not isinstance(legend_pos, str): 

415 raise type_error(legend_pos, "legend_pos", str) 

416 if not isinstance(title_x, float): 

417 raise type_error(title_x, "title_x", float) 

418 

419 logger(f"now plotting end statistics over parameter {title!r}.") 

420 

421 end_stats: Final[list[EndStatistics]] = [] 

422 if plot_single_instances: 

423 end_stats.extend(from_end_results(end_results)) 

424 if plot_instance_summary: 

425 end_stats.extend(from_end_results( 

426 end_results, join_all_instances=True)) 

427 if len(end_stats) <= 0: 

428 raise ValueError("no end statistics records to plot!") 

429 result: list[Path] = [] 

430 

431 for lang in Lang.all_langs(): 

432 lang.set_current() 

433 figure = pu.create_figure(width=5.5) 

434 

435 axes = plot_end_statistics_over_param( 

436 data=end_stats, figure=figure, 

437 algorithm_getter=algorithm_getter, 

438 x_axis=x_axis, 

439 x_getter=x_getter, 

440 x_label=x_label, 

441 x_label_location=x_label_location, 

442 algorithm_sort_key=algorithm_sort_key, 

443 instance_sort_key=instance_sort_key, 

444 legend_pos=legend_pos, 

445 y_label_location=y_label_location) 

446 if title is not None: 

447 pu.label_box(axes, title, x=title_x, y=1) 

448 

449 result.extend(pu.save_figure(fig=figure, 

450 file_name=lang.filename(name_base), 

451 dir_name=dest_dir)) 

452 

453 logger(f"done plotting end statistics over parameter {title!r}.") 

454 return result