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
« 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."""
5import sys
6import ast
7from filelock import FileLock
8import argparse
9from pathlib import Path
10import random
11import time
13from runrunner import Runner
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
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 )
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 )
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)
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 )
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)
156 if not objectives:
157 objectives = performance_dataframe.objectives
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 )
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 )
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 )
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 ]
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 )
216 # Desyncronize from other possible jobs writing to the same file
217 time.sleep(random.random() * 100)
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 )
235if __name__ == "__main__":
236 main(sys.argv[1:])