Coverage for sparkle/CLI/add_solver.py: 81%

94 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 add a solver to the Sparkle platform.""" 

3import os 

4import stat 

5import sys 

6import argparse 

7import shutil 

8from pathlib import Path 

9 

10from sparkle.tools.parameters import PCSConvention 

11 

12from sparkle.platform import file_help as sfh 

13from sparkle.CLI.help import global_variables as gv 

14from sparkle.structures import PerformanceDataFrame 

15from sparkle.solver import Solver, verifiers 

16from sparkle.CLI.help import logging as sl 

17from sparkle.CLI.initialise import check_for_initialise 

18from sparkle.CLI.help import argparse_custom as ac 

19 

20 

21def parser_function() -> argparse.ArgumentParser: 

22 """Define the command line arguments.""" 

23 parser = argparse.ArgumentParser( 

24 description="Add a solver to the Sparkle platform.") 

25 parser.add_argument(*ac.DeterministicArgument.names, 

26 **ac.DeterministicArgument.kwargs) 

27 parser.add_argument(*ac.SolutionVerifierArgument.names, 

28 **ac.SolutionVerifierArgument.kwargs) 

29 parser.add_argument(*ac.NicknameSolverArgument.names, 

30 **ac.NicknameSolverArgument.kwargs) 

31 parser.add_argument(*ac.SolverPathArgument.names, **ac.SolverPathArgument.kwargs) 

32 parser.add_argument(*ac.SkipChecksArgument.names, **ac.SkipChecksArgument.kwargs) 

33 parser.add_argument(*ac.NoCopyArgument.names, **ac.NoCopyArgument.kwargs) 

34 return parser 

35 

36 

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

38 """Main function of the command.""" 

39 # Log command call 

40 sl.log_command(sys.argv) 

41 check_for_initialise() 

42 

43 # Define command line arguments 

44 parser = parser_function() 

45 

46 # Process command line arguments 

47 args = parser.parse_args(argv) 

48 solver_source = Path(args.solver_path) 

49 deterministic = args.deterministic 

50 solution_verifier = args.solution_verifier 

51 

52 if not solver_source.exists(): 

53 print(f'Solver path "{solver_source}" does not exist!') 

54 sys.exit(-1) 

55 

56 # Make sure it is pointing to the verifiers module 

57 if solution_verifier: 

58 if Path(solution_verifier).is_file(): # File verifier 

59 solution_verifier = (verifiers.SolutionFileVerifier.__name__, 

60 solution_verifier) 

61 elif solution_verifier not in verifiers.mapping: 

62 print(f"ERROR: Unknown solution verifier {solution_verifier}!") 

63 sys.exit(-1) 

64 

65 nickname = args.nickname 

66 

67 if args.run_checks: 

68 print("Running checks...") 

69 solver = Solver(Path(solver_source)) 

70 if solver.pcs_file is None: 

71 print("None or multiple .pcs files found. Solver " 

72 "is not valid for configuration.") 

73 else: 

74 print(f"PCS file detected: {solver.pcs_file.name}. ", end="") 

75 if solver.read_pcs_file(): 

76 print("Can read the pcs file.") 

77 else: 

78 print("WARNING: Can not read the provided pcs file format.") 

79 

80 wrapper_path = solver.directory / solver.wrapper 

81 if not wrapper_path.is_file(): 

82 print(f"ERROR: Solver {solver_source.name} does not have a solver wrapper " 

83 f"(Missing file {solver.wrapper}).") 

84 sys.exit(-1) 

85 elif not os.access(wrapper_path, os.X_OK): 

86 print(f"ERROR: Solver {solver_source.name} wrapper file {solver.wrapper} " 

87 f" does not have execution rights set!") 

88 sys.exit(-1) 

89 

90 # Start add solver 

91 solver_directory = gv.settings().DEFAULT_solver_dir / solver_source.name 

92 if solver_directory.exists(): 

93 print(f"ERROR: Solver {solver_source.name} already exists! " 

94 "Can not add new solver.") 

95 sys.exit(-1) 

96 if args.no_copy: 

97 print(f"Creating symbolic link from {solver_source} " 

98 f"to {solver_directory}...") 

99 if not os.access(solver_source, os.W_OK): 

100 raise PermissionError("Sparkle does not have the right to write to the " 

101 "destination folder.") 

102 solver_directory.symlink_to(solver_source.absolute()) 

103 else: 

104 print(f"Copying {solver_source.name} to platform...") 

105 solver_directory.mkdir(parents=True) 

106 shutil.copytree(solver_source, solver_directory, dirs_exist_ok=True) 

107 

108 # Save the deterministic bool in the solver 

109 with (solver_directory / Solver.meta_data).open("w+") as fout: 

110 fout.write(str({"deterministic": deterministic, 

111 "verifier": solution_verifier})) 

112 

113 # Add RunSolver executable to the solver 

114 runsolver_path = gv.settings().DEFAULT_runsolver_exec 

115 if runsolver_path.name in [file.name for file in solver_directory.iterdir()]: 

116 print("Warning! RunSolver executable detected in Solver " 

117 f"{solver_source.name}. This will be replaced with " 

118 f"Sparkle's version of RunSolver. ({runsolver_path})") 

119 runsolver_target = solver_directory / runsolver_path.name 

120 shutil.copyfile(runsolver_path, runsolver_target) 

121 runsolver_target.chmod(os.stat(runsolver_target).st_mode | stat.S_IEXEC) 

122 

123 performance_data = PerformanceDataFrame( 

124 gv.settings().DEFAULT_performance_data_path, 

125 objectives=gv.settings().get_general_sparkle_objectives()) 

126 performance_data.add_solver(str(solver_directory)) 

127 performance_data.save_csv() 

128 

129 print(f"Adding solver {solver_source.name} done!") 

130 

131 if nickname is not None: 

132 sfh.add_remove_platform_item( 

133 solver_directory, 

134 gv.solver_nickname_list_path, 

135 gv.file_storage_data_mapping[gv.solver_nickname_list_path], 

136 key=nickname) 

137 

138 solver = Solver(solver_directory) # Recreate solver from its new directory 

139 if solver.pcs_file is not None: 

140 # Generate missing PCS files 

141 # TODO: Only generate missing files 

142 print("Generating missing PCS files...") 

143 solver.port_pcs(PCSConvention.IRACE) # Create PCS file for IRACE 

144 print("Generating IRACE done!") 

145 solver.port_pcs(PCSConvention.ParamILS) # Create PCS file for ParamILS 

146 print("Generating ParamILS done!") 

147 

148 # Write used settings to file 

149 gv.settings().write_used_settings() 

150 sys.exit(0) 

151 

152 

153if __name__ == "__main__": 

154 main(sys.argv[1:])