Coverage for src/sparkle/CLI/add_solver.py: 80%
101 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-08 12:00 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-08 12:00 +0000
1#!/usr/bin/env python3
2"""Sparkle command to add a solver to the Sparkle platform."""
4import os
5import stat
6import sys
7import argparse
8import shutil
9from pathlib import Path
11from sparkle.tools.parameters import PCSConvention
13from sparkle.platform import file_help as sfh
14from sparkle.CLI.help import global_variables as gv
15from sparkle.structures import PerformanceDataFrame
16from sparkle.solver import Solver, verifiers
17from sparkle.CLI.help import logging as sl
18from sparkle.CLI.initialise import check_for_initialise
19from sparkle.CLI.help import argparse_custom as ac
20from sparkle.CLI.help import jobs as jobs_help
23def parser_function() -> argparse.ArgumentParser:
24 """Define the command line arguments."""
25 parser = argparse.ArgumentParser(description="Add a solver to the Sparkle platform.")
26 parser.add_argument(
27 *ac.DeterministicArgument.names, **ac.DeterministicArgument.kwargs
28 )
29 parser.add_argument(
30 *ac.SolutionVerifierArgument.names, **ac.SolutionVerifierArgument.kwargs
31 )
32 parser.add_argument(
33 *ac.NicknameSolverArgument.names, **ac.NicknameSolverArgument.kwargs
34 )
35 parser.add_argument(*ac.SolverPathArgument.names, **ac.SolverPathArgument.kwargs)
36 parser.add_argument(*ac.SkipChecksArgument.names, **ac.SkipChecksArgument.kwargs)
37 parser.add_argument(*ac.NoCopyArgument.names, **ac.NoCopyArgument.kwargs)
38 return parser
41def main(argv: list[str]) -> None:
42 """Main function of the command."""
43 # Log command call
44 sl.log_command(sys.argv, gv.settings().random_state)
45 check_for_initialise()
47 # Define command line arguments
48 parser = parser_function()
50 # Process command line arguments
51 args = parser.parse_args(argv)
52 solver_source = Path(args.solver_path)
53 deterministic = args.deterministic
54 solution_verifier = args.solution_verifier
56 if not solver_source.exists():
57 print(f'Solver path "{solver_source}" does not exist!')
58 sys.exit(-1)
60 # Make sure it is pointing to the verifiers module
61 if solution_verifier:
62 if Path(solution_verifier).is_file(): # File verifier
63 solution_verifier = (
64 verifiers.SolutionFileVerifier.__name__,
65 solution_verifier,
66 )
67 elif solution_verifier not in verifiers.mapping:
68 print(f"ERROR: Unknown solution verifier {solution_verifier}!")
69 sys.exit(-1)
71 nickname = args.nickname
73 if args.run_checks:
74 print("Running checks...")
75 solver = Solver(Path(solver_source))
76 if solver.pcs_file is None:
77 print(
78 "None or multiple .pcs files found. Solver "
79 "is not valid for configuration."
80 )
81 else:
82 print(f"PCS file detected: {solver.pcs_file.name}. ", end="")
83 if solver.read_pcs_file():
84 print("Can read the pcs file.")
85 else:
86 print("WARNING: Can not read the provided pcs file format.")
88 wrapper_path = solver.directory / solver.wrapper
89 if not wrapper_path.is_file():
90 print(
91 f"ERROR: Solver {solver_source.name} does not have a solver wrapper "
92 f"(Missing file {solver.wrapper})."
93 )
94 sys.exit(-1)
95 elif not os.access(wrapper_path, os.X_OK):
96 print(
97 f"ERROR: Solver {solver_source.name} wrapper file {solver.wrapper} "
98 f" does not have execution rights set!"
99 )
100 sys.exit(-1)
102 # Start add solver
103 solver_directory = gv.settings().DEFAULT_solver_dir / solver_source.name
104 if solver_directory.exists():
105 print(
106 f"ERROR: Solver {solver_source.name} already exists! Can not add new solver."
107 )
108 sys.exit(-1)
109 if args.no_copy:
110 print(f"Creating symbolic link from {solver_source} to {solver_directory}...")
111 if not os.access(solver_source, os.W_OK):
112 raise PermissionError(
113 "Sparkle does not have the right to write to the destination folder."
114 )
115 solver_directory.symlink_to(solver_source.absolute())
116 else:
117 print(f"Copying {solver_source.name} to platform...")
118 solver_directory.mkdir(parents=True)
119 shutil.copytree(solver_source, solver_directory, dirs_exist_ok=True)
121 # Save the deterministic bool in the solver
122 with (solver_directory / Solver.meta_data).open("w+") as fout:
123 fout.write(str({"deterministic": deterministic, "verifier": solution_verifier}))
125 # Add RunSolver executable to the solver
126 runsolver_path = gv.settings().DEFAULT_runsolver_exec
127 if runsolver_path.name in [file.name for file in solver_directory.iterdir()]:
128 print(
129 "Warning! RunSolver executable detected in Solver "
130 f"{solver_source.name}. This will be replaced with "
131 f"Sparkle's version of RunSolver. ({runsolver_path})"
132 )
134 if runsolver_path.exists():
135 runsolver_target = solver_directory / runsolver_path.name
136 shutil.copyfile(runsolver_path, runsolver_target)
137 runsolver_target.chmod(runsolver_target.stat().st_mode | stat.S_IEXEC)
138 else:
139 print("Warning! RunSolver does not exists. Falling back to PyRunSolver.")
141 jobs_help.check_running_waiting_jobs(
142 gv.settings().DEFAULT_log_output,
143 )
145 performance_data = PerformanceDataFrame(
146 gv.settings().DEFAULT_performance_data_path,
147 objectives=gv.settings().objectives,
148 )
149 performance_data.add_solver(str(solver_directory))
150 performance_data.save_csv()
152 print(f"Adding solver {solver_source.name} done!")
154 if nickname is not None:
155 sfh.add_remove_platform_item(
156 solver_directory,
157 gv.solver_nickname_list_path,
158 gv.file_storage_data_mapping[gv.solver_nickname_list_path],
159 key=nickname,
160 )
162 solver = Solver(solver_directory) # Recreate solver from its new directory
163 if solver.pcs_file is not None:
164 # Generate missing PCS files
165 print("Checking for missing PCS files to generate...")
166 if solver.get_pcs_file_type(PCSConvention.IRACE) is None:
167 solver.port_pcs(PCSConvention.IRACE) # Create PCS file for IRACE
168 print("\t- Generating IRACE done!")
169 if solver.get_pcs_file_type(PCSConvention.ParamILS) is None:
170 solver.port_pcs(PCSConvention.ParamILS) # Create PCS file for ParamILS
171 print("\t- Generating ParamILS done!")
173 print(f"Solver {solver.name} added to platform!")
175 # Write used settings to file
176 gv.settings().write_used_settings()
177 sys.exit(0)
180if __name__ == "__main__":
181 main(sys.argv[1:])