solver-support 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,132 @@
1
+ """solver_support -- a generic solver pipeline.
2
+
3
+ Domain-agnostic infrastructure for driving a Conjure / Savile Row / Fast Downward
4
+ / SymK / ENHSP / UPF pipeline: spawn solvers, manage phases and timeouts, name
5
+ files, parse solver output into timing / cost-layer stats, and run a horizon
6
+ loop. A consumer supplies its own model registry and plugs its domain scoring
7
+ in via injection; this package knows nothing about any particular problem or
8
+ objective.
9
+
10
+ The core ships *unwired*: no objective extractor is registered and the registry
11
+ has no default config path, so a bare ``import solver_support`` yields an
12
+ unconfigured pipeline. A consumer registers its scoring (see :mod:`objective`)
13
+ and its packaging paths (see :mod:`model_registry`) once at its own import time.
14
+ """
15
+
16
+ __version__ = "0.1.1"
17
+
18
+ # --- model registry (loader + command builders + packaging injection) ---
19
+ from .model_registry import (
20
+ ModelRegistry,
21
+ ModelRegistryError,
22
+ get_default_registry,
23
+ check_model_registry_consistency,
24
+ set_default_config_path,
25
+ set_resource_root,
26
+ set_user_config_finder,
27
+ )
28
+
29
+ # --- run configuration singleton ---
30
+ from .global_state import global_state, GlobalState
31
+
32
+ # --- objective-extraction injection point (a consumer registers extractors) ---
33
+ from . import objective
34
+ from .objective import (
35
+ set_reported_score_from_solution,
36
+ set_reported_score_from_stdout,
37
+ set_reported_score_from_timeout_solution,
38
+ set_raw_objective_from_solution,
39
+ )
40
+
41
+ # --- solvers ---
42
+ from .solver_base import Solver
43
+ from .solver_fd import FastDownwardSolver
44
+ from .solver_enhsp import EnhspSolver
45
+ from .solver_upfplan import UpfPlanSolver
46
+ from .solver_horizon import HorizonBasedSolver, SavileRowSolver, ConjureSolver
47
+
48
+ # --- execution, stats, run model ---
49
+ from .solver_runner import SolverRunner, SolverResult, SolverExecutableError
50
+ from .solver_stats import SolverStatsExtractor, detect_solver_from_command
51
+ from .run_summary import RunSummary, PhaseResult, create_run_summary
52
+
53
+ # --- file naming and horizon loop ---
54
+ from .file_management import (
55
+ SolutionFileNamer,
56
+ ObjectiveFileManager,
57
+ VisualizationFileManager,
58
+ compute_tune_slot,
59
+ compute_options_label,
60
+ )
61
+ from .horizon_utils import (
62
+ horizon_search,
63
+ calculate_horizon_timeout,
64
+ reconcile_horizon_bounds,
65
+ )
66
+
67
+ # --- generic helpers ---
68
+ from .utilities import (
69
+ setup_logger,
70
+ relativize_path,
71
+ warn,
72
+ set_warn_logger_name,
73
+ format_timestamp,
74
+ arrayify,
75
+ indices,
76
+ BrokenPipeHandler,
77
+ )
78
+
79
+ __all__ = [
80
+ # registry
81
+ "ModelRegistry",
82
+ "ModelRegistryError",
83
+ "get_default_registry",
84
+ "check_model_registry_consistency",
85
+ "set_default_config_path",
86
+ "set_resource_root",
87
+ "set_user_config_finder",
88
+ # run config
89
+ "global_state",
90
+ "GlobalState",
91
+ # objective injection
92
+ "objective",
93
+ "set_reported_score_from_solution",
94
+ "set_reported_score_from_stdout",
95
+ "set_reported_score_from_timeout_solution",
96
+ "set_raw_objective_from_solution",
97
+ # solvers
98
+ "Solver",
99
+ "FastDownwardSolver",
100
+ "EnhspSolver",
101
+ "UpfPlanSolver",
102
+ "HorizonBasedSolver",
103
+ "SavileRowSolver",
104
+ "ConjureSolver",
105
+ # execution / stats / run model
106
+ "SolverRunner",
107
+ "SolverResult",
108
+ "SolverExecutableError",
109
+ "SolverStatsExtractor",
110
+ "detect_solver_from_command",
111
+ "RunSummary",
112
+ "PhaseResult",
113
+ "create_run_summary",
114
+ # file naming / horizon loop
115
+ "SolutionFileNamer",
116
+ "ObjectiveFileManager",
117
+ "VisualizationFileManager",
118
+ "compute_tune_slot",
119
+ "compute_options_label",
120
+ "horizon_search",
121
+ "calculate_horizon_timeout",
122
+ "reconcile_horizon_bounds",
123
+ # helpers
124
+ "setup_logger",
125
+ "relativize_path",
126
+ "warn",
127
+ "set_warn_logger_name",
128
+ "format_timestamp",
129
+ "arrayify",
130
+ "indices",
131
+ "BrokenPipeHandler",
132
+ ]
@@ -0,0 +1,334 @@
1
+ #!/usr/bin/env python3
2
+ """Solver command builders for the model registry.
3
+
4
+ The ``build_*_command`` methods assemble the argv for each solver backend (Savile
5
+ Row, Fast Downward / SymK, ENHSP, Conjure) from a solver's registry config and the
6
+ effective ``--tune`` preset. This is a distinct concern from the registry's config
7
+ loading and its model/solver queries, so the builders live here as a mixin that
8
+ :class:`ModelRegistry` inherits.
9
+
10
+ The split is behaviour-preserving: the methods reference the registry only through
11
+ ``self`` (``get_solver_config``, ``_effective_tune``, ``is_command_available``, the
12
+ ``_savilerow_native_warning_shown`` flag), so on the combined instance they resolve
13
+ exactly as before. ``ModelRegistryError`` is imported from ``model_registry``; that
14
+ back-import is safe because the package ``__init__`` always imports
15
+ ``model_registry`` first, and ``model_registry`` imports this mixin only after the
16
+ exception is defined.
17
+ """
18
+
19
+ from typing import List, Optional
20
+
21
+ from .model_registry import ModelRegistryError
22
+
23
+
24
+ class CommandBuilderMixin:
25
+ """The ``build_*_command`` methods mixed into :class:`ModelRegistry`."""
26
+
27
+ def build_savilerow_command(self, model_path: str, param_path: str,
28
+ use_srjava: bool = False,
29
+ timeout: Optional[int] = None,
30
+ objective_file_path: Optional[str] = None,
31
+ extra_options: Optional[List[str]] = None,
32
+ solver_name: str = 'kissat',
33
+ tune: Optional[str] = None) -> List[str]:
34
+ """
35
+ Build a Savile Row command with proper flags for horizon scanning.
36
+
37
+ original run script's SRARGS logic:
38
+ - Minion mode: just "-run-solver"
39
+ - SAT mode: "-run-solver -O0 -deletevars -identical-cse -sat -sat-polarity -sat-family kissat"
40
+
41
+ Args:
42
+ model_path: path to the model file
43
+ param_path: path to the parameter file
44
+ use_srjava: use Java savilerow instead of savilerow-native
45
+ timeout: override timeout in seconds (uses default_timeout if None)
46
+ objective_file_path: path for intermediate objective values file (default: obj.txt)
47
+ extra_options: additional command line options to pass to Savile Row
48
+ solver_name: name of the solver configuration to use (default: 'kissat').
49
+ The backend (kissat vs minion) is a property of this config:
50
+ a config with ``backend: minion`` uses its tune presets'
51
+ minion_args and Minion's timeout dialect, otherwise the kissat
52
+ SAT arg-set is used.
53
+ tune: name of a ``tune:`` preset to apply instead of the config's
54
+ ``default_tune`` (kissat: 'safe'/'bare'/'srdefault'/'gac';
55
+ minion: 'bare'). The preset supplies this backend's arg-set
56
+ (sat_args for kissat, minion_args for minion). A name that is
57
+ unknown or lacks this backend's arg key falls back to
58
+ ``default_tune``. This is the savilerow arm of the same ``--tune``
59
+ dial fd uses for its search presets.
60
+
61
+ Returns:
62
+ Complete command as list of strings.
63
+
64
+ Raises:
65
+ ModelRegistryError: If savilerow solver not found in config, or the
66
+ resolved tune has no arg-set for this backend.
67
+ """
68
+ solver_config = self.get_solver_config(solver_name)
69
+
70
+ # The backend solver (kissat SAT vs Minion CP) is declared by the config,
71
+ # not passed in: `--solver minion` selects the minion backend config. This
72
+ # replaced the old cross-cutting `--minion` boolean.
73
+ use_minion = solver_config.get('backend') == 'minion'
74
+
75
+ # Choose command variant
76
+ if use_srjava:
77
+ # User explicitly requested Java version
78
+ command = solver_config['command']
79
+ else:
80
+ # Try to use native version if available
81
+ native_command = solver_config.get('command_native', 'savilerow-native')
82
+ if self.is_command_available(native_command):
83
+ command = native_command
84
+ else:
85
+ # Fall back to Java version with warning (only warn once)
86
+ if not self._savilerow_native_warning_shown:
87
+ import logging
88
+ logger = logging.getLogger(__name__)
89
+ logger.warning(f"savilerow-native not found in PATH, falling back to Java version (savilerow)")
90
+ self._savilerow_native_warning_shown = True
91
+ command = solver_config['command']
92
+
93
+ # Choose the argument set from the active tune preset. kissat presets
94
+ # carry sat_args, minion presets carry minion_args; a tune that is
95
+ # unknown or lacks this backend's key falls back to default_tune (the
96
+ # lenient behaviour the fd builder uses too). The model-default tune is
97
+ # resolved by the caller, so `tune` here is already the effective name.
98
+ arg_key = 'minion_args' if use_minion else 'sat_args'
99
+ tunes = solver_config.get('tune', {})
100
+ tune_name = self._effective_tune(solver_config, tune, arg_key)
101
+ preset = tunes.get(tune_name, {})
102
+ if arg_key not in preset:
103
+ raise ModelRegistryError(
104
+ f"Solver '{solver_name}' tune '{tune_name}' has no {arg_key}"
105
+ )
106
+ args = preset[arg_key].copy()
107
+ if not use_minion:
108
+ # Add objective recording arguments only when needed
109
+ if objective_file_path is not None:
110
+ args.extend(["-record-intermediate-objective-values", objective_file_path])
111
+
112
+ # Add timeout arguments
113
+ if timeout is None:
114
+ timeout = solver_config['default_timeout']
115
+
116
+ # Different timeout handling for minion vs SAT solver
117
+ if timeout is not None:
118
+ if use_minion:
119
+ # For minion, just use -timelimit (no solver-options needed)
120
+ args.extend(["-timelimit", str(timeout)])
121
+ else:
122
+ # For SAT solver (kissat), use both -solver-options and -timelimit
123
+ args.extend([
124
+ "-solver-options", f"--time={timeout}",
125
+ "-timelimit", str(timeout)
126
+ ])
127
+
128
+ # Add extra options if provided
129
+ if extra_options:
130
+ args.extend(extra_options)
131
+
132
+ # Build complete command
133
+ cmd = [command] + args + [str(model_path), str(param_path)]
134
+
135
+ return cmd
136
+
137
+ def build_fd_command(self, model_path: str, param_path: str,
138
+ anytime: bool = False, timeout: Optional[int] = None,
139
+ tune: Optional[str] = None,
140
+ extra_options: Optional[List[str]] = None,
141
+ solver_name: str = 'fd') -> List[str]:
142
+ """
143
+ Build a Fast Downward command with proper flags.
144
+
145
+ Args:
146
+ model_path: Path to the PDDL domain file.
147
+ param_path: Path to the PDDL problem file.
148
+ anytime: Whether to run the anytime portfolio (the ``soup`` tune).
149
+ When true the pre-file ``--alias`` form is emitted and ``tune``
150
+ is ignored; the caller decides this via ``is_anytime_tune`` (the
151
+ tune's ``regime: anytime``), not the model name.
152
+ timeout: Override timeout in seconds (uses default_timeout if None).
153
+ tune: Name of a ``tune:`` preset to apply instead of the config's
154
+ ``default_tune`` (fd: 'hmax'/'ff'/'lmcut'/'verbose';
155
+ symk: 'fw'/'bw'/'bd'). A search-bearing preset becomes the
156
+ post-file ``--search`` argument; the ``soup`` preset (an
157
+ ``alias``) is the pre-file portfolio. An unknown or searchless
158
+ name falls back to ``default_tune``. This is the fd-family arm
159
+ of the same ``--tune`` dial savilerow uses for its arg-sets.
160
+ extra_options: raw ``--options`` tokens passed straight through,
161
+ appended after all generated arguments (the runner does not
162
+ interpret them - the caller is responsible for their placement
163
+ being valid for Fast Downward's command line).
164
+ solver_name: Which fd-type solver config to use ('fd' or 'symk').
165
+ SymK is a Fast Downward fork, so it shares this builder; the
166
+ config supplies the command (fd vs symk) and its tune presets.
167
+
168
+ Returns:
169
+ Complete command as list of strings.
170
+
171
+ Raises:
172
+ ModelRegistryError: If the solver is not found in config, or has no
173
+ usable tune preset for the requested mode.
174
+ """
175
+ solver_config = self.get_solver_config(solver_name)
176
+
177
+ # Add timeout argument
178
+ if timeout is None:
179
+ timeout = solver_config['default_timeout']
180
+
181
+ tunes = solver_config.get('tune', {})
182
+
183
+ # Build command with correct argument order for FD
184
+ if anytime:
185
+ # Anytime portfolio: the pre-file --alias from the soup tune preset,
186
+ # e.g. fd --alias seq-sat-fdss-2018 --search-time-limit <timeout>
187
+ # domain.pddl problem.pddl. `soup` is currently the only anytime fd
188
+ # tune, so it is the one resolved here.
189
+ alias = tunes.get('soup', {}).get('alias')
190
+ if not alias:
191
+ raise ModelRegistryError(
192
+ f"Solver '{solver_name}' has no 'soup' tune preset for anytime mode"
193
+ )
194
+ pre_file_args = ["--alias", alias]
195
+ if timeout is not None:
196
+ pre_file_args.extend(["--search-time-limit", str(timeout)])
197
+ cmd = [solver_config['command']] + pre_file_args + [str(model_path), str(param_path)]
198
+ else:
199
+ # Standard mode: the post-file --search string from a tune preset,
200
+ # e.g. fd --search-time-limit <timeout> domain.pddl problem.pddl
201
+ # --search "astar(blind())". A requested tune that doesn't exist or
202
+ # carries no search string (e.g. soup) falls back to default_tune.
203
+ name = self._effective_tune(solver_config, tune, 'search')
204
+ search = tunes.get(name, {}).get('search')
205
+ if not search:
206
+ raise ModelRegistryError(
207
+ f"Solver '{solver_name}' has no search string for tune '{name}'"
208
+ )
209
+ pre_file_args = []
210
+ if timeout is not None:
211
+ pre_file_args = ["--search-time-limit", str(timeout)]
212
+ post_file_args = ["--search", search]
213
+ cmd = [solver_config['command']] + pre_file_args + [str(model_path), str(param_path)] + post_file_args
214
+
215
+ # Raw --options passthrough, appended last (uninterpreted).
216
+ if extra_options:
217
+ cmd.extend(extra_options)
218
+
219
+ return cmd
220
+
221
+ def build_enhsp_command(self, model_path: str, param_path: str,
222
+ plan_path: str, tune: Optional[str] = None,
223
+ extra_options: Optional[List[str]] = None,
224
+ solver_name: str = 'enhsp') -> List[str]:
225
+ """
226
+ Build an ENHSP command.
227
+
228
+ ENHSP takes the domain and problem as ``-o``/``-f`` and writes its plan
229
+ (the same ``(action args)`` form as Fast Downward's ``sas_plan``) to the
230
+ ``-sp`` file. The active tune preset selects the ``-planner``
231
+ preconfiguration (sat-* satisficing / opt-* metric-optimal); an unknown
232
+ or planner-less ``tune`` falls back to the config's ``default_tune``,
233
+ the same leniency as the fd/savilerow builders. ENHSP has no internal
234
+ time limit outside anytime mode, so no timeout flag is emitted - the
235
+ SolverRunner's external timeout terminates it.
236
+
237
+ Args:
238
+ model_path: Path to the PDDL domain file.
239
+ param_path: Path to the PDDL problem file.
240
+ plan_path: Where ENHSP should write the plan (the ``-sp`` argument).
241
+ tune: Name of a ``tune:`` preset (a planner config) to apply instead
242
+ of the config's ``default_tune``.
243
+ extra_options: raw ``--options`` tokens passed straight through,
244
+ appended after the generated arguments (uninterpreted).
245
+ solver_name: Which enhsp-type solver config to use.
246
+
247
+ Returns:
248
+ Complete command as list of strings.
249
+
250
+ Raises:
251
+ ModelRegistryError: If the solver has no usable planner tune.
252
+ """
253
+ solver_config = self.get_solver_config(solver_name)
254
+ tunes = solver_config.get('tune', {})
255
+ name = self._effective_tune(solver_config, tune, 'planner')
256
+ planner = tunes.get(name, {}).get('planner')
257
+ if not planner:
258
+ raise ModelRegistryError(
259
+ f"Solver '{solver_name}' has no planner for tune '{name}'"
260
+ )
261
+ cmd = [
262
+ solver_config['command'],
263
+ "-o", str(model_path),
264
+ "-f", str(param_path),
265
+ "-planner", planner,
266
+ "-sp", str(plan_path),
267
+ ]
268
+ if extra_options:
269
+ cmd.extend(extra_options)
270
+ return cmd
271
+
272
+ def build_conjure_command(self, action: str, **kwargs) -> List[str]:
273
+ """
274
+ Build a Conjure command for different actions.
275
+
276
+ Args:
277
+ action: Action type ('modelling', 'translate-parameter', 'translate-solution')
278
+ **kwargs: Action-specific arguments:
279
+ - modelling: model_path
280
+ - translate-parameter: essence_param, eprime
281
+ - translate-solution: essence_param, eprime, eprime_solution, essence_solution
282
+
283
+ Returns:
284
+ Complete command as list of strings.
285
+
286
+ Raises:
287
+ ModelRegistryError: If conjure solver not found in config or invalid action.
288
+ """
289
+ solver_config = self.get_solver_config('conjure')
290
+
291
+ # Get base arguments for the action
292
+ action_key = f"{action.replace('-', '_')}_args"
293
+ if action_key not in solver_config:
294
+ raise ModelRegistryError(f"Unknown conjure action: {action}")
295
+
296
+ args = solver_config[action_key].copy()
297
+
298
+ # Build action-specific arguments
299
+ if action == 'modelling':
300
+ model_path = kwargs.get('model_path')
301
+ if not model_path:
302
+ raise ModelRegistryError("model_path required for conjure modelling")
303
+ cmd = [solver_config['command']] + args + [str(model_path)]
304
+
305
+ elif action == 'translate-parameter':
306
+ essence_param = kwargs.get('essence_param')
307
+ eprime = kwargs.get('eprime')
308
+ if not essence_param or not eprime:
309
+ raise ModelRegistryError("essence_param and eprime required for translate-parameter")
310
+ cmd = [solver_config['command']] + args + [
311
+ f"--essence-param={essence_param}",
312
+ f"--eprime={eprime}"
313
+ ]
314
+
315
+ elif action == 'translate-solution':
316
+ essence_param = kwargs.get('essence_param')
317
+ eprime = kwargs.get('eprime')
318
+ eprime_solution = kwargs.get('eprime_solution')
319
+ essence_solution = kwargs.get('essence_solution')
320
+ if not all([essence_param, eprime, eprime_solution, essence_solution]):
321
+ raise ModelRegistryError(
322
+ "essence_param, eprime, eprime_solution, and essence_solution required for translate-solution"
323
+ )
324
+ cmd = [solver_config['command']] + args + [
325
+ f"--essence-param={essence_param}",
326
+ f"--eprime={eprime}",
327
+ f"--eprime-solution={eprime_solution}",
328
+ f"--essence-solution={essence_solution}"
329
+ ]
330
+
331
+ else:
332
+ raise ModelRegistryError(f"Unknown conjure action: {action}")
333
+
334
+ return cmd