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
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-15 11:18 +0000
1"""Some internal helper functions and base classes."""
3from dataclasses import dataclass
4from typing import Any, Final, Iterable
6from pycommons.io.path import Path
7from pycommons.types import check_int_range, type_error
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
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"
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"
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"
37def check_time_unit(time_unit: Any) -> str:
38 """
39 Check that the time unit is OK.
41 :param time_unit: the time unit
42 :return: the time unit string
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.
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.")
69def check_f_name(f_name: Any) -> str:
70 """
71 Check whether an objective value name is valid.
73 :param f_name: the name of the objective function dimension
74 :return: the name of the objective function dimension
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.
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.")
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.
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`
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.
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.
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'.
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.
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)
186class EvaluationDataElement:
187 """A base class for all the data classes in this module."""
189 def _tuple(self) -> tuple[Any, ...]:
190 """
191 Create a tuple with all the data of this data class for comparison.
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:
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
212 If the tuples are longer, then all values following after this must be
213 integers or floats.
215 >>> EvaluationDataElement()._tuple()
216 ('EvaluationDataElement',)
218 :returns: a tuple with all the data of this class, where `None` values
219 are masked out
220 """
221 return (self.__class__.__name__, )
223 def __hash__(self) -> int:
224 """
225 Compute the hash code of this object.
227 :returns: the hash code
228 """
229 return hash(self._tuple())
231 def __eq__(self, other) -> bool:
232 """
233 Compare for `==` with another object based on the `_tuple()` value.
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.
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 ==.")
261 def __ne__(self, other) -> bool:
262 """
263 Compare for `!=` with another object based on the `_tuple()` value.
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.
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 !=.")
291 def __lt__(self, other) -> bool:
292 """
293 Compare for `<` with another object based on the `_tuple()` value.
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.
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 <.")
324 def __le__(self, other) -> bool:
325 """
326 Compare for `<=` with another object based on the `_tuple()` value.
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.
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 <=.")
357 def __gt__(self, other) -> bool:
358 """
359 Compare for `>` with another object based on the `_tuple()` value.
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.
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 >.")
390 def __ge__(self, other) -> bool:
391 """
392 Compare for `>=` with another object based on the `_tuple()` value.
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.
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 >=.")
424@dataclass(frozen=True, init=False, order=False, eq=False)
425class PerRunData(EvaluationDataElement):
426 """
427 An immutable record of information over a single run.
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.
457 >>> try:
458 ... PerRunData("@1 2", "i", "f", "e", 234)
459 ... except ValueError as ve:
460 ... print(ve)
461 Invalid algorithm name '@1 2'.
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.
469 >>> try:
470 ... PerRunData("x", "sdf i", "f", "e", 234)
471 ... except ValueError as ve:
472 ... print(ve)
473 Invalid instance name 'sdf i'.
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.
481 >>> try:
482 ... PerRunData("x", "i", "d-f", "e", 234)
483 ... except ValueError as ve:
484 ... print(ve)
485 Invalid objective name 'd-f'.
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.
494 >>> try:
495 ... PerRunData("y", "i", "f", "x x", 234)
496 ... except ValueError as ve:
497 ... print(ve)
498 Invalid encoding name 'x x'.
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.
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 """
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
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.
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))
542 def _tuple(self) -> tuple[Any, ...]:
543 """
544 Get the tuple representation of this object used in comparisons.
546 :return: the comparison-relevant data of this object in a tuple
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)
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.
562 Obtain a path that would correspond to the log file of this end
563 result, resolved from a base directory `base_dir`.
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)
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.
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.
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.
612 >>> try:
613 ... MultiRunData("x x", "i", "f", "e", 234)
614 ... except ValueError as ve:
615 ... print(ve)
616 Invalid algorithm name 'x x'.
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.
625 >>> try:
626 ... MultiRunData("x", "a-i", "f", "e", 234)
627 ... except ValueError as ve:
628 ... print(ve)
629 Invalid instance name 'a-i'.
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.
638 >>> try:
639 ... MultiRunData("xx", "i", "d'@f", "e", 234)
640 ... except ValueError as ve:
641 ... print(ve)
642 Invalid objective name "d'@f".
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.
651 >>> try:
652 ... MultiRunData("xx", "i", "f", "e-{a", 234)
653 ... except ValueError as ve:
654 ... print(ve)
655 Invalid encoding name 'e-{a'.
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.
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 """
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
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.
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))
704 def _tuple(self) -> tuple[Any, ...]:
705 """
706 Get the tuple representation of this object used in comparisons.
708 :return: the comparison-relevant data of this object in a tuple
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)
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.
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.
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.
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.
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 """
778 #: The unit of the time axis.
779 time_unit: str
780 #: the name of the objective value axis.
781 f_name: str
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.
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))
805 def _tuple(self) -> tuple[Any, ...]:
806 """
807 Get the tuple representation of this object used in comparisons.
809 :return: the comparison-relevant data of this object in a tuple
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)
829def get_instance(obj: PerRunData | MultiRunData) -> str | None:
830 """
831 Get the instance of a given object.
833 :param obj: the object
834 :return: the instance string, or `None` if no instance is specified
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
846def get_algorithm(obj: PerRunData | MultiRunData) -> str | None:
847 """
848 Get the algorithm of a given object.
850 :param obj: the object
851 :return: the algorithm string, or `None` if no algorithm is specified
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
863def sort_key(obj: PerRunData | MultiRunData) -> tuple[Any, ...]:
864 """
865 Get the default sort key for the given object.
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.
872 :param obj: the object
873 :return: the sort key
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()
892def motipy_footer_bottom_comments(
893 _: Any, additional: str | None = None) -> Iterable[str]:
894 """
895 Print the standard csv footer for moptipy.
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
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
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."
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.")