Coverage for moptipy / evaluation / tabulate_end_stats.py: 6%
154 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +0000
1"""Make an LaTeX end-statistics table with column wrapping."""
3from io import TextIOBase
4from math import inf
5from typing import Any, Callable, Final, Iterable, TextIO
7from pycommons.io.console import logger
8from pycommons.io.path import line_writer
10from moptipy.evaluation.end_statistics import EndStatistics
11from moptipy.evaluation.end_statistics import getter as es_getter
12from moptipy.utils.number_renderer import default_get_int_renderer
14#: the integer number renderer
15__INT_2_STR: Final[Callable[[int], str]] = default_get_int_renderer()
18def tabulate_end_stats(
19 data: Iterable[EndStatistics],
20 dest: Callable[[int], TextIO | TextIOBase],
21 n_wrap: int = 3,
22 max_rows: int = 50,
23 stats: Iterable[tuple[Callable[[
24 EndStatistics], int | float | None], str, bool,
25 Callable[[int | float], str]]] = ((
26 es_getter("bestF.mean"),
27 r"\bestFmean", True,
28 lambda v: __INT_2_STR(round(v))), ),
29 instance_get: Callable[[EndStatistics], str] =
30 lambda es: es.instance,
31 instance_sort_key: Callable[[str], Any] = lambda x: x,
32 instance_name: Callable[[str], str] =
33 lambda x: f"{{\\instStyle{{{x}}}}}",
34 algorithm_get: Callable[[EndStatistics], str] = lambda x: x.algorithm,
35 algorithm_sort_key: Callable[[str], Any] = lambda x: x,
36 algorithm_name: Callable[[str], str] = lambda x: x,
37 instance_cols: Iterable[tuple[str, Callable[[str], str]]] = (),
38 best_format: Callable[[str], str] =
39 lambda x: f"{{\\textbf{{{x}}}}}",
40 instance_header: str = "instance",
41 best_count_header: str | None = r"\nBest") -> None:
42 """
43 Make a table of end statistics that can wrap multiple pages, if need be.
45 :param data: the source data
46 :param dest: the destination generator
47 :param n_wrap: the number of times we can wrap a table
48 :param max_rows: the maximum rows per destination
49 :param stats: the set of statistics: tuples of statistic, title, whether
50 minimization or maximization, and a to-string converter
51 :param instance_get: get the instance identifier
52 :param instance_name: get the instance name, as it should be printed
53 :param instance_sort_key: get the sort key for the instance
54 :param algorithm_get: get the algorithm identifier
55 :param algorithm_name: get the algorithm name, as it should be printed
56 :param algorithm_sort_key: get the sort key for the algorithm
57 :param instance_cols: the fixed instance columns
58 :param best_format: format the best value
59 :param instance_header: the header for the instance
60 :param best_count_header: the header for the best count
61 """
62 if not isinstance(data, list):
63 data = list(data)
64 if list.__len__(data) <= 0:
65 raise ValueError("Empty data?")
66 if not isinstance(stats, list):
67 stats = list(stats)
68 n_stats: Final[int] = list.__len__(stats)
69 if n_stats <= 0:
70 raise ValueError("No statistics data?")
72 # We create a map of data
73 datamap: Final[dict[str, dict[str, EndStatistics]]] = {}
74 for es in data:
75 algorithm: str = algorithm_get(es)
76 instance: str = instance_get(es)
77 if instance in datamap:
78 datamap[instance][algorithm] = es
79 else:
80 datamap[instance] = {algorithm: es}
82 instances: Final[list[str]] = sorted(
83 {es.instance for es in data}, key=instance_sort_key)
84 if list.__len__(instances) <= 0:
85 raise ValueError("No instance!")
87 algorithms: Final[list[str]] = sorted({
88 k for x in datamap.values() for k in x}, key=algorithm_sort_key)
89 n_algorithms: Final[int] = list.__len__(algorithms)
90 if n_algorithms <= 0:
91 raise ValueError("No algorithms!")
92 n_data_cols: Final[int] = n_algorithms * n_stats
94 if not isinstance(instance_cols, tuple):
95 instance_cols = tuple(instance_cols)
96 n_inst_cols: Final[int] = tuple.__len__(instance_cols)
98 # Now we compute all the values
99 output: list[list[str]] = []
101 best_count: Final[list[int]] = [0] * n_data_cols
102 for instance in instances:
103 row: list[str] = [instance_name(instance)]
104 row.extend(ic[1](instance) for ic in instance_cols)
105 values: list[int | float | None] = [None] * n_data_cols
106 best: list[int | float] = [
107 (inf if stat[2] else -inf) for stat in stats]
109 # get the values
110 idx: int = 0
111 for algorithm in algorithms:
112 es = datamap[instance][algorithm]
113 for si, stat in enumerate(stats):
114 values[idx] = value = stat[0](es)
115 idx += 1
116 if (value is not None) and ((stat[2] and (
117 value < best[si])) or ((not stat[2]) and (
118 value > best[si]))):
119 best[si] = value
121 # format the values
122 idx = 0
123 for _ in range(n_algorithms):
124 for si, stat in enumerate(stats):
125 value = values[idx]
126 idx += 1
127 if value is None:
128 row.append("")
129 continue
130 printer: str = stat[3](value)
131 if ((stat[2] and (value <= best[si])) or ((not stat[2]) and (
132 value >= best[si]))):
133 best_count[idx - 1] += 1
134 printer = best_format(printer)
135 row.append(printer)
136 output.append(row)
138 if best_count_header is not None:
139 nc: int = n_inst_cols + 1
140 row = [f"\\multicolumn{{{nc}}}{{r}}{{{best_count_header}}}"]
141 best = [-1] * n_stats
143 # get the best best counts
144 idx = 0
145 for _ in range(n_algorithms):
146 for si in range(n_stats):
147 value = best_count[idx]
148 best[si] = max(value, best[si])
149 idx += 1
151 # format the best count values
152 idx = 0
153 for _ in range(n_algorithms):
154 for si in range(n_stats):
155 ivalue: int = best_count[idx]
156 idx += 1
157 printer = str(ivalue)
158 if ivalue >= best[si]:
159 printer = best_format(printer)
160 row.append(printer)
161 output.append(row)
163 # now we got all the data prepared and just need to arrange it
164 n_output: Final[int] = list.__len__(output)
165 pages: Final[list[list[list[int]]]] = []
166 idx = 0
167 keep_going: bool = True
168 while keep_going:
169 current_page: list[list[int]] = []
170 pages.append(current_page)
171 for _ in range(max_rows):
172 current_row: list[int] = []
173 current_page.append(current_row)
174 for _ in range(n_wrap):
175 current_row.append(idx)
176 idx += 1
177 if idx >= n_output:
178 keep_going = False
179 break
180 if not keep_going:
181 break
182 # ok, now we know the data that we can put on each page
183 # we now re-arrange it
184 for current_page in pages:
185 ids: list[int] = sorted(x for y in current_page for x in y)
186 idx = 0
187 for col in range(n_wrap):
188 for current_row in current_page:
189 if col < list.__len__(current_row):
190 current_row[col] = ids[idx]
191 idx += 1
193 # now we construct the header and footer that should go into each
194 # partial table
195 header: list[str] = []
196 footer: list[str] = [r"\hline%", r"\end{tabular}%"]
198 writer: list[str] = ["l"]
199 writer.extend("r" for _ in range(n_inst_cols))
200 writer.extend(["r"] * n_stats * n_algorithms)
201 txt: str = "".join(writer)
202 header.extend((f"\\begin{{tabular}}{{{'|'.join([txt] * n_wrap)}}}%",
203 r"\hline%"))
205 writer.clear()
206 writer.append(instance_header)
207 writer.extend(f"\\multicolumn{{1}}{{c}}{{{ic[0]}}}"
208 for ic in instance_cols)
209 writer.extend(f"\\multicolumn{{{n_stats}}}{{c}}{{{algorithm_name(algo)}}}"
210 for algo in algorithms)
211 txt = "&".join(writer)
212 if n_wrap > 1:
213 writer[-1] = writer[-1].replace("{c}", "{c|}")
214 txt_inner = "&".join(writer)
215 header.append(f"{'&'.join([txt_inner] * (n_wrap - 1))}&{txt}\\\\%")
216 else:
217 header.append(f"{txt}\\\\%")
219 writer.clear()
220 if n_stats > 1:
221 writer.clear()
222 writer.append("")
223 writer.extend("" for _ in range(n_inst_cols))
224 writer.extend(f"\\multicolumn{{1}}{{c}}{{{sss[2]}}}"
225 for _ in range(n_algorithms) for sss in stats)
226 txt = "&".join(writer)
227 if n_wrap > 1:
228 writer[-1] = writer[-1].replace("{c}", "{c|}")
229 txt_inner = "&".join(writer)
230 header.append(f"{'&'.join([txt_inner] * (n_wrap - 1))}&{txt}\\\\%")
231 else:
232 header.append(f"{txt}\\\\%")
233 writer.clear()
234 header.append(r"\hline%")
236 for page_idx, current_page in enumerate(pages):
237 logger(f"Now tacking data part {page_idx + 1}.")
238 with dest(page_idx + 1) as stream:
239 out_stream = line_writer(stream)
240 for txt in header:
241 out_stream(txt)
242 for current_row in current_page:
243 line: list[str] = [x for y in current_row for x in output[y]]
244 out_stream(f"{'&'.join(line)}\\\\%")
245 for txt in footer:
246 out_stream(txt)