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

81 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-08 12:00 +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.instance import resolve_instance_pair 

17from sparkle.types import resolve_objective 

18from sparkle.structures import PerformanceDataFrame 

19from sparkle.tools.solver_wrapper_parsing import parse_commandline_dict 

20 

21 

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

23 """Main function of the command.""" 

24 # Define command line arguments 

25 parser = argparse.ArgumentParser() 

26 parser.add_argument( 

27 "--performance-dataframe", 

28 required=True, 

29 type=Path, 

30 help="path to the performance dataframe", 

31 ) 

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

33 parser.add_argument( 

34 "--instance", 

35 required=True, 

36 type=Path, 

37 nargs="+", 

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

39 ) 

40 parser.add_argument( 

41 "--run-index", 

42 required=True, 

43 type=int, 

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

45 ) 

46 parser.add_argument( 

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

48 ) 

49 

50 # These two arguments should be mutually exclusive 

51 parser.add_argument( 

52 "--configuration-id", 

53 type=str, 

54 required=False, 

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

56 ) 

57 parser.add_argument( 

58 "--configuration", 

59 type=str, 

60 nargs="+", 

61 required=False, 

62 help="configuration for the solver", 

63 ) 

64 

65 parser.add_argument( 

66 "--seed", 

67 type=int, 

68 required=False, 

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

70 ) 

71 parser.add_argument( 

72 "--cutoff-time", 

73 type=int, 

74 required=False, 

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

76 ) 

77 parser.add_argument( 

78 "--objectives", 

79 type=str, 

80 required=False, 

81 nargs="+", 

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

83 ) 

84 parser.add_argument( 

85 "--target-objective", 

86 required=False, 

87 type=str, 

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

89 ) 

90 parser.add_argument( 

91 "--best-configuration-instances", 

92 required=False, 

93 type=str, 

94 nargs="+", 

95 metavar="SET_NAME,INSTANCE_NAME", 

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

97 " determine the best found configurations over the given " 

98 "instances, each passed as a 'set_name,instance_name' pair. Uses the" 

99 " 'target-objective' given in the arguments or the first one given by" 

100 " the dataframe to determine the best configuration.", 

101 ) 

102 args = parser.parse_args(argv) 

103 # Process command line arguments 

104 log_dir = args.log_dir 

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

106 # Resolve possible multi-file instance 

107 instance_path: list[Path] = args.instance 

108 # The PerformanceDataFrame is keyed by the canonical (set_name, instance_name) pair. 

109 # Deriving the name from the path with .stem is only correct for FileInstanceSet, so 

110 # resolve it from the owning set instead and let the subclass supply its convention. 

111 # All files of a multi-file instance share the same pair, so the first file resolves it. 

112 instance_pair = resolve_instance_pair(instance_path[0]) 

113 instance_set_name, instance_name = instance_pair 

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

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

116 run_index = args.run_index 

117 # Ensure stringifcation of path objects 

118 if isinstance(instance_path, list): 

119 # Double list because of solver.run 

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

121 else: 

122 run_instances = str(instance_path) 

123 

124 solver = Solver(args.solver) 

125 # By default, run the default configuration 

126 config_id = PerformanceDataFrame.default_configuration 

127 configuration = None 

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

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

130 # Parse the provided objectives if present 

131 objectives = ( 

132 [resolve_objective(objective) for objective in args.objectives] 

133 if args.objectives 

134 else None 

135 ) 

136 

137 if args.configuration: # Configuration provided, override 

138 if isinstance(args.configuration, list): 

139 configuration = parse_commandline_dict(args.configuration) 

140 else: 

141 configuration = ast.literal_eval(args.configuration) 

142 print(configuration) 

143 config_id = configuration["configuration_id"] 

144 elif ( 

145 ( 

146 args.configuration_id 

147 and args.configuration_id != PerformanceDataFrame.default_configuration 

148 ) 

149 or args.best_configuration_instances 

150 or not objectives 

151 ): # Read from PerformanceDataFrame, can be slow 

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

153 print( 

154 "Reading from Performance DataFrame.. " 

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

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

157 ) 

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

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

160 with lock.acquire(timeout=600): 

161 performance_dataframe = PerformanceDataFrame(args.performance_dataframe) 

162 

163 if not objectives: 

164 objectives = performance_dataframe.objectives 

165 

166 if args.best_configuration_instances: # Determine best configuration 

167 # Each token is a 'set_name,instance_name' pair, split into a tuple on the comma 

168 best_configuration_instances: list[tuple[str, str]] = list( 

169 {tuple(pair.split(",")) for pair in args.best_configuration_instances} 

170 ) 

171 target_objective = ( 

172 resolve_objective(args.target_objective) 

173 if args.target_objective 

174 else objectives[0] 

175 ) 

176 config_id, _ = performance_dataframe.best_configuration( 

177 solver=str(args.solver), 

178 objective=target_objective, 

179 instance_pairs=best_configuration_instances, 

180 ) 

181 configuration = performance_dataframe.get_full_configuration( 

182 str(args.solver), config_id 

183 ) 

184 

185 elif ( 

186 args.configuration_id 

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

188 config_id = args.configuration_id 

189 configuration = performance_dataframe.get_full_configuration( 

190 str(args.solver), config_id 

191 ) 

192 

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

194 solver_output = solver.run( 

195 run_instances, 

196 objectives=objectives, 

197 seed=seed, 

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

199 cutoff_time=args.cutoff_time, 

200 log_dir=log_dir, 

201 run_on=Runner.LOCAL, 

202 ) 

203 

204 # Prepare the results for the DataFrame for each objective 

205 result = [ 

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

207 [seed] * len(objectives), 

208 ] 

209 solver_fields = [ 

210 PerformanceDataFrame.column_value, 

211 PerformanceDataFrame.column_seed, 

212 ] 

213 

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

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

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

217 for objective in objectives: 

218 print( 

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

220 ) 

221 

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

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

224 

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

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

227 with lock.acquire(timeout=600): 

228 performance_dataframe = PerformanceDataFrame(args.performance_dataframe) 

229 performance_dataframe.set_value( 

230 result, 

231 solver=str(args.solver), 

232 instance_pair=instance_pair, 

233 configuration=config_id, 

234 objective=[objective.name for objective in objectives], 

235 run=run_index, 

236 solver_fields=solver_fields, 

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

238 ) 

239 

240 

241if __name__ == "__main__": 

242 main(sys.argv[1:])