Coverage for sparkle/CLI/generate_report.py: 24%

382 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-07-01 13:21 +0000

1#!/usr/bin/env python3 

2"""Sparkle command to generate a report for an executed experiment.""" 

3import sys 

4import shutil 

5import argparse 

6from pathlib import Path 

7import time 

8import json 

9 

10import pylatex as pl 

11from sparkle import __version__ as __sparkle_version__ 

12 

13from sparkle.CLI.help import global_variables as gv 

14from sparkle.CLI.help import resolve_object_name 

15from sparkle.CLI.help import logging as sl 

16from sparkle.CLI.help import argparse_custom as ac 

17 

18from sparkle.solver import Solver 

19from sparkle.instance import Instance_Set 

20from sparkle.selector import Extractor 

21from sparkle.structures import PerformanceDataFrame, FeatureDataFrame 

22from sparkle.configurator.configurator import ConfigurationScenario 

23from sparkle.selector.selector import SelectionScenario 

24from sparkle.types import SolverStatus 

25 

26from sparkle.platform import latex 

27from sparkle.platform.output.configuration_output import ConfigurationOutput 

28from sparkle.platform.output.selection_output import SelectionOutput 

29 

30 

31MAX_DEC = 4 # Maximum decimals used for each reported value 

32 

33 

34def parser_function() -> argparse.ArgumentParser: 

35 """Define the command line arguments.""" 

36 parser = argparse.ArgumentParser( 

37 description="Generates a report for all known selection, configuration and " 

38 "parallel portfolio scenarios will be generated.", 

39 epilog="If you wish to filter specific solvers, instance sets, ... have a look " 

40 "at the command line arguments.") 

41 # Add argument for filtering solvers 

42 parser.add_argument(*ac.SolversReportArgument.names, 

43 **ac.SolversReportArgument.kwargs) 

44 # Add argument for filtering instance sets 

45 parser.add_argument(*ac.InstanceSetsReportArgument.names, 

46 **ac.InstanceSetsReportArgument.kwargs) 

47 # Add argument for filtering configurators? 

48 # Add argument for filtering selectors? 

49 # Add argument for filtering ??? scenario ids? configuration ids? 

50 parser.add_argument(*ac.GenerateJSONArgument.names, 

51 **ac.GenerateJSONArgument.kwargs) 

52 return parser 

53 

54 

55def generate_configuration_section(report: pl.Document, scenario: ConfigurationScenario, 

56 scenario_output: ConfigurationOutput) -> None: 

57 """Generate a section for a configuration scenario.""" 

58 report_dir = Path(report.default_filepath).parent 

59 time_stamp = time.strftime("%Y%m%d%H%M%S", time.localtime(time.time())) 

60 plot_dir = report_dir /\ 

61 f"{scenario.configurator.__name__}_{scenario.name}_plots_{time_stamp}" 

62 plot_dir.mkdir(exist_ok=True) 

63 

64 # 1. Write section intro 

65 report.append(pl.Section( 

66 f"{scenario.configurator.__name__} Configuration: " 

67 f"{scenario.solver.name} on {scenario.instance_set.name}")) 

68 report.append("In this scenario, ") 

69 report.append(pl.UnsafeCommand( 

70 f"textbf{{{scenario.configurator.__name__}}} " 

71 f"({scenario.configurator.full_name})~\\cite" 

72 f"{{{scenario.configurator.__name__}}} with version " 

73 f"{scenario.configurator.version} was used for configuration. ")) 

74 report.append( 

75 f"The Solver {scenario.solver} was optimised on training set " 

76 f"{scenario.instance_set}. The scenario was run {scenario.number_of_runs} " 

77 f"times independently with different seeds, yielding {scenario.number_of_runs} " 

78 f"configurations. The cutoff time for the solver was set to " 

79 f"{scenario.solver_cutoff_time} seconds. The optimised objective is " 

80 f"{scenario.sparkle_objectives[0]}. Each Configuration was evaluated on the " 

81 "training set to determine the best configuration, e.g. the best " 

82 f"{scenario.sparkle_objectives[0]} value on the training set.") 

83 

84 # 2. Report all the configurator settings in table format 

85 report.append(pl.Subsection("Configurator Settings")) 

86 report.append("The following settings were used for " 

87 f"{scenario.configurator.__name__}:\n") 

88 tabular = pl.Tabular("l|r") 

89 tabular.add_row("Setting", "Value") 

90 tabular.add_hline() 

91 for setting, value in scenario.serialise().items(): 

92 tabular.add_row([setting, str(value)]) 

93 table_conf_settings = pl.Table(position="h") 

94 table_conf_settings.append(pl.UnsafeCommand("centering")) 

95 table_conf_settings.append(tabular) 

96 table_conf_settings.add_caption("Configurator Settings") 

97 report.append(table_conf_settings) 

98 

99 # 3. Report details on instance and solver used 

100 report.append(pl.Subsection("Solver & Instance Set(s) Details")) 

101 cs = scenario_output.solver.get_configuration_space() 

102 report.append(f"The solver {scenario_output.solver} was configured using " 

103 f"{len(cs.values())} configurable (hyper)parameters. " 

104 f"The configuration space has {len(cs.conditions)} conditions. ") 

105 report.append("The following instance sets were used for the scenario:") 

106 with report.create(pl.Itemize()) as instance_set_latex_list: 

107 for instance_set in [ 

108 scenario_output.instance_set_train] + scenario_output.test_instance_sets: 

109 training_set_name = instance_set.name.replace("_", " ") # Latex fix 

110 instance_set_latex_list.add_item( 

111 pl.UnsafeCommand( 

112 f"textbf{{{training_set_name}}} ({instance_set.size} instances)")) 

113 

114 # Function to generate a results summary of default vs best on an instance set 

115 def instance_set_summary(instance_set_name: str) -> None: 

116 """Generate a results summary of default vs best on an instance set.""" 

117 instance_set_results = scenario_output.instance_set_results[instance_set_name] 

118 report.append(f"The {scenario.sparkle_objectives[0]} value of the Default " 

119 f"Configuration on {instance_set_name} was ") 

120 report.append(pl.UnsafeCommand( 

121 f"textbf{{{round(instance_set_results.default_performance, MAX_DEC)}}}.\n")) 

122 report.append(f"The {scenario.sparkle_objectives[0]} value of the Best " 

123 f"Configuration on {instance_set_name} was ") 

124 report.append(pl.UnsafeCommand( 

125 f"textbf{{{round(instance_set_results.best_performance, MAX_DEC)}}}.\n")) 

126 report.append("In ") 

127 report.append(latex.AutoRef(f"fig:bestvsdefault{instance_set_name}{time_stamp}")) 

128 report.append(pl.utils.bold(" ")) # Force white space 

129 report.append("the results are plotted per instance.") 

130 # Create graph to compare best configuration vs default on the instance set 

131 import pandas as pd 

132 df = pd.DataFrame( 

133 [instance_set_results.default_instance_performance, 

134 instance_set_results.best_instance_performance], 

135 index=["Default Configuration", "Best Configuration"], dtype=float).T 

136 plot = latex.comparison_plot(df, None) 

137 plot_path = plot_dir /\ 

138 f"{scenario_output.best_configuration_key}_vs_"\ 

139 f"Default_{instance_set_name}.pdf" 

140 plot.write_image(plot_path, width=500, height=500) 

141 with report.create(pl.Figure(position="h")) as figure: 

142 figure.add_image(str(plot_path.relative_to(report_dir)), 

143 width=pl.utils.NoEscape(r"0.6\textwidth")) 

144 figure.add_caption( 

145 f"Best vs Default Performance on {instance_set_name} " 

146 f"({scenario.sparkle_objectives[0]})") 

147 figure.append(pl.UnsafeCommand( 

148 r"label{" 

149 f"fig:bestvsdefault{instance_set_name}{time_stamp}" 

150 r"}")) 

151 if scenario.sparkle_objectives[0].time: # Write status table 

152 report.append("The following Solver status were found per instance:") 

153 tabular = pl.Tabular("l|c|c|c") 

154 tabular.add_row("Status", "Default", "Best", "Overlap") 

155 tabular.add_hline() 

156 # Count the statuses 

157 for status in SolverStatus: 

158 default_count, best_count, overlap_count = 0, 0, 0 

159 for instance in instance_set_results.instance_status_default.keys(): 

160 instance = str(instance) 

161 default_hit = instance_set_results.instance_status_default[ 

162 instance] == status 

163 best_hit = instance_set_results.instance_status_best[ 

164 instance] == status 

165 default_count += default_hit 

166 best_count += best_hit 

167 overlap_count += (default_hit and best_hit) 

168 if default_count or best_count: 

169 tabular.add_row( 

170 status, default_count, best_count, overlap_count 

171 ) 

172 table_status_values = pl.Table(position="h") 

173 table_status_values.append(pl.UnsafeCommand("centering")) 

174 table_status_values.append(tabular) 

175 table_status_values.add_caption( 

176 "Status count for the best and default configuration.") 

177 report.append(table_status_values) 

178 

179 # 4. Report the results of the best configuration on the training set vs the default 

180 report.append(pl.Subsection( 

181 f"Comparison of Default and Best Configuration on Training Set " 

182 f"{scenario_output.instance_set_train.name}")) 

183 instance_set_summary(scenario_output.instance_set_train.name) 

184 

185 # 5. Report the actual config values 

186 report.append(pl.Subsubsection("Best Configuration Values")) 

187 if scenario_output.best_configuration_key ==\ 

188 PerformanceDataFrame.default_configuration: 

189 report.append("The configurator failed to find a better configuration than the " 

190 "default configuration on the training set in this scenario.") 

191 else: 

192 report.append("The following parameter values " 

193 "were found to be the best on the training set:\n") 

194 tabular = pl.Tabular("l|r") 

195 tabular.add_row("Parameter", "Value") 

196 tabular.add_hline() 

197 for parameter, value in scenario_output.best_configuration.items(): 

198 tabular.add_row([parameter, str(value)]) 

199 table_best_values = pl.Table(position="h") 

200 table_best_values.append(pl.UnsafeCommand("centering")) 

201 table_best_values.append(tabular) 

202 table_best_values.add_caption("Best found configuration values") 

203 report.append(table_best_values) 

204 

205 # 6. Report the results of best vs default conf on the test sets 

206 

207 for test_set in scenario_output.test_instance_sets: 

208 report.append(pl.Subsection( 

209 f"Comparison of Default and Best Configuration on Test Set " 

210 f"{test_set.name}")) 

211 instance_set_summary(test_set.name) 

212 

213 # 7. Report the parameter ablation scenario if present 

214 if scenario.ablation_scenario: 

215 report.append(pl.Subsection("Parameter importance via Ablation")) 

216 report.append("Ablation analysis ") 

217 report.append(pl.UnsafeCommand(r"cite{FawcettHoos16} ")) 

218 test_set = scenario.ablation_scenario.test_set 

219 if not scenario.ablation_scenario.test_set: 

220 test_set = scenario.ablation_scenario.train_set 

221 report.append( 

222 f"is performed from the default configuration of {scenario.solver} to the " 

223 f"best found configuration ({scenario_output.best_configuration_key}) " 

224 "to see which parameter changes between them contribute most to the improved" 

225 " performance. The ablation path uses the training set " 

226 f"{scenario.ablation_scenario.train_set.name} and validation is performed " 

227 f"on the test set {test_set.name}. The set of parameters that differ in the " 

228 "two configurations will form the ablation path. Starting from the default " 

229 "configuration, the path is computed by performing a sequence of rounds. In" 

230 " a round, each available parameter is flipped in the configuration and is " 

231 "validated on its performance. The flipped parameter with the best " 

232 "performance in that round, is added to the configuration and the next round" 

233 " starts with the remaining parameters. This repeats until all parameters " 

234 "are flipped, which is the best found configuration. The analysis resulted " 

235 "in the ablation presented in ") 

236 report.append(latex.AutoRef("tab:ablationtable")) 

237 report.append(".") 

238 

239 # Add ablation table 

240 tabular = pl.Tabular("r|l|r|r|r") 

241 data = scenario.ablation_scenario.read_ablation_table() 

242 for index, row in enumerate(data): 

243 tabular.add_row(*row) 

244 if index == 0: 

245 tabular.add_hline() 

246 table_ablation = pl.Table(position="h") 

247 table_ablation.append(pl.UnsafeCommand("centering")) 

248 table_ablation.append(tabular) 

249 table_ablation.add_caption("Ablation table") 

250 table_ablation.append(pl.UnsafeCommand(r"label{tab:ablationtable}")) 

251 report.append(table_ablation) 

252 

253 

254def generate_selection_section(report: pl.Document, scenario: SelectionScenario, 

255 scenario_output: SelectionOutput) -> None: 

256 """Generate a section for a selection scenario.""" 

257 report_dir = Path(report.default_filepath).parent 

258 time_stamp = time.strftime("%Y%m%d%H%M%S", time.localtime(time.time())) 

259 plot_dir = report_dir / f"{scenario.name.replace(' ', '_')}_plots_{time_stamp}" 

260 plot_dir.mkdir(exist_ok=True) 

261 report.append(pl.Section( 

262 f"Selection: {scenario.selector.model_class.__name__} on " 

263 f"{' '.join([s[0] for s in scenario_output.training_instance_sets])}")) 

264 report.append(f"In this scenario, a {scenario.selector.model_class.__name__} " 

265 f" ({scenario.selector.selector_class.__name__}) was trained on the " 

266 "performance and feature data using ASF-lib. The following solvers " 

267 f"were run with a cutoff time of {scenario.solver_cutoff} seconds:") 

268 with report.create(pl.Itemize()) as solver_latex_list: 

269 for solver_name in scenario_output.solvers.keys(): 

270 solver_name = solver_name.replace("_", " ") 

271 solver_latex_list.add_item( 

272 pl.UnsafeCommand( 

273 f"textbf{{{solver_name}}} " 

274 f"({len(scenario_output.solvers[solver_name])} configurations)")) 

275 # Report training instance sets 

276 report.append("The following training instance sets were used:") 

277 with report.create(pl.Itemize()) as instance_set_latex_list: 

278 for training_set_name, set_size in scenario_output.training_instance_sets: 

279 training_set_name = training_set_name.replace("_", " ") # Latex fix 

280 instance_set_latex_list.add_item( 

281 pl.UnsafeCommand( 

282 f"textbf{{{training_set_name}}} " 

283 f"({set_size} instances)")) 

284 # Report feature extractors 

285 report.append("The following feature extractors were used with a extractor cutoff " 

286 f"time of {scenario.extractor_cutoff} seconds:") 

287 with report.create(pl.Itemize()) as feature_extractor_latex_list: 

288 for feature_extractor_name in scenario.feature_extractors: 

289 extractor = resolve_object_name( 

290 feature_extractor_name, 

291 gv.file_storage_data_mapping[gv.extractor_nickname_list_path], 

292 gv.settings().DEFAULT_extractor_dir, class_name=Extractor) 

293 feature_extractor_name = feature_extractor_name.replace("_", " ") # Latex 

294 feature_extractor_latex_list.add_item( 

295 pl.UnsafeCommand(f"textbf{{{feature_extractor_name}}} " 

296 f"({extractor.output_dimension} features)")) 

297 # Report Training results 

298 report.append(pl.Subsection("Training Results")) 

299 # 1. Report VBS and selector performance, create ranking list of the solvers 

300 # TODO Add ref here to the training sets section? 

301 report.append(f"In this section, the {scenario.objective.name} results for the " 

302 "portfolio selector on solving the training instance set(s) listed " 

303 "is reported. ") 

304 report.append(f"The {scenario.objective.name} values for the Virtual Best Solver " 

305 "(VBS), i.e., the perfect portfolio selector is ") 

306 report.append(pl.utils.bold(f"{round(scenario_output.vbs_performance, MAX_DEC)}")) 

307 report.append(", the actual portfolio selector performance is ") 

308 report.append( 

309 pl.utils.bold(f"{round(scenario_output.actual_performance, MAX_DEC)}.\n")) 

310 

311 report.append("Below, the solvers are ranked based on " 

312 f"{scenario.objective.name} performance:") 

313 with report.create(pl.Enumerate()) as ranking_list: 

314 for solver_name, conf_id, value in scenario_output.solver_performance_ranking: 

315 value = round(value, MAX_DEC) 

316 solver_name = solver_name.replace("_", " ") # Latex fix 

317 conf_id = conf_id.replace("_", " ") # Latex fix 

318 ranking_list.add_item( 

319 pl.UnsafeCommand(f"textbf{{{solver_name}}} ({conf_id}): {value}")) 

320 

321 # 2. Marginal contribution ranking list VBS 

322 report.append(pl.Subsubsection("Marginal Contribution Ranking List")) 

323 report.append( 

324 "The following list shows the marginal contribution ranking list for the VBS:") 

325 with report.create(pl.Enumerate()) as ranking_list: 

326 for (solver_name, conf_id, 

327 contribution, performance) in scenario_output.marginal_contribution_perfect: 

328 contribution, performance =\ 

329 round(contribution, MAX_DEC), round(performance, MAX_DEC) 

330 solver_name = solver_name.replace("_", " ") # Latex fix 

331 conf_id = conf_id.replace("_", " ") # Latex fix 

332 ranking_list.add_item(pl.UnsafeCommand( 

333 f"textbf{{{solver_name}}} ({conf_id}): {contribution} ({performance})")) 

334 

335 # 3. Marginal contribution ranking list actual selector 

336 report.append("The following list shows the marginal contribution ranking list for " 

337 "the actual portfolio selector:") 

338 with report.create(pl.Enumerate()) as ranking_list: 

339 for (solver_name, conf_id, 

340 contribution, performance) in scenario_output.marginal_contribution_actual: 

341 contribution, performance =\ 

342 round(contribution, MAX_DEC), round(performance, MAX_DEC) 

343 solver_name = solver_name.replace("_", " ") # Latex fix 

344 conf_id = conf_id.replace("_", " ") # Latex fix 

345 ranking_list.add_item(pl.UnsafeCommand( 

346 f"textbf{{{solver_name}}} ({conf_id}): {contribution} ({performance})")) 

347 

348 # 4. Create scatter plot analysis 

349 report.append(pl.Subsubsection("Scatter Plot Analysis")) 

350 report.append(latex.AutoRef(f"fig:sbsvsselector{time_stamp}")) 

351 report.append(pl.utils.bold(" ")) # Trick to force a white space 

352 report.append("shows the empirical comparison between the portfolio " 

353 "selector and the single best solver (SBS). ") 

354 report.append(latex.AutoRef("fig:vbsvsselector")) 

355 report.append(pl.utils.bold(" ")) # Trick to force a white space 

356 report.append("shows the empirical comparison between the actual portfolio selector " 

357 "and the virtual best solver (VBS).") 

358 # Create figure on SBS versus the selector 

359 sbs_name, sbs_config, _ = scenario_output.solver_performance_ranking[0] 

360 # sbs_plot_name = f"{Path(sbs_name).name} ({sbs_config})" 

361 sbs_performance = scenario_output.sbs_performance 

362 selector_performance = scenario_output.actual_performance_data 

363 

364 # Join the data together 

365 import pandas as pd 

366 df = pd.DataFrame( 

367 [sbs_performance, selector_performance], 

368 index=[f"{Path(sbs_name).name} ({sbs_config})", "Selector"], dtype=float).T 

369 plot = latex.comparison_plot(df, "Single Best Solver vs Selector") 

370 plot_path = plot_dir /\ 

371 f"{Path(sbs_name).name}_{sbs_config}_vs_"\ 

372 f"Selector_{scenario.selector.model_class.__name__}.pdf" 

373 plot.write_image(plot_path, width=500, height=500) 

374 with report.create(pl.Figure()) as figure: 

375 figure.add_image(str(plot_path.relative_to(report_dir)), 

376 width=pl.utils.NoEscape(r"0.6\textwidth")) 

377 figure.add_caption("Empirical comparison between the Single Best Solver and the " 

378 "Selector") 

379 label = r"label{fig:sbsvsselector" + str(time_stamp) + r"}" 

380 figure.append(pl.UnsafeCommand(f"{label}")) 

381 

382 # Comparison between the actual portfolio selector in Sparkle and the VBS. 

383 vbs_performance = scenario_output.vbs_performance_data.tolist() 

384 df = pd.DataFrame([vbs_performance, selector_performance], 

385 index=["Virtual Best Solver", "Selector"], dtype=float).T 

386 plot = latex.comparison_plot(df, "Virtual Best Solver vs Selector") 

387 plot_path = plot_dir /\ 

388 f"Virtual_Best_Solver_vs_Selector_{scenario.selector.model_class.__name__}.pdf" 

389 plot.write_image(plot_path, width=500, height=500) 

390 with report.create(pl.Figure()) as figure: 

391 figure.add_image(str(plot_path.relative_to(report_dir)), 

392 width=pl.utils.NoEscape(r"0.6\textwidth")) 

393 figure.add_caption( 

394 "Empirical comparison between the Virtual Best Solver and the Selector") 

395 figure.append(pl.UnsafeCommand(r"label{fig:vbsvsselector}")) 

396 

397 if scenario_output.test_sets: 

398 report.append(pl.Subsection("Test Results")) 

399 report.append("The following results are reported on the test set(s):") 

400 with report.create(pl.Itemize()) as latex_list: 

401 for test_set_name, test_set_size in scenario_output.test_sets: 

402 result = round(scenario_output.test_set_performance[test_set_name], 

403 MAX_DEC) 

404 latex_list.add_item(pl.UnsafeCommand( 

405 f"textbf{{{test_set_name}}} ({test_set_size} instances): {result}")) 

406 

407 

408def generate_parallel_portfolio_section(report: pl.Document, 

409 scenario: PerformanceDataFrame) -> None: 

410 """Generate a section for a parallel portfolio scenario.""" 

411 report_dir = Path(report.default_filepath).parent 

412 portfolio_name = scenario.csv_filepath.parent.name 

413 time_stamp = time.strftime("%Y%m%d%H%M%S", time.localtime(time.time())) 

414 plot_dir = report_dir / f"{portfolio_name.replace(' ', '_')}_plots_{time_stamp}" 

415 plot_dir.mkdir() 

416 report.append(pl.Section(f"Parallel Portfolio {portfolio_name}")) 

417 report.append( 

418 "In this scenario, Sparkle runs the portfolio of Solvers on each instance in " 

419 "parallel with " 

420 f"{gv.settings().get_parallel_portfolio_number_of_seeds_per_solver()} different " 

421 "seeds. The cutoff time for each solver run is set to " 

422 f"{gv.settings().get_general_solver_cutoff_time()} seconds.") 

423 report.append(pl.Subsection("Solvers & Instance Sets")) 

424 report.append("The following Solvers were used in the portfolio:") 

425 # 1. Report on the Solvers and Instance Sets used for the portfolio 

426 with report.create(pl.Itemize()) as solver_latex_list: 

427 configs = scenario.configurations 

428 for solver in scenario.solvers: 

429 solver_name = solver.replace("_", " ") 

430 solver_latex_list.add_item( 

431 pl.UnsafeCommand( 

432 f"textbf{{{solver_name}}} " 

433 f"({len(configs[solver])} configurations)")) 

434 report.append("The following Instance Sets were used in the portfolio:") 

435 instance_sets = set(Path(instance).parent.name for instance in scenario.instances) 

436 instance_set_count = [ 

437 len([i for i in scenario.instances if Path(i).parent.name == s]) 

438 for s in instance_sets] 

439 with report.create(pl.Itemize()) as instance_set_latex_list: 

440 for set_name, set_size in zip(instance_sets, instance_set_count): 

441 set_name = set_name.replace("_", " ") # Latex fix 

442 instance_set_latex_list.add_item( 

443 pl.UnsafeCommand( 

444 f"textbf{{{set_name}}} ({set_size} instances)")) 

445 # 2. List which solver was the best on how many instances 

446 report.append(pl.Subsection("Portfolio Performance")) 

447 objective = scenario.objectives[0] 

448 report.append(f"The objective for the portfolio is {objective}. The " 

449 "following performance of the solvers was found over the instances: ") 

450 best_solver_count = {solver: 0 for solver in scenario.solvers} 

451 for instance in scenario.instances: 

452 ranking = scenario.get_solver_ranking(objective=objective, instances=[instance]) 

453 best_solver_count[ranking[0][0]] += 1 

454 

455 with report.create(pl.Itemize()) as latex_list: 

456 for solver, count in best_solver_count.items(): 

457 solver_name = solver.replace("_", " ") 

458 latex_list.add_item(pl.UnsafeCommand( 

459 f"textbf{{{solver_name}}} was the best solver on {count} instance(s).")) 

460 # TODO Report how many instances remained unsolved 

461 

462 # 3. Create table showing the performance of the portfolio vs and all solvers, 

463 # by showing the status count and number of times the solver was best 

464 solver_cancelled_count = {solver: 0 for solver in scenario.solvers} 

465 solver_timeout_count = {solver: 0 for solver in scenario.solvers} 

466 status_objective = [o for o in scenario.objective_names 

467 if o.lower().startswith("status")][0] 

468 cancelled_status = [SolverStatus.UNKNOWN, SolverStatus.CRASHED, SolverStatus.WRONG, 

469 SolverStatus.ERROR, SolverStatus.KILLED] 

470 for solver in scenario.solvers: 

471 status = scenario.get_value(solver=solver, objective=status_objective) 

472 for status in scenario.get_value(solver=solver, objective=status_objective): 

473 status = SolverStatus(status) 

474 if status in cancelled_status: 

475 solver_cancelled_count[solver] += 1 

476 elif status == SolverStatus.TIMEOUT: 

477 solver_timeout_count[solver] += 1 

478 

479 report.append(latex.AutoRef("tab:parallelportfoliotable")) 

480 report.append(pl.utils.bold(" ")) 

481 report.append(" shows the performance of the portfolio on the test set(s).") 

482 tabular = pl.Tabular("r|rrrr") 

483 tabular.add_row(["Solver", objective, "# Timeouts", "# Cancelled", "# Best"]) 

484 tabular.add_hline() 

485 solver_performance = {solver: round(performance, MAX_DEC) 

486 for solver, _, performance in 

487 scenario.get_solver_ranking(objective=objective)} 

488 for solver in scenario.solvers: 

489 tabular.add_row(solver, 

490 solver_performance[solver], 

491 solver_timeout_count[solver], 

492 solver_cancelled_count[solver], 

493 best_solver_count[solver]) 

494 tabular.add_hline() 

495 portfolio_performance = round( 

496 scenario.best_performance(objective=objective), MAX_DEC) 

497 tabular.add_row(portfolio_name, portfolio_performance, 

498 sum(solver_timeout_count.values()), 

499 sum(solver_cancelled_count.values()), 

500 sum(best_solver_count.values())) 

501 table_portfolio = pl.Table(position="h") 

502 table_portfolio.append(pl.UnsafeCommand("centering")) 

503 table_portfolio.append(tabular) 

504 table_portfolio.add_caption("Parallel Portfolio Performance") 

505 table_portfolio.append(pl.UnsafeCommand(r"label{tab:parallelportfoliotable}")) 

506 report.append(table_portfolio) 

507 

508 # 4. Create scatter plot analysis between the portfolio and the single best solver 

509 sbs_name = scenario.get_solver_ranking(objective=objective)[0][0] 

510 sbs_instance_performance = scenario.get_value( 

511 solver=sbs_name, objective=objective.name) 

512 sbs_name = Path(sbs_name).name 

513 report.append(latex.AutoRef("fig:portfoliovssbs")) 

514 report.append(pl.utils.bold(" ")) 

515 report.append(" shows the emprical comparison between the portfolio and the single " 

516 f"best solver (SBS) {sbs_name}.") 

517 portfolio_instance_performance = scenario.best_instance_performance( 

518 objective=objective.name).tolist() 

519 import pandas as pd 

520 df = pd.DataFrame( 

521 [sbs_instance_performance, portfolio_instance_performance], 

522 index=[f"SBS ({sbs_name}) Performance", "Portfolio Performance"], dtype=float).T 

523 plot = latex.comparison_plot(df, None) 

524 plot_path = plot_dir /\ 

525 f"sbs_{sbs_name}_vs_"\ 

526 f"parallel_portfolio.pdf" 

527 plot.write_image(plot_path, width=500, height=500) 

528 with report.create(pl.Figure(position="h")) as figure: 

529 figure.add_image(str(plot_path.relative_to(report_dir)), 

530 width=pl.utils.NoEscape(r"0.6\textwidth")) 

531 figure.add_caption( 

532 f"Portfolio vs SBS Performance ({objective})") 

533 figure.append(pl.UnsafeCommand( 

534 r"label{fig:portfoliovssbs}")) 

535 

536 

537def generate_appendix(report: pl.Document, 

538 performance_data: PerformanceDataFrame, 

539 feature_data: FeatureDataFrame) -> None: 

540 """Generate an appendix for the report.""" 

541 # report.append(pl.UnsafeCommand("appendix")) 

542 # report.append("Below are the full performance and feature data frames.") 

543 # TODO: Add long table for the entire performance data frame 

544 # performance_data.to_latex() # maybe? 

545 # TODO: Add long table for the entire feature data frame 

546 

547 

548def main(argv: list[str]) -> None: 

549 """Generate a report for executed experiments in the platform.""" 

550 # Log command call 

551 sl.log_command(sys.argv) 

552 

553 # Define command line arguments 

554 parser = parser_function() 

555 

556 # Process command line arguments 

557 args = parser.parse_args(argv) 

558 

559 performance_data = PerformanceDataFrame(gv.settings().DEFAULT_performance_data_path) 

560 feature_data = FeatureDataFrame(gv.settings().DEFAULT_feature_data_path) 

561 

562 # Fetch all known scenarios 

563 configuration_scenarios = gv.configuration_scenarios(refresh=True) 

564 selection_scenarios = gv.selection_scenarios(refresh=True) 

565 parallel_portfolio_scenarios = gv.parallel_portfolio_scenarios() 

566 

567 # Filter scenarios based on args 

568 if args.solvers: 

569 solvers = [resolve_object_name(s, 

570 gv.solver_nickname_mapping, 

571 gv.settings().DEFAULT_solver_dir, 

572 Solver) 

573 for s in args.solvers] 

574 configuration_scenarios = [ 

575 s for s in configuration_scenarios 

576 if s.solver.directory in [s.directory for s in solvers] 

577 ] 

578 selection_scenarios = [ 

579 s for s in selection_scenarios 

580 if set(s.solvers).intersection([str(s.directory) for s in solvers]) 

581 ] 

582 parallel_portfolio_scenarios = [ 

583 s for s in parallel_portfolio_scenarios 

584 if set(s.solvers).intersection([str(s.directory) for s in solvers]) 

585 ] 

586 if args.instance_sets: 

587 instance_sets = [resolve_object_name(s, 

588 gv.instance_set_nickname_mapping, 

589 gv.settings().DEFAULT_instance_dir, 

590 Instance_Set) 

591 for s in args.instance_sets] 

592 configuration_scenarios = [ 

593 s for s in configuration_scenarios 

594 if s.instance_set.directory in [s.directory for s in instance_sets] 

595 ] 

596 selection_scenarios = [ 

597 s for s in selection_scenarios 

598 if set(s.instance_sets).intersection([str(s.name) for s in instance_sets]) 

599 ] 

600 parallel_portfolio_scenarios = [ 

601 s for s in parallel_portfolio_scenarios 

602 if set(s.instance_sets).intersection([str(s.name) for s in instance_sets]) 

603 ] 

604 

605 processed_configuration_scenarios = [] 

606 processed_selection_scenarios = [] 

607 possible_test_sets = [Instance_Set(p) 

608 for p in gv.settings().DEFAULT_instance_dir.iterdir()] 

609 for configuration_scenario in configuration_scenarios: 

610 processed_configuration_scenarios.append( 

611 (ConfigurationOutput( 

612 configuration_scenario, performance_data, possible_test_sets), 

613 configuration_scenario)) 

614 for selection_scenario in selection_scenarios: 

615 processed_selection_scenarios.append( 

616 (SelectionOutput(selection_scenario, 

617 feature_data), selection_scenario)) 

618 

619 raw_output = gv.settings().DEFAULT_output_analysis / "JSON" 

620 if raw_output.exists(): # Clean 

621 shutil.rmtree(raw_output) 

622 raw_output.mkdir() 

623 

624 # Write JSON 

625 output_json = {} 

626 for output, configuration_scenario in processed_configuration_scenarios: 

627 output_json[configuration_scenario.name] = output.serialise() 

628 for output, selection_scenario in processed_selection_scenarios: 

629 output_json[selection_scenario.name] = output.serialise() 

630 # TODO: We do not have an output object for parallel portfolios 

631 

632 raw_output_json = raw_output / "output.json" 

633 with raw_output_json.open("w") as f: 

634 json.dump(output_json, f, indent=4) 

635 

636 print(f"Machine readable output written to: {raw_output_json}") 

637 

638 if args.only_json: # Done 

639 sys.exit(0) 

640 

641 # TODO: Group scenarios based on: 

642 # - Configuration / Selection / Parallel Portfolio 

643 # - Training Instance Set / Testing Instance Set 

644 # - Configurators can be merged as long as we can match their budgets clearly 

645 report_directory = gv.settings().DEFAULT_output_analysis / "report" 

646 if report_directory.exists(): # Clean it 

647 shutil.rmtree(report_directory) 

648 report_directory.mkdir() 

649 target_path = report_directory / "report" 

650 report = pl.document.Document( 

651 default_filepath=str(target_path), 

652 document_options=["british"]) 

653 bibpath = gv.settings().bibliography_path 

654 newbibpath = report_directory / "report.bib" 

655 shutil.copy(bibpath, newbibpath) 

656 # BUGFIX for unknown package load in PyLatex 

657 p = pl.package.Package("lastpage") 

658 if p in report.packages: 

659 report.packages.remove(p) 

660 report.packages.append(pl.package.Package("geometry", 

661 options=["verbose", 

662 "tmargin=3.5cm", 

663 "bmargin=3.5cm", 

664 "lmargin=3cm", 

665 "rmargin=3cm"])) 

666 # Unsafe command for \emph{Sparkle} 

667 report.preamble.extend([ 

668 pl.UnsafeCommand( 

669 "title", 

670 r"\emph{Sparkle} Algorithm Portfolio report"), 

671 pl.UnsafeCommand( 

672 "author", 

673 r"Generated by \emph{Sparkle} " 

674 f"(version: {__sparkle_version__})")]) 

675 report.append(pl.Command("maketitle")) 

676 report.append(pl.Section("Introduction")) 

677 # TODO: A quick overview to the introduction on whats considered in the report 

678 # regarding Solvers, Instance Sets and Feature Extractors 

679 report.append(pl.UnsafeCommand( 

680 r"emph{Sparkle}~\cite{Hoos15} is a multi-agent problem-solving platform based on" 

681 r" Programming by Optimisation (PbO)~\cite{Hoos12}, and would provide a number " 

682 "of effective algorithm optimisation techniques (such as automated algorithm " 

683 "configuration, portfolio-based algorithm selection, etc.) to accelerate the " 

684 "existing solvers.")) 

685 

686 for (scenario_output, scenario) in processed_configuration_scenarios: 

687 generate_configuration_section(report, scenario, scenario_output) 

688 

689 for (scenario_output, scenario) in processed_selection_scenarios: 

690 generate_selection_section(report, scenario, scenario_output) 

691 

692 for parallel_dataframe in parallel_portfolio_scenarios: 

693 generate_parallel_portfolio_section(report, parallel_dataframe) 

694 

695 generate_appendix(report, performance_data, feature_data) 

696 

697 # Adding bibliography 

698 report.append(pl.NewPage()) # Ensure it starts on new page 

699 report.append(pl.Command("bibliographystyle", arguments=["plain"])) 

700 report.append(pl.Command("bibliography", arguments=[str(newbibpath)])) 

701 # Generate the report .tex and .pdf 

702 report.generate_pdf(target_path, clean=False, clean_tex=False, compiler="pdflatex") 

703 # TODO: This should be done by PyLatex. Generate the bib and regenerate the report 

704 # Reference for the (terrible) solution: https://tex.stackexchange.com/ 

705 # questions/63852/question-mark-or-bold-citation-key-instead-of-citation-number 

706 import subprocess 

707 # Run BibTex silently 

708 subprocess.run(["bibtex", newbibpath.with_suffix("")], 

709 stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 

710 report.generate_pdf(target_path, clean=False, clean_tex=False, compiler="pdflatex") 

711 report.generate_pdf(target_path, clean=False, clean_tex=False, compiler="pdflatex") 

712 print(f"Report generated at {target_path}.pdf") 

713 sys.exit(0) 

714 

715 

716if __name__ == "__main__": 

717 main(sys.argv[1:])