Coverage for src/sparkle/CLI/compute_features.py: 86%
92 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 compute features for instances."""
4from __future__ import annotations
5import sys
6import argparse
8from pathlib import Path
9from runrunner.base import Run, Runner
11from sparkle.selector import Extractor
12from sparkle.platform.settings_objects import Settings
13from sparkle.structures import FeatureDataFrame
14from sparkle.instance import Instance_Set, InstanceSet
17from sparkle.CLI.help import global_variables as gv
18from sparkle.CLI.help import logging as sl
19from sparkle.CLI.help import argparse_custom as ac
20from sparkle.CLI.initialise import check_for_initialise
21from sparkle.CLI.help.nicknames import resolve_object_name, resolve_instance_name
24def parser_function() -> argparse.ArgumentParser:
25 """Define the command line arguments."""
26 parser = argparse.ArgumentParser(
27 description="Sparkle command to Compute features "
28 "for instances using added extractors "
29 "and instances."
30 )
31 parser.add_argument(
32 *ac.InstanceSetPathsArgument.names, **ac.InstanceSetPathsArgument.kwargs
33 )
34 parser.add_argument(*ac.ExtractorsArgument.names, **ac.ExtractorsArgument.kwargs)
35 parser.add_argument(
36 *ac.RecomputeFeaturesArgument.names, **ac.RecomputeFeaturesArgument.kwargs
37 )
38 # Settings arguments
39 parser.add_argument(*ac.SettingsFileArgument.names, **ac.SettingsFileArgument.kwargs)
40 parser.add_argument(*Settings.OPTION_run_on.args, **Settings.OPTION_run_on.kwargs)
41 parser.add_argument(
42 *Settings.OPTION_groupwise_computation.args,
43 **Settings.OPTION_groupwise_computation.kwargs,
44 )
45 return parser
48def compute_features(
49 feature_data: FeatureDataFrame,
50 recompute: bool,
51 run_on: Runner = Runner.SLURM,
52 instance_sets: list[InstanceSet] = None,
53) -> list[Run]:
54 """Compute features for all instance and feature extractor combinations.
56 A RunRunner run is submitted for the computation of the features.
57 The results are then stored in the csv file specified by feature_data_csv_path.
59 Args:
60 feature_data: Feature Data Frame to use
61 recompute: Specifies if features should be recomputed.
62 run_on: Runner
63 On which computer or cluster environment to run the solvers.
64 Available: Runner.LOCAL, Runner.SLURM. Default: Runner.SLURM
65 instance_sets: Optional resolved instance sets to search. By default, instances
66 are resolved from the registered platform instance directory.
68 Returns:
69 Submitted runs. Empty if there are no jobs to execute.
70 """
71 settings = gv.settings()
72 if recompute:
73 feature_data.reset_dataframe()
75 remaining_jobs = feature_data.remaining_jobs(
76 groupwise_computation=settings.groupwise_computation
77 )
79 # If there are no jobs, stop
80 if not remaining_jobs:
81 print(
82 "No feature computation jobs to run; stopping execution! To recompute "
83 "feature values use the --recompute flag."
84 )
85 return []
87 cutoff = settings.extractor_cutoff_time
88 sbatch_options = settings.sbatch_settings
89 slurm_prepend = settings.slurm_job_prepend
90 srun_options = ["-N1", "-n1"] + sbatch_options
91 search_location = (
92 settings.DEFAULT_instance_dir if instance_sets is None else instance_sets
93 )
94 runs = []
95 for (instance_set, instance_name), extractor_name, feature_group in remaining_jobs:
96 extractor_path = settings.DEFAULT_extractor_dir / extractor_name
97 extractor = Extractor(extractor_path)
99 instance_path = resolve_instance_name(
100 instance_set, instance_name, search_location
101 )
102 if instance_path is None:
103 raise ValueError(
104 f"ERROR: The instance {instance_name} ({instance_set}) could not be found. "
105 f"Please make sure the path is correct."
106 )
108 instance_paths = []
109 if isinstance(instance_path, list):
110 instance_paths = [Path(path) for path in instance_path]
111 elif isinstance(instance_path, (str, Path)):
112 instance_paths = [Path(instance_path)]
114 run = extractor.run_cli(
115 instance_paths,
116 feature_data,
117 cutoff,
118 feature_group,
119 run_on,
120 sbatch_options,
121 srun_options,
122 settings.slurm_jobs_in_parallel,
123 slurm_prepend,
124 log_dir=sl.caller_log_dir,
125 )
126 runs.append(run)
127 return runs
130def main(argv: list[str]) -> None:
131 """Main function of the compute features command."""
132 # Define command line arguments
133 parser = parser_function()
135 # Process command line arguments
136 args = parser.parse_args(argv)
137 settings = gv.settings(args)
138 run_on = settings.run_on
140 # Log command call
141 sl.log_command(sys.argv, settings.random_state)
142 check_for_initialise()
144 # Check if there are any feature extractors registered
145 if not any([p.is_dir() for p in gv.settings().DEFAULT_extractor_dir.iterdir()]):
146 print(
147 "No feature extractors present! Add feature extractors to Sparkle "
148 "by using the add_feature_extractor command."
149 )
150 sys.exit()
152 # Load feature data
153 feature_data = FeatureDataFrame(settings.DEFAULT_feature_data_path)
155 # Narrow the work down to only the instances and/or extractors the user named on CLI by filtering the full FDF.
156 if args.instance_path:
157 instances = set()
158 for instance_arg in args.instance_path:
159 instance: InstanceSet = resolve_object_name(
160 instance_arg,
161 gv.instance_set_nickname_mapping,
162 settings.DEFAULT_instance_dir,
163 Instance_Set,
164 )
165 if instance is None:
166 raise ValueError(
167 f"Argument Error! Could not resolve instance: '{instance_arg}'"
168 )
169 # resolve_object_name may hand back a single-file FileInstanceSet whose .name
170 # is the file stem rather than the owning set (e.g. "PTN/bce7824.cnf" -> name
171 # "bce7824"). The FeatureDataFrame is keyed by the owning set, so pair each
172 # requested instance with its directory name to match how it was stored.
173 for instance_name in instance.instance_names:
174 instances.add((instance.directory.name, instance_name))
176 filtered_instances = [
177 (instance_set, instance_name)
178 for instance_set, instance_name in feature_data.instance_pairs
179 if (instance_set, instance_name) not in instances
180 ]
181 feature_data.remove_instance(filtered_instances)
182 if feature_data.num_instances == 0:
183 raise ValueError("Argument Error! No instances left after filtering.")
184 if args.extractors:
185 extractors = set()
186 for extractor in args.extractors:
187 extractor: Extractor = resolve_object_name(
188 extractor,
189 nickname_dict=gv.extractor_nickname_mapping,
190 target_dir=settings.DEFAULT_extractor_dir,
191 class_name=Extractor,
192 )
193 if extractor is None:
194 raise ValueError(
195 f"Argument Error! Could not resolve extractor: '{extractor}'"
196 )
197 extractors.add(extractor.name)
198 for extractor in feature_data.extractors:
199 if extractor not in extractors:
200 feature_data.remove_extractor(extractor)
201 if feature_data.num_extractors == 0:
202 raise ValueError(
203 "Argument Error! No feature extractors left after filtering."
204 )
206 # Start compute features
207 print("Start computing features ...")
208 compute_features(feature_data, args.recompute, run_on)
210 # Write used settings to file
211 gv.settings().write_used_settings()
212 sys.exit(0)
215if __name__ == "__main__":
216 main(sys.argv[1:])