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

184 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-09-27 09:10 +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 

9from sparkle.solver import Solver 

10from sparkle.instance import instance_set, InstanceSet 

11 

12 

13class Scenario(str, Enum): 

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

15 

16 NONE = "NONE" 

17 SELECTION = "SELECTION" 

18 CONFIGURATION = "CONFIGURATION" 

19 PARALLEL_PORTFOLIO = "PARALLEL_PORTFOLIO" 

20 

21 

22class ReportingScenario: 

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

24 

25 # ReportingScenario path names and defaults 

26 __reporting_scenario_file = Path("latest_scenario.ini") 

27 __reporting_scenario_dir = Path("Output") 

28 DEFAULT_reporting_scenario_path = Path( 

29 PurePath(__reporting_scenario_dir / __reporting_scenario_file)) 

30 

31 # Constant default values 

32 DEFAULT_latest_scenario = Scenario.NONE 

33 

34 DEFAULT_selection_portfolio_path = Path("") 

35 DEFAULT_selection_test_case_directory = Path("") 

36 

37 DEFAULT_parallel_portfolio_path = Path("") 

38 DEFAULT_parallel_portfolio_instance_list = [] 

39 

40 DEFAULT_config_solver = Path("") 

41 DEFAULT_config_instance_set_train = Path("") 

42 DEFAULT_config_instance_set_test = Path("") 

43 

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

45 """Initialise a ReportingScenario object.""" 

46 # ReportingScenario 'dictionary' in configparser format 

47 self.__scenario = configparser.ConfigParser() 

48 

49 # Initialise scenario in default file path 

50 self.read_scenario_ini() 

51 

52 return 

53 

54 def read_scenario_ini( 

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

56 -> None: 

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

58 

59 Args: 

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

61 DEFAULT_reporting_scenario_path. 

62 """ 

63 # If the file does not exist set default values 

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

65 self.set_latest_scenario() 

66 self.set_selection_scenario_path() 

67 self.set_selection_test_case_directory() 

68 self.set_parallel_portfolio_path() 

69 self.set_parallel_portfolio_instance_path() 

70 self.set_config_solver() 

71 self.set_config_instance_set_train() 

72 self.set_config_instance_set_test() 

73 

74 # Read file 

75 file_scenario = configparser.ConfigParser() 

76 file_scenario.read(str(file_path)) 

77 

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

79 # successfully 

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

81 section = "latest" 

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

83 for option in option_names: 

84 if file_scenario.has_option(section, option): 

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

86 self.set_latest_scenario(value) 

87 file_scenario.remove_option(section, option) 

88 

89 section = "selection" 

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

91 for option in option_names: 

92 if file_scenario.has_option(section, option): 

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

94 self.set_selection_scenario_path(value) 

95 file_scenario.remove_option(section, option) 

96 

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

98 for option in option_names: 

99 if file_scenario.has_option(section, option): 

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

101 self.set_selection_test_case_directory(value) 

102 file_scenario.remove_option(section, option) 

103 

104 section = "configuration" 

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

106 for option in option_names: 

107 if file_scenario.has_option(section, option): 

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

109 self.set_config_solver(value) 

110 file_scenario.remove_option(section, option) 

111 

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

113 for option in option_names: 

114 if file_scenario.has_option(section, option): 

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

116 self.set_config_instance_set_train(value) 

117 file_scenario.remove_option(section, option) 

118 

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

120 for option in option_names: 

121 if file_scenario.has_option(section, option): 

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

123 self.set_config_instance_set_test(value) 

124 file_scenario.remove_option(section, option) 

125 

126 section = "parallel_portfolio" 

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

128 for option in option_names: 

129 if file_scenario.has_option(section, option): 

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

131 self.set_parallel_portfolio_path(value) 

132 file_scenario.remove_option(section, option) 

133 

134 section = "parallel_portfolio" 

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

136 for option in option_names: 

137 if file_scenario.has_option(section, option): 

138 value = file_scenario.get(section, option) 

139 self.set_parallel_portfolio_instance_path(value) 

140 file_scenario.remove_option(section, option) 

141 

142 # Report on any unknown settings that were read 

143 sections = file_scenario.sections() 

144 

145 for section in sections: 

146 for option in file_scenario[section]: 

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

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

149 

150 # Print error if unable to read the scenario file 

151 else: 

152 print(f"WARNING: Failed to read latest scenario from {file_path} The " 

153 "file may have been empty, or is in another format than INI. Default " 

154 "values will be used.") 

155 

156 def write_scenario_ini( 

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

158 -> None: 

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

160 

161 Args: 

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

163 DEFAULT_reporting_scenario_path. 

164 """ 

165 # Create needed directories if they don't exist 

166 file_dir = file_path.parents[0] 

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

168 

169 # Write the scenario to file 

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

171 self.__scenario.write(scenario_file) 

172 

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

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

175 

176 Args: 

177 section: Name of the section. 

178 """ 

179 if section not in self.__scenario: 

180 self.__scenario[section] = {} 

181 

182 # Generic setters ### 

183 

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

185 -> None: 

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

187 

188 Args: 

189 section: Name of the section. 

190 name: Name of the path element. 

191 value: Value of the path given. 

192 """ 

193 if value is not None: 

194 self.__init_section(section) 

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

196 

197 # Generic getters ### 

198 

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

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

201 

202 Args: 

203 path: Path value given. 

204 

205 Returns: 

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

207 """ 

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

209 return None 

210 return path 

211 

212 # Latest settings ### 

213 

214 def set_latest_scenario(self: ReportingScenario, 

215 value: Scenario = DEFAULT_latest_scenario) -> None: 

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

217 section = "latest" 

218 name = "scenario" 

219 

220 if value is not None: 

221 self.__init_section(section) 

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

223 

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

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

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

227 

228 # Selection settings ### 

229 

230 def set_selection_scenario_path( 

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

232 -> None: 

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

234 section = "selection" 

235 name = "scenario_path" 

236 self.path_setter(section, name, value) 

237 

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

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

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

241 

242 def set_selection_test_case_directory( 

243 self: ReportingScenario, 

244 value: Path = DEFAULT_selection_test_case_directory) -> None: 

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

246 section = "selection" 

247 name = "test_case_directory" 

248 self.path_setter(section, name, value) 

249 

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

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

252 try: 

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

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

255 path = None 

256 except KeyError: 

257 path = None 

258 return path 

259 

260 # Parallel portfolio settings ### 

261 

262 def set_parallel_portfolio_path( 

263 self: ReportingScenario, 

264 value: Path = DEFAULT_parallel_portfolio_path) -> None: 

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

266 section = "parallel_portfolio" 

267 name = "portfolio_path" 

268 self.path_setter(section, name, value) 

269 

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

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

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

273 

274 def set_parallel_portfolio_instance_path( 

275 self: ReportingScenario, 

276 value: Path = None) -> None: 

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

278 section = "parallel_portfolio" 

279 name = "instance_path" 

280 self.path_setter(section, name, value) 

281 

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

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

284 

285 If instance list is empty return None. 

286 """ 

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

288 return None 

289 return instance_set(Path(self.__scenario["parallel_portfolio"]["instance_path"])) 

290 

291 # Configuration settings ### 

292 

293 def set_config_solver(self: ReportingScenario, 

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

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

296 section = "configuration" 

297 name = "solver" 

298 if isinstance(value, Solver): 

299 value = value.directory 

300 self.path_setter(section, name, value) 

301 

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

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

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

305 if path is not None: 

306 return Solver(path) 

307 return None 

308 

309 def set_config_instance_set_train( 

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

311 -> None: 

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

313 section = "configuration" 

314 name = "instance_set_train" 

315 self.path_setter(section, name, value) 

316 

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

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

319 path = self.none_if_empty_path( 

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

321 if path is None: 

322 return None 

323 return instance_set(path) 

324 

325 def set_config_instance_set_test( 

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

327 -> None: 

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

329 section = "configuration" 

330 name = "instance_set_test" 

331 self.path_setter(section, name, value) 

332 

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

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

335 path = self.none_if_empty_path( 

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

337 if path is None: 

338 return None 

339 return instance_set(path)