Coverage for moptipy / evaluation / base.py: 99%

127 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-15 11:18 +0000

1"""Some internal helper functions and base classes.""" 

2 

3from dataclasses import dataclass 

4from typing import Any, Final, Iterable 

5 

6from pycommons.io.path import Path 

7from pycommons.types import check_int_range, type_error 

8 

9from moptipy.api.logging import FILE_SUFFIX 

10from moptipy.utils.nputils import rand_seed_check 

11from moptipy.utils.strings import ( 

12 sanitize_name, 

13 sanitize_names, 

14) 

15from moptipy.version import __version__ as moptipy_version 

16 

17#: The key for the total number of runs. 

18KEY_N: Final[str] = "n" 

19#: a key for the objective function name 

20KEY_OBJECTIVE_FUNCTION: Final[str] = "objective" 

21#: a key for the encoding name 

22KEY_ENCODING: Final[str] = "encoding" 

23 

24#: The unit of the time axis if time is measured in milliseconds. 

25TIME_UNIT_MILLIS: Final[str] = "ms" 

26#: The unit of the time axis of time is measured in FEs 

27TIME_UNIT_FES: Final[str] = "FEs" 

28 

29#: The name of the raw objective values data. 

30F_NAME_RAW: Final[str] = "plainF" 

31#: The name of the scaled objective values data. 

32F_NAME_SCALED: Final[str] = "scaledF" 

33#: The name of the normalized objective values data. 

34F_NAME_NORMALIZED: Final[str] = "normalizedF" 

35 

36 

37def check_time_unit(time_unit: Any) -> str: 

38 """ 

39 Check that the time unit is OK. 

40 

41 :param time_unit: the time unit 

42 :return: the time unit string 

43 

44 >>> check_time_unit("FEs") 

45 'FEs' 

46 >>> check_time_unit("ms") 

47 'ms' 

48 >>> try: 

49 ... check_time_unit(1) 

50 ... except TypeError as te: 

51 ... print(te) 

52 time_unit should be an instance of str but is int, namely 1. 

53 

54 >>> try: 

55 ... check_time_unit("blabedibla") 

56 ... except ValueError as ve: 

57 ... print(ve) 

58 Invalid time unit 'blabedibla', only 'FEs' and 'ms' are permitted. 

59 """ 

60 if not isinstance(time_unit, str): 

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

62 if time_unit in {TIME_UNIT_FES, TIME_UNIT_MILLIS}: 

63 return time_unit 

64 raise ValueError( 

65 f"Invalid time unit {time_unit!r}, only {TIME_UNIT_FES!r} " 

66 f"and {TIME_UNIT_MILLIS!r} are permitted.") 

67 

68 

69def check_f_name(f_name: Any) -> str: 

70 """ 

71 Check whether an objective value name is valid. 

72 

73 :param f_name: the name of the objective function dimension 

74 :return: the name of the objective function dimension 

75 

76 >>> check_f_name("plainF") 

77 'plainF' 

78 >>> check_f_name("scaledF") 

79 'scaledF' 

80 >>> check_f_name("normalizedF") 

81 'normalizedF' 

82 >>> try: 

83 ... check_f_name(1.0) 

84 ... except TypeError as te: 

85 ... print(te) 

86 f_name should be an instance of str but is float, namely 1.0. 

87 

88 >>> try: 

89 ... check_f_name("oops") 

90 ... except ValueError as ve: 

91 ... print(ve) 

92 Invalid f name 'oops', only 'plainF', 'scaledF', and 'normalizedF' \ 

93are permitted. 

94 """ 

95 if not isinstance(f_name, str): 

96 raise type_error(f_name, "f_name", str) 

97 if f_name in {F_NAME_RAW, F_NAME_SCALED, F_NAME_NORMALIZED}: 

98 return f_name 

99 raise ValueError( 

100 f"Invalid f name {f_name!r}, only {F_NAME_RAW!r}, " 

101 f"{F_NAME_SCALED!r}, and {F_NAME_NORMALIZED!r} are permitted.") 

102 

103 

104def _set_name(dest: object, name: str, what: str, 

105 none_allowed: bool = False, 

106 empty_to_none: bool = True) -> None: 

107 """ 

108 Check and set a name. 

109 

110 :param dest: the destination 

111 :param name: the name to set 

112 :param what: the name's type 

113 :param none_allowed: is `None` allowed? 

114 :param empty_to_none: If both `none_allowed` and `empty_to_none` are 

115 `True`, then empty strings are converted to `None` 

116 

117 >>> class TV: 

118 ... algorithm: str 

119 ... instance: str | None 

120 >>> t = TV() 

121 >>> _set_name(t, "bla", "algorithm", False) 

122 >>> t.algorithm 

123 'bla' 

124 >>> _set_name(t, "xbla", "instance", True) 

125 >>> t.instance 

126 'xbla' 

127 >>> _set_name(t, None, "instance", True) 

128 >>> print(t.instance) 

129 None 

130 >>> t.instance = "x" 

131 >>> _set_name(t, " ", "instance", True) 

132 >>> print(t.instance) 

133 None 

134 >>> try: 

135 ... _set_name(t, 1, "algorithm") 

136 ... except TypeError as te: 

137 ... print(te) 

138 algorithm name should be an instance of str but is int, namely 1. 

139 

140 >>> t.algorithm 

141 'bla' 

142 >>> try: 

143 ... _set_name(t, " ", "algorithm") 

144 ... except ValueError as ve: 

145 ... print(ve) 

146 algorithm name cannot be empty of just consist of white space, but \ 

147' ' does. 

148 

149 >>> t.algorithm 

150 'bla' 

151 >>> try: 

152 ... _set_name(t, "a a", "instance") 

153 ... except ValueError as ve: 

154 ... print(ve) 

155 Invalid instance name 'a a'. 

156 

157 >>> print(t.instance) 

158 None 

159 >>> try: 

160 ... _set_name(t, " ", "instance", True, False) 

161 ... except ValueError as ve: 

162 ... print(ve) 

163 instance name cannot be empty of just consist of white space, but \ 

164' ' does. 

165 

166 >>> print(t.instance) 

167 None 

168 """ 

169 use_name = name 

170 if isinstance(name, str): 

171 use_name = use_name.strip() 

172 if len(use_name) <= 0: 

173 if empty_to_none and none_allowed: 

174 use_name = None 

175 else: 

176 raise ValueError(f"{what} name cannot be empty of just cons" 

177 f"ist of white space, but {name!r} does.") 

178 elif use_name != sanitize_name(use_name): 

179 raise ValueError(f"Invalid {what} name {name!r}.") 

180 elif not ((name is None) and none_allowed): 

181 raise type_error(name, f"{what} name", 

182 (str, None) if none_allowed else str) 

183 object.__setattr__(dest, what, use_name) 

184 

185 

186class EvaluationDataElement: 

187 """A base class for all the data classes in this module.""" 

188 

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

190 """ 

191 Create a tuple with all the data of this data class for comparison. 

192 

193 All the relevant data of an instance of this class is stored in a 

194 tuple. The tuple is then used in the dunder methods for comparisons. 

195 The returned tuple *must* be based on the scheme 

196 `tuple[str, str, str, str, str, int, int, str, str]`. 

197 They can be shorter than this and they can be longer, but they must 

198 adhere to this basic scheme: 

199 

200 1. class name 

201 2. algorithm name, or `""` if algorithm name is `None` 

202 3. instance name, or `""` if instance name is `None` 

203 4. objective name, `""` objective name is `None` 

204 5. encoding name, or `""` encoding name is `None` 

205 6. number of runs, or `0` if no number of runs is specified or `1` if 

206 the data concerns exactly one run 

207 7. the random seed, or `-1` if no random seed is specified 

208 8. the string time unit, or `""` if no time unit is given 

209 9. the scaling name of the objective function, or `""` if no scaling 

210 name is given 

211 

212 If the tuples are longer, then all values following after this must be 

213 integers or floats. 

214 

215 >>> EvaluationDataElement()._tuple() 

216 ('EvaluationDataElement',) 

217 

218 :returns: a tuple with all the data of this class, where `None` values 

219 are masked out 

220 """ 

221 return (self.__class__.__name__, ) 

222 

223 def __hash__(self) -> int: 

224 """ 

225 Compute the hash code of this object. 

226 

227 :returns: the hash code 

228 """ 

229 return hash(self._tuple()) 

230 

231 def __eq__(self, other) -> bool: 

232 """ 

233 Compare for `==` with another object based on the `_tuple()` value. 

234 

235 :param other: the other object to compare to, must be an instance of 

236 :class:`EvaluationDataElement` 

237 :retval `True`: if the `other` object's `_tuple()` representation is 

238 `==` with this object's `_tuple()` representation 

239 :retval `False`: otherwise 

240 :raises NotImplementedError: if the other object is not an instance of 

241 :class:`EvaluationDataElement` and therefore cannot be compared. 

242 

243 >>> PerRunData("a", "i", "f", "e", 234) == PerRunData( 

244 ... "a", "i", "f", "e", 234) 

245 True 

246 >>> PerRunData("a", "i", "f", "e", 234) == PerRunData( 

247 ... "a", "j", "f", "e", 234) 

248 False 

249 >>> try: 

250 ... PerRunData("a", "i", "f", "e", 234) == 3 

251 ... except NotImplementedError as ni: 

252 ... print(ni) 

253 Cannot compare PerRunData(algorithm='a', instance='i', \ 

254objective='f', encoding='e', rand_seed=234) with 3 for ==. 

255 """ 

256 if isinstance(other, EvaluationDataElement): 

257 return self._tuple() == other._tuple() 

258 raise NotImplementedError( 

259 f"Cannot compare {self} with {other} for ==.") 

260 

261 def __ne__(self, other) -> bool: 

262 """ 

263 Compare for `!=` with another object based on the `_tuple()` value. 

264 

265 :param other: the other object to compare to, must be an instance of 

266 :class:`EvaluationDataElement` 

267 :retval `True`: if the `other` object's `_tuple()` representation is 

268 `!=` with this object's `_tuple()` representation 

269 :retval `False`: otherwise 

270 :raises NotImplementedError: if the other object is not an instance of 

271 :class:`EvaluationDataElement` and therefore cannot be compared. 

272 

273 >>> PerRunData("a", "i", "f", "e", 234) != PerRunData( 

274 ... "a", "i", "f", "e", 234) 

275 False 

276 >>> PerRunData("a", "i", "f", "e", 234) != PerRunData( 

277 ... "a", "j", "f", "e", 234) 

278 True 

279 >>> try: 

280 ... PerRunData("a", "i", "f", "e", 234) != 3 

281 ... except NotImplementedError as ni: 

282 ... print(ni) 

283 Cannot compare PerRunData(algorithm='a', instance='i', \ 

284objective='f', encoding='e', rand_seed=234) with 3 for !=. 

285 """ 

286 if isinstance(other, EvaluationDataElement): 

287 return self._tuple() != other._tuple() 

288 raise NotImplementedError( 

289 f"Cannot compare {self} with {other} for !=.") 

290 

291 def __lt__(self, other) -> bool: 

292 """ 

293 Compare for `<` with another object based on the `_tuple()` value. 

294 

295 :param other: the other object to compare to, must be an instance of 

296 :class:`EvaluationDataElement` 

297 :retval `True`: if the `other` object's `_tuple()` representation is 

298 `<` with this object's `_tuple()` representation 

299 :retval `False`: otherwise 

300 :raises NotImplementedError: if the other object is not an instance of 

301 :class:`EvaluationDataElement` and therefore cannot be compared. 

302 

303 >>> PerRunData("a", "i", "f", "e", 234) < PerRunData( 

304 ... "a", "i", "f", "e", 234) 

305 False 

306 >>> PerRunData("a", "i", "f", "e", 234) < PerRunData( 

307 ... "a", "j", "f", "e", 234) 

308 True 

309 >>> PerRunData("a", "j", "f", "e", 234) < PerRunData( 

310 ... "a", "i", "f", "e", 234) 

311 False 

312 >>> try: 

313 ... PerRunData("a", "i", "f", "e", 234) < 3 

314 ... except NotImplementedError as ni: 

315 ... print(ni) 

316 Cannot compare PerRunData(algorithm='a', instance='i', \ 

317objective='f', encoding='e', rand_seed=234) with 3 for <. 

318 """ 

319 if isinstance(other, EvaluationDataElement): 

320 return self._tuple() < other._tuple() 

321 raise NotImplementedError( 

322 f"Cannot compare {self} with {other} for <.") 

323 

324 def __le__(self, other) -> bool: 

325 """ 

326 Compare for `<=` with another object based on the `_tuple()` value. 

327 

328 :param other: the other object to compare to, must be an instance of 

329 :class:`EvaluationDataElement` 

330 :retval `True`: if the `other` object's `_tuple()` representation is 

331 `<=` with this object's `_tuple()` representation 

332 :retval `False`: otherwise 

333 :raises NotImplementedError: if the other object is not an instance of 

334 :class:`EvaluationDataElement` and therefore cannot be compared. 

335 

336 >>> PerRunData("a", "i", "f", "e", 234) <= PerRunData( 

337 ... "a", "i", "f", "e", 234) 

338 True 

339 >>> PerRunData("a", "i", "f", "e", 234) <= PerRunData( 

340 ... "a", "j", "f", "e", 234) 

341 True 

342 >>> PerRunData("a", "j", "f", "e", 234) < PerRunData( 

343 ... "a", "i", "f", "e", 234) 

344 False 

345 >>> try: 

346 ... PerRunData("a", "i", "f", "e", 234) <= 3 

347 ... except NotImplementedError as ni: 

348 ... print(ni) 

349 Cannot compare PerRunData(algorithm='a', instance='i', \ 

350objective='f', encoding='e', rand_seed=234) with 3 for <=. 

351 """ 

352 if isinstance(other, EvaluationDataElement): 

353 return self._tuple() <= other._tuple() 

354 raise NotImplementedError( 

355 f"Cannot compare {self} with {other} for <=.") 

356 

357 def __gt__(self, other) -> bool: 

358 """ 

359 Compare for `>` with another object based on the `_tuple()` value. 

360 

361 :param other: the other object to compare to, must be an instance of 

362 :class:`EvaluationDataElement` 

363 :retval `True`: if the `other` object's `_tuple()` representation is 

364 `>` with this object's `_tuple()` representation 

365 :retval `False`: otherwise 

366 :raises NotImplementedError: if the other object is not an instance of 

367 :class:`EvaluationDataElement` and therefore cannot be compared. 

368 

369 >>> PerRunData("a", "i", "f", "e", 234) > PerRunData( 

370 ... "a", "i", "f", "e", 234) 

371 False 

372 >>> PerRunData("a", "i", "f", "e", 234) > PerRunData( 

373 ... "a", "j", "f", "e", 234) 

374 False 

375 >>> PerRunData("a", "j", "f", "e", 234) > PerRunData( 

376 ... "a", "i", "f", "e", 234) 

377 True 

378 >>> try: 

379 ... PerRunData("a", "i", "f", "e", 234) > 3 

380 ... except NotImplementedError as ni: 

381 ... print(ni) 

382 Cannot compare PerRunData(algorithm='a', instance='i', \ 

383objective='f', encoding='e', rand_seed=234) with 3 for >. 

384 """ 

385 if isinstance(other, EvaluationDataElement): 

386 return self._tuple() > other._tuple() 

387 raise NotImplementedError( 

388 f"Cannot compare {self} with {other} for >.") 

389 

390 def __ge__(self, other) -> bool: 

391 """ 

392 Compare for `>=` with another object based on the `_tuple()` value. 

393 

394 :param other: the other object to compare to, must be an instance of 

395 :class:`EvaluationDataElement` 

396 :retval `True`: if the `other` object's `_tuple()` representation is 

397 `>=` with this object's `_tuple()` representation 

398 :retval `False`: otherwise 

399 :raises NotImplementedError: if the other object is not an instance of 

400 :class:`EvaluationDataElement` and therefore cannot be compared. 

401 

402 >>> PerRunData("a", "i", "f", "e", 234) >= PerRunData( 

403 ... "a", "i", "f", "e", 234) 

404 True 

405 >>> PerRunData("a", "i", "f", "e", 234) >= PerRunData( 

406 ... "a", "j", "f", "e", 234) 

407 False 

408 >>> PerRunData("a", "j", "f", "e", 234) >= PerRunData( 

409 ... "a", "i", "f", "e", 234) 

410 True 

411 >>> try: 

412 ... PerRunData("a", "i", "f", "e", 234) >= 3 

413 ... except NotImplementedError as ni: 

414 ... print(ni) 

415 Cannot compare PerRunData(algorithm='a', instance='i', \ 

416objective='f', encoding='e', rand_seed=234) with 3 for >=. 

417 """ 

418 if isinstance(other, EvaluationDataElement): 

419 return self._tuple() >= other._tuple() 

420 raise NotImplementedError( 

421 f"Cannot compare {self} with {other} for >=.") 

422 

423 

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

425class PerRunData(EvaluationDataElement): 

426 """ 

427 An immutable record of information over a single run. 

428 

429 >>> p = PerRunData("a", "i", "f", None, 234) 

430 >>> p.instance 

431 'i' 

432 >>> p.algorithm 

433 'a' 

434 >>> p.objective 

435 'f' 

436 >>> print(p.encoding) 

437 None 

438 >>> p.rand_seed 

439 234 

440 >>> p = PerRunData("a", "i", "f", "e", 234) 

441 >>> p.instance 

442 'i' 

443 >>> p.algorithm 

444 'a' 

445 >>> p.objective 

446 'f' 

447 >>> p.encoding 

448 'e' 

449 >>> p.rand_seed 

450 234 

451 >>> try: 

452 ... PerRunData(3, "i", "f", "e", 234) 

453 ... except TypeError as te: 

454 ... print(te) 

455 algorithm name should be an instance of str but is int, namely 3. 

456 

457 >>> try: 

458 ... PerRunData("@1 2", "i", "f", "e", 234) 

459 ... except ValueError as ve: 

460 ... print(ve) 

461 Invalid algorithm name '@1 2'. 

462 

463 >>> try: 

464 ... PerRunData("x", 3.2, "f", "e", 234) 

465 ... except TypeError as te: 

466 ... print(te) 

467 instance name should be an instance of str but is float, namely 3.2. 

468 

469 >>> try: 

470 ... PerRunData("x", "sdf i", "f", "e", 234) 

471 ... except ValueError as ve: 

472 ... print(ve) 

473 Invalid instance name 'sdf i'. 

474 

475 >>> try: 

476 ... PerRunData("a", "i", True, "e", 234) 

477 ... except TypeError as te: 

478 ... print(te) 

479 objective name should be an instance of str but is bool, namely True. 

480 

481 >>> try: 

482 ... PerRunData("x", "i", "d-f", "e", 234) 

483 ... except ValueError as ve: 

484 ... print(ve) 

485 Invalid objective name 'd-f'. 

486 

487 >>> try: 

488 ... PerRunData("x", "i", "f", 54.2, 234) 

489 ... except TypeError as te: 

490 ... print(te) 

491 encoding name should be an instance of any in {None, str} but is float, \ 

492namely 54.2. 

493 

494 >>> try: 

495 ... PerRunData("y", "i", "f", "x x", 234) 

496 ... except ValueError as ve: 

497 ... print(ve) 

498 Invalid encoding name 'x x'. 

499 

500 >>> try: 

501 ... PerRunData("x", "i", "f", "e", 3.3) 

502 ... except TypeError as te: 

503 ... print(te) 

504 rand_seed should be an instance of int but is float, namely 3.3. 

505 

506 >>> try: 

507 ... PerRunData("x", "i", "f", "e", -234) 

508 ... except ValueError as ve: 

509 ... print(ve) 

510 rand_seed=-234 is invalid, must be in 0..18446744073709551615. 

511 """ 

512 

513 #: The algorithm that was applied. 

514 algorithm: str 

515 #: The problem instance that was solved. 

516 instance: str 

517 #: the name of the objective function 

518 objective: str 

519 #: the encoding, if any, or `None` if no encoding was used 

520 encoding: str | None 

521 #: The seed of the random number generator. 

522 rand_seed: int 

523 

524 def __init__(self, algorithm: str, instance: str, objective: str, 

525 encoding: str | None, rand_seed: int): 

526 """ 

527 Create a per-run data record. 

528 

529 :param algorithm: the algorithm name 

530 :param instance: the instance name 

531 :param objective: the name of the objective function 

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

533 `None` if no encoding was used 

534 :param rand_seed: the random seed 

535 """ 

536 _set_name(self, algorithm, "algorithm") 

537 _set_name(self, instance, "instance") 

538 _set_name(self, objective, "objective") 

539 _set_name(self, encoding, "encoding", True, False) 

540 object.__setattr__(self, "rand_seed", rand_seed_check(rand_seed)) 

541 

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

543 """ 

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

545 

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

547 

548 >>> PerRunData("a", "i", "f", "e", 234)._tuple() 

549 ('PerRunData', 'a', 'i', 'f', 'e', 1, 234) 

550 >>> PerRunData("a", "i", "f", None, 234)._tuple() 

551 ('PerRunData', 'a', 'i', 'f', '', 1, 234) 

552 """ 

553 return (self.__class__.__name__, self.algorithm, self.instance, 

554 self.objective, 

555 "" if self.encoding is None else self.encoding, 1, 

556 self.rand_seed) 

557 

558 def path_to_file(self, base_dir: str) -> Path: 

559 """ 

560 Get the path that would correspond to the log file of this end result. 

561 

562 Obtain a path that would correspond to the log file of this end 

563 result, resolved from a base directory `base_dir`. 

564 

565 :param base_dir: the base directory 

566 :returns: the path to a file corresponding to the end result record 

567 """ 

568 return Path(base_dir).resolve_inside( 

569 self.algorithm).resolve_inside(self.instance).resolve_inside( 

570 sanitize_names([self.algorithm, self.instance, 

571 hex(self.rand_seed)]) + FILE_SUFFIX) 

572 

573 

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

575class MultiRunData(EvaluationDataElement): 

576 """ 

577 A class that represents statistics over a set of runs. 

578 

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

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

581 runs is defined. 

582 

583 >>> p = MultiRunData("a", "i", "f", None, 3) 

584 >>> p.instance 

585 'i' 

586 >>> p.algorithm 

587 'a' 

588 >>> p.objective 

589 'f' 

590 >>> print(p.encoding) 

591 None 

592 >>> p.n 

593 3 

594 >>> p = MultiRunData(None, None, None, "x", 3) 

595 >>> print(p.instance) 

596 None 

597 >>> print(p.algorithm) 

598 None 

599 >>> print(p.objective) 

600 None 

601 >>> p.encoding 

602 'x' 

603 >>> p.n 

604 3 

605 >>> try: 

606 ... MultiRunData(1, "i", "f", "e", 234) 

607 ... except TypeError as te: 

608 ... print(te) 

609 algorithm name should be an instance of any in {None, str} but is int, \ 

610namely 1. 

611 

612 >>> try: 

613 ... MultiRunData("x x", "i", "f", "e", 234) 

614 ... except ValueError as ve: 

615 ... print(ve) 

616 Invalid algorithm name 'x x'. 

617 

618 >>> try: 

619 ... MultiRunData("a", 5.5, "f", "e", 234) 

620 ... except TypeError as te: 

621 ... print(te) 

622 instance name should be an instance of any in {None, str} but is float, \ 

623namely 5.5. 

624 

625 >>> try: 

626 ... MultiRunData("x", "a-i", "f", "e", 234) 

627 ... except ValueError as ve: 

628 ... print(ve) 

629 Invalid instance name 'a-i'. 

630 

631 >>> try: 

632 ... MultiRunData("a", "i", True, "e", 234) 

633 ... except TypeError as te: 

634 ... print(te) 

635 objective name should be an instance of any in {None, str} but is bool, \ 

636namely True. 

637 

638 >>> try: 

639 ... MultiRunData("xx", "i", "d'@f", "e", 234) 

640 ... except ValueError as ve: 

641 ... print(ve) 

642 Invalid objective name "d'@f". 

643 

644 >>> try: 

645 ... MultiRunData("yy", "i", "f", -9.4, 234) 

646 ... except TypeError as te: 

647 ... print(te) 

648 encoding name should be an instance of any in {None, str} but is float, \ 

649namely -9.4. 

650 

651 >>> try: 

652 ... MultiRunData("xx", "i", "f", "e-{a", 234) 

653 ... except ValueError as ve: 

654 ... print(ve) 

655 Invalid encoding name 'e-{a'. 

656 

657 >>> try: 

658 ... MultiRunData("x", "i", "f", "e", -1.234) 

659 ... except TypeError as te: 

660 ... print(te) 

661 n should be an instance of int but is float, namely -1.234. 

662 

663 >>> try: 

664 ... MultiRunData("xx", "i", "f", "e", 1_000_000_000_000_000_000_000) 

665 ... except ValueError as ve: 

666 ... print(ve) 

667 n=1000000000000000000000 is invalid, must be in 1..1000000000000000. 

668 """ 

669 

670 #: The algorithm that was applied, if the same over all runs. 

671 algorithm: str | None 

672 #: The problem instance that was solved, if the same over all runs. 

673 instance: str | None 

674 #: the name of the objective function, if the same over all runs 

675 objective: str | None 

676 #: the encoding, if any, or `None` if no encoding was used or if it was 

677 #: not the same over all runs 

678 encoding: str | None 

679 #: The number of runs over which the statistic information is computed. 

680 n: int 

681 

682 def __init__(self, algorithm: str | None, instance: str | None, 

683 objective: str | None, encoding: str | None, n: int): 

684 """ 

685 Create the dataset of an experiment-setup combination. 

686 

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

688 algorithm, `None` otherwise 

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

690 instance, `None` otherwise 

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

692 objective function, `None` otherwise 

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

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

695 :param n: the total number of runs 

696 """ 

697 _set_name(self, algorithm, "algorithm", True, False) 

698 _set_name(self, instance, "instance", True, False) 

699 _set_name(self, objective, "objective", True, False) 

700 _set_name(self, encoding, "encoding", True, False) 

701 object.__setattr__(self, "n", check_int_range( 

702 n, "n", 1, 1_000_000_000_000_000)) 

703 

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

705 """ 

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

707 

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

709 

710 >>> MultiRunData("a", "i", "f", None, 3)._tuple() 

711 ('MultiRunData', 'a', 'i', 'f', '', 3, -1) 

712 >>> MultiRunData(None, "i", "f", "e", 31)._tuple() 

713 ('MultiRunData', '', 'i', 'f', 'e', 31, -1) 

714 >>> MultiRunData("x", None, "fy", "e1", 131)._tuple() 

715 ('MultiRunData', 'x', '', 'fy', 'e1', 131, -1) 

716 >>> MultiRunData("yx", "z", None, "xe1", 2131)._tuple() 

717 ('MultiRunData', 'yx', 'z', '', 'xe1', 2131, -1) 

718 """ 

719 return (self.__class__.__name__, 

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

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

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

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

724 self.n, -1) 

725 

726 

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

728class MultiRun2DData(MultiRunData): 

729 """ 

730 A multi-run data based on one time and one objective dimension. 

731 

732 >>> p = MultiRun2DData("a", "i", "f", None, 3, 

733 ... TIME_UNIT_FES, F_NAME_SCALED) 

734 >>> p.instance 

735 'i' 

736 >>> p.algorithm 

737 'a' 

738 >>> p.objective 

739 'f' 

740 >>> print(p.encoding) 

741 None 

742 >>> p.n 

743 3 

744 >>> print(p.time_unit) 

745 FEs 

746 >>> print(p.f_name) 

747 scaledF 

748 >>> try: 

749 ... MultiRun2DData("a", "i", "f", None, 3, 

750 ... 3, F_NAME_SCALED) 

751 ... except TypeError as te: 

752 ... print(te) 

753 time_unit should be an instance of str but is int, namely 3. 

754 

755 >>> try: 

756 ... MultiRun2DData("a", "i", "f", None, 3, 

757 ... "sdfjsdf", F_NAME_SCALED) 

758 ... except ValueError as ve: 

759 ... print(ve) 

760 Invalid time unit 'sdfjsdf', only 'FEs' and 'ms' are permitted. 

761 

762 >>> try: 

763 ... MultiRun2DData("a", "i", "f", None, 3, 

764 ... TIME_UNIT_FES, True) 

765 ... except TypeError as te: 

766 ... print(te) 

767 f_name should be an instance of str but is bool, namely True. 

768 

769 >>> try: 

770 ... MultiRun2DData("a", "i", "f", None, 3, 

771 ... TIME_UNIT_FES, "blablue") 

772 ... except ValueError as ve: 

773 ... print(ve) 

774 Invalid f name 'blablue', only 'plainF', 'scaledF', and 'normalizedF' \ 

775are permitted. 

776 """ 

777 

778 #: The unit of the time axis. 

779 time_unit: str 

780 #: the name of the objective value axis. 

781 f_name: str 

782 

783 def __init__(self, algorithm: str | None, instance: str | None, 

784 objective: str | None, encoding: str | None, n: int, 

785 time_unit: str, f_name: str): 

786 """ 

787 Create multi-run data based on one time and one objective dimension. 

788 

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

790 algorithm 

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

792 instance 

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

794 objective function, `None` otherwise 

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

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

797 :param n: the total number of runs 

798 :param time_unit: the time unit 

799 :param f_name: the objective dimension name 

800 """ 

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

802 object.__setattr__(self, "time_unit", check_time_unit(time_unit)) 

803 object.__setattr__(self, "f_name", check_f_name(f_name)) 

804 

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

806 """ 

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

808 

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

810 

811 >>> MultiRun2DData("a", "i", "f", None, 3, 

812 ... TIME_UNIT_FES, F_NAME_SCALED)._tuple() 

813 ('MultiRun2DData', 'a', 'i', 'f', '', 3, -1, 'FEs', 'scaledF') 

814 >>> MultiRun2DData(None, "ix", None, "x", 43, 

815 ... TIME_UNIT_MILLIS, F_NAME_RAW)._tuple() 

816 ('MultiRun2DData', '', 'ix', '', 'x', 43, -1, 'ms', 'plainF') 

817 >>> MultiRun2DData("xa", None, None, None, 143, 

818 ... TIME_UNIT_MILLIS, F_NAME_NORMALIZED)._tuple() 

819 ('MultiRun2DData', 'xa', '', '', '', 143, -1, 'ms', 'normalizedF') 

820 """ 

821 return (self.__class__.__name__, 

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

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

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

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

826 self.n, -1, self.time_unit, self.f_name) 

827 

828 

829def get_instance(obj: PerRunData | MultiRunData) -> str | None: 

830 """ 

831 Get the instance of a given object. 

832 

833 :param obj: the object 

834 :return: the instance string, or `None` if no instance is specified 

835 

836 >>> p1 = MultiRunData("a", "i1", None, "x", 3) 

837 >>> get_instance(p1) 

838 'i1' 

839 >>> p2 = PerRunData("a", "i2", "f", "x", 31) 

840 >>> get_instance(p2) 

841 'i2' 

842 """ 

843 return obj.instance 

844 

845 

846def get_algorithm(obj: PerRunData | MultiRunData) -> str | None: 

847 """ 

848 Get the algorithm of a given object. 

849 

850 :param obj: the object 

851 :return: the algorithm string, or `None` if no algorithm is specified 

852 

853 >>> p1 = MultiRunData("a1", "i1", "f", "y", 3) 

854 >>> get_algorithm(p1) 

855 'a1' 

856 >>> p2 = PerRunData("a2", "i2", "y", None, 31) 

857 >>> get_algorithm(p2) 

858 'a2' 

859 """ 

860 return obj.algorithm 

861 

862 

863def sort_key(obj: PerRunData | MultiRunData) -> tuple[Any, ...]: 

864 """ 

865 Get the default sort key for the given object. 

866 

867 The sort key is a tuple with well-defined field elements that should 

868 allow for a default and consistent sorting over many different elements of 

869 the experiment evaluation data API. Sorting should work also for lists 

870 containing elements of different classes. 

871 

872 :param obj: the object 

873 :return: the sort key 

874 

875 >>> p1 = MultiRunData("a1", "i1", "f", None, 3) 

876 >>> p2 = PerRunData("a2", "i2", "f", None, 31) 

877 >>> sort_key(p1) < sort_key(p2) 

878 True 

879 >>> sort_key(p1) >= sort_key(p2) 

880 False 

881 >>> p3 = MultiRun2DData("a", "i", "f", None, 3, 

882 ... TIME_UNIT_FES, F_NAME_SCALED) 

883 >>> sort_key(p3) < sort_key(p1) 

884 True 

885 >>> sort_key(p3) >= sort_key(p1) 

886 False 

887 """ 

888 # noinspection PyProtectedMember 

889 return obj._tuple() 

890 

891 

892def motipy_footer_bottom_comments( 

893 _: Any, additional: str | None = None) -> Iterable[str]: 

894 """ 

895 Print the standard csv footer for moptipy. 

896 

897 :param _: the setup object, ignored 

898 :param dest: the destination callable 

899 :param additional: any additional output string 

900 :returns: the iterable with the footer comments 

901 

902 >>> for s in motipy_footer_bottom_comments(None, "bla"): 

903 ... print(s[:49]) 

904 This data has been generated with moptipy version 

905 bla 

906 You can find moptipy at https://thomasweise.githu 

907 

908 >>> for s in motipy_footer_bottom_comments(None, None): 

909 ... print(s[:49]) 

910 This data has been generated with moptipy version 

911 You can find moptipy at https://thomasweise.githu 

912 """ 

913 yield ("This data has been generated with moptipy version " 

914 f"{moptipy_version}.") 

915 if (additional is not None) and (str.__len__(additional) > 0): 

916 yield additional 

917 yield "You can find moptipy at https://thomasweise.github.io/mopitpy." 

918 

919 

920#: a description of the algorithm field 

921DESC_ALGORITHM: Final[str] = "the name of the algorithm setup that was used." 

922#: a description of the instance field 

923DESC_INSTANCE: Final[str] = ("the name of the problem instance to which the " 

924 "algorithm was applied.") 

925#: a description of the objective function field 

926DESC_OBJECTIVE_FUNCTION: Final[str] = \ 

927 ("the name of the objective function (often also called fitness function " 

928 "or cost function) that was used to rate the solution quality.") 

929#: a description of the encoding field 

930DESC_ENCODING: Final[str] = \ 

931 ("the name of the encoding, often also called genotype-phenotype mapping" 

932 ", used. In some problems, the search space on which the algorithm " 

933 "works is different from the space of possible solutions. For example, " 

934 "when solving a scheduling problem, maybe our optimization algorithm " 

935 "navigates in the space of permutations, but the solutions are Gantt " 

936 "charts. The encoding is the function that translates the points in " 

937 "the search space (e.g., permutations) to the points in the solution " 

938 "space (e.g., Gantt charts). Nothing if no encoding was used.")