Coverage for moptipy/evaluation/end_results.py: 85%

477 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-18 11:24 +0000

1""" 

2Record for EndResult as well as parsing, serialization, and parsing. 

3 

4When doing experiments with `moptipy`, you apply algorithm setups to problem 

5instances. For each `setup x instance` combination, you may conduct a series 

6of repetitions (so-called runs) with different random seeds. Each single run 

7of an algorithm setup on a problem instances can produce a separate log file. 

8From each log file, we can load a :class:`EndResult` instance, which 

9represents, well, the end result of the run, i.e., information such as the 

10best solution quality reached, when it was reached, and the termination 

11criterion. These end result records then can be the basis for, e.g., computing 

12summary statistics via :mod:`~moptipy.evaluation.end_statistics` or for 

13plotting the end result distribution via 

14:mod:`~moptipy.evaluation.plot_end_results`. 

15""" 

16import argparse 

17from dataclasses import dataclass 

18from math import inf, isfinite 

19from typing import Any, Callable, Final, Generator, Iterable, TypeVar, cast 

20 

21from pycommons.ds.sequences import reiterable 

22from pycommons.io.console import logger 

23from pycommons.io.csv import ( 

24 CSV_SEPARATOR, 

25 SCOPE_SEPARATOR, 

26 csv_column, 

27 csv_column_or_none, 

28 csv_scope, 

29 csv_str_or_none, 

30 csv_val_or_none, 

31 pycommons_footer_bottom_comments, 

32) 

33from pycommons.io.csv import CsvReader as CsvReaderBase 

34from pycommons.io.csv import CsvWriter as CsvWriterBase 

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

36from pycommons.strings.chars import NEWLINE 

37from pycommons.strings.string_conv import ( 

38 int_or_none_to_str, 

39 num_or_none_to_str, 

40 num_to_str, 

41 str_to_num, 

42) 

43from pycommons.types import ( 

44 check_int_range, 

45 check_to_int_range, 

46 type_error, 

47) 

48 

49from moptipy.api.logging import ( 

50 KEY_ALGORITHM, 

51 KEY_BEST_F, 

52 KEY_GOAL_F, 

53 KEY_INSTANCE, 

54 KEY_LAST_IMPROVEMENT_FE, 

55 KEY_LAST_IMPROVEMENT_TIME_MILLIS, 

56 KEY_MAX_FES, 

57 KEY_MAX_TIME_MILLIS, 

58 KEY_RAND_SEED, 

59 KEY_TOTAL_FES, 

60 KEY_TOTAL_TIME_MILLIS, 

61 PROGRESS_CURRENT_F, 

62 PROGRESS_FES, 

63 PROGRESS_TIME_MILLIS, 

64 SCOPE_SEARCH_SPACE, 

65 SCOPE_SOLUTION_SPACE, 

66 SECTION_PROGRESS, 

67) 

68from moptipy.evaluation._utils import ( 

69 _check_max_time_millis, 

70) 

71from moptipy.evaluation.base import ( 

72 DESC_ALGORITHM, 

73 DESC_ENCODING, 

74 DESC_INSTANCE, 

75 DESC_OBJECTIVE_FUNCTION, 

76 F_NAME_NORMALIZED, 

77 F_NAME_RAW, 

78 F_NAME_SCALED, 

79 KEY_ENCODING, 

80 KEY_OBJECTIVE_FUNCTION, 

81 PerRunData, 

82 motipy_footer_bottom_comments, 

83) 

84from moptipy.evaluation.log_parser import SetupAndStateParser 

85from moptipy.utils.help import moptipy_argparser 

86from moptipy.utils.math import try_float_div, try_int, try_int_div 

87 

88#: a description of the random seed 

89DESC_RAND_SEED: Final[str] = ( 

90 "the value of the seed of the random number generator used in the run. " 

91 f"Random seeds are in 0..{((1 << (8 * 8)) - 1)} and the random " 

92 f"number generators are those from numpy.") 

93#: the description of best-F 

94DESC_BEST_F: Final[str] = ( 

95 " the best (smallest) objective value ever encountered during the run (" 

96 "regardless whether the algorithm later forgot it again or not).") 

97#: the description of the last improvement FE 

98DESC_LAST_IMPROVEMENT_FE: Final[str] = ( 

99 "the objective function evaluation (FE) when the last improving move took" 

100 " place. 1 FE corresponds to the construction and evaluation " 

101 "of one solution. The first FE has index 1. With 'last " 

102 "improving move' we mean the last time when a solution was " 

103 "discovered that was better than all previous solutions. This " 

104 "time / FE index is the one when the solution with objective " 

105 f"value {KEY_BEST_F} was discovered.") 

106#: the description of the last improvement time milliseconds 

107DESC_LAST_IMPROVEMENT_TIME_MILLIS: Final[str] = ( 

108 "the clock time in milliseconds after the begin of the run when " 

109 "the last improving search move took place.") 

110#: the description of the total FEs 

111DESC_TOTAL_FES: Final[str] = ( 

112 "the total number of objective function evaluations (FEs) that were " 

113 "performed during the run.") 

114#: the total consumed time in milliseconds 

115DESC_TOTAL_TIME_MILLIS: Final[str] = ( 

116 "the clock time in milliseconds that has passed between the begin of the " 

117 "run and the end of the run.") 

118#: the description of the goal objective value 

119DESC_GOAL_F: Final[str] = ( 

120 "the goal objective value. A run will stop as soon as a solution was" 

121 "discovered which has an objective value less than or equal to " 

122 f"{KEY_GOAL_F}. In other words, as soon as {KEY_BEST_F} reaches or dips " 

123 f"under {KEY_GOAL_F}, the algorithm will stop. If {KEY_GOAL_F} is not " 

124 "reached, the run will continue until other budget limits are exhausted. " 

125 "If a lower bound for the objective function is known, this is often used" 

126 " as a goal objective value. If o goal objective value is specified, this" 

127 " field is empty.") 

128#: a description of the budget as the maximum objective function evaluation 

129DESC_MAX_FES: Final[str] = ( 

130 "the maximum number of permissible FEs per run. As soon as this limit is " 

131 f"reached, the run will stop. In other words, {KEY_TOTAL_FES} will never " 

132 f"be more than {KEY_MAX_FES}. A run may stop earlier if some other " 

133 "termination criterion is reached, but never later.") 

134#: a description of the budget in terms of maximum runtime 

135DESC_MAX_TIME_MILLIS: Final[str] = ( 

136 "the maximum number of milliseconds of clock time that a run is permitted" 

137 " to use as computational budget before being terminated. This limit is " 

138 "more of a soft limit, as we cannot physically stop a run at arbitrary " 

139 "points without causing mayhem. Thus, it may be that some runs consume " 

140 "slightly more runtime than this limit. But the rule is that the " 

141 "algorithm gets told to stop (via should_terminate() becoming True) as " 

142 f"soon as this time has elapsed. But generally, {KEY_TOTAL_TIME_MILLIS}<=" 

143 f"{KEY_MAX_TIME_MILLIS} approximately holds.") 

144#: the CSV separator replacement 

145__REPLACE_CSVSEP: Final[str] = "[SEP]" 

146#: the newline replacement 

147__REPLACE_NEWLINE: Final[str] = "[NEWLINE]" 

148#: a description of the point in the search space 

149DESC_SEARCH_SPACE: Final[str] = ( 

150 "the point in the search space corresponding to the result, if any." 

151 " Here, any newline character or line break is replaced with " 

152 f"{__REPLACE_NEWLINE!r} and any occurrence of {CSV_SEPARATOR!r} is " 

153 f"replaced with {__REPLACE_CSVSEP!r}.") 

154#: a description of the point in the solution space 

155DESC_SOLUTION_SPACE: Final[str] = ( 

156 "the point in the candidate solution corresponding to the result, if any." 

157 " Here, any newline character or line break is replaced with " 

158 f"{__REPLACE_NEWLINE!r} and any occurrence of {CSV_SEPARATOR!r} is " 

159 f"replaced with {__REPLACE_CSVSEP!r}.") 

160 

161 

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

163class EndResult(PerRunData): 

164 """ 

165 An immutable end result record of one run of one algorithm on one problem. 

166 

167 This record provides the information of the outcome of one application of 

168 one algorithm to one problem instance in an immutable way. 

169 """ 

170 

171 #: The best objective value encountered. 

172 best_f: int | float 

173 

174 #: The index of the function evaluation when best_f was reached. 

175 last_improvement_fe: int 

176 

177 #: The time when best_f was reached. 

178 last_improvement_time_millis: int 

179 

180 #: The total number of performed FEs. 

181 total_fes: int 

182 

183 #: The total time consumed by the run. 

184 total_time_millis: int 

185 

186 #: The goal objective value if provided 

187 goal_f: int | float | None 

188 

189 #: The (optional) maximum permitted FEs. 

190 max_fes: int | None 

191 

192 #: The (optional) maximum runtime. 

193 max_time_millis: int | None 

194 

195 #: The (optional) point in the search space 

196 x: str | None 

197 #: The (optional) point in the solution space 

198 y: str | None 

199 

200 def __init__(self, 

201 algorithm: str, 

202 instance: str, 

203 objective: str, 

204 encoding: str | None, 

205 rand_seed: int, 

206 best_f: int | float, 

207 last_improvement_fe: int, 

208 last_improvement_time_millis: int, 

209 total_fes: int, 

210 total_time_millis: int, 

211 goal_f: int | float | None = None, 

212 max_fes: int | None = None, 

213 max_time_millis: int | None = None, 

214 x: str | None = None, 

215 y: str | None = None): 

216 """ 

217 Create a consistent instance of :class:`EndResult`. 

218 

219 :param algorithm: the algorithm name 

220 :param instance: the instance name 

221 :param objective: the name of the objective function 

222 :param encoding: the name of the encoding that was used, if any, or 

223 `None` if no encoding was used 

224 :param rand_seed: the random seed 

225 :param best_f: the best reached objective value 

226 :param last_improvement_fe: the FE when best_f was reached 

227 :param last_improvement_time_millis: the time when best_f was reached 

228 :param total_fes: the total FEs 

229 :param total_time_millis: the total runtime 

230 :param goal_f: the goal objective value, if provide 

231 :param max_fes: the optional maximum FEs 

232 :param max_time_millis: the optional maximum runtime 

233 :param x: the point in the search space, if any 

234 :param y: the point in the solution space, if any 

235 

236 :raises TypeError: if any parameter has a wrong type 

237 :raises ValueError: if the parameter values are inconsistent 

238 """ 

239 super().__init__(algorithm, instance, objective, encoding, rand_seed) 

240 object.__setattr__(self, "best_f", try_int(best_f)) 

241 object.__setattr__( 

242 self, "last_improvement_fe", check_int_range( 

243 last_improvement_fe, "last_improvement_fe", 

244 1, 1_000_000_000_000_000)) 

245 object.__setattr__( 

246 self, "last_improvement_time_millis", check_int_range( 

247 last_improvement_time_millis, "last_improvement_time_millis", 

248 0, 100_000_000_000)) 

249 object.__setattr__( 

250 self, "total_fes", check_int_range( 

251 total_fes, "total_fes", last_improvement_fe, 

252 1_000_000_000_000_000)) 

253 object.__setattr__( 

254 self, "total_time_millis", check_int_range( 

255 total_time_millis, "total_time_millis", 

256 last_improvement_time_millis, 100_000_000_000)) 

257 

258 if goal_f is not None: 

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

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

261 

262 if max_fes is not None: 

263 check_int_range(max_fes, "max_fes", total_fes, 

264 1_000_000_000_000_000_000) 

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

266 

267 if max_time_millis is not None: 

268 check_int_range( 

269 max_time_millis, "max_time_millis", 1, 100_000_000_000) 

270 _check_max_time_millis(max_time_millis, 

271 total_fes, 

272 total_time_millis) 

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

274 

275 use_x = x 

276 if use_x is not None: 

277 use_x = str.strip(use_x) 

278 if str.__len__(use_x) <= 0: 

279 raise ValueError(f"x cannot be {x!r}.") 

280 object.__setattr__(self, "x", use_x) 

281 

282 use_y = y 

283 if use_y is not None: 

284 use_y = str.strip(use_y) 

285 if str.__len__(use_y) <= 0: 

286 raise ValueError(f"y cannot be {y!r}.") 

287 object.__setattr__(self, "y", use_y) 

288 

289 def _tuple(self) -> tuple[Any, ...]: 

290 """ 

291 Get the tuple representation of this object used in comparisons. 

292 

293 :return: the comparison-relevant data of this object in a tuple 

294 """ 

295 return (self.__class__.__name__, 

296 "" if self.algorithm is None else self.algorithm, 

297 "" if self.instance is None else self.instance, 

298 "" if self.objective is None else self.objective, 

299 "" if self.encoding is None else self.encoding, 

300 1, self.rand_seed, "", "", 

301 inf if self.goal_f is None else self.goal_f, 

302 inf if self.max_fes is None else self.max_fes, 

303 inf if self.max_time_millis is None else self.max_time_millis, 

304 self.best_f, self.last_improvement_fe, 

305 self.last_improvement_time_millis, self.total_fes, 

306 self.total_time_millis) 

307 

308 def success(self) -> bool: 

309 """ 

310 Check if a run is successful. 

311 

312 This method returns `True` if and only if `goal_f` is defined and 

313 `best_f <= goal_f` (and `False` otherwise). 

314 

315 :return: `True` if and only if `best_f<=goal_f` 

316 """ 

317 return False if self.goal_f is None else self.best_f <= self.goal_f 

318 

319 def get_best_f(self) -> int | float: 

320 """ 

321 Get the best objective value reached. 

322 

323 :returns: the best objective value reached 

324 """ 

325 if not isinstance(self, EndResult): 

326 raise type_error(self, "self", EndResult) 

327 return self.best_f 

328 

329 def get_last_improvement_fe(self) -> int: 

330 """ 

331 Get the index of the function evaluation when `best_f` was reached. 

332 

333 :returns: the index of the function evaluation when `best_f` was 

334 reached 

335 """ 

336 if not isinstance(self, EndResult): 

337 raise type_error(self, "self", EndResult) 

338 return self.last_improvement_fe 

339 

340 def get_last_improvement_time_millis(self) -> int: 

341 """ 

342 Get the milliseconds when `best_f` was reached. 

343 

344 :returns: the milliseconds when `best_f` was reached 

345 """ 

346 if not isinstance(self, EndResult): 

347 raise type_error(self, "self", EndResult) 

348 return self.last_improvement_time_millis 

349 

350 def get_total_fes(self) -> int: 

351 """ 

352 Get the total number of performed FEs. 

353 

354 :returns: the total number of performed FEs 

355 """ 

356 if not isinstance(self, EndResult): 

357 raise type_error(self, "self", EndResult) 

358 return self.total_fes 

359 

360 def get_total_time_millis(self) -> int: 

361 """ 

362 Get the total time consumed by the run. 

363 

364 :returns: the total time consumed by the run 

365 """ 

366 if not isinstance(self, EndResult): 

367 raise type_error(self, "self", EndResult) 

368 return self.total_time_millis 

369 

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

371 """ 

372 Get the goal objective value, if any. 

373 

374 :returns: the goal objective value, if any 

375 """ 

376 if not isinstance(self, EndResult): 

377 raise type_error(self, "self", EndResult) 

378 return self.goal_f 

379 

380 def get_max_fes(self) -> int | None: 

381 """ 

382 Get the maximum number of FEs permissible. 

383 

384 :returns: the maximum number of FEs permissible 

385 """ 

386 if not isinstance(self, EndResult): 

387 raise type_error(self, "self", EndResult) 

388 return self.max_fes 

389 

390 def get_max_time_millis(self) -> int | None: 

391 """ 

392 Get the maximum permissible milliseconds permitted. 

393 

394 :returns: the maximum permissible milliseconds permitted 

395 """ 

396 if not isinstance(self, EndResult): 

397 raise type_error(self, "self", EndResult) 

398 return self.max_time_millis 

399 

400 def get_normalized_best_f(self) -> int | float | None: 

401 """ 

402 Get the normalized f. 

403 

404 :returns: the normalized f 

405 """ 

406 g: Final[int | float | None] = EndResult.get_goal_f(self) 

407 if (g is None) or (g <= 0): 

408 return None 

409 return try_float_div(self.best_f - g, g) 

410 

411 def get_scaled_best_f(self) -> int | float | None: 

412 """ 

413 Get the normalized f. 

414 

415 :returns: the normalized f 

416 """ 

417 g: Final[int | float | None] = EndResult.get_goal_f(self) 

418 if (g is None) or (g <= 0): 

419 return None 

420 return try_float_div(self.best_f, g) 

421 

422 def get_fes_per_time_milli(self) -> int | float: 

423 """ 

424 Get the fes per time milliseconds. 

425 

426 :returns: the fes per time milliseconds 

427 """ 

428 return try_int_div(EndResult.get_total_fes(self), max( 

429 1, EndResult.get_total_time_millis(self))) 

430 

431 

432#: A set of getters for accessing variables of the end result 

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

434 EndResult], int | float | None]]] = { 

435 KEY_LAST_IMPROVEMENT_FE: EndResult.get_last_improvement_fe, 

436 "last improvement FE": EndResult.get_last_improvement_fe, 

437 KEY_LAST_IMPROVEMENT_TIME_MILLIS: 

438 EndResult.get_last_improvement_time_millis, 

439 "last improvement ms": EndResult.get_last_improvement_time_millis, 

440 KEY_TOTAL_FES: EndResult.get_total_fes, 

441 "fes": EndResult.get_total_fes, 

442 KEY_TOTAL_TIME_MILLIS: EndResult.get_total_time_millis, 

443 "ms": EndResult.get_total_time_millis, 

444 KEY_GOAL_F: EndResult.get_goal_f, 

445 F_NAME_RAW: EndResult.get_best_f, 

446 KEY_BEST_F: EndResult.get_best_f, 

447 "f": EndResult.get_best_f, 

448 F_NAME_SCALED: EndResult.get_scaled_best_f, 

449 "bestFscaled": EndResult.get_scaled_best_f, 

450 F_NAME_NORMALIZED: EndResult.get_normalized_best_f, 

451 "bestFnormalized": EndResult.get_normalized_best_f, 

452 KEY_MAX_FES: EndResult.get_max_fes, 

453 "budgetFEs": EndResult.get_max_fes, 

454 KEY_MAX_TIME_MILLIS: EndResult.get_max_time_millis, 

455 "budgetMS": EndResult.get_max_time_millis, 

456 "fesPerTimeMilli": EndResult.get_fes_per_time_milli, 

457}.get 

458 

459 

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

461 """ 

462 Produce a function that obtains the given dimension from EndResults. 

463 

464 The following dimensions are supported: 

465 

466 1. `lastImprovementFE`: :attr:`~EndResult.last_improvement_fe` 

467 2. `lastImprovementTimeMillis`: 

468 :attr:`~EndResult.last_improvement_time_millis` 

469 3. `totalFEs`: :attr:`~EndResult.total_fes` 

470 4. `totalTimeMillis`: :attr:`~EndResult.total_time_millis` 

471 5. `goalF`: :attr:`~EndResult.goal_f` 

472 6. `plainF`, `bestF`: :attr:`~EndResult.best_f` 

473 7. `scaledF`: :attr:`~EndResult.best_f`/:attr:`~EndResult.goal_f` 

474 8. `normalizedF`: (:attr:`~EndResult.best_f`-attr:`~EndResult.goal_f`)/ 

475 :attr:`~EndResult.goal_f` 

476 9. `maxFEs`: :attr:`~EndResult.max_fes` 

477 10. `maxTimeMillis`: :attr:`~EndResult.max_time_millis` 

478 11. `fesPerTimeMilli`: :attr:`~EndResult.total_fes` 

479 /:attr:`~EndResult.total_time_millis` 

480 

481 :param dimension: the dimension 

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

483 dimension from its input value, which must be an :class:`EndResult` 

484 """ 

485 result: Callable[[EndResult], int | float] | None = __PROPERTIES( 

486 str.strip(dimension)) 

487 if result is None: 

488 raise ValueError(f"Unknown EndResult dimension {dimension!r}.") 

489 return result 

490 

491 

492def to_csv(results: Iterable[EndResult], file: str) -> Path: 

493 """ 

494 Write a sequence of end results to a file in CSV format. 

495 

496 :param results: the end results 

497 :param file: the path 

498 :return: the path of the file that was written 

499 """ 

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

501 logger(f"Writing end results to CSV file {path!r}.") 

502 path.ensure_parent_dir_exists() 

503 with path.open_for_write() as wt: 

504 write_lines(CsvWriter.write(results), wt) 

505 logger(f"Done writing end results to CSV file {path!r}.") 

506 return path 

507 

508 

509def from_csv(file: str, 

510 filterer: Callable[[EndResult], bool] 

511 = lambda _: True) -> Generator[EndResult, None, None]: 

512 """ 

513 Parse a given CSV file to get :class:`EndResult` Records. 

514 

515 :param file: the path to parse 

516 :param filterer: an optional filter function 

517 """ 

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

519 logger(f"Now reading CSV file {path!r}.") 

520 with path.open_for_read() as rd: 

521 for r in CsvReader.read(rd): 

522 if filterer(r): 

523 yield r 

524 logger(f"Done reading CSV file {path!r}.") 

525 

526 

527def _point_to_csv(s: str | None) -> str: 

528 """ 

529 Convert a solution or search space point to CSV. 

530 

531 :param s: the solution or point in the search space 

532 :return: the CSV string 

533 :raises ValueError: if the original string contains a character sequence 

534 that cannot be converted to a CSV string 

535 """ 

536 if s is None: 

537 return "" 

538 if __REPLACE_CSVSEP in s: 

539 raise ValueError(f"Cannot serialize {s!r} to CSV because " 

540 f"it contains {__REPLACE_CSVSEP!r}.") 

541 if __REPLACE_NEWLINE in s: 

542 raise ValueError(f"Cannot serialize {s!r} to CSV because " 

543 f"it contains {__REPLACE_NEWLINE!r}.") 

544 for nl in NEWLINE: 

545 s = str.replace(s, nl, __REPLACE_NEWLINE) 

546 return str.replace(s, CSV_SEPARATOR, __REPLACE_CSVSEP) 

547 

548 

549def _csv_to_point(s: str | None) -> str | None: 

550 """ 

551 Convert a CSV string to a solution or search space point. 

552 

553 :param s: the CSV string 

554 :return: the CSV string 

555 """ 

556 if s is None: 

557 return None 

558 s = str.strip(s) 

559 if str.__len__(s) <= 0: 

560 return None 

561 return str.replace(str.replace(s, __REPLACE_NEWLINE, "\n"), 

562 __REPLACE_CSVSEP, CSV_SEPARATOR) 

563 

564 

565class CsvWriter(CsvWriterBase): 

566 """A class for CSV writing of :class:`EndResult`.""" 

567 

568 def __init__(self, data: Iterable[EndResult], 

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

570 """ 

571 Initialize the csv writer. 

572 

573 :param data: the data 

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

575 """ 

576 data = reiterable(data) 

577 super().__init__(data, scope) 

578 no_encoding: bool = True 

579 no_max_fes: bool = True 

580 no_max_ms: bool = True 

581 no_goal_f: bool = True 

582 no_x: bool = True 

583 no_y: bool = True 

584 check: int = 6 

585 for er in data: 

586 if no_encoding and (er.encoding is not None): 

587 no_encoding = False 

588 check -= 1 

589 if check <= 0: 

590 break 

591 if no_max_fes and (er.max_fes is not None): 

592 no_max_fes = False 

593 check -= 1 

594 if check <= 0: 

595 break 

596 if no_max_ms and (er.max_time_millis is not None): 

597 no_max_ms = False 

598 check -= 1 

599 if check <= 0: 

600 break 

601 if no_goal_f and (er.goal_f is not None) and ( 

602 isfinite(er.goal_f)): 

603 no_goal_f = False 

604 check -= 1 

605 if check <= 0: 

606 break 

607 if no_x and (er.x is not None): 

608 no_x = False 

609 check -= 1 

610 if check <= 0: 

611 break 

612 if no_y and (er.y is not None): 

613 no_y = False 

614 check -= 1 

615 if check <= 0: 

616 break 

617 

618 #: do we need the encoding? 

619 self.__needs_encoding: Final[bool] = not no_encoding 

620 #: do we need the max FEs? 

621 self.__needs_max_fes: Final[bool] = not no_max_fes 

622 #: do we need the max millis? 

623 self.__needs_max_ms: Final[bool] = not no_max_ms 

624 #: do we need the goal F? 

625 self.__needs_goal_f: Final[bool] = not no_goal_f 

626 #: do we need an `x` column? 

627 self.__needs_x: Final[bool] = not no_x 

628 #: do we need an `y` column? 

629 self.__needs_y: Final[bool] = not no_y 

630 

631 def get_column_titles(self) -> Iterable[str]: 

632 """ 

633 Get the column titles. 

634 

635 :returns: the column titles 

636 """ 

637 p: Final[str] = self.scope 

638 data: list[str] = [ 

639 KEY_ALGORITHM, KEY_INSTANCE, KEY_OBJECTIVE_FUNCTION] 

640 if self.__needs_encoding: 

641 data.append(KEY_ENCODING) 

642 data.extend((KEY_RAND_SEED, KEY_BEST_F, KEY_LAST_IMPROVEMENT_FE, 

643 KEY_LAST_IMPROVEMENT_TIME_MILLIS, KEY_TOTAL_FES, 

644 KEY_TOTAL_TIME_MILLIS)) 

645 

646 if self.__needs_goal_f: 

647 data.append(KEY_GOAL_F) 

648 if self.__needs_max_fes: 

649 data.append(KEY_MAX_FES) 

650 if self.__needs_max_ms: 

651 data.append(KEY_MAX_TIME_MILLIS) 

652 if self.__needs_x: 

653 data.append(SCOPE_SEARCH_SPACE) 

654 if self.__needs_y: 

655 data.append(SCOPE_SOLUTION_SPACE) 

656 return (csv_scope(p, q) for q in data) 

657 

658 def get_row(self, data: EndResult) -> Iterable[str]: 

659 """ 

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

661 

662 :param data: the end result record 

663 :returns: the row iterator 

664 """ 

665 yield data.algorithm 

666 yield data.instance 

667 yield data.objective 

668 if self.__needs_encoding: 

669 yield data.encoding or "" 

670 yield hex(data.rand_seed) 

671 yield num_to_str(data.best_f) 

672 yield str(data.last_improvement_fe) 

673 yield str(data.last_improvement_time_millis) 

674 yield str(data.total_fes) 

675 yield str(data.total_time_millis) 

676 if self.__needs_goal_f: 

677 yield num_or_none_to_str(data.goal_f) 

678 if self.__needs_max_fes: 

679 yield int_or_none_to_str(data.max_fes) 

680 if self.__needs_max_ms: 

681 yield int_or_none_to_str(data.max_time_millis) 

682 if self.__needs_x: 

683 yield _point_to_csv(data.x) 

684 if self.__needs_y: 

685 yield _point_to_csv(data.y) 

686 

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

688 """ 

689 Get any possible header comments. 

690 

691 :returns: the header comments 

692 """ 

693 return ("Experiment End Results", 

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

695 

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

697 """ 

698 Get any possible footer comments. 

699 

700 :returns: the footer comments 

701 """ 

702 yield "" 

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

704 yield ("Records describing the end results of single runs (" 

705 "single executions) of algorithms applied to optimization " 

706 "problems.") 

707 yield ("Each run is characterized by an algorithm setup, a problem " 

708 "instance, and a random seed.") 

709 if scope: 

710 yield ("All end result records start with prefix " 

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

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

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

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

715 f" {DESC_OBJECTIVE_FUNCTION}") 

716 if self.__needs_encoding: 

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

718 yield f"{csv_scope(scope, KEY_RAND_SEED)}: {DESC_RAND_SEED}" 

719 yield f"{csv_scope(scope, KEY_BEST_F)}: {DESC_BEST_F}" 

720 yield (f"{csv_scope(scope, KEY_LAST_IMPROVEMENT_FE)}: " 

721 f"{DESC_LAST_IMPROVEMENT_FE}") 

722 yield (f"{csv_scope(scope, KEY_LAST_IMPROVEMENT_TIME_MILLIS)}: " 

723 f"{DESC_LAST_IMPROVEMENT_TIME_MILLIS}") 

724 yield f"{csv_scope(scope, KEY_TOTAL_FES)}: {DESC_TOTAL_FES}" 

725 yield (f"{csv_scope(scope, KEY_TOTAL_TIME_MILLIS)}: " 

726 f"{DESC_TOTAL_TIME_MILLIS}") 

727 if self.__needs_goal_f: 

728 yield f"{csv_scope(scope, KEY_GOAL_F)}: {DESC_GOAL_F}" 

729 if self.__needs_max_fes: 

730 yield f"{csv_scope(scope, KEY_MAX_FES)}: {DESC_MAX_FES}" 

731 if self.__needs_max_ms: 

732 yield (f"{csv_scope(scope, KEY_MAX_TIME_MILLIS)}: " 

733 f"{DESC_MAX_TIME_MILLIS}") 

734 if self.__needs_x: 

735 yield (f"{csv_scope(scope, SCOPE_SEARCH_SPACE)}: " 

736 f"{DESC_SEARCH_SPACE}") 

737 if self.__needs_y: 

738 yield (f"{csv_scope(scope, SCOPE_SOLUTION_SPACE)}: " 

739 f"{SCOPE_SOLUTION_SPACE}") 

740 

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

742 """ 

743 Get the footer bottom comments. 

744 

745 :returns: the footer comments 

746 """ 

747 yield from motipy_footer_bottom_comments( 

748 self, ("The end results data is produced using module " 

749 "moptipy.evaluation.end_results.")) 

750 yield from pycommons_footer_bottom_comments(self) 

751 

752 

753class CsvReader(CsvReaderBase): 

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

755 

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

757 """ 

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

759 

760 :param columns: the columns 

761 """ 

762 super().__init__(columns) 

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

764 self.__idx_algorithm: Final[int] = csv_column(columns, KEY_ALGORITHM) 

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

766 self.__idx_instance: Final[int] = csv_column(columns, KEY_INSTANCE) 

767 #: the index of the objective function column, if any 

768 self.__idx_objective: Final[int] = csv_column( 

769 columns, KEY_OBJECTIVE_FUNCTION) 

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

771 self.__idx_encoding = csv_column_or_none(columns, KEY_ENCODING) 

772 

773 #: the index of the random seed column 

774 self.__idx_seed: Final[int] = csv_column(columns, KEY_RAND_SEED) 

775 #: the column with the last improvement FE 

776 self.__idx_li_fe: Final[int] = csv_column( 

777 columns, KEY_LAST_IMPROVEMENT_FE) 

778 #: the column with the last improvement time milliseconds 

779 self.__idx_li_ms: Final[int] = csv_column( 

780 columns, KEY_LAST_IMPROVEMENT_TIME_MILLIS) 

781 #: the column with the best obtained objective value 

782 self.__idx_best_f: Final[int] = csv_column(columns, KEY_BEST_F) 

783 #: the column with the total time in FEs 

784 self.__idx_tt_fe: Final[int] = csv_column(columns, KEY_TOTAL_FES) 

785 #: the column with the total time in milliseconds 

786 self.__idx_tt_ms: Final[int] = csv_column( 

787 columns, KEY_TOTAL_TIME_MILLIS) 

788 

789 #: the column with the goal objective value, if any 

790 self.__idx_goal_f: Final[int | None] = csv_column_or_none( 

791 columns, KEY_GOAL_F) 

792 #: the column with the maximum FEs, if any such budget constraint was 

793 #: defined 

794 self.__idx_max_fes: Final[int | None] = csv_column_or_none( 

795 columns, KEY_MAX_FES) 

796 #: the column with the maximum runtime in milliseconds, if any such 

797 #: budget constraint was defined 

798 self.__idx_max_ms: Final[int | None] = csv_column_or_none( 

799 columns, KEY_MAX_TIME_MILLIS) 

800 #: the column with the search space element 

801 self.__idx_x: Final[int | None] = csv_column_or_none( 

802 columns, SCOPE_SEARCH_SPACE) 

803 #: the column with the solution space element 

804 self.__idx_y: Final[int | None] = csv_column_or_none( 

805 columns, SCOPE_SOLUTION_SPACE) 

806 

807 def parse_row(self, data: list[str]) -> EndResult: 

808 """ 

809 Parse a row of data. 

810 

811 :param data: the data row 

812 :return: the end result statistics 

813 """ 

814 return EndResult( 

815 data[self.__idx_algorithm], # algorithm 

816 data[self.__idx_instance], # instance 

817 data[self.__idx_objective], # objective 

818 csv_str_or_none(data, self.__idx_encoding), # encoding 

819 int(data[self.__idx_seed], base=0), # rand seed 

820 str_to_num(data[self.__idx_best_f]), # best_f 

821 int(data[self.__idx_li_fe]), # last_improvement_fe 

822 int(data[self.__idx_li_ms]), # last_improvement_time_millis 

823 int(data[self.__idx_tt_fe]), # total_fes 

824 int(data[self.__idx_tt_ms]), # total_time_millis 

825 csv_val_or_none(data, self.__idx_goal_f, str_to_num), 

826 csv_val_or_none(data, self.__idx_max_fes, int), # max_fes 

827 csv_val_or_none(data, self.__idx_max_ms, int), # max_time_ms 

828 csv_val_or_none(data, self.__idx_x, _csv_to_point), # x 

829 csv_val_or_none(data, self.__idx_y, _csv_to_point)) # y 

830 

831 

832#: the type variable for data to be read from the directories 

833T = TypeVar("T", bound=EndResult) 

834 

835 

836class EndResultLogParser[T](SetupAndStateParser[T]): 

837 """The internal log parser class.""" 

838 

839 def _parse_file(self, file: Path) -> T: 

840 """ 

841 Get the parsing result. 

842 

843 :returns: the :class:`EndResult` instance 

844 """ 

845 super()._parse_file(file) 

846 return cast("T", EndResult(self.algorithm, 

847 self.instance, 

848 self.objective, 

849 self.encoding, 

850 self.rand_seed, 

851 self.best_f, 

852 self.last_improvement_fe, 

853 self.last_improvement_time_millis, 

854 self.total_fes, 

855 self.total_time_millis, 

856 self.goal_f, 

857 self.max_fes, 

858 self.max_time_millis)) 

859 

860 

861def _join_goals(vlimit, vgoal, select): # noqa 

862 if vlimit is None: 

863 return vgoal 

864 if vgoal is None: 

865 return vlimit 

866 return select(vlimit, vgoal) 

867 

868 

869class __EndResultProgressLogParser(SetupAndStateParser[EndResult]): 

870 """The internal log parser class for virtual end results.""" 

871 

872 def __init__( 

873 self, 

874 max_fes: int | Callable[[str, str], int | None] | None, 

875 max_time_millis: int | Callable[[str, str], int | None] | None, 

876 goal_f: int | float | Callable[ 

877 [str, str], int | float | None] | None, 

878 path_filter: Callable[[Path], bool] | None = None): 

879 """ 

880 Create the internal log parser. 

881 

882 :param max_fes: the maximum FEs, or `None` if unspecified 

883 :param max_time_millis: the maximum runtime in milliseconds, or 

884 `None` if unspecified 

885 :param goal_f: the goal objective value, or `None` if unspecified 

886 :param path_filter: the path filter 

887 """ 

888 super().__init__(path_filter) 

889 self.__src_limit_ms: Final[ 

890 int | Callable[[str, str], int | None] | None] = max_time_millis 

891 self.__src_limit_fes: Final[ 

892 int | Callable[[str, str], int | None] | None] = max_fes 

893 self.__src_limit_f: Final[ 

894 int | float | Callable[ 

895 [str, str], int | float | None] | None] = goal_f 

896 

897 self.__limit_ms: int | float = inf 

898 self.__limit_ms_n: int | None = None 

899 self.__limit_fes: int | float = inf 

900 self.__limit_fes_n: int | None = None 

901 self.__limit_f: int | float = -inf 

902 self.__limit_f_n: int | float | None = None 

903 

904 self.__stop_fes: int | None = None 

905 self.__stop_ms: int | None = None 

906 self.__stop_f: int | float | None = None 

907 self.__stop_li_fe: int | None = None 

908 self.__stop_li_ms: int | None = None 

909 self.__hit_goal: bool = False 

910 self.__state: int = 0 

911 

912 def _parse_file(self, file: Path) -> EndResult: 

913 super()._parse_file(file) 

914 if self.__state != 2: 

915 raise ValueError( 

916 "Illegal state, log file must have a " 

917 f"{SECTION_PROGRESS!r} section.") 

918 self.__state = 0 

919 l_hit_goal = self.__hit_goal 

920 stop_fes: int = self.__stop_fes 

921 stop_ms: int = self.__stop_ms 

922 if not l_hit_goal: 

923 stop_ms = max(stop_ms, cast("int", min( 

924 self.total_time_millis, self.__limit_ms))) 

925 ul_fes = self.total_fes 

926 if stop_ms < self.total_time_millis: 

927 ul_fes -= 1 

928 stop_fes = max(stop_fes, cast("int", min( 

929 ul_fes, self.__limit_fes))) 

930 

931 return EndResult( 

932 algorithm=self.algorithm, 

933 instance=self.instance, 

934 objective=self.objective, 

935 encoding=self.encoding, 

936 rand_seed=self.rand_seed, 

937 best_f=self.__stop_f, 

938 last_improvement_fe=self.__stop_li_fe, 

939 last_improvement_time_millis=self.__stop_li_ms, 

940 total_fes=stop_fes, 

941 total_time_millis=stop_ms, 

942 goal_f=_join_goals(self.__limit_f_n, self.goal_f, max), 

943 max_fes=_join_goals(self.__limit_fes_n, self.max_fes, min), 

944 max_time_millis=_join_goals( 

945 self.__limit_ms_n, self.max_time_millis, min)) 

946 

947 def _end_parse_file(self, file: Path) -> None: 

948 """ 

949 Cleanup. 

950 

951 :param file: the file that was parsed. 

952 """ 

953 self.__stop_fes = None 

954 self.__stop_ms = None 

955 self.__stop_f = None 

956 self.__stop_li_fe = None 

957 self.__stop_li_ms = None 

958 self.__limit_fes_n = None 

959 self.__limit_fes = inf 

960 self.__limit_ms_n = None 

961 self.__limit_ms = inf 

962 self.__limit_f_n = None 

963 self.__limit_f = -inf 

964 self.__hit_goal = False 

965 super()._end_parse_file(file) 

966 

967 def _start_parse_file(self, file: Path) -> None: 

968 super()._start_parse_file(file) 

969 a: Final[str | None] = self.algorithm 

970 i: Final[str | None] = self.instance 

971 

972 fes = self.__src_limit_fes(a, i) if (a and i and callable( 

973 self.__src_limit_fes)) else ( 

974 self.__src_limit_fes if isinstance(self.__src_limit_fes, int) 

975 else None) 

976 self.__limit_fes_n = None if fes is None else \ 

977 check_int_range(fes, "limit_fes", 1, 1_000_000_000_000_000) 

978 self.__limit_fes = inf if self.__limit_fes_n is None \ 

979 else self.__limit_fes_n 

980 

981 time = self.__src_limit_ms(a, i) if (a and i and callable( 

982 self.__src_limit_ms)) else ( 

983 self.__src_limit_ms if isinstance(self.__src_limit_ms, int) 

984 else None) 

985 self.__limit_ms_n = None if time is None else \ 

986 check_int_range(time, "l_limit_ms", 1, 1_000_000_000_000) 

987 self.__limit_ms = inf if self.__limit_ms_n is None \ 

988 else self.__limit_ms_n 

989 

990 self.__limit_f_n = self.__src_limit_f(a, i) if (a and i and callable( 

991 self.__src_limit_f)) else ( 

992 self.__src_limit_f if isinstance(self.__src_limit_f, int | float) 

993 else None) 

994 if self.__limit_f_n is not None: 

995 if not isinstance(self.__limit_f_n, int | float): 

996 raise type_error(self.__limit_f_n, "limit_f", ( 

997 int, float)) 

998 if not isfinite(self.__limit_f_n): 

999 if self.__limit_f_n <= -inf: 

1000 self.__limit_f_n = None 

1001 else: 

1002 raise ValueError( 

1003 f"invalid limit f={self.__limit_f_n} for " 

1004 f"{self.algorithm} on {self.instance}") 

1005 self.__limit_f = -inf if self.__limit_f_n is None \ 

1006 else self.__limit_f_n 

1007 

1008 def _start_section(self, title: str) -> bool: 

1009 if title == SECTION_PROGRESS: 

1010 if self.__state != 0: 

1011 raise ValueError(f"Already did section {title}.") 

1012 self.__state = 1 

1013 return True 

1014 return super()._start_section(title) 

1015 

1016 def _needs_more_lines(self) -> bool: 

1017 return (self.__state < 2) or super()._needs_more_lines() 

1018 

1019 def _lines(self, lines: list[str]) -> bool: 

1020 if self.__state != 1: 

1021 return super()._lines(lines) 

1022 self.__state = 2 

1023 

1024 n_rows = len(lines) 

1025 if n_rows < 2: 

1026 raise ValueError("lines must contain at least two elements," 

1027 f"but contains {n_rows}.") 

1028 

1029 columns = [c.strip() for c in lines[0].split(CSV_SEPARATOR)] 

1030 fe_col: Final[int] = columns.index(PROGRESS_FES) 

1031 ms_col: Final[int] = columns.index(PROGRESS_TIME_MILLIS) 

1032 f_col: Final[int] = columns.index(PROGRESS_CURRENT_F) 

1033 current_fes: int = -1 

1034 current_ms: int = -1 

1035 current_f: int | float = inf 

1036 current_li_fe: int | None = None 

1037 current_li_ms: int | None = None 

1038 stop_fes: int | None = None 

1039 stop_ms: int | None = None 

1040 stop_f: int | float | None = None 

1041 stop_li_fe: int | None = None 

1042 stop_li_ms: int | None = None 

1043 limit_fes: Final[int | float] = self.__limit_fes 

1044 l_limit_ms: Final[int | float] = self.__limit_ms 

1045 limit_f: Final[int | float] = self.__limit_f 

1046 

1047 for line in lines[1:]: 

1048 values = line.split(CSV_SEPARATOR) 

1049 current_fes = check_to_int_range( 

1050 values[fe_col], "fes", current_fes, 1_000_000_000_000_000) 

1051 current_ms = check_to_int_range( 

1052 values[ms_col], "ms", current_ms, 1_000_000_000_00) 

1053 f: int | float = str_to_num(values[f_col]) 

1054 if (current_fes <= limit_fes) and (current_ms <= l_limit_ms): 

1055 if f < current_f: # can only update best within budget 

1056 current_f = f 

1057 current_li_fe = current_fes 

1058 current_li_ms = current_ms 

1059 stop_ms = current_ms 

1060 stop_fes = current_fes 

1061 stop_f = current_f 

1062 stop_li_fe = current_li_fe 

1063 stop_li_ms = current_li_ms 

1064 if (current_fes >= limit_fes) or (current_ms >= l_limit_ms) or \ 

1065 (current_f <= limit_f): 

1066 self.__hit_goal = True 

1067 break # we can stop parsing the stuff 

1068 

1069 if (stop_fes is None) or (stop_ms is None) or (stop_f is None) \ 

1070 or (current_fes <= 0) or (not isfinite(current_f)): 

1071 raise ValueError( 

1072 "Illegal state, no fitting data point found: stop_fes=" 

1073 f"{stop_fes}, stop_ms={stop_ms}, stop_f={stop_f}, " 

1074 f"current_fes={current_fes}, current_ms={current_ms}, " 

1075 f"current_f={current_f}.") 

1076 

1077 if current_fes >= limit_fes: 

1078 stop_fes = max(stop_fes, min( 

1079 cast("int", limit_fes), current_fes)) 

1080 elif current_ms > l_limit_ms: 

1081 stop_fes = max(stop_fes, current_fes - 1) 

1082 else: 

1083 stop_fes = max(stop_fes, current_fes) 

1084 

1085 if current_ms >= l_limit_ms: 

1086 stop_ms = max(stop_ms, min(cast("int", l_limit_ms), current_ms)) 

1087 else: 

1088 stop_ms = max(stop_ms, current_ms) 

1089 

1090 self.__stop_fes = stop_fes 

1091 self.__stop_ms = stop_ms 

1092 self.__stop_f = stop_f 

1093 self.__stop_li_fe = stop_li_fe 

1094 self.__stop_li_ms = stop_li_ms 

1095 return self._needs_more_lines() 

1096 

1097 

1098def from_logs( 

1099 path: str, max_fes: int | Callable[ 

1100 [str, str], int | None] | None = None, 

1101 max_time_millis: int | Callable[ 

1102 [str, str], int | None] | None = None, 

1103 goal_f: int | float | Callable[ 

1104 [str, str], int | float | None] | None = None, 

1105 path_filter: Callable[[Path], bool] | None = None) \ 

1106 -> Generator[EndResult, None, None]: 

1107 """ 

1108 Parse a given path and yield all end results found. 

1109 

1110 If `path` identifies a file with suffix `.txt`, then this file is 

1111 parsed. The appropriate :class:`EndResult` is created and yielded. 

1112 If `path` identifies a directory, then this directory is parsed 

1113 recursively for each log file found, one record is yielded. 

1114 

1115 Via the parameters `max_fes`, `max_time_millis`, and `goal_f`, you can 

1116 set virtual limits for the objective function evaluations, the maximum 

1117 runtime, and the objective value. The :class:`EndResult` records will 

1118 then not represent the actual final state of the runs but be 

1119 synthesized from the logged progress information. This, of course, 

1120 requires such information to be present. It will also raise a 

1121 `ValueError` if the goals are invalid, e.g., if a runtime limit is 

1122 specified that is before the first logged points. 

1123 

1124 There is one caveat when specifying `max_time_millis`: Let's say that 

1125 the log files only log improvements. Then you might have a log point 

1126 for 7000 FEs, 1000ms, and f=100. The next log point could be 8000 FEs, 

1127 1200ms, and f=90. Now if your time limit specified is 1100ms, we know 

1128 that the end result is f=100 (because f=90 was reached too late) and 

1129 that the total runtime is 1100ms, as this is the limit you specified 

1130 and it was also reached. But we do not know the number of consumed 

1131 FEs. We know you consumed at least 7000 FEs, but you did not consume 

1132 8000 FEs. It would be wrong to claim that 7000 FEs were consumed, 

1133 since it could have been more. We therefore set a virtual end point at 

1134 7999 FEs. In terms of performance metrics such as the 

1135 :mod:`~moptipy.evaluation.ert`, this would be the most conservative 

1136 choice in that it does not over-estimate the speed of the algorithm. 

1137 It can, however, lead to very big deviations from the actual values. 

1138 For example, if your algorithm quickly converged to a local optimum 

1139 and there simply is no log point that exceeds the virtual time limit 

1140 but the original run had a huge FE-based budget while your virtual 

1141 time limit was small, this could lead to an estimate of millions of 

1142 FEs taking part within seconds... 

1143 

1144 :param path: the path to parse 

1145 :param max_fes: the maximum FEs, a callable to compute the maximum 

1146 FEs from the algorithm and instance name, or `None` if unspecified 

1147 :param max_time_millis: the maximum runtime in milliseconds, a 

1148 callable to compute the maximum runtime from the algorithm and 

1149 instance name, or `None` if unspecified 

1150 :param goal_f: the goal objective value, a callable to compute the 

1151 goal objective value from the algorithm and instance name, or 

1152 `None` if unspecified 

1153 :param path_filter: a filter allowing us to skip paths or files. If 

1154 this :class:`Callable` returns `True`, the file or directory is 

1155 considered for parsing. If it returns `False`, it is skipped. 

1156 """ 

1157 need_goals: bool = False 

1158 if max_fes is not None: 

1159 if not callable(max_fes): 

1160 max_fes = check_int_range( 

1161 max_fes, "max_fes", 1, 1_000_000_000_000_000) 

1162 need_goals = True 

1163 if max_time_millis is not None: 

1164 if not callable(max_time_millis): 

1165 max_time_millis = check_int_range( 

1166 max_time_millis, "max_time_millis", 1, 1_000_000_000_000) 

1167 need_goals = True 

1168 if goal_f is not None: 

1169 if callable(goal_f): 

1170 need_goals = True 

1171 else: 

1172 if not isinstance(goal_f, int | float): 

1173 raise type_error(goal_f, "goal_f", (int, float, None)) 

1174 if isfinite(goal_f): 

1175 need_goals = True 

1176 elif goal_f <= -inf: 

1177 goal_f = None 

1178 else: 

1179 raise ValueError(f"goal_f={goal_f} is not permissible.") 

1180 if need_goals: 

1181 return __EndResultProgressLogParser( 

1182 max_fes, max_time_millis, goal_f, path_filter).parse(path) 

1183 return EndResultLogParser(path_filter).parse(path) 

1184 

1185 

1186# Run log files to end results if executed as script 

1187if __name__ == "__main__": 

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

1189 __file__, 

1190 "Convert log files obtained with moptipy to the end results CSV " 

1191 "format that can be post-processed or exported to other tools.", 

1192 "This program recursively parses a folder hierarchy created by" 

1193 " the moptipy experiment execution facility. This folder " 

1194 "structure follows the scheme of algorithm/instance/log_file " 

1195 "and has one log file per run. As result of the parsing, one " 

1196 "CSV file (where columns are separated by ';') is created with" 

1197 " one row per log file. This row contains the end-of-run state" 

1198 " loaded from the log file. Whereas the log files may store " 

1199 "the complete progress of one run of one algorithm on one " 

1200 "problem instance as well as the algorithm configuration " 

1201 "parameters, instance features, system settings, and the final" 

1202 " results, the end results CSV file will only represent the " 

1203 "final result quality, when it was obtained, how long the runs" 

1204 " took, etc. This information is much denser and smaller and " 

1205 "suitable for importing into other tools such as Excel or for " 

1206 "postprocessing.") 

1207 parser.add_argument( 

1208 "source", nargs="?", default="./results", 

1209 help="the location of the experimental results, i.e., the root folder " 

1210 "under which to search for log files", type=Path) 

1211 parser.add_argument( 

1212 "dest", help="the path to the end results CSV file to be created", 

1213 type=Path, nargs="?", default="./evaluation/end_results.txt") 

1214 parser.add_argument( 

1215 "--maxFEs", help="the maximum permitted FEs", 

1216 type=int, nargs="?", default=None) 

1217 parser.add_argument( 

1218 "--maxTime", help="the maximum permitted time in milliseconds", 

1219 type=int, nargs="?", default=None) 

1220 parser.add_argument( 

1221 "--goalF", help="the goal objective value", 

1222 type=str_to_num, nargs="?", default=None) 

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

1224 

1225 to_csv(from_logs(args.source, args.maxFEs, args.maxTime, args.goalF), 

1226 args.dest)