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

1"""Make an LaTeX end-statistics table with column wrapping.""" 

2 

3from io import TextIOBase 

4from math import inf 

5from typing import Any, Callable, Final, Iterable, TextIO 

6 

7from pycommons.io.console import logger 

8from pycommons.io.path import line_writer 

9 

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 

13 

14#: the integer number renderer 

15__INT_2_STR: Final[Callable[[int], str]] = default_get_int_renderer() 

16 

17 

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. 

44 

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?") 

71 

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} 

81 

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!") 

86 

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 

93 

94 if not isinstance(instance_cols, tuple): 

95 instance_cols = tuple(instance_cols) 

96 n_inst_cols: Final[int] = tuple.__len__(instance_cols) 

97 

98 # Now we compute all the values 

99 output: list[list[str]] = [] 

100 

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] 

108 

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 

120 

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) 

137 

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 

142 

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 

150 

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) 

162 

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 

192 

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}%"] 

197 

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%")) 

204 

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}\\\\%") 

218 

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%") 

235 

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)