Coverage for sparkle/CLI/help/reporting_scenario.py: 96%

202 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-11-05 14:48 +0000

1"""Helper module to manage Sparkle scenarios.""" 

2# Keep in CLI help 

3 

4from __future__ import annotations 

5import configparser 

6from enum import Enum 

7from pathlib import Path 

8from pathlib import PurePath 

9 

10from sparkle.configurator.configurator import ConfigurationScenario 

11from sparkle.solver import Solver 

12from sparkle.instance import Instance_Set, InstanceSet 

13 

14 

15class Scenario(str, Enum): 

16 """Enum of possible execution scenarios for Sparkle.""" 

17 

18 NONE = "NONE" 

19 SELECTION = "SELECTION" 

20 CONFIGURATION = "CONFIGURATION" 

21 PARALLEL_PORTFOLIO = "PARALLEL_PORTFOLIO" 

22 

23 

24class ReportingScenario: 

25 """Class to manage scenarios executed with Sparkle.""" 

26 

27 # ReportingScenario path names and defaults 

28 __reporting_scenario_file = Path("latest_scenario.ini") 

29 __reporting_scenario_dir = Path("Output") 

30 DEFAULT_reporting_scenario_path = Path( 

31 PurePath(__reporting_scenario_dir / __reporting_scenario_file)) 

32 

33 # Constant default values 

34 DEFAULT_latest_scenario = Scenario.NONE 

35 

36 DEFAULT_selection_portfolio_path = Path("") 

37 DEFAULT_selection_test_case_directory = Path("") 

38 

39 DEFAULT_parallel_portfolio_path = Path("") 

40 DEFAULT_parallel_portfolio_instance_list = [] 

41 

42 DEFAULT_config_solver = Path("") 

43 DEFAULT_config_instance_set_train = Path("") 

44 DEFAULT_config_instance_set_test = Path("") 

45 

46 def __init__(self: ReportingScenario) -> None: 

47 """Initialise a ReportingScenario object.""" 

48 # ReportingScenario 'dictionary' in configparser format 

49 self.__scenario = configparser.ConfigParser() 

50 

51 # Initialise scenario in default file path 

52 self.read_scenario_ini() 

53 

54 return 

55 

56 def read_scenario_ini( 

57 self: ReportingScenario, file_path: Path = DEFAULT_reporting_scenario_path)\ 

58 -> None: 

59 """Read the scenario from an INI file. 

60 

61 Args: 

62 file_path: Path of the INI file for the scenario. Defaults to 

63 DEFAULT_reporting_scenario_path. 

64 """ 

65 # If the file does not exist set default values 

66 if not Path(file_path).is_file(): 

67 self.set_latest_scenario() 

68 self.set_selection_scenario_path() 

69 self.set_selection_test_case_directory() 

70 self.set_parallel_portfolio_path() 

71 self.set_parallel_portfolio_instance_path() 

72 self.set_config_solver() 

73 self.set_config_instance_set_train() 

74 self.set_config_instance_set_test() 

75 

76 # Read file 

77 file_scenario = configparser.ConfigParser() 

78 file_scenario.read(file_path) 

79 

80 # Set internal scenario based on data read from FILE if they were read 

81 # successfully 

82 if file_scenario.sections() != []: 

83 section = "latest" 

84 option_names = ("scenario",) # Comma to make it a tuple 

85 for option in option_names: 

86 if file_scenario.has_option(section, option): 

87 value = Scenario(file_scenario.get(section, option)) 

88 self.set_latest_scenario(value) 

89 file_scenario.remove_option(section, option) 

90 

91 section = "selection" 

92 option_names = ("scenario_path",) # Comma to make it a tuple 

93 for option in option_names: 

94 if file_scenario.has_option(section, option): 

95 value = Path(file_scenario.get(section, option)) 

96 self.set_selection_scenario_path(value) 

97 file_scenario.remove_option(section, option) 

98 

99 option_names = ("test_case_directory",) # Comma to make it a tuple 

100 for option in option_names: 

101 if file_scenario.has_option(section, option): 

102 value = Path(file_scenario.get(section, option)) 

103 self.set_selection_test_case_directory(value) 

104 file_scenario.remove_option(section, option) 

105 

106 section = "configuration" 

107 option_names = ("solver",) # Comma to make it a tuple 

108 for option in option_names: 

109 if file_scenario.has_option(section, option): 

110 value = Path(file_scenario.get(section, option)) 

111 self.set_config_solver(value) 

112 file_scenario.remove_option(section, option) 

113 

114 option_names = ("instance_set_train",) # Comma to make it a tuple 

115 for option in option_names: 

116 if file_scenario.has_option(section, option): 

117 value = Path(file_scenario.get(section, option)) 

118 self.set_config_instance_set_train(value) 

119 file_scenario.remove_option(section, option) 

120 

121 option_names = ("instance_set_test",) # Comma to make it a tuple 

122 for option in option_names: 

123 if file_scenario.has_option(section, option): 

124 value = Path(file_scenario.get(section, option)) 

125 self.set_config_instance_set_test(value) 

126 file_scenario.remove_option(section, option) 

127 

128 option_names = ("scenario_file_path", ) 

129 for option in option_names: 

130 if file_scenario.has_option(section, option): 

131 value = Path(file_scenario.get(section, option)) 

132 self.set_configuration_scenario(value) 

133 file_scenario.remove_option(section, option) 

134 

135 section = "parallel_portfolio" 

136 option_names = ("portfolio_path",) # Comma to make it a tuple 

137 for option in option_names: 

138 if file_scenario.has_option(section, option): 

139 value = Path(file_scenario.get(section, option)) 

140 self.set_parallel_portfolio_path(value) 

141 file_scenario.remove_option(section, option) 

142 

143 option_names = ("instance_path",) # Comma to make it a tuple 

144 for option in option_names: 

145 if file_scenario.has_option(section, option): 

146 value = file_scenario.get(section, option) 

147 self.set_parallel_portfolio_instance_path(value) 

148 file_scenario.remove_option(section, option) 

149 

150 # Report on any unknown settings that were read 

151 sections = file_scenario.sections() 

152 

153 for section in sections: 

154 for option in file_scenario[section]: 

155 print(f'Unrecognised section - option combination:"{section} ' 

156 f'{option}" in file {file_path} ignored') 

157 

158 # Print error if unable to read the scenario file 

159 elif not file_path.exists(): 

160 print(f"WARNING: Failed to read latest scenario from {file_path}. " 

161 "Default values will be used.") 

162 

163 def write_scenario_ini( 

164 self: ReportingScenario, file_path: Path = DEFAULT_reporting_scenario_path)\ 

165 -> None: 

166 """Write the scenario file in INI format. 

167 

168 Args: 

169 file_path: Path of the INI file for the scenario. Defaults to 

170 DEFAULT_reporting_scenario_path. 

171 """ 

172 # Create needed directories if they don't exist 

173 file_dir = file_path.parents[0] 

174 file_dir.mkdir(parents=True, exist_ok=True) 

175 

176 # Write the scenario to file 

177 with Path(str(file_path)).open("w") as scenario_file: 

178 self.__scenario.write(scenario_file) 

179 

180 def __init_section(self: ReportingScenario, section: str) -> None: 

181 """Initialise a section in the scenario file. 

182 

183 Args: 

184 section: Name of the section. 

185 """ 

186 if section not in self.__scenario: 

187 self.__scenario[section] = {} 

188 

189 # Generic setters ### 

190 

191 def path_setter(self: ReportingScenario, section: str, name: str, value: Path)\ 

192 -> None: 

193 """Set a generic Path for the scenario. 

194 

195 Args: 

196 section: Name of the section. 

197 name: Name of the path element. 

198 value: Value of the path given. 

199 """ 

200 if value is not None: 

201 self.__init_section(section) 

202 self.__scenario[section][name] = str(value) 

203 

204 # Generic getters ### 

205 

206 def none_if_empty_path(self: ReportingScenario, path: Path) -> Path: 

207 """Return None if a path is empty or the Path otherwise. 

208 

209 Args: 

210 path: Path value given. 

211 

212 Returns: 

213 None if the given path is empty, the given Path value otherwise. 

214 """ 

215 if str(path) == "" or str(path) == ".": 

216 return None 

217 return path 

218 

219 # Latest settings ### 

220 

221 def set_latest_scenario(self: ReportingScenario, 

222 value: Scenario = DEFAULT_latest_scenario) -> None: 

223 """Set the latest Scenario that was executed.""" 

224 section = "latest" 

225 name = "scenario" 

226 

227 if value is not None: 

228 self.__init_section(section) 

229 self.__scenario[section][name] = value.name 

230 

231 def get_latest_scenario(self: ReportingScenario) -> Scenario: 

232 """Return the latest Scenario that was executed.""" 

233 return Scenario(self.__scenario["latest"]["scenario"]) 

234 

235 # Selection settings ### 

236 

237 def set_selection_scenario_path( 

238 self: ReportingScenario, value: Path = DEFAULT_selection_portfolio_path)\ 

239 -> None: 

240 """Set the path to portfolio selector used for algorithm selection.""" 

241 section = "selection" 

242 name = "scenario_path" 

243 self.path_setter(section, name, value) 

244 

245 def get_selection_scenario_path(self: ReportingScenario) -> Path: 

246 """Return the path to portfolio selector used for algorithm selection.""" 

247 return Path(self.__scenario["selection"]["scenario_path"]) 

248 

249 def set_selection_test_case_directory( 

250 self: ReportingScenario, 

251 value: Path = DEFAULT_selection_test_case_directory) -> None: 

252 """Set the path to the testing set that was used for algorithm selection.""" 

253 section = "selection" 

254 name = "test_case_directory" 

255 self.path_setter(section, name, value) 

256 

257 def get_selection_test_case_directory(self: ReportingScenario) -> str: 

258 """Return the path to the testing set that was used for algorithm selection.""" 

259 try: 

260 path = self.__scenario["selection"]["test_case_directory"] 

261 if Path(path) == Path("."): 

262 path = None 

263 except KeyError: 

264 path = None 

265 return path 

266 

267 # Parallel portfolio settings ### 

268 

269 def set_parallel_portfolio_path( 

270 self: ReportingScenario, 

271 value: Path = DEFAULT_parallel_portfolio_path) -> None: 

272 """Set the path to the parallel portfolio.""" 

273 section = "parallel_portfolio" 

274 name = "portfolio_path" 

275 self.path_setter(section, name, value) 

276 

277 def get_parallel_portfolio_path(self: ReportingScenario) -> Path: 

278 """Return the path to the parallel portfolio.""" 

279 return Path(self.__scenario["parallel_portfolio"]["portfolio_path"]) 

280 

281 def set_parallel_portfolio_instance_path( 

282 self: ReportingScenario, 

283 value: Path = None) -> None: 

284 """Set the instance path used with the parallel portfolio.""" 

285 section = "parallel_portfolio" 

286 name = "instance_path" 

287 self.path_setter(section, name, value) 

288 

289 def get_parallel_portfolio_instance_set(self: ReportingScenario) -> InstanceSet: 

290 """Return the instance list used with the parallel portfolio. 

291 

292 If instance list is empty return None. 

293 """ 

294 if self.__scenario["parallel_portfolio"]["instance_path"] is None: 

295 return None 

296 return Instance_Set(Path(self.__scenario["parallel_portfolio"]["instance_path"])) 

297 

298 # Configuration settings ### 

299 

300 def set_config_solver(self: ReportingScenario, 

301 value: Solver | Path = DEFAULT_config_solver) -> None: 

302 """Set the path to the solver that was configured.""" 

303 section = "configuration" 

304 name = "solver" 

305 if isinstance(value, Solver): 

306 value = value.directory 

307 self.path_setter(section, name, value) 

308 

309 def get_config_solver(self: ReportingScenario) -> Solver: 

310 """Return the path to the solver that was configured.""" 

311 path = self.none_if_empty_path(Path(self.__scenario["configuration"]["solver"])) 

312 if path is not None: 

313 return Solver(path) 

314 return None 

315 

316 def set_config_instance_set_train( 

317 self: ReportingScenario, value: Path = DEFAULT_config_instance_set_train)\ 

318 -> None: 

319 """Set the path to the training instance set used for configuration.""" 

320 section = "configuration" 

321 name = "instance_set_train" 

322 self.path_setter(section, name, value) 

323 

324 def get_config_instance_set_train(self: ReportingScenario) -> InstanceSet: 

325 """Return the path to the training instance set used for configuration.""" 

326 path = self.none_if_empty_path( 

327 Path(self.__scenario["configuration"]["instance_set_train"])) 

328 if path is None: 

329 return None 

330 return Instance_Set(path) 

331 

332 def set_configuration_scenario(self: ReportingScenario, 

333 value: Scenario | Path) -> None: 

334 """Set the path to the scenario that was used for configuration.""" 

335 section = "configuration" 

336 name = "scenario_file_path" 

337 if isinstance(value, ConfigurationScenario): 

338 value = value.scenario_file_path 

339 self.path_setter(section, name, value) 

340 

341 def get_configuration_scenario(self: ReportingScenario, 

342 scenario_class: ConfigurationScenario) -> Scenario: 

343 """Return the path to the scenario that was used for configuration.""" 

344 path = self.none_if_empty_path( 

345 Path(self.__scenario["configuration"]["scenario_file_path"])) 

346 if path is None: 

347 return None 

348 return scenario_class.from_file(path) 

349 

350 def set_config_instance_set_test( 

351 self: ReportingScenario, value: Path = DEFAULT_config_instance_set_test)\ 

352 -> None: 

353 """Set the path to the testing instance set used for configuration.""" 

354 section = "configuration" 

355 name = "instance_set_test" 

356 self.path_setter(section, name, value) 

357 

358 def get_config_instance_set_test(self: ReportingScenario) -> InstanceSet: 

359 """Return the path to the testing instance set used for configuration.""" 

360 path = self.none_if_empty_path( 

361 Path(self.__scenario["configuration"]["instance_set_test"])) 

362 if path is None: 

363 return None 

364 return Instance_Set(path)