Coverage for src / sparkle / solver / solver_cli.py: 71%

80 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-21 15:31 +0000

1#!/usr/bin/env python3 

2# -*- coding: UTF-8 -*- 

3"""Run a solver, read/write to performance dataframe.""" 

4 

5import sys 

6import ast 

7from filelock import FileLock 

8import argparse 

9from pathlib import Path 

10import random 

11import time 

12 

13from runrunner import Runner 

14 

15from sparkle.solver import Solver 

16from sparkle.types import resolve_objective 

17from sparkle.structures import PerformanceDataFrame 

18from sparkle.tools.solver_wrapper_parsing import parse_commandline_dict 

19 

20 

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

22 """Main function of the command.""" 

23 # Define command line arguments 

24 parser = argparse.ArgumentParser() 

25 parser.add_argument( 

26 "--performance-dataframe", 

27 required=True, 

28 type=Path, 

29 help="path to the performance dataframe", 

30 ) 

31 parser.add_argument("--solver", required=True, type=Path, help="path to solver") 

32 parser.add_argument( 

33 "--instance", 

34 required=True, 

35 type=Path, 

36 nargs="+", 

37 help="path to instance to run on", 

38 ) 

39 parser.add_argument( 

40 "--run-index", 

41 required=True, 

42 type=int, 

43 help="run index in the dataframe to set.", 

44 ) 

45 parser.add_argument( 

46 "--log-dir", type=Path, required=True, help="path to the log directory" 

47 ) 

48 

49 # These two arguments should be mutually exclusive 

50 parser.add_argument( 

51 "--configuration-id", 

52 type=str, 

53 required=False, 

54 help="configuration id to read from the PerformanceDataFrame.", 

55 ) 

56 parser.add_argument( 

57 "--configuration", 

58 type=str, 

59 nargs="+", 

60 required=False, 

61 help="configuration for the solver", 

62 ) 

63 

64 parser.add_argument( 

65 "--seed", 

66 type=int, 

67 required=False, 

68 help="seed to use for the solver. If not provided, generates one.", 

69 ) 

70 parser.add_argument( 

71 "--cutoff-time", 

72 type=int, 

73 required=False, 

74 help="the cutoff time for the solver.", 

75 ) 

76 parser.add_argument( 

77 "--objectives", 

78 type=str, 

79 required=False, 

80 nargs="+", 

81 help="The objectives to evaluate to Solver on. If not provided, read from the PerformanceDataFrame.", 

82 ) 

83 parser.add_argument( 

84 "--target-objective", 

85 required=False, 

86 type=str, 

87 help="The objective to use to determine the best configuration.", 

88 ) 

89 parser.add_argument( 

90 "--best-configuration-instances", 

91 required=False, 

92 type=str, 

93 nargs="+", 

94 help="If given, will ignore any given configurations, and try to" 

95 " determine the best found configurations over the given " 

96 "instances. Uses the 'target-objective' given in the arguments" 

97 " or the first one given by the dataframe to determine the best" 

98 "configuration.", 

99 ) 

100 args = parser.parse_args(argv) 

101 # Process command line arguments 

102 log_dir = args.log_dir 

103 print(f"Running Solver and read/writing results with {args.performance_dataframe}") 

104 # Resolve possible multi-file instance 

105 instance_path: list[Path] = args.instance 

106 # If instance is only one file then we don't need a list 

107 instance_path = instance_path[0] if len(instance_path) == 1 else instance_path 

108 instance_name = ( 

109 instance_path.stem if isinstance(instance_path, Path) else instance_path[0].stem 

110 ) 

111 run_index = args.run_index 

112 # Ensure stringifcation of path objects 

113 if isinstance(instance_path, list): 

114 # Double list because of solver.run 

115 run_instances = [[str(filepath) for filepath in instance_path]] 

116 else: 

117 run_instances = str(instance_path) 

118 

119 solver = Solver(args.solver) 

120 # By default, run the default configuration 

121 config_id = PerformanceDataFrame.default_configuration 

122 configuration = None 

123 # If no seed is provided by CLI, generate one 

124 seed = args.seed if args.seed else random.randint(0, 2**32 - 1) 

125 # Parse the provided objectives if present 

126 objectives = ( 

127 [resolve_objective(o) for o in args.objectives] if args.objectives else None 

128 ) 

129 

130 if args.configuration: # Configuration provided, override 

131 if isinstance(args.configuration, list): 

132 configuration = parse_commandline_dict(args.configuration) 

133 else: 

134 configuration = ast.literal_eval(args.configuration) 

135 print(configuration) 

136 config_id = configuration["configuration_id"] 

137 elif ( 

138 ( 

139 args.configuration_id 

140 and args.configuration_id != PerformanceDataFrame.default_configuration 

141 ) 

142 or args.best_configuration_instances 

143 or not objectives 

144 ): # Read from PerformanceDataFrame, can be slow 

145 # Desyncronize from other possible jobs writing to the same file 

146 print( 

147 "Reading from Performance DataFrame.. " 

148 f"[{'configuration' if (args.configuration_id or args.best_configuration_instances) else ''} " 

149 f"{'objectives' if not objectives else ''}]" 

150 ) 

151 time.sleep(random.random() * 10) 

152 lock = FileLock(f"{args.performance_dataframe}.lock") # Lock the file 

153 with lock.acquire(timeout=600): 

154 performance_dataframe = PerformanceDataFrame(args.performance_dataframe) 

155 

156 if not objectives: 

157 objectives = performance_dataframe.objectives 

158 

159 if args.best_configuration_instances: # Determine best configuration 

160 best_configuration_instances: list[str] = args.best_configuration_instances 

161 # Get the unique instance names 

162 best_configuration_instances = list( 

163 set([Path(instance).stem for instance in best_configuration_instances]) 

164 ) 

165 target_objective = ( 

166 resolve_objective(args.target_objective) 

167 if args.target_objective 

168 else objectives[0] 

169 ) 

170 config_id, _ = performance_dataframe.best_configuration( 

171 solver=str(args.solver), 

172 objective=target_objective, 

173 instances=best_configuration_instances, 

174 ) 

175 configuration = performance_dataframe.get_full_configuration( 

176 str(args.solver), config_id 

177 ) 

178 

179 elif ( 

180 args.configuration_id 

181 ): # Read from PerformanceDataFrame the configuration using the ID 

182 config_id = args.configuration_id 

183 configuration = performance_dataframe.get_full_configuration( 

184 str(args.solver), config_id 

185 ) 

186 

187 print(f"Running Solver {solver} on instance {instance_name} with seed {seed}..") 

188 solver_output = solver.run( 

189 run_instances, 

190 objectives=objectives, 

191 seed=seed, 

192 configuration=configuration.copy() if configuration else None, 

193 cutoff_time=args.cutoff_time, 

194 log_dir=log_dir, 

195 run_on=Runner.LOCAL, 

196 ) 

197 

198 # Prepare the results for the DataFrame for each objective 

199 result = [ 

200 [solver_output[objective.name] for objective in objectives], 

201 [seed] * len(objectives), 

202 ] 

203 solver_fields = [ 

204 PerformanceDataFrame.column_value, 

205 PerformanceDataFrame.column_seed, 

206 ] 

207 

208 print(f"For Solver/config: {solver}/{config_id}") 

209 print(f"For index: Instance {instance_name}, Run {args.run_index}, Seed {seed}") 

210 print("Appending the following objective values:") # {', '.join(objective_values)}") 

211 for objective in objectives: 

212 print( 

213 f"{objective.name}, {instance_name}, {args.run_index} | {args.solver}, {config_id}: {solver_output[objective.name]}" 

214 ) 

215 

216 # Desyncronize from other possible jobs writing to the same file 

217 time.sleep(random.random() * 100) 

218 

219 # Now that we have all the results, we can add them to the performance dataframe 

220 lock = FileLock(f"{args.performance_dataframe}.lock") # Lock the file 

221 with lock.acquire(timeout=600): 

222 performance_dataframe = PerformanceDataFrame(args.performance_dataframe) 

223 performance_dataframe.set_value( 

224 result, 

225 solver=str(args.solver), 

226 instance=instance_name, 

227 configuration=config_id, 

228 objective=[o.name for o in objectives], 

229 run=run_index, 

230 solver_fields=solver_fields, 

231 append_write_csv=True, # We do not have to save the PDF here, thanks to this argument 

232 ) 

233 

234 

235if __name__ == "__main__": 

236 main(sys.argv[1:])