Coverage for moptipy / evaluation / end_statistics.py: 79%

671 statements  

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

1""" 

2SampleStatistics aggregated over multiple instances of `EndResult`. 

3 

4The :mod:`~moptipy.evaluation.end_results` records hold the final result of 

5a run of an optimization algorithm on a problem instance. Often, we do not 

6want to compare these single results directly, but instead analyze summary 

7statistics, such as the mean best objective value found. For this purpose, 

8:class:`EndStatistics` exists. It summarizes the singular results from the 

9runs into a record with the most important statistics. 

10""" 

11import argparse 

12import os.path 

13from dataclasses import dataclass 

14from itertools import starmap 

15from math import ceil, inf, isfinite 

16from typing import Callable, Final, Generator, Iterable, Iterator, cast 

17 

18from pycommons.ds.sequences import reiterable 

19from pycommons.io.console import logger 

20from pycommons.io.csv import ( 

21 SCOPE_SEPARATOR, 

22 csv_column, 

23 csv_column_or_none, 

24 csv_scope, 

25 csv_select_scope, 

26 csv_select_scope_or_none, 

27 csv_str_or_none, 

28 csv_val_or_none, 

29) 

30from pycommons.io.csv import CsvReader as CsvReaderBase 

31from pycommons.io.csv import CsvWriter as CsvWriterBase 

32from pycommons.io.path import Path, file_path, write_lines 

33from pycommons.math.sample_statistics import ( 

34 KEY_MEAN_ARITH, 

35 KEY_STDDEV, 

36 SampleStatistics, 

37 from_samples, 

38 from_single_value, 

39) 

40from pycommons.math.sample_statistics import CsvReader as StatReader 

41from pycommons.math.sample_statistics import CsvWriter as StatWriter 

42from pycommons.math.sample_statistics import getter as stat_getter 

43from pycommons.strings.string_conv import ( 

44 num_or_none_to_str, 

45 str_to_num, 

46) 

47from pycommons.types import ( 

48 check_int_range, 

49 type_error, 

50 type_name_of, 

51) 

52 

53from moptipy.api.logging import ( 

54 KEY_ALGORITHM, 

55 KEY_BEST_F, 

56 KEY_GOAL_F, 

57 KEY_INSTANCE, 

58 KEY_LAST_IMPROVEMENT_FE, 

59 KEY_LAST_IMPROVEMENT_TIME_MILLIS, 

60 KEY_MAX_FES, 

61 KEY_MAX_TIME_MILLIS, 

62 KEY_TOTAL_FES, 

63 KEY_TOTAL_TIME_MILLIS, 

64) 

65from moptipy.evaluation._utils import ( 

66 _check_max_time_millis, 

67) 

68from moptipy.evaluation.base import ( 

69 DESC_ALGORITHM, 

70 DESC_ENCODING, 

71 DESC_INSTANCE, 

72 DESC_OBJECTIVE_FUNCTION, 

73 F_NAME_RAW, 

74 F_NAME_SCALED, 

75 KEY_ENCODING, 

76 KEY_N, 

77 KEY_OBJECTIVE_FUNCTION, 

78 MultiRunData, 

79 motipy_footer_bottom_comments, 

80) 

81from moptipy.evaluation.end_results import ( 

82 DESC_BEST_F, 

83 DESC_GOAL_F, 

84 DESC_LAST_IMPROVEMENT_FE, 

85 DESC_LAST_IMPROVEMENT_TIME_MILLIS, 

86 DESC_MAX_FES, 

87 DESC_MAX_TIME_MILLIS, 

88 DESC_TOTAL_FES, 

89 DESC_TOTAL_TIME_MILLIS, 

90 EndResult, 

91) 

92from moptipy.evaluation.end_results import from_csv as end_results_from_csv 

93from moptipy.evaluation.end_results import from_logs as end_results_from_logs 

94from moptipy.utils.help import moptipy_argparser 

95from moptipy.utils.math import try_int, try_int_div 

96 

97#: The key for the best F. 

98KEY_BEST_F_SCALED: Final[str] = KEY_BEST_F + "scaled" 

99#: The key for the number of successful runs. 

100KEY_N_SUCCESS: Final[str] = "successN" 

101#: The key for the success FEs. 

102KEY_SUCCESS_FES: Final[str] = "successFEs" 

103#: The key for the success time millis. 

104KEY_SUCCESS_TIME_MILLIS: Final[str] = "successTimeMillis" 

105#: The key for the ERT in FEs. 

106KEY_ERT_FES: Final[str] = "ertFEs" 

107#: The key for the ERT in milliseconds. 

108KEY_ERT_TIME_MILLIS: Final[str] = "ertTimeMillis" 

109 

110 

111@dataclass(frozen=True, init=False, order=False, eq=False) 

112class EndStatistics(MultiRunData): 

113 """ 

114 Statistics over end results of one or multiple algorithm*instance setups. 

115 

116 If one algorithm*instance is used, then `algorithm` and `instance` are 

117 defined. Otherwise, only the parameter which is the same over all recorded 

118 runs is defined. 

119 """ 

120 

121 #: The statistics about the best encountered result. 

122 best_f: SampleStatistics 

123 #: The statistics about the last improvement FE. 

124 last_improvement_fe: SampleStatistics 

125 #: The statistics about the last improvement time. 

126 last_improvement_time_millis: SampleStatistics 

127 #: The statistics about the total number of FEs. 

128 total_fes: SampleStatistics 

129 #: The statistics about the total time. 

130 total_time_millis: SampleStatistics 

131 #: The goal objective value. 

132 goal_f: SampleStatistics | int | float | None 

133 #: best_f / goal_f if goal_f is consistently defined and always positive. 

134 best_f_scaled: SampleStatistics | None 

135 #: The number of successful runs, if goal_f != None, else None. 

136 n_success: int | None 

137 #: The FEs to success, if n_success > 0, None otherwise. 

138 success_fes: SampleStatistics | None 

139 #: The time to success, if n_success > 0, None otherwise. 

140 success_time_millis: SampleStatistics | None 

141 #: The ERT if FEs, while is inf if n_success=0, None if goal_f is None, 

142 #: and finite otherwise. 

143 ert_fes: int | float | None 

144 #: The ERT if milliseconds, while is inf if n_success=0, None if goal_f 

145 #: is None, and finite otherwise. 

146 ert_time_millis: int | float | None 

147 #: The budget in FEs, if every run had one; None otherwise. 

148 max_fes: SampleStatistics | int | None 

149 #: The budget in milliseconds, if every run had one; None otherwise. 

150 max_time_millis: SampleStatistics | int | None 

151 

152 def __init__(self, 

153 algorithm: str | None, 

154 instance: str | None, 

155 objective: str | None, 

156 encoding: str | None, 

157 n: int, 

158 best_f: SampleStatistics, 

159 last_improvement_fe: SampleStatistics, 

160 last_improvement_time_millis: SampleStatistics, 

161 total_fes: SampleStatistics, 

162 total_time_millis: SampleStatistics, 

163 goal_f: float | int | SampleStatistics | None, 

164 best_f_scaled: SampleStatistics | None, 

165 n_success: int | None, 

166 success_fes: SampleStatistics | None, 

167 success_time_millis: SampleStatistics | None, 

168 ert_fes: int | float | None, 

169 ert_time_millis: int | float | None, 

170 max_fes: SampleStatistics | int | None, 

171 max_time_millis: SampleStatistics | int | None): 

172 """ 

173 Create the end statistics of an experiment-setup combination. 

174 

175 :param algorithm: the algorithm name, if all runs are with the same 

176 algorithm 

177 :param instance: the instance name, if all runs are on the same 

178 instance 

179 :param objective: the objective name, if all runs are on the same 

180 objective function, `None` otherwise 

181 :param encoding: the encoding name, if all runs are on the same 

182 encoding and an encoding was actually used, `None` otherwise 

183 :param n: the total number of runs 

184 :param best_f: statistics about the best achieved result 

185 :param last_improvement_fe: statistics about the last improvement FE 

186 :param last_improvement_time_millis: statistics about the last 

187 improvement time 

188 :param total_fes: statistics about the total FEs 

189 :param total_time_millis: statistics about the total runtime in 

190 milliseconds 

191 :param goal_f: if the goal objective value is not defined sometimes, 

192 this will be `None`. If it is always defined and always the same, 

193 then this will be that value. If different goal values exist, then 

194 this is the `SampleStatistics` record about them 

195 :param best_f_scaled: if `goal_f` is not `None` and greater than zero, 

196 then here we provide statistics about `best_f` divided by the 

197 corresponding `goal_f` 

198 :param n_success: the number of successful runs is only defined if 

199 `goal_f` is not `None` and counts the number of runs that reach or 

200 surpass their corresponding `goal_f` 

201 :param success_fes: if `goal_f` is not `None`, 

202 then this holds statistics about the last improvement FE of only 

203 the successful runs 

204 :param success_time_millis: if `goal_f` is not `None`, then this holds 

205 statistics about the last improvement times of only the successful 

206 runs 

207 :param ert_fes: if `goal_f` is always defined, then this is the 

208 empirically estimated running time to solve the problem in FEs if 

209 `n_success>0` and `inf` otherwise 

210 :param ert_time_millis: if `goal_f` is always defined, then this is 

211 the empirically estimated running time to solve the problem in 

212 milliseconds if `n_success>0` and `inf` otherwise 

213 :param max_fes: the budget in FEs, if any 

214 :param max_time_millis: the budget in terms of milliseconds 

215 """ 

216 super().__init__(algorithm, instance, objective, encoding, n) 

217 

218 if not isinstance(best_f, SampleStatistics): 

219 raise type_error(best_f, "best_f", SampleStatistics) 

220 object.__setattr__(self, "best_f", best_f) 

221 if best_f.n != n: 

222 raise ValueError(f"best_f.n={best_f.n} != n={n}") 

223 

224 if not isinstance(last_improvement_fe, SampleStatistics): 

225 raise type_error(last_improvement_fe, "last_improvement_fe", 

226 SampleStatistics) 

227 if last_improvement_fe.n != n: 

228 raise ValueError( 

229 f"last_improvement_fe.n={last_improvement_fe.n} != n={n}") 

230 check_int_range( 

231 last_improvement_fe.minimum, "last_improvement_fe.minimum", 

232 1, 1_000_000_000_000_000) 

233 check_int_range( 

234 last_improvement_fe.maximum, "last_improvement_fe.maximum", 

235 last_improvement_fe.minimum, 1_000_000_000_000_000) 

236 object.__setattr__(self, "last_improvement_fe", last_improvement_fe) 

237 

238 if not isinstance(last_improvement_time_millis, SampleStatistics): 

239 raise type_error(last_improvement_time_millis, 

240 "last_improvement_time_millis", SampleStatistics) 

241 if last_improvement_time_millis.n != n: 

242 raise ValueError("last_improvement_time_millis.n=" 

243 f"{last_improvement_time_millis.n} != n={n}") 

244 check_int_range( 

245 last_improvement_time_millis.minimum, 

246 "last_improvement_time_millis.minimum", 

247 0, 100_000_000_000) 

248 check_int_range( 

249 last_improvement_time_millis.maximum, 

250 "last_improvement_time_millis.maximum", 

251 last_improvement_time_millis.minimum, 100_000_000_000) 

252 object.__setattr__(self, "last_improvement_time_millis", 

253 last_improvement_time_millis) 

254 

255 if not isinstance(total_fes, SampleStatistics): 

256 raise type_error(total_fes, "total_fes", SampleStatistics) 

257 if total_fes.n != n: 

258 raise ValueError( 

259 f"total_fes.n={total_fes.n} != n={n}") 

260 check_int_range( 

261 total_fes.minimum, "total_fes.minimum", 

262 last_improvement_fe.minimum, 1_000_000_000_000_000) 

263 check_int_range( 

264 total_fes.maximum, "total_fes.maximum", 

265 max(total_fes.minimum, last_improvement_fe.maximum), 

266 1_000_000_000_000_000) 

267 object.__setattr__(self, "total_fes", total_fes) 

268 

269 if not isinstance(total_time_millis, SampleStatistics): 

270 raise type_error(total_time_millis, "total_time_millis", 

271 SampleStatistics) 

272 if total_time_millis.n != n: 

273 raise ValueError( 

274 f"total_time_millis.n={total_time_millis.n} != n={n}") 

275 check_int_range( 

276 total_time_millis.minimum, "total_time_millis.minimum", 

277 last_improvement_time_millis.minimum, 100_000_000_000) 

278 check_int_range( 

279 total_time_millis.maximum, "total_time_millis.maximum", 

280 max(total_time_millis.minimum, 

281 last_improvement_time_millis.maximum), 

282 100_000_000_000) 

283 object.__setattr__(self, "total_time_millis", total_time_millis) 

284 

285 if goal_f is None: 

286 if best_f_scaled is not None: 

287 raise ValueError( 

288 "If goal_f is None, best_f_scaled must also be None, " 

289 f"but is {type(best_f_scaled)}.") 

290 if n_success is not None: 

291 raise ValueError( 

292 "If goal_f is None, n_success must also be None, " 

293 f"but is {type(n_success)}.") 

294 if success_fes is not None: 

295 raise ValueError( 

296 "If success_fes is None, best_f_scaled must also be None, " 

297 f"but is {type(success_fes)}.") 

298 if success_time_millis is not None: 

299 raise ValueError( 

300 "If success_time_millis is None, best_f_scaled " 

301 "must also be None, " 

302 f"but is {type(success_time_millis)}.") 

303 if ert_fes is not None: 

304 raise ValueError( 

305 "If goal_f is None, ert_fes must also be None, " 

306 f"but is {type(ert_fes)}.") 

307 if ert_time_millis is not None: 

308 raise ValueError( 

309 "If goal_f is None, ert_time_millis must also be None, " 

310 f"but is {type(ert_time_millis)}.") 

311 else: # goal_f is not None 

312 if isinstance(goal_f, SampleStatistics): 

313 if goal_f.n != n: 

314 raise ValueError(f"goal_f.n={goal_f.n} != n={n}") 

315 goal_f = goal_f.compact(False) 

316 if isinstance(goal_f, float): 

317 goal_f = None if goal_f <= (-inf) else try_int(goal_f) 

318 elif not isinstance(goal_f, int | SampleStatistics): 

319 raise type_error(goal_f, "goal_f", ( 

320 int, float, SampleStatistics)) 

321 

322 if best_f_scaled is not None: 

323 goal_f_min: Final[int | float] = \ 

324 goal_f.minimum if isinstance(goal_f, SampleStatistics) \ 

325 else goal_f 

326 if goal_f_min <= 0: 

327 raise ValueError( 

328 f"best_f_scaled must be None if minimum goal_f " 

329 f"({goal_f_min}) of goal_f {goal_f} is not positive," 

330 f" but is {best_f_scaled}.") 

331 if not isinstance(best_f_scaled, SampleStatistics): 

332 raise type_error(best_f_scaled, "best_f_scaled", 

333 SampleStatistics) 

334 if best_f_scaled.n != n: 

335 raise ValueError( 

336 f"best_f_scaled.n={best_f_scaled.n} != n={n}") 

337 if best_f_scaled.minimum < 0: 

338 raise ValueError( 

339 "best_f_scaled cannot be negative, but encountered " 

340 f"{best_f_scaled.minimum}.") 

341 

342 check_int_range(n_success, "n_success") 

343 if not isinstance(ert_fes, int | float): 

344 raise type_error(ert_fes, "ert_fes", (int, float)) 

345 if not isinstance(ert_time_millis, int | float): 

346 raise type_error(ert_time_millis, "ert_time_millis", 

347 (int, float)) 

348 

349 if n_success > 0: 

350 if not isinstance(success_fes, SampleStatistics): 

351 raise type_error(success_fes, 

352 "if n_success>0, then success_fes", 

353 SampleStatistics) 

354 if success_fes.n != n_success: 

355 raise ValueError(f"success_fes.n={success_fes.n} != " 

356 f"n_success={n_success}") 

357 check_int_range( 

358 success_fes.minimum, "success_fes.minimum", 

359 last_improvement_fe.minimum, 1_000_000_000_000_000) 

360 check_int_range( 

361 success_fes.maximum, "success_fes.maximum", 

362 success_fes.minimum, last_improvement_fe.maximum) 

363 if not isinstance(success_time_millis, SampleStatistics): 

364 raise type_error( 

365 success_time_millis, 

366 "if n_success>0, then success_time_millis", 

367 SampleStatistics) 

368 if success_time_millis.n != n_success: 

369 raise ValueError( 

370 f"success_time_millis.n={success_time_millis.n} != " 

371 f"n_success={n_success}") 

372 check_int_range( 

373 success_time_millis.minimum, 

374 "success_time_millis.minimum", 

375 last_improvement_time_millis.minimum, 100_000_000_000) 

376 check_int_range( 

377 success_time_millis.maximum, 

378 "success_time_millis.maximum", 

379 success_time_millis.minimum, 

380 last_improvement_time_millis.maximum) 

381 ert_fes = try_int(ert_fes) 

382 if ert_fes < success_fes.minimum: 

383 raise ValueError( 

384 "ert_fes must be >= " 

385 f"{success_fes.minimum}, but is {ert_fes}.") 

386 ert_fe_max = ceil(total_fes.mean_arith * n) 

387 if ert_fes > ert_fe_max: 

388 raise ValueError( 

389 "ert_fes must be <= " 

390 f"{ert_fe_max}, but is {ert_fes}.") 

391 

392 ert_time_millis = try_int(ert_time_millis) 

393 if ert_time_millis < success_time_millis.minimum: 

394 raise ValueError( 

395 "ert_time_millis must be >= " 

396 f"{success_time_millis.minimum}, but " 

397 f"is {ert_time_millis}.") 

398 ert_time_max = ceil(total_time_millis.mean_arith * n) 

399 if ert_time_millis > ert_time_max: 

400 raise ValueError( 

401 "ert_time_millis must be <= " 

402 f"{ert_time_max}, but is {ert_time_millis}.") 

403 else: 

404 if success_fes is not None: 

405 raise ValueError( 

406 "If n_success<=0, then success_fes must be None, " 

407 f"but it's a {type_name_of(success_fes)}.") 

408 if success_time_millis is not None: 

409 raise ValueError( 

410 "If n_success<=0, then success_time_millis must be " 

411 f"None, but it is a " 

412 f"{type_name_of(success_time_millis)}.") 

413 if ert_fes < inf: 

414 raise ValueError( 

415 "If n_success<=0, then ert_fes must " 

416 f"be inf, but it's {ert_fes}.") 

417 if ert_time_millis < inf: 

418 raise ValueError( 

419 "If n_success<=0, then ert_time_millis must " 

420 f"be inf, but it's {ert_time_millis}.") 

421 

422 object.__setattr__(self, "goal_f", goal_f) 

423 object.__setattr__(self, "best_f_scaled", best_f_scaled) 

424 object.__setattr__(self, "n_success", n_success) 

425 object.__setattr__(self, "success_fes", success_fes) 

426 object.__setattr__(self, "success_time_millis", success_time_millis) 

427 object.__setattr__(self, "ert_fes", ert_fes) 

428 object.__setattr__(self, "ert_time_millis", ert_time_millis) 

429 

430 if isinstance(max_fes, SampleStatistics): 

431 if max_fes.n != n: 

432 raise ValueError(f"max_fes.n={max_fes.n} != n={n}") 

433 max_fes_f: int | float | SampleStatistics = max_fes.compact( 

434 needs_n=False) 

435 if isinstance(max_fes_f, float): 

436 raise type_error(max_fes_f, "max_fes", ( 

437 int, SampleStatistics, None)) 

438 max_fes = max_fes_f 

439 if isinstance(max_fes, int): 

440 if (max_fes < total_fes.maximum) or (max_fes < 0): 

441 raise ValueError(f"0<max_fes must be >= " 

442 f"{total_fes.maximum}, but is {max_fes}.") 

443 elif isinstance(max_fes, SampleStatistics): 

444 if (max_fes.minimum < total_fes.minimum) or ( 

445 max_fes.minimum <= 0): 

446 raise ValueError( 

447 f"0<max_fes.minimum must be >= {total_fes.minimum}," 

448 f" but is {max_fes.minimum}.") 

449 if max_fes.maximum < total_fes.maximum: 

450 raise ValueError( 

451 f"max_fes.maximum must be >= {total_fes.maximum}," 

452 f" but is {max_fes.maximum}.") 

453 elif max_fes is not None: 

454 raise type_error(max_fes, "max_fes", (int, SampleStatistics, None)) 

455 object.__setattr__(self, "max_fes", max_fes) 

456 

457 if isinstance(max_time_millis, SampleStatistics): 

458 if max_time_millis.n != n: 

459 raise ValueError( 

460 f"max_time_millis.n={max_time_millis.n} != n={n}") 

461 max_time_millis_f: int | float | SampleStatistics = ( 

462 max_time_millis.compact(False)) 

463 if isinstance(max_time_millis_f, float): 

464 raise type_error(max_time_millis_f, "max_time_millis", ( 

465 int, SampleStatistics, None)) 

466 if isinstance(max_time_millis, int): 

467 _check_max_time_millis(max_time_millis, 

468 total_fes.minimum, 

469 total_time_millis.maximum) 

470 elif isinstance(max_time_millis, SampleStatistics): 

471 _check_max_time_millis(max_time_millis.minimum, 

472 total_fes.minimum, 

473 total_time_millis.minimum) 

474 _check_max_time_millis(max_time_millis.maximum, 

475 total_fes.minimum, 

476 total_time_millis.maximum) 

477 elif max_time_millis is not None: 

478 raise type_error(max_time_millis, "max_time_millis", 

479 (int, SampleStatistics, None)) 

480 object.__setattr__(self, "max_time_millis", max_time_millis) 

481 

482 def get_n(self) -> int: 

483 """ 

484 Get the number of runs. 

485 

486 :returns: the number of runs. 

487 """ 

488 if not isinstance(self, EndStatistics): 

489 raise type_error(self, "self", EndStatistics) 

490 return self.n 

491 

492 def get_best_f(self) -> SampleStatistics: 

493 """ 

494 Get the statistics about the best objective value reached. 

495 

496 :returns: the statistics about the best objective value reached 

497 """ 

498 if not isinstance(self, EndStatistics): 

499 raise type_error(self, "self", EndStatistics) 

500 return self.best_f 

501 

502 def get_last_improvement_fe(self) -> SampleStatistics: 

503 """ 

504 Get the statistics about the last improvement FE. 

505 

506 :returns: the statistics about the last improvement FE 

507 """ 

508 if not isinstance(self, EndStatistics): 

509 raise type_error(self, "self", EndStatistics) 

510 return self.last_improvement_fe 

511 

512 def get_last_improvement_time_millis(self) -> SampleStatistics: 

513 """ 

514 Get the statistics about the last improvement time millis. 

515 

516 :returns: the statistics about the last improvement time millis 

517 """ 

518 if not isinstance(self, EndStatistics): 

519 raise type_error(self, "self", EndStatistics) 

520 return self.last_improvement_time_millis 

521 

522 def get_total_fes(self) -> SampleStatistics: 

523 """ 

524 Get the statistics about the total FEs. 

525 

526 :returns: the statistics about the total FEs 

527 """ 

528 if not isinstance(self, EndStatistics): 

529 raise type_error(self, "self", EndStatistics) 

530 return self.total_fes 

531 

532 def get_total_time_millis(self) -> SampleStatistics: 

533 """ 

534 Get the statistics about the total time millis. 

535 

536 :returns: the statistics about the total time millis 

537 """ 

538 if not isinstance(self, EndStatistics): 

539 raise type_error(self, "self", EndStatistics) 

540 return self.total_time_millis 

541 

542 def get_goal_f(self) -> SampleStatistics | int | float | None: 

543 """ 

544 Get the statistics about the goal objective value. 

545 

546 :returns: the statistics about the goal objective value 

547 """ 

548 if not isinstance(self, EndStatistics): 

549 raise type_error(self, "self", EndStatistics) 

550 return self.goal_f 

551 

552 def get_best_f_scaled(self) -> SampleStatistics | None: 

553 """ 

554 Get the statistics about the scaled best objective value. 

555 

556 :returns: the statistics about the scaled best objective value 

557 """ 

558 if not isinstance(self, EndStatistics): 

559 raise type_error(self, "self", EndStatistics) 

560 return self.best_f_scaled 

561 

562 def get_n_success(self) -> int | None: 

563 """ 

564 Get the number of successful runs. 

565 

566 :returns: the number of successful runs. 

567 """ 

568 if not isinstance(self, EndStatistics): 

569 raise type_error(self, "self", EndStatistics) 

570 return self.n_success 

571 

572 def get_success_fes(self) -> SampleStatistics | None: 

573 """ 

574 Get the statistics about the FEs until success of the successful runs. 

575 

576 :returns: the statistics about the FEs until success of the successful 

577 runs 

578 """ 

579 if not isinstance(self, EndStatistics): 

580 raise type_error(self, "self", EndStatistics) 

581 return self.success_fes 

582 

583 def get_success_time_millis(self) -> SampleStatistics | None: 

584 """ 

585 Get the statistics about the ms until success of the successful runs. 

586 

587 :returns: the statistics about the ms until success of the successful 

588 runs 

589 """ 

590 if not isinstance(self, EndStatistics): 

591 raise type_error(self, "self", EndStatistics) 

592 return self.success_time_millis 

593 

594 def get_ert_fes(self) -> int | float | None: 

595 """ 

596 Get the expected FEs until success. 

597 

598 :returns: the statistics about the expected FEs until success. 

599 """ 

600 if not isinstance(self, EndStatistics): 

601 raise type_error(self, "self", EndStatistics) 

602 return self.ert_fes 

603 

604 def get_ert_time_millis(self) -> int | float | None: 

605 """ 

606 Get the expected milliseconds until success. 

607 

608 :returns: the statistics about the expected milliseconds until 

609 success. 

610 """ 

611 if not isinstance(self, EndStatistics): 

612 raise type_error(self, "self", EndStatistics) 

613 return self.ert_time_millis 

614 

615 def get_max_fes(self) -> SampleStatistics | int | None: 

616 """ 

617 Get the statistics about the maximum permitted FEs. 

618 

619 :returns: the statistics about the maximum permitted FEs 

620 """ 

621 if not isinstance(self, EndStatistics): 

622 raise type_error(self, "self", EndStatistics) 

623 return self.max_fes 

624 

625 def get_max_time_millis(self) -> SampleStatistics | int | None: 

626 """ 

627 Get the statistics about the maximum permitted runtime in ms. 

628 

629 :returns: the statistics about the maximum permitted runtime in ms 

630 """ 

631 if not isinstance(self, EndStatistics): 

632 raise type_error(self, "self", EndStatistics) 

633 return self.max_time_millis 

634 

635 

636def create(source: Iterable[EndResult]) -> EndStatistics: 

637 """ 

638 Create an `EndStatistics` Record from an Iterable of `EndResult`. 

639 

640 :param source: the source 

641 :return: the statistics 

642 :rtype: EndStatistics 

643 """ 

644 if not isinstance(source, Iterable): 

645 raise type_error(source, "source", Iterable) 

646 

647 n: int = 0 

648 best_f: list[int | float] = [] 

649 last_improvement_fe: list[int] = [] 

650 last_improvement_time_millis: list[int] = [] 

651 total_fes: list[int] = [] 

652 total_time_millis: list[int] = [] 

653 max_fes: list[int] | None = [] 

654 max_fes_same: bool = True 

655 max_time_millis: list[int] | None = [] 

656 max_time_same: bool = True 

657 goal_f: list[int | float] | None = [] 

658 goal_f_same: bool = True 

659 best_f_scaled: list[float] | None = [] 

660 n_success: int | None = 0 

661 success_fes: list[int] | None = [] 

662 success_times: list[int] | None = [] 

663 

664 fes: int = 0 

665 time: int = 0 

666 algorithm: str | None = None 

667 instance: str | None = None 

668 objective: str | None = None 

669 encoding: str | None = None 

670 

671 for er in source: 

672 if not isinstance(er, EndResult): 

673 raise type_error(er, "end result", EndResult) 

674 if n == 0: 

675 algorithm = er.algorithm 

676 instance = er.instance 

677 objective = er.objective 

678 encoding = er.encoding 

679 else: 

680 if algorithm != er.algorithm: 

681 algorithm = None 

682 if instance != er.instance: 

683 instance = None 

684 if objective != er.objective: 

685 objective = None 

686 if encoding != er.encoding: 

687 encoding = None 

688 n += 1 

689 best_f.append(er.best_f) 

690 last_improvement_fe.append(er.last_improvement_fe) 

691 last_improvement_time_millis.append( 

692 er.last_improvement_time_millis) 

693 total_fes.append(er.total_fes) 

694 total_time_millis.append(er.total_time_millis) 

695 if er.max_fes is None: 

696 max_fes = None 

697 elif max_fes is not None: 

698 if n > 1: 

699 max_fes_same = max_fes_same \ 

700 and (max_fes[-1] == er.max_fes) 

701 max_fes.append(er.max_fes) 

702 if er.max_time_millis is None: 

703 max_time_millis = None 

704 elif max_time_millis is not None: 

705 if n > 1: 

706 max_time_same = \ 

707 max_time_same \ 

708 and (max_time_millis[-1] == er.max_time_millis) 

709 max_time_millis.append(er.max_time_millis) 

710 

711 if er.goal_f is None: 

712 goal_f = None 

713 best_f_scaled = None 

714 n_success = None 

715 success_fes = None 

716 success_times = None 

717 elif goal_f is not None: 

718 if n > 1: 

719 goal_f_same = goal_f_same and (goal_f[-1] == er.goal_f) 

720 goal_f.append(er.goal_f) 

721 

722 if er.goal_f <= 0: 

723 best_f_scaled = None 

724 elif best_f_scaled is not None: 

725 best_f_scaled.append(er.best_f / er.goal_f) 

726 

727 if er.best_f <= er.goal_f: 

728 n_success += 1 

729 success_fes.append(er.last_improvement_fe) 

730 success_times.append(er.last_improvement_time_millis) 

731 fes += er.last_improvement_fe 

732 time += er.last_improvement_time_millis 

733 else: 

734 fes += er.total_fes 

735 time += er.total_time_millis 

736 if n <= 0: 

737 raise ValueError("There must be at least one end result record.") 

738 

739 return EndStatistics( 

740 algorithm, 

741 instance, 

742 objective, 

743 encoding, 

744 n, 

745 from_samples(best_f), 

746 from_samples(last_improvement_fe), 

747 from_samples(last_improvement_time_millis), 

748 from_samples(total_fes), 

749 from_samples(total_time_millis), 

750 None if (goal_f is None) 

751 else (goal_f[0] if goal_f_same else from_samples(goal_f)), 

752 None if (best_f_scaled is None) 

753 else from_samples(best_f_scaled), 

754 n_success, 

755 None if (n_success is None) or (n_success <= 0) 

756 else from_samples(success_fes), 

757 None if (n_success is None) or (n_success <= 0) 

758 else from_samples(success_times), 

759 None if (n_success is None) 

760 else (inf if (n_success <= 0) else try_int_div(fes, n_success)), 

761 None if (n_success is None) else 

762 (inf if (n_success <= 0) else try_int_div(time, n_success)), 

763 None if max_fes is None else 

764 (max_fes[0] if max_fes_same else from_samples(max_fes)), 

765 None if max_time_millis is None 

766 else (max_time_millis[0] if max_time_same 

767 else from_samples(max_time_millis))) 

768 

769 

770def from_end_results(source: Iterable[EndResult], 

771 join_all_algorithms: bool = False, 

772 join_all_instances: bool = False, 

773 join_all_objectives: bool = False, 

774 join_all_encodings: bool = False) \ 

775 -> Generator[EndStatistics, None, None]: 

776 """ 

777 Aggregate statistics over a stream of end results. 

778 

779 :param source: the stream of end results 

780 :param join_all_algorithms: should the statistics be aggregated 

781 over all algorithms 

782 :param join_all_instances: should the statistics be aggregated 

783 over all algorithms 

784 :param join_all_objectives: should the statistics be aggregated over 

785 all objectives? 

786 :param join_all_encodings: should statistics be aggregated over all 

787 encodings 

788 :returns: iterates over the generated end statistics records 

789 """ 

790 if not isinstance(source, Iterable): 

791 raise type_error(source, "source", Iterable) 

792 if not isinstance(join_all_algorithms, bool): 

793 raise type_error(join_all_algorithms, 

794 "join_all_algorithms", bool) 

795 if not isinstance(join_all_instances, bool): 

796 raise type_error(join_all_instances, "join_all_instances", bool) 

797 if not isinstance(join_all_objectives, bool): 

798 raise type_error(join_all_objectives, "join_all_objectives", bool) 

799 if not isinstance(join_all_encodings, bool): 

800 raise type_error(join_all_encodings, "join_all_encodings", bool) 

801 

802 if (join_all_algorithms and join_all_instances 

803 and join_all_objectives and join_all_encodings): 

804 yield create(source) 

805 return 

806 

807 sorter: dict[tuple[str, str, str, str], list[EndResult]] = {} 

808 for er in source: 

809 if not isinstance(er, EndResult): 

810 raise type_error(source, "end results from source", 

811 EndResult) 

812 key = ("" if join_all_algorithms else er.algorithm, 

813 "" if join_all_instances else er.instance, 

814 "" if join_all_objectives else er.objective, 

815 "" if join_all_encodings else ( 

816 "" if er.encoding is None else er.encoding)) 

817 if key in sorter: 

818 lst = sorter[key] 

819 else: 

820 lst = [] 

821 sorter[key] = lst 

822 lst.append(er) 

823 

824 if len(sorter) <= 0: 

825 raise ValueError("source must not be empty") 

826 

827 if len(sorter) > 1: 

828 for key in sorted(sorter.keys()): 

829 yield create(sorter[key]) 

830 else: 

831 yield create(next(iter(sorter.values()))) #: pylint: disable=R1708 

832 

833 

834def to_csv(data: EndStatistics | Iterable[EndStatistics], 

835 file: str) -> Path: 

836 """ 

837 Store a set of :class:`EndStatistics` in a CSV file. 

838 

839 :param data: the data to store 

840 :param file: the file to generate 

841 :return: the path to the generated CSV file 

842 """ 

843 path: Final[Path] = Path(file) 

844 logger(f"Writing end result statistics to CSV file {path!r}.") 

845 path.ensure_parent_dir_exists() 

846 with path.open_for_write() as wt: 

847 write_lines(CsvWriter.write( 

848 (data, ) if isinstance(data, EndStatistics) else data), wt) 

849 

850 logger(f"Done writing end result statistics to CSV file {path!r}.") 

851 return path 

852 

853 

854def from_csv(file: str) -> Generator[EndStatistics, None, None]: 

855 """ 

856 Parse a CSV file and collect all encountered :class:`EndStatistics`. 

857 

858 :param file: the file to parse 

859 :returns: the iterator with the results 

860 """ 

861 path: Final[Path] = file_path(file) 

862 logger(f"Begin reading end result statistics from CSV file {path!r}.") 

863 with path.open_for_read() as rd: 

864 yield from CsvReader.read(rows=rd) 

865 logger("Finished reading end result statistics from CSV " 

866 f"file {path!r}.") 

867 

868 

869#: the internal getters that can work directly 

870__PROPERTIES: Final[Callable[[str], Callable[[ 

871 EndStatistics], SampleStatistics | int | float | None] | None]] = { 

872 KEY_N: EndStatistics.get_n, 

873 KEY_N_SUCCESS: EndStatistics.get_n_success, 

874 KEY_ERT_FES: EndStatistics.get_ert_fes, 

875 KEY_ERT_TIME_MILLIS: EndStatistics.get_ert_time_millis, 

876 KEY_GOAL_F: EndStatistics.get_goal_f, 

877 KEY_MAX_TIME_MILLIS: EndStatistics.get_max_time_millis, 

878 KEY_MAX_FES: EndStatistics.get_max_fes, 

879 KEY_BEST_F: EndStatistics.get_best_f, 

880 F_NAME_RAW: EndStatistics.get_best_f, 

881 KEY_LAST_IMPROVEMENT_FE: EndStatistics.get_last_improvement_fe, 

882 "last improvement FE": EndStatistics.get_last_improvement_fe, 

883 KEY_LAST_IMPROVEMENT_TIME_MILLIS: 

884 EndStatistics.get_last_improvement_time_millis, 

885 "last improvement ms": EndStatistics.get_last_improvement_time_millis, 

886 KEY_BEST_F_SCALED: EndStatistics.get_best_f_scaled, 

887 KEY_SUCCESS_FES: EndStatistics.get_success_fes, 

888 KEY_SUCCESS_TIME_MILLIS: EndStatistics.get_success_time_millis, 

889 F_NAME_SCALED: EndStatistics.get_best_f_scaled, 

890 KEY_TOTAL_FES: EndStatistics.get_total_fes, 

891 "fes": EndStatistics.get_total_fes, 

892 KEY_TOTAL_TIME_MILLIS: EndStatistics.get_total_time_millis, 

893 "ms": EndStatistics.get_total_time_millis, 

894 "f": EndStatistics.get_best_f, 

895 "budgetFEs": EndStatistics.get_max_fes, 

896 "budgetMS": EndStatistics.get_max_time_millis, 

897}.get 

898 

899#: the success keys 

900__SUCCESS_KEYS: Final[Callable[[str], bool]] = { 

901 KEY_SUCCESS_FES, KEY_SUCCESS_TIME_MILLIS, 

902}.__contains__ 

903 

904#: the internal static getters 

905__STATIC: Final[dict[str, Callable[[EndStatistics], int | float | None]]] = { 

906 KEY_N: EndStatistics.get_n, 

907 KEY_N_SUCCESS: EndStatistics.get_n_success, 

908 KEY_ERT_FES: EndStatistics.get_ert_fes, 

909 KEY_ERT_TIME_MILLIS: EndStatistics.get_ert_time_millis, 

910} 

911 

912 

913def getter(dimension: str) -> Callable[[EndStatistics], int | float | None]: 

914 """ 

915 Create a function that obtains the given dimension from EndStatistics. 

916 

917 :param dimension: the dimension 

918 :returns: a callable that returns the value corresponding to the 

919 dimension 

920 """ 

921 dimension = str.strip(dimension) 

922 direct: Callable[[EndStatistics], int | float | None] = \ 

923 __STATIC.get(dimension) 

924 if direct is not None: 

925 return direct 

926 

927 names: Final[list[str]] = str.split(str.strip(dimension), SCOPE_SEPARATOR) 

928 n_names: Final[int] = list.__len__(names) 

929 if not (0 < n_names < 3): 

930 raise ValueError( 

931 f"Invalid name combination {dimension!r} -> {names!r}.") 

932 getter_1: Final[Callable[[ 

933 EndStatistics], int | float | SampleStatistics | None] | None] = \ 

934 __PROPERTIES(names[0]) 

935 if getter_1 is None: 

936 raise ValueError(f"Invalid dimension {names[0]!r} in {dimension!r}.") 

937 getter_2: Final[Callable[[ 

938 SampleStatistics], int | float | None]] = \ 

939 stat_getter(names[1] if n_names > 1 else KEY_MEAN_ARITH) 

940 

941 if getter_2 is stat_getter(KEY_STDDEV): # it is sd 

942 n_prop: Final[Callable[[EndStatistics], int | None]] = \ 

943 EndStatistics.get_n_success if __SUCCESS_KEYS( 

944 names[0]) else EndStatistics.get_n 

945 

946 def __combo_sd( 

947 data: EndStatistics, __g1=getter_1, __g2=getter_2, 

948 __n=n_prop) -> int | float | None: 

949 val: int | float | SampleStatistics | None = __g1(data) 

950 if val is None: 

951 return None 

952 if isinstance(val, int | float): 

953 n = __n(data) 

954 return None if (n is None) or (n <= 0) else 0 

955 return __g2(val) 

956 direct = cast("Callable[[EndStatistics], int | float | None]", 

957 __combo_sd) 

958 else: # any other form of mean or statistic 

959 

960 def __combo_no_sd(data: EndStatistics, 

961 __g1=getter_1, __g2=getter_2) -> int | float | None: 

962 val: int | float | SampleStatistics | None = __g1(data) 

963 if (val is None) or (isinstance(val, int | float)): 

964 return val 

965 return __g2(val) 

966 direct = cast("Callable[[EndStatistics], int | float | None]", 

967 __combo_no_sd) 

968 

969 __STATIC[dimension] = direct 

970 return direct 

971 

972 

973def _to_csv_writer( 

974 data: Iterable[EndStatistics], 

975 get_func: Callable[ 

976 [EndStatistics], SampleStatistics | int | float | None], 

977 n_func: Callable[[EndStatistics], int], 

978 scope: str | None = None, 

979 what_short: str | None = None, 

980 what_long: str | None = None) -> StatWriter | None: 

981 """ 

982 Get a CSV Writer for the given data subset. 

983 

984 :param data: the data iterator 

985 :param get_func: the getter for the value 

986 :param n_func: the n-getter 

987 :param scope: the scope to use 

988 :param what_short: the short description 

989 :param what_long: the long description 

990 :returns: the writer, if there was any associated data 

991 """ 

992 refined: list[tuple[SampleStatistics | int | float | None, int]] = [ 

993 v for v in ((get_func(es), n_func(es)) for es in data) 

994 if v[0] is not None] 

995 if list.__len__(refined) <= 0: 

996 return None 

997 return StatWriter(data=starmap(from_single_value, refined), 

998 scope=scope, n_not_needed=True, what_short=what_short, 

999 what_long=what_long) 

1000 

1001 

1002class CsvWriter(CsvWriterBase[EndStatistics]): 

1003 """A class for CSV writing of :class:`EndStatistics`.""" 

1004 

1005 def __init__(self, data: Iterable[EndStatistics], 

1006 scope: str | None = None) -> None: 

1007 """ 

1008 Initialize the csv writer. 

1009 

1010 :param scope: the prefix to be pre-pended to all columns 

1011 :param data: the data to write 

1012 """ 

1013 data = reiterable(data) 

1014 super().__init__(data, scope) 

1015 checker: int = 127 

1016 has_algorithm: bool = False 

1017 has_instance: bool = False 

1018 has_objective: bool = False 

1019 has_encoding: bool = False 

1020 has_n_success: bool = False 

1021 has_ert_fes: bool = False 

1022 has_ert_time_millis: bool = False 

1023 for es in data: 

1024 if es.algorithm is not None: 

1025 has_algorithm = True 

1026 checker &= ~1 

1027 if es.instance is not None: 

1028 has_instance = True 

1029 checker &= ~2 

1030 if es.objective is not None: 

1031 has_objective = True 

1032 checker &= ~4 

1033 if es.encoding is not None: 

1034 has_encoding = True 

1035 checker &= ~8 

1036 if es.n_success is not None: 

1037 has_n_success = True 

1038 checker &= ~16 

1039 if es.ert_fes is not None: 

1040 has_ert_fes = True 

1041 checker &= ~32 

1042 if es.ert_time_millis is not None: 

1043 has_ert_time_millis = True 

1044 checker &= ~64 

1045 if checker == 0: 

1046 break 

1047 

1048 #: do we put the algorithm column? 

1049 self.__has_algorithm: Final[bool] = has_algorithm 

1050 #: do we put the instance column? 

1051 self.__has_instance: Final[bool] = has_instance 

1052 #: do we put the objective column? 

1053 self.__has_objective: Final[bool] = has_objective 

1054 #: do we put the encoding column? 

1055 self.__has_encoding: Final[bool] = has_encoding 

1056 #: do we put the n_success column? 

1057 self.__has_n_success: Final[bool] = has_n_success 

1058 #: do we put the ert-fes column? 

1059 self.__has_ert_fes: Final[bool] = has_ert_fes 

1060 #: do we put the ert time millis column? 

1061 self.__has_ert_time_millis: Final[bool] = has_ert_time_millis 

1062 

1063 self.__goal_f: Final[StatWriter | None] = _to_csv_writer( 

1064 data, EndStatistics.get_goal_f, EndStatistics.get_n, 

1065 csv_scope(scope, KEY_GOAL_F), KEY_GOAL_F, 

1066 "the goal objective value after which the runs can stop") 

1067 self.__best_f_scaled: Final[StatWriter | None] = _to_csv_writer( 

1068 data, EndStatistics.get_best_f_scaled, EndStatistics.get_n, 

1069 csv_scope(scope, KEY_BEST_F_SCALED), KEY_BEST_F_SCALED, 

1070 f"best objective value reached ({KEY_BEST_F}), divided by" 

1071 f" the goal objective value ({KEY_GOAL_F})") 

1072 self.__success_fes: Final[StatWriter | None] = _to_csv_writer( 

1073 data, EndStatistics.get_success_fes, EndStatistics.get_n_success, 

1074 csv_scope(scope, KEY_SUCCESS_FES), KEY_SUCCESS_FES, 

1075 f"the FEs needed to reach {KEY_GOAL_F} for the successful runs") 

1076 self.__success_time_millis: Final[StatWriter | None] = _to_csv_writer( 

1077 data, EndStatistics.get_success_time_millis, 

1078 EndStatistics.get_n_success, csv_scope( 

1079 scope, KEY_SUCCESS_TIME_MILLIS), KEY_SUCCESS_TIME_MILLIS, 

1080 f"the milliseconds needed to reach {KEY_GOAL_F} for the " 

1081 "successful runs") 

1082 self.__max_fes: Final[StatWriter | None] = _to_csv_writer( 

1083 data, EndStatistics.get_max_fes, EndStatistics.get_n, 

1084 csv_scope(scope, KEY_MAX_FES), KEY_MAX_FES, 

1085 "the maximum number of FEs in the computational budget") 

1086 self.__max_time_millis: Final[StatWriter | None] = _to_csv_writer( 

1087 data, EndStatistics.get_max_time_millis, EndStatistics.get_n, 

1088 csv_scope(scope, KEY_MAX_TIME_MILLIS), KEY_MAX_TIME_MILLIS, 

1089 "the maximum milliseconds per run in the computational budget") 

1090 

1091 #: the best objective value reached 

1092 self.__best_f: Final[StatWriter] = StatWriter( 

1093 data=map(EndStatistics.get_best_f, data), 

1094 scope=csv_scope(scope, KEY_BEST_F), 

1095 n_not_needed=True, what_short=KEY_BEST_F, 

1096 what_long="the best objective value reached per run") 

1097 #: the FE when the last improvement happened 

1098 self.__life: Final[StatWriter] = StatWriter( 

1099 data=map(EndStatistics.get_last_improvement_fe, data), 

1100 scope=csv_scope(scope, KEY_LAST_IMPROVEMENT_FE), 

1101 n_not_needed=True, what_short=KEY_LAST_IMPROVEMENT_FE, 

1102 what_long="the FE when the last improvement happened in a run") 

1103 #: the milliseconds when the last improvement happened 

1104 self.__lims: Final[StatWriter] = StatWriter( 

1105 data=map(EndStatistics.get_last_improvement_time_millis, data), 

1106 scope=csv_scope( 

1107 scope, KEY_LAST_IMPROVEMENT_TIME_MILLIS), 

1108 n_not_needed=True, what_short=KEY_LAST_IMPROVEMENT_TIME_MILLIS, 

1109 what_long="the millisecond when the last " 

1110 "improvement happened in a run") 

1111 #: the total FEs 

1112 self.__total_fes: Final[StatWriter] = StatWriter( 

1113 data=map(EndStatistics.get_total_fes, data), 

1114 scope=csv_scope(scope, KEY_TOTAL_FES), 

1115 n_not_needed=True, what_short=KEY_TOTAL_FES, 

1116 what_long="the total FEs consumed by the runs") 

1117 #: the total milliseconds 

1118 self.__total_ms: Final[StatWriter] = StatWriter( 

1119 data=map(EndStatistics.get_total_time_millis, data), 

1120 scope=csv_scope(scope, KEY_TOTAL_TIME_MILLIS), 

1121 n_not_needed=True, what_short=KEY_TOTAL_TIME_MILLIS, 

1122 what_long="the total millisecond consumed by a run") 

1123 

1124 def get_column_titles(self) -> Iterator[str]: 

1125 """ 

1126 Get the column titles. 

1127 

1128 :returns: the column titles 

1129 """ 

1130 p: Final[str] = self.scope 

1131 if self.__has_algorithm: 

1132 yield csv_scope(p, KEY_ALGORITHM) 

1133 if self.__has_instance: 

1134 yield csv_scope(p, KEY_INSTANCE) 

1135 if self.__has_objective: 

1136 yield csv_scope(p, KEY_OBJECTIVE_FUNCTION) 

1137 if self.__has_encoding: 

1138 yield csv_scope(p, KEY_ENCODING) 

1139 yield csv_scope(p, KEY_N) 

1140 yield from self.__best_f.get_column_titles() 

1141 yield from self.__life.get_column_titles() 

1142 yield from self.__lims.get_column_titles() 

1143 yield from self.__total_fes.get_column_titles() 

1144 yield from self.__total_ms.get_column_titles() 

1145 if self.__goal_f is not None: 

1146 yield from self.__goal_f.get_column_titles() 

1147 if self.__best_f_scaled is not None: 

1148 yield from self.__best_f_scaled.get_column_titles() 

1149 if self.__has_n_success: 

1150 yield csv_scope(p, KEY_N_SUCCESS) 

1151 if self.__success_fes is not None: 

1152 yield from self.__success_fes.get_column_titles() 

1153 if self.__success_time_millis is not None: 

1154 yield from self.__success_time_millis.get_column_titles() 

1155 if self.__has_ert_fes: 

1156 yield csv_scope(p, KEY_ERT_FES) 

1157 if self.__has_ert_time_millis: 

1158 yield csv_scope(p, KEY_ERT_TIME_MILLIS) 

1159 if self.__max_fes is not None: 

1160 yield from self.__max_fes.get_column_titles() 

1161 if self.__max_time_millis is not None: 

1162 yield from self.__max_time_millis.get_column_titles() 

1163 

1164 def get_row(self, data: EndStatistics) -> Iterable[str]: 

1165 """ 

1166 Render a single end result record to a CSV row. 

1167 

1168 :param data: the end result record 

1169 :returns: the row strings 

1170 """ 

1171 if self.__has_algorithm: 

1172 yield "" if data.algorithm is None else data.algorithm 

1173 if self.__has_instance: 

1174 yield "" if data.instance is None else data.instance 

1175 if self.__has_objective: 

1176 yield "" if data.objective is None else data.objective 

1177 if self.__has_encoding: 

1178 yield "" if data.encoding is None else data.encoding 

1179 yield str(data.n) 

1180 yield from self.__best_f.get_row(data.best_f) 

1181 yield from self.__life.get_row(data.last_improvement_fe) 

1182 yield from self.__lims.get_row(data.last_improvement_time_millis) 

1183 yield from self.__total_fes.get_row(data.total_fes) 

1184 yield from self.__total_ms.get_row(data.total_time_millis) 

1185 if self.__goal_f is not None: 

1186 yield from self.__goal_f.get_optional_row(data.goal_f, data.n) 

1187 if self.__best_f_scaled is not None: 

1188 yield from self.__best_f_scaled.get_optional_row( 

1189 data.best_f_scaled, data.n) 

1190 if self.__has_n_success: 

1191 yield str(data.n_success) 

1192 if self.__success_fes is not None: 

1193 yield from self.__success_fes.get_optional_row( 

1194 data.success_fes, data.n_success) 

1195 if self.__success_time_millis is not None: 

1196 yield from self.__success_time_millis.get_optional_row( 

1197 data.success_time_millis, data.n_success) 

1198 if self.__has_ert_fes: 

1199 yield num_or_none_to_str(data.ert_fes) 

1200 if self.__has_ert_time_millis: 

1201 yield num_or_none_to_str(data.ert_time_millis) 

1202 if self.__max_fes is not None: 

1203 yield from self.__max_fes.get_optional_row(data.max_fes, data.n) 

1204 if self.__max_time_millis is not None: 

1205 yield from self.__max_time_millis.get_optional_row( 

1206 data.max_time_millis, data.n) 

1207 

1208 def get_header_comments(self) -> Iterable[str]: 

1209 """ 

1210 Get any possible header comments. 

1211 

1212 :returns: the header comments 

1213 """ 

1214 return ("Experiment End Results Statistics", 

1215 "See the description at the bottom of the file.") 

1216 

1217 def get_footer_comments(self) -> Iterable[str]: 

1218 """ 

1219 Get any possible footer comments. 

1220 

1221 :param dest: the destination 

1222 """ 

1223 yield "" 

1224 scope: Final[str | None] = self.scope 

1225 

1226 yield ("This file presents statistics gathered over multiple runs " 

1227 "of optimization algorithms applied to problem instances.") 

1228 if scope: 

1229 yield ("All end result statistics records start with prefix " 

1230 f"{scope}{SCOPE_SEPARATOR}.") 

1231 if self.__has_algorithm: 

1232 yield f"{csv_scope(scope, KEY_ALGORITHM)}: {DESC_ALGORITHM}" 

1233 if self.__has_instance: 

1234 yield f"{csv_scope(scope, KEY_INSTANCE)}: {DESC_INSTANCE}" 

1235 if self.__has_objective: 

1236 yield (f"{csv_scope(scope, KEY_OBJECTIVE_FUNCTION)}:" 

1237 f" {DESC_OBJECTIVE_FUNCTION}") 

1238 if self.__has_encoding: 

1239 yield f"{csv_scope(scope, KEY_ENCODING)}: {DESC_ENCODING}" 

1240 yield (f"{csv_scope(scope, KEY_N)}: the number of runs that were " 

1241 f"performed for the given setup.") 

1242 

1243 yield from self.__best_f.get_footer_comments() 

1244 yield f"In summary {csv_scope(scope, KEY_BEST_F)} is {DESC_BEST_F}." 

1245 

1246 yield from self.__life.get_footer_comments() 

1247 yield (f"In summary {csv_scope(scope, KEY_LAST_IMPROVEMENT_FE)} " 

1248 f"is {DESC_LAST_IMPROVEMENT_FE}.") 

1249 

1250 yield from self.__lims.get_footer_comments() 

1251 yield ("In summary " 

1252 f"{csv_scope(scope, KEY_LAST_IMPROVEMENT_TIME_MILLIS)} " 

1253 f"is {DESC_LAST_IMPROVEMENT_TIME_MILLIS}.") 

1254 

1255 yield from self.__total_fes.get_footer_comments() 

1256 yield (f"In summary {csv_scope(scope, KEY_TOTAL_FES)} " 

1257 f"is {DESC_TOTAL_FES}.") 

1258 

1259 yield from self.__total_ms.get_footer_comments() 

1260 yield (f"In summary {csv_scope(scope, KEY_TOTAL_TIME_MILLIS)} " 

1261 f"is {DESC_TOTAL_TIME_MILLIS}.") 

1262 

1263 if self.__goal_f is not None: 

1264 yield from self.__goal_f.get_footer_comments() 

1265 yield (f"In summary {csv_scope(scope, KEY_GOAL_F)} is" 

1266 f" {DESC_GOAL_F}.") 

1267 

1268 if self.__best_f_scaled is not None: 

1269 yield from self.__best_f_scaled.get_footer_comments() 

1270 yield (f"In summary {csv_scope(scope, KEY_BEST_F_SCALED)} " 

1271 "describes the best objective value reached (" 

1272 f"{csv_scope(scope, KEY_BEST_F)}) divided by the goal " 

1273 f"objective value ({csv_scope(scope, KEY_GOAL_F)}).") 

1274 

1275 if self.__has_n_success: 

1276 yield (f"{csv_scope(scope, KEY_N_SUCCESS)} is the number of " 

1277 "runs that reached goal objective value " 

1278 f"{csv_scope(scope, KEY_GOAL_F)}. Obviously, " 

1279 f"0<={csv_scope(scope, KEY_N_SUCCESS)}<=" 

1280 f"{csv_scope(scope, KEY_N)}.") 

1281 if self.__success_fes is not None: 

1282 yield from self.__success_fes.get_footer_comments() 

1283 yield (f"{csv_scope(scope, KEY_SUCCESS_FES)} offers statistics " 

1284 "about the number of FEs that the 0<=" 

1285 f"{csv_scope(scope, KEY_N_SUCCESS)}<=" 

1286 f"{csv_scope(scope, KEY_N)} successful runs needed to " 

1287 "reach the goal objective value " 

1288 f"{csv_scope(scope, KEY_GOAL_F)}.") 

1289 

1290 if self.__success_time_millis is not None: 

1291 yield from self.__success_fes.get_footer_comments() 

1292 yield (f"{csv_scope(scope, KEY_SUCCESS_TIME_MILLIS)} offers " 

1293 "statistics about the number of milliseconds of clock time" 

1294 f" that the 0<={csv_scope(scope, KEY_N_SUCCESS)}<=" 

1295 f"{csv_scope(scope, KEY_N)} successful runs needed to " 

1296 "reach the goal objective value " 

1297 f"{csv_scope(scope, KEY_GOAL_F)}.") 

1298 

1299 if self.__has_ert_fes: 

1300 yield (f"{csv_scope(scope, KEY_ERT_FES)} is the empirical " 

1301 "estimate of the number of FEs to solve the problem. It " 

1302 "can be approximated by dividing the sum of " 

1303 f"{csv_scope(scope, KEY_TOTAL_FES)} over all runs by the " 

1304 f"number {csv_scope(scope, KEY_N_SUCCESS)} of successful " 

1305 "runs.") 

1306 

1307 if self.__has_ert_time_millis: 

1308 yield (f"{csv_scope(scope, KEY_ERT_TIME_MILLIS)} is the empirical" 

1309 " estimate of the number of FEs to solve the problem. It " 

1310 "can be approximated by dividing the sum of " 

1311 f"{csv_scope(scope, KEY_TOTAL_TIME_MILLIS)} over all runs " 

1312 f"by the number {csv_scope(scope, KEY_N_SUCCESS)} of " 

1313 "successful runs.") 

1314 

1315 if self.__max_fes is not None: 

1316 yield from self.__max_fes.get_footer_comments() 

1317 yield (f"In summary {csv_scope(scope, KEY_MAX_FES)} is" 

1318 f" {DESC_MAX_FES}.") 

1319 if self.__max_time_millis is not None: 

1320 yield from self.__max_time_millis.get_footer_comments() 

1321 yield (f"In summary {csv_scope(scope, KEY_MAX_TIME_MILLIS)} is" 

1322 f" {DESC_MAX_TIME_MILLIS}.") 

1323 

1324 def get_footer_bottom_comments(self) -> Iterable[str]: 

1325 """ 

1326 Get the footer bottom comments. 

1327 

1328 :returns: the footer comments 

1329 """ 

1330 yield from motipy_footer_bottom_comments( 

1331 self, ("The end statistics data is produced using module " 

1332 "moptipy.evaluation.end_statistics.")) 

1333 yield from StatWriter.get_footer_bottom_comments(self.__best_f) 

1334 

1335 

1336class CsvReader(CsvReaderBase[EndStatistics]): 

1337 """A csv parser for end results.""" 

1338 

1339 def __init__(self, columns: dict[str, int]) -> None: 

1340 """ 

1341 Create a CSV parser for :class:`EndResult`. 

1342 

1343 :param columns: the columns 

1344 """ 

1345 super().__init__(columns) 

1346 #: the index of the algorithm column, if any 

1347 self.__idx_algorithm: Final[int | None] = csv_column_or_none( 

1348 columns, KEY_ALGORITHM) 

1349 #: the index of the instance column, if any 

1350 self.__idx_instance: Final[int | None] = csv_column_or_none( 

1351 columns, KEY_INSTANCE) 

1352 #: the index of the objective column, if any 

1353 self.__idx_objective: Final[int | None] = csv_column_or_none( 

1354 columns, KEY_OBJECTIVE_FUNCTION) 

1355 #: the index of the encoding column, if any 

1356 self.__idx_encoding: Final[int | None] = csv_column_or_none( 

1357 columns, KEY_ENCODING) 

1358 

1359 #: the index of the `N` column, i.e., where the number of runs is 

1360 #: stored 

1361 self.idx_n: Final[int] = csv_column(columns, KEY_N, True) 

1362 

1363 n_key: Final[tuple[tuple[str, int]]] = ((KEY_N, self.idx_n), ) 

1364 #: the reader for the best-objective-value-reached statistics 

1365 self.__best_f: Final[StatReader] = csv_select_scope( 

1366 StatReader, columns, KEY_BEST_F, n_key) 

1367 #: the reader for the last improvement FE statistics 

1368 self.__life: Final[StatReader] = csv_select_scope( 

1369 StatReader, columns, KEY_LAST_IMPROVEMENT_FE, n_key) 

1370 #: the reader for the last improvement millisecond index statistics 

1371 self.__lims: Final[StatReader] = csv_select_scope( 

1372 StatReader, columns, KEY_LAST_IMPROVEMENT_TIME_MILLIS, n_key) 

1373 #: the reader for the total FEs statistics 

1374 self.__total_fes: Final[StatReader] = csv_select_scope( 

1375 StatReader, columns, KEY_TOTAL_FES, n_key) 

1376 #: the reader for the total milliseconds consumed statistics 

1377 self.__total_ms: Final[StatReader] = csv_select_scope( 

1378 StatReader, columns, KEY_TOTAL_TIME_MILLIS, n_key) 

1379 

1380 #: the reader for the goal objective value statistics, if any 

1381 self.__goal_f: Final[StatReader | None] = csv_select_scope_or_none( 

1382 StatReader, columns, KEY_GOAL_F, n_key) 

1383 #: the reader for the best-f / goal-f statistics, if any 

1384 self.__best_f_scaled: Final[StatReader | None] = \ 

1385 csv_select_scope_or_none( 

1386 StatReader, columns, KEY_BEST_F_SCALED, n_key) 

1387 

1388 #: the index of the column where the number of successful runs is 

1389 #: stored 

1390 self.__idx_n_success: Final[int | None] = csv_column_or_none( 

1391 columns, KEY_N_SUCCESS) 

1392 succ_key: Final[tuple[tuple[str, int], ...]] = () \ 

1393 if self.__idx_n_success is None else ( 

1394 (KEY_N, self.__idx_n_success), ) 

1395 #: the reader for the success FE data, if any 

1396 self.__success_fes: Final[StatReader | None] = \ 

1397 None if self.__idx_n_success is None else \ 

1398 csv_select_scope_or_none( 

1399 StatReader, columns, KEY_SUCCESS_FES, succ_key) 

1400 #: the reader for the success time milliseconds data, if any 

1401 self.__success_time_millis: Final[StatReader | None] = \ 

1402 None if self.__idx_n_success is None else \ 

1403 csv_select_scope_or_none( 

1404 StatReader, columns, KEY_SUCCESS_TIME_MILLIS, succ_key) 

1405 

1406 #: the index of the expected FEs until success 

1407 self.__idx_ert_fes: Final[int | None] = csv_column_or_none( 

1408 columns, KEY_ERT_FES) 

1409 #: the index of the expected milliseconds until success 

1410 self.__idx_ert_time_millis: Final[int | None] = csv_column_or_none( 

1411 columns, KEY_ERT_TIME_MILLIS) 

1412 

1413 #: the columns with the maximum FE-based budget statistics 

1414 self.__max_fes: Final[StatReader | None] = csv_select_scope_or_none( 

1415 StatReader, columns, KEY_MAX_FES, n_key) 

1416 #: the columns with the maximum time-based budget statistics 

1417 self.__max_time_millis: Final[StatReader | None] = \ 

1418 csv_select_scope_or_none( 

1419 StatReader, columns, KEY_MAX_TIME_MILLIS, n_key) 

1420 

1421 def parse_row(self, data: list[str]) -> EndStatistics: 

1422 """ 

1423 Parse a row of data. 

1424 

1425 :param data: the data row 

1426 :return: the end result statistics 

1427 """ 

1428 return EndStatistics( 

1429 algorithm=csv_str_or_none(data, self.__idx_algorithm), 

1430 instance=csv_str_or_none(data, self.__idx_instance), 

1431 objective=csv_str_or_none(data, self.__idx_objective), 

1432 encoding=csv_str_or_none(data, self.__idx_encoding), 

1433 n=int(data[self.idx_n]), 

1434 best_f=self.__best_f.parse_row(data), 

1435 last_improvement_fe=self.__life.parse_row(data), 

1436 last_improvement_time_millis=self.__lims.parse_row(data), 

1437 total_fes=self.__total_fes.parse_row(data), 

1438 total_time_millis=self.__total_ms.parse_row(data), 

1439 goal_f=StatReader.parse_optional_row(self.__goal_f, data), 

1440 best_f_scaled=StatReader.parse_optional_row( 

1441 self.__best_f_scaled, data), 

1442 n_success=csv_val_or_none(data, self.__idx_n_success, int), 

1443 success_fes=StatReader.parse_optional_row( 

1444 self.__success_fes, data), 

1445 success_time_millis=StatReader.parse_optional_row( 

1446 self.__success_time_millis, data), 

1447 ert_fes=csv_val_or_none(data, self.__idx_ert_fes, str_to_num), 

1448 ert_time_millis=csv_val_or_none( 

1449 data, self.__idx_ert_time_millis, str_to_num), 

1450 max_fes=StatReader.parse_optional_row(self.__max_fes, data), 

1451 max_time_millis=StatReader.parse_optional_row( 

1452 self.__max_time_millis, data), 

1453 ) 

1454 

1455 

1456@dataclass(frozen=True, init=False, order=False, eq=False) 

1457class __PvEndStatistics(EndStatistics): 

1458 """Aggregated end statistics.""" 

1459 

1460 #: the value of the parameter over which it is aggregated 

1461 pv: int | float 

1462 

1463 def __init__(self, es: EndStatistics, pv: int | float): 

1464 """ 

1465 Create the end statistics of an experiment-setup combination. 

1466 

1467 :param es: the original end statistics object 

1468 :param pv: the parameter value 

1469 """ 

1470 super().__init__( 

1471 es.algorithm, es.instance, es.objective, es.encoding, es.n, 

1472 es.best_f, es.last_improvement_fe, 

1473 es.last_improvement_time_millis, es.total_fes, 

1474 es.total_time_millis, es.goal_f, es.best_f_scaled, es.n_success, 

1475 es.success_fes, es.success_time_millis, es.ert_fes, 

1476 es.ert_time_millis, es.max_fes, es.max_time_millis) 

1477 if not isinstance(pv, (int | float)): 

1478 raise type_error(pv, "pv", (int, float)) 

1479 if not isfinite(pv): 

1480 raise ValueError(f"got {pv=}") 

1481 object.__setattr__(self, "pv", pv) 

1482 

1483 def get_param_value(self) -> int | float: 

1484 """ 

1485 Get the parameter value. 

1486 

1487 :return: the parameter value 

1488 """ 

1489 return self.pv 

1490 

1491 

1492def aggregate_over_parameter( 

1493 data: Iterable[EndResult], 

1494 param_value: Callable[[EndResult], int | float], 

1495 join_all_algorithms: bool = False, 

1496 join_all_instances: bool = False, 

1497 join_all_objectives: bool = False, 

1498 join_all_encodings: bool = False) -> tuple[ 

1499 Callable[[EndStatistics], int | float], Iterable[EndStatistics]]: 

1500 """ 

1501 Aggregate a stream of data into groups based on a parameter. 

1502 

1503 :param data: the source data 

1504 :param param_value: the function obtaining a parameter value 

1505 :param join_all_algorithms: should the statistics be aggregated 

1506 over all algorithms 

1507 :param join_all_instances: should the statistics be aggregated 

1508 over all algorithms 

1509 :param join_all_objectives: should the statistics be aggregated over 

1510 all objectives? 

1511 :param join_all_encodings: should statistics be aggregated over all 

1512 encodings 

1513 """ 

1514 param_map: Final[dict[int | float, list[EndResult]]] = {} 

1515 for er in data: 

1516 pv = param_value(er) 

1517 if not isinstance(pv, (int | float)): 

1518 raise type_error(pv, f"param_value{er}", (int, float)) 

1519 if not isfinite(pv): 

1520 raise ValueError(f"got {pv} = param({er})") 

1521 if pv in param_map: 

1522 param_map[pv].append(er) 

1523 else: 

1524 param_map[pv] = [er] 

1525 if dict.__len__(param_map) <= 0: 

1526 raise ValueError("Did not encounter any data.") 

1527 

1528 stats: Final[list[EndStatistics]] = [] 

1529 for pv in sorted(param_map.keys()): 

1530 for ess in from_end_results( 

1531 param_map[pv], join_all_algorithms, join_all_instances, 

1532 join_all_objectives, join_all_encodings): 

1533 stats.append(__PvEndStatistics(ess, pv)) 

1534 return cast("Callable[[EndStatistics], int | float]", 

1535 __PvEndStatistics.get_param_value), tuple(stats) 

1536 

1537 

1538# Run end-results to stat file if executed as script 

1539if __name__ == "__main__": 

1540 parser: Final[argparse.ArgumentParser] = moptipy_argparser( 

1541 __file__, "Build an end-results statistics CSV file.", 

1542 "This program creates a CSV file with basic statistics on the " 

1543 "end-of-run state of experiments conducted with moptipy. It " 

1544 "therefore either parses a directory structure with log files " 

1545 "(if src identifies a directory) or a end results CSV file (if" 

1546 " src identifies a file). In the former case, the directory " 

1547 "will follow the form 'algorithm/instance/log_file' with one " 

1548 "log file per run. In the latter case, it will be a file " 

1549 "generated by the end_results.py tool of moptipy. The output " 

1550 "of this tool is a CSV file where the columns are separated by" 

1551 " ';' and the rows contain the statistics.") 

1552 def_src: str = "./evaluation/end_results.txt" 

1553 if not os.path.isfile(def_src): 

1554 def_src = "./results" 

1555 parser.add_argument( 

1556 "source", nargs="?", default=def_src, 

1557 help="either the directory with moptipy log files or the path to the " 

1558 "end-results CSV file", type=Path) 

1559 parser.add_argument( 

1560 "dest", type=Path, nargs="?", 

1561 default="./evaluation/end_statistics.txt", 

1562 help="the path to the end results statistics CSV file to be created") 

1563 parser.add_argument( 

1564 "--join_algorithms", 

1565 help="compute statistics over all algorithms, i.e., the statistics" 

1566 " are not separated by algorithm but all algorithms are treated " 

1567 "as one", action="store_true") 

1568 parser.add_argument( 

1569 "--join_instances", 

1570 help="compute statistics over all instances, i.e., the statistics" 

1571 " are not separated by instance but all instances are treated " 

1572 "as one", action="store_true") 

1573 parser.add_argument( 

1574 "--join_objectives", 

1575 help="compute statistics over all objective functions, i.e., the " 

1576 "statistics are not separated by objective functions but all " 

1577 "objectives functions are treated as one", action="store_true") 

1578 parser.add_argument( 

1579 "--join_encodings", 

1580 help="compute statistics over all encodings, i.e., the statistics" 

1581 " are not separated by encodings but all encodings are treated " 

1582 "as one", action="store_true") 

1583 args: Final[argparse.Namespace] = parser.parse_args() 

1584 

1585 src_path: Final[Path] = args.source 

1586 end_results: Iterable[EndResult] 

1587 if src_path.is_file(): 

1588 logger(f"{src_path!r} identifies as file, load as end-results csv") 

1589 end_results = end_results_from_csv(src_path) 

1590 else: 

1591 logger(f"{src_path!r} identifies as directory, load it as log files") 

1592 end_results = end_results_from_logs(src_path) 

1593 

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

1595 to_csv(from_end_results( 

1596 source=end_results, 

1597 join_all_algorithms=args.join_algorithms, 

1598 join_all_instances=args.join_instances, 

1599 join_all_objectives=args.join_objectives, 

1600 join_all_encodings=args.join_encodings), args.dest)