Skip to content

Commit a906dc0

Browse files
committedJan 31, 2025·
significantly improved Generator-based API
1 parent 54ac55f commit a906dc0

19 files changed

+282
-286
lines changed
 

‎README.md

+2-5
Original file line numberDiff line numberDiff line change
@@ -1697,11 +1697,8 @@ with temp_dir() as td:
16971697
max_fes=10000, # we grant 10000 FEs per run
16981698
n_runs=4) # perform 4 runs per algorithm * instance combination
16991699

1700-
end_results = list(from_logs(td)) # get results from log files
1701-
1702-
end_stats = [] # the list to receive the statistics records
1703-
# compute end result statistics for all algorithm+instance combinations
1704-
from_end_results(end_results, end_stats.append)
1700+
# Compute the end statistics from end results loaded from log files.
1701+
end_stats = list(from_end_results(from_logs(td)))
17051702

17061703
# store the statistics to a CSV file
17071704
es_csv = to_csv(end_stats, td.resolve_inside("end_stats.txt"))

‎examples/end_statistics_jssp.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,8 @@
2323
max_fes=10000, # we grant 10000 FEs per run
2424
n_runs=4) # perform 4 runs per algorithm * instance combination
2525

26-
end_results = list(from_logs(td)) # get results from log files
27-
28-
end_stats = [] # the list to receive the statistics records
29-
# compute end result statistics for all algorithm+instance combinations
30-
from_end_results(end_results, end_stats.append)
26+
# Compute the end statistics from end results loaded from log files.
27+
end_stats = list(from_end_results(from_logs(td)))
3128

3229
# store the statistics to a CSV file
3330
es_csv = to_csv(end_stats, td.resolve_inside("end_stats.txt"))

‎examples/end_statistics_over_feature_plot.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,8 @@ def make_rls(problem) -> Execution:
8080
n_runs=7) # we will execute 7 runs per setup
8181
# Once we arrived here, the 20*7 = 140 runs have completed.
8282

83-
end_results = list(from_logs(td)) # load results
84-
85-
end_stats = [] # the end statistics go into this list
86-
from_end_results(end_results, end_stats.append)
83+
# Compute the end statistics from end results loaded from log files.
84+
end_stats = list(from_end_results(from_logs(td)))
8785

8886
files = [] # the collection of files
8987

‎examples/end_statistics_over_param_plot.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
of `m` as `int(name[9:])` and the base name of the algorithm as `name[9:]`,
3838
i.e., `rls_flipB` in our example.
3939
"""
40+
from itertools import chain
4041
from time import sleep
4142
from webbrowser import open_new_tab
4243

@@ -95,11 +96,10 @@ def make_rls(problem, m: int) -> Execution:
9596

9697
end_results = list(from_logs(td)) # load results
9798

98-
end_stats = [] # the end statistics go into this list
99-
from_end_results(end_results, end_stats.append)
100-
from_end_results( # over all instances summary
101-
end_results, end_stats.append, join_all_instances=True,
102-
join_all_objectives=True)
99+
end_stats = list(chain( # Compute two sets of end statistics
100+
from_end_results(end_results), # One per algorithm-instance combo
101+
from_end_results( # and one on a per-algorithm basis.
102+
end_results, join_all_instances=True, join_all_objectives=True)))
103103

104104
files = [] # the collection of files
105105

‎moptipy/evaluation/end_results.py

+62-64
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
)
3333
from pycommons.io.csv import CsvReader as CsvReaderBase
3434
from pycommons.io.csv import CsvWriter as CsvWriterBase
35-
from pycommons.io.path import Path, file_path, line_writer
35+
from pycommons.io.path import Path, file_path, write_lines
3636
from pycommons.strings.string_conv import (
3737
int_or_none_to_str,
3838
num_or_none_to_str,
@@ -459,9 +459,7 @@ def to_csv(results: Iterable[EndResult], file: str) -> Path:
459459
logger(f"Writing end results to CSV file {path!r}.")
460460
path.ensure_parent_dir_exists()
461461
with path.open_for_write() as wt:
462-
consumer: Final[Callable[[str], None]] = line_writer(wt)
463-
for r in CsvWriter.write(results):
464-
consumer(r)
462+
write_lines(CsvWriter.write(results), wt)
465463
logger(f"Done writing end results to CSV file {path!r}.")
466464
return path
467465

@@ -722,12 +720,13 @@ def parse_row(self, data: list[str]) -> EndResult:
722720
class EndResultLogParser[T](SetupAndStateParser[T]):
723721
"""The internal log parser class."""
724722

725-
def get_result(self) -> T:
723+
def _parse_file(self, file: Path) -> T:
726724
"""
727725
Get the parsing result.
728726
729727
:returns: the :class:`EndResult` instance
730728
"""
729+
super()._parse_file(file)
731730
return cast(T, EndResult(self.algorithm,
732731
self.instance,
733732
self.objective,
@@ -794,16 +793,13 @@ def __init__(
794793
self.__hit_goal: bool = False
795794
self.__state: int = 0
796795

797-
def before_get_result(self) -> None:
798-
"""Before we get the result."""
796+
def _parse_file(self, file: Path) -> EndResult:
797+
super()._parse_file(file)
799798
if self.__state != 2:
800799
raise ValueError(
801800
"Illegal state, log file must have a "
802801
f"{SECTION_PROGRESS!r} section.")
803802
self.__state = 0
804-
return super().before_get_result()
805-
806-
def get_result(self) -> EndResult:
807803
__hit_goal = self.__hit_goal
808804
stop_fes: int = self.__stop_fes
809805
stop_ms: int = self.__stop_ms
@@ -832,8 +828,12 @@ def get_result(self) -> EndResult:
832828
max_time_millis=_join_goals(
833829
self.__limit_ms_n, self.max_time_millis, min))
834830

835-
def after_get_result(self) -> None:
836-
"""Cleanup."""
831+
def _end_parse_file(self, file: Path) -> None:
832+
"""
833+
Cleanup.
834+
835+
:param file: the file that was parsed.
836+
"""
837837
self.__stop_fes = None
838838
self.__stop_ms = None
839839
self.__stop_f = None
@@ -846,65 +846,63 @@ def after_get_result(self) -> None:
846846
self.__limit_f_n = None
847847
self.__limit_f = -inf
848848
self.__hit_goal = False
849-
super().after_get_result()
850-
851-
def start_parse_file(self, root: Path, current: Path) -> bool:
852-
if super().start_parse_file(root, current):
853-
if (self.algorithm is None) or (self.instance is None):
854-
raise ValueError(
855-
f"Invalid state: algorithm={self.algorithm!r}, "
856-
f"instance={self.instance!r}.")
857-
858-
fes = self.__src_limit_fes(self.algorithm, self.instance) \
859-
if callable(self.__src_limit_fes) else self.__src_limit_fes
860-
self.__limit_fes_n = None if fes is None else \
861-
check_int_range(fes, "limit_fes", 1, 1_000_000_000_000_000)
862-
self.__limit_fes = inf if self.__limit_fes_n is None \
863-
else self.__limit_fes_n
864-
865-
time = self.__src_limit_ms(self.algorithm, self.instance) \
866-
if callable(self.__src_limit_ms) else self.__src_limit_ms
867-
self.__limit_ms_n = None if time is None else \
868-
check_int_range(time, "__limit_ms", 1, 1_000_000_000_000)
869-
self.__limit_ms = inf if self.__limit_ms_n is None \
870-
else self.__limit_ms_n
871-
872-
self.__limit_f_n = self.__src_limit_f(
873-
self.algorithm, self.instance) \
874-
if callable(self.__src_limit_f) else self.__src_limit_f
875-
if self.__limit_f_n is not None:
876-
if not isinstance(self.__limit_f_n, int | float):
877-
raise type_error(self.__limit_f_n, "limit_f", (
878-
int, float))
879-
if not isfinite(self.__limit_f_n):
880-
if self.__limit_f_n <= -inf:
881-
self.__limit_f_n = None
882-
else:
883-
raise ValueError(
884-
f"invalid limit f={self.__limit_f_n} for "
885-
f"{self.algorithm} on {self.instance}")
886-
self.__limit_f = -inf if self.__limit_f_n is None \
887-
else self.__limit_f_n
888-
return True
889-
return False
890-
891-
def start_section(self, title: str) -> bool:
849+
super()._end_parse_file(file)
850+
851+
def _start_parse_file(self, file: Path) -> None:
852+
super()._start_parse_file(file)
853+
a: Final[str | None] = self.algorithm
854+
i: Final[str | None] = self.instance
855+
856+
fes = self.__src_limit_fes(a, i) if (a and i and callable(
857+
self.__src_limit_fes)) else (
858+
self.__src_limit_fes if isinstance(self.__src_limit_fes, int)
859+
else None)
860+
self.__limit_fes_n = None if fes is None else \
861+
check_int_range(fes, "limit_fes", 1, 1_000_000_000_000_000)
862+
self.__limit_fes = inf if self.__limit_fes_n is None \
863+
else self.__limit_fes_n
864+
865+
time = self.__src_limit_ms(a, i) if (a and i and callable(
866+
self.__src_limit_ms)) else (
867+
self.__src_limit_ms if isinstance(self.__src_limit_ms, int)
868+
else None)
869+
self.__limit_ms_n = None if time is None else \
870+
check_int_range(time, "__limit_ms", 1, 1_000_000_000_000)
871+
self.__limit_ms = inf if self.__limit_ms_n is None \
872+
else self.__limit_ms_n
873+
874+
self.__limit_f_n = self.__src_limit_f(a, i) if (a and i and callable(
875+
self.__src_limit_f)) else (
876+
self.__src_limit_f if isinstance(self.__src_limit_f, int | float)
877+
else None)
878+
if self.__limit_f_n is not None:
879+
if not isinstance(self.__limit_f_n, int | float):
880+
raise type_error(self.__limit_f_n, "limit_f", (
881+
int, float))
882+
if not isfinite(self.__limit_f_n):
883+
if self.__limit_f_n <= -inf:
884+
self.__limit_f_n = None
885+
else:
886+
raise ValueError(
887+
f"invalid limit f={self.__limit_f_n} for "
888+
f"{self.algorithm} on {self.instance}")
889+
self.__limit_f = -inf if self.__limit_f_n is None \
890+
else self.__limit_f_n
891+
892+
def _start_section(self, title: str) -> bool:
892893
if title == SECTION_PROGRESS:
893894
if self.__state != 0:
894895
raise ValueError(f"Already did section {title}.")
895896
self.__state = 1
896897
return True
897-
return super().start_section(title)
898-
899-
def needs_more_lines(self) -> bool:
900-
return (self.__state < 2) or super().needs_more_lines()
898+
return super()._start_section(title)
901899

902-
def lines(self, lines: list[str]) -> bool:
903-
if not isinstance(lines, list):
904-
raise type_error(lines, "lines", list)
900+
def _needs_more_lines(self) -> bool:
901+
return (self.__state < 2) or super()._needs_more_lines()
905902

903+
def _lines(self, lines: list[str]) -> bool:
906904
if self.__state != 1:
907-
return super().lines(lines)
905+
return super()._lines(lines)
908906
self.__state = 2
909907

910908
n_rows = len(lines)
@@ -978,7 +976,7 @@ def lines(self, lines: list[str]) -> bool:
978976
self.__stop_f = stop_f
979977
self.__stop_li_fe = stop_li_fe
980978
self.__stop_li_ms = stop_li_ms
981-
return self.needs_more_lines()
979+
return self._needs_more_lines()
982980

983981

984982
def from_logs(

‎moptipy/evaluation/end_statistics.py

+21-30
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import os.path
1313
from dataclasses import dataclass
1414
from math import ceil, inf, isfinite
15-
from typing import Any, Callable, Final, Iterable, Iterator, cast
15+
from typing import Callable, Final, Generator, Iterable, Iterator, cast
1616

1717
from pycommons.ds.sequences import reiterable
1818
from pycommons.io.console import logger
@@ -28,7 +28,7 @@
2828
)
2929
from pycommons.io.csv import CsvReader as CsvReaderBase
3030
from pycommons.io.csv import CsvWriter as CsvWriterBase
31-
from pycommons.io.path import Path, file_path, line_writer
31+
from pycommons.io.path import Path, file_path, write_lines
3232
from pycommons.math.sample_statistics import (
3333
KEY_MEAN_ARITH,
3434
KEY_STDDEV,
@@ -767,17 +767,15 @@ def create(source: Iterable[EndResult]) -> EndStatistics:
767767

768768

769769
def from_end_results(source: Iterable[EndResult],
770-
consumer: Callable[[EndStatistics], Any],
771770
join_all_algorithms: bool = False,
772771
join_all_instances: bool = False,
773772
join_all_objectives: bool = False,
774-
join_all_encodings: bool = False) -> None:
773+
join_all_encodings: bool = False) \
774+
-> Generator[EndStatistics, None, None]:
775775
"""
776776
Aggregate statistics over a stream of end results.
777777
778778
:param source: the stream of end results
779-
:param consumer: the destination to which the new records will be
780-
sent, can be the `append` method of a :class:`list`
781779
:param join_all_algorithms: should the statistics be aggregated
782780
over all algorithms
783781
:param join_all_instances: should the statistics be aggregated
@@ -786,11 +784,10 @@ def from_end_results(source: Iterable[EndResult],
786784
all objectives?
787785
:param join_all_encodings: should statistics be aggregated over all
788786
encodings
787+
:returns: iterates over the generated end statistics records
789788
"""
790789
if not isinstance(source, Iterable):
791790
raise type_error(source, "source", Iterable)
792-
if not callable(consumer):
793-
raise type_error(consumer, "consumer", call=True)
794791
if not isinstance(join_all_algorithms, bool):
795792
raise type_error(join_all_algorithms,
796793
"join_all_algorithms", bool)
@@ -803,7 +800,7 @@ def from_end_results(source: Iterable[EndResult],
803800

804801
if (join_all_algorithms and join_all_instances
805802
and join_all_objectives and join_all_encodings):
806-
consumer(create(source))
803+
yield create(source)
807804
return
808805

809806
sorter: dict[tuple[str, str, str, str], list[EndResult]] = {}
@@ -828,9 +825,9 @@ def from_end_results(source: Iterable[EndResult],
828825

829826
if len(sorter) > 1:
830827
for key in sorted(sorter.keys()):
831-
consumer(create(sorter[key]))
828+
yield create(sorter[key])
832829
else:
833-
consumer(create(next(iter(sorter.values()))))
830+
yield create(next(iter(sorter.values()))) #: pylint: disable=R1708
834831

835832

836833
def to_csv(data: EndStatistics | Iterable[EndStatistics],
@@ -846,16 +843,14 @@ def to_csv(data: EndStatistics | Iterable[EndStatistics],
846843
logger(f"Writing end result statistics to CSV file {path!r}.")
847844
path.ensure_parent_dir_exists()
848845
with path.open_for_write() as wt:
849-
consumer: Final[Callable[[str], None]] = line_writer(wt)
850-
for r in CsvWriter.write(
851-
(data, ) if isinstance(data, EndStatistics) else data):
852-
consumer(r)
846+
write_lines(CsvWriter.write(
847+
(data, ) if isinstance(data, EndStatistics) else data), wt)
853848

854849
logger(f"Done writing end result statistics to CSV file {path!r}.")
855850
return path
856851

857852

858-
def from_csv(file: str) -> Iterator[EndStatistics]:
853+
def from_csv(file: str) -> Generator[EndStatistics, None, None]:
859854
"""
860855
Parse a CSV file and collect all encountered :class:`EndStatistics`.
861856
@@ -1531,13 +1526,10 @@ def aggregate_over_parameter(
15311526

15321527
stats: Final[list[EndStatistics]] = []
15331528
for pv in sorted(param_map.keys()):
1534-
def __make(es: EndStatistics, ppv=pv, ss=stats.append) -> None:
1535-
ss(__PvEndStatistics(es, ppv))
1536-
1537-
from_end_results(
1538-
param_map[pv], cast(Callable[[EndStatistics], None], __make),
1539-
join_all_algorithms, join_all_instances, join_all_objectives,
1540-
join_all_encodings)
1529+
for ess in from_end_results(
1530+
param_map[pv], join_all_algorithms, join_all_instances,
1531+
join_all_objectives, join_all_encodings):
1532+
stats.append(__PvEndStatistics(ess, pv))
15411533
return cast(Callable[[EndStatistics], int | float],
15421534
__PvEndStatistics.get_param_value), tuple(stats)
15431535

@@ -1590,19 +1582,18 @@ def __make(es: EndStatistics, ppv=pv, ss=stats.append) -> None:
15901582
args: Final[argparse.Namespace] = parser.parse_args()
15911583

15921584
src_path: Final[Path] = args.source
1593-
end_results: list[EndResult]
1585+
end_results: Iterable[EndResult]
15941586
if src_path.is_file():
15951587
logger(f"{src_path!r} identifies as file, load as end-results csv")
1596-
end_results = list(end_results_from_csv(src_path))
1588+
end_results = end_results_from_csv(src_path)
15971589
else:
15981590
logger(f"{src_path!r} identifies as directory, load it as log files")
1599-
end_results = list(end_results_from_logs(src_path))
1591+
end_results = end_results_from_logs(src_path)
16001592

16011593
end_stats: Final[list[EndStatistics]] = []
1602-
from_end_results(
1603-
source=end_results, consumer=end_stats.append,
1594+
to_csv(from_end_results(
1595+
source=end_results,
16041596
join_all_algorithms=args.join_algorithms,
16051597
join_all_instances=args.join_instances,
16061598
join_all_objectives=args.join_objectives,
1607-
join_all_encodings=args.join_encodings)
1608-
to_csv(end_stats, args.dest)
1599+
join_all_encodings=args.join_encodings), args.dest)

0 commit comments

Comments
 (0)