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.
- solver_support/__init__.py +132 -0
- solver_support/command_builders.py +334 -0
- solver_support/file_management.py +243 -0
- solver_support/global_state.py +62 -0
- solver_support/horizon_utils.py +350 -0
- solver_support/model_registry.py +792 -0
- solver_support/objective.py +92 -0
- solver_support/run_summary.py +338 -0
- solver_support/solver_base.py +140 -0
- solver_support/solver_enhsp.py +101 -0
- solver_support/solver_fd.py +261 -0
- solver_support/solver_horizon.py +634 -0
- solver_support/solver_runner.py +702 -0
- solver_support/solver_stats.py +724 -0
- solver_support/solver_upfplan.py +168 -0
- solver_support/utilities.py +146 -0
- solver_support-0.1.1.dist-info/METADATA +120 -0
- solver_support-0.1.1.dist-info/RECORD +21 -0
- solver_support-0.1.1.dist-info/WHEEL +5 -0
- solver_support-0.1.1.dist-info/licenses/LICENSE +29 -0
- solver_support-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UPF solver implementation - the single class behind every UPF-routed model.
|
|
3
|
+
|
|
4
|
+
UpfPlanSolver drives the consumer's UPF planner CLI through run_phase, so it
|
|
5
|
+
inherits the timeout/signal handling and SIGTERM summary generation every solver
|
|
6
|
+
gets. The CLI name is not baked in: the consumer supplies it (``upfplan_command=``
|
|
7
|
+
or by overriding the ``UPFPLAN_COMMAND`` class attribute). It reaches a UPF engine
|
|
8
|
+
by the two routes that CLI supports, selected here by ``build_mode``:
|
|
9
|
+
|
|
10
|
+
- build_mode is None (PDDL models, e.g. pddlnoaxiom): pass ``--domain`` so the
|
|
11
|
+
CLI reads the PDDL through unified_planning's PDDLReader and solves with the
|
|
12
|
+
engine chosen by the effective ``--tune`` (enhsp-opt / fast-downward / symk;
|
|
13
|
+
see ModelRegistry.get_upf_engine). The same class therefore backs the upfenhsp,
|
|
14
|
+
upffd and upfsymk solvers. The CLI streams that engine's progress live,
|
|
15
|
+
so run_phase tees it to the terminal the way fd/symk do.
|
|
16
|
+
|
|
17
|
+
- build_mode is 'upf'/'upflocal' (constructed models): pass ``--build`` so the
|
|
18
|
+
CLI builds the Problem in Python and solves it with the tune's engine (Fast
|
|
19
|
+
Downward blind by default; upfenhsp defaults to the metric-optimal enhsp-opt).
|
|
20
|
+
This replaces the former UPFSolver/UPFLocalSolver classes; that path buffers the
|
|
21
|
+
engine output (it surfaces only once solving finishes) and emits a scored plan.
|
|
22
|
+
|
|
23
|
+
Optimality is a ``--tune`` dial, not a solver name: every upf* solver optimises the
|
|
24
|
+
score by default (upffd/upfsymk are cost-optimal by construction, upfenhsp defaults
|
|
25
|
+
to enhsp-opt), and satisficing ENHSP is opt-in via ``--tune satisficing``.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Optional
|
|
30
|
+
|
|
31
|
+
from .solver_base import Solver
|
|
32
|
+
from .global_state import global_state
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class UpfPlanSolver(Solver):
|
|
36
|
+
"""Solver that runs a model through a UPF engine (PDDL or constructed)."""
|
|
37
|
+
|
|
38
|
+
# CLI entry point that does the UPF PDDLReader/construction + OneshotPlanner
|
|
39
|
+
# solve. No generic default (the CLI is consumer-provided); a consumer sets it
|
|
40
|
+
# per instance via ``upfplan_command=`` or by overriding this class attribute.
|
|
41
|
+
UPFPLAN_COMMAND: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
def __init__(self, solver_runner, solver_name: str = "upfenhsp",
|
|
44
|
+
build_mode: Optional[str] = None,
|
|
45
|
+
upfplan_command: Optional[str] = None):
|
|
46
|
+
super().__init__(solver_runner)
|
|
47
|
+
self.solver_name = solver_name
|
|
48
|
+
# None -> read PDDL via --domain; 'upf'/'upflocal' -> construct via --build.
|
|
49
|
+
self.build_mode = build_mode
|
|
50
|
+
# The consumer's UPF planner CLI; an explicit argument wins, else the
|
|
51
|
+
# class-attribute override, else unset.
|
|
52
|
+
self.upfplan_command = upfplan_command or self.UPFPLAN_COMMAND
|
|
53
|
+
|
|
54
|
+
def _tune_fd_search(self, registry, tune, engine_name) -> Optional[str]:
|
|
55
|
+
"""FD search config for the active tune - only for fast-downward engines
|
|
56
|
+
(enhsp/symk ignore it, and get_upf_fd_search returns None for them anyway)."""
|
|
57
|
+
if not (engine_name and engine_name.startswith('fast-downward')):
|
|
58
|
+
return None
|
|
59
|
+
try:
|
|
60
|
+
return registry.get_upf_fd_search(self.solver_name, tune)
|
|
61
|
+
except Exception:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def solve(self, param_file: Path, **kwargs) -> Optional[Path]:
|
|
65
|
+
"""
|
|
66
|
+
Solve the problem in ``param_file`` through a UPF engine.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
param_file: generated PDDL problem (PDDL models) or .upf instance
|
|
70
|
+
(constructed upf/upflocal models)
|
|
71
|
+
**kwargs: model_path (PDDL domain) - required unless build_mode is set
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Path to the solution file if a plan was found, else None.
|
|
75
|
+
"""
|
|
76
|
+
# Effective --tune: an explicit --tune wins, else the model's pinned default.
|
|
77
|
+
# It selects the UPF engine (the optimality mode) the way it selects
|
|
78
|
+
# savilerow's sat_args or the direct enhsp -planner (see get_upf_engine).
|
|
79
|
+
# upfenhsp defaults to the metric-optimal engine so a plain run maximises the
|
|
80
|
+
# score, matching the cost-optimal upffd/upfsymk; --tune satisficing opts out.
|
|
81
|
+
if not self.upfplan_command:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
"no UPF planner command configured; pass upfplan_command= to "
|
|
84
|
+
"UpfPlanSolver or set its UPFPLAN_COMMAND to the consumer's UPF CLI")
|
|
85
|
+
|
|
86
|
+
registry = global_state.model_registry
|
|
87
|
+
tune = self._effective_tune()
|
|
88
|
+
|
|
89
|
+
if self.build_mode:
|
|
90
|
+
# Constructed models build the Problem in Python (no PDDL domain). The
|
|
91
|
+
# engine comes from the effective tune, so the axiom-free builds
|
|
92
|
+
# (upflocalnoaxiom) can be solved by an axiom-rejecting engine like ENHSP,
|
|
93
|
+
# not just Fast Downward.
|
|
94
|
+
try:
|
|
95
|
+
engine_name = registry.get_upf_engine(self.solver_name, tune)
|
|
96
|
+
except Exception:
|
|
97
|
+
engine_name = None
|
|
98
|
+
engine_name = engine_name or 'fast-downward'
|
|
99
|
+
cmd = [self.upfplan_command, "--build", self.build_mode,
|
|
100
|
+
"-s", engine_name, str(param_file)]
|
|
101
|
+
# For the fast-downward engine the tune also picks the FD search config
|
|
102
|
+
# (upffd: optimal blind vs satisficing greedy); forward it so --build
|
|
103
|
+
# honours --tune instead of the search compile_and_solve would pin.
|
|
104
|
+
fd_search = self._tune_fd_search(registry, tune, engine_name)
|
|
105
|
+
if fd_search:
|
|
106
|
+
cmd += ["--fd-search", fd_search]
|
|
107
|
+
self.logger.info(f"Building UPF model '{self.build_mode}' (engine: {engine_name})")
|
|
108
|
+
else:
|
|
109
|
+
model_path = kwargs.get('model_path')
|
|
110
|
+
if not model_path:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
"model_path (PDDL domain) is required for the upfplan solver")
|
|
113
|
+
|
|
114
|
+
# UPF engine name comes from the effective tune (upfenhsp: optimal ->
|
|
115
|
+
# enhsp-opt by default, satisficing -> enhsp, anytime -> enhsp-any),
|
|
116
|
+
# defaulting to the bare 'enhsp' engine if the solver defines none.
|
|
117
|
+
try:
|
|
118
|
+
engine_name = registry.get_upf_engine(self.solver_name, tune)
|
|
119
|
+
except Exception:
|
|
120
|
+
engine_name = None
|
|
121
|
+
engine_name = engine_name or 'enhsp'
|
|
122
|
+
|
|
123
|
+
cmd = [
|
|
124
|
+
self.upfplan_command,
|
|
125
|
+
"--domain", str(model_path),
|
|
126
|
+
"--engine", engine_name,
|
|
127
|
+
]
|
|
128
|
+
# Optional fast-downward search config from the active tune (upffd: optimal
|
|
129
|
+
# blind vs satisficing greedy), so the upffd config both measures pure UPF
|
|
130
|
+
# overhead against the direct fd path (blind) and exposes the search dial.
|
|
131
|
+
fd_search = self._tune_fd_search(registry, tune, engine_name)
|
|
132
|
+
if fd_search:
|
|
133
|
+
cmd += ["--fd-search", fd_search]
|
|
134
|
+
cmd.append(str(param_file))
|
|
135
|
+
|
|
136
|
+
self.logger.info(f"Running UPF engine '{engine_name}'")
|
|
137
|
+
|
|
138
|
+
# Forward debug so the upfplan subprocess's diagnostics (the compilation
|
|
139
|
+
# trace, the dumped compiled problem) print on its stderr under the
|
|
140
|
+
# consumer's debug flag (its `-d` / `--debug`) - where run_phase
|
|
141
|
+
# captures/tees them, and clear of the plan-only stdout the .solution is
|
|
142
|
+
# built from.
|
|
143
|
+
if global_state.debug:
|
|
144
|
+
cmd.append("--debug")
|
|
145
|
+
|
|
146
|
+
result = self.runner.run_phase("solving", cmd, expected_outputs=[])
|
|
147
|
+
|
|
148
|
+
if result.interrupted:
|
|
149
|
+
self.logger.warning("upfplan solver was interrupted")
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
# Timeout with no plan is a timeout (mirrors UPF/pddl handling).
|
|
153
|
+
self._handle_timeout_result(result, "upfplan solver")
|
|
154
|
+
|
|
155
|
+
if result.success and result.stdout and result.stdout.strip():
|
|
156
|
+
solution_file = self._generate_solution_file_path()
|
|
157
|
+
solution_file.write_text(result.stdout)
|
|
158
|
+
if self.runner.summary.phases:
|
|
159
|
+
self.runner.summary.phases[-1].output_files = [str(solution_file)]
|
|
160
|
+
self.logger.info(f"upfplan solution written to {solution_file}")
|
|
161
|
+
return solution_file
|
|
162
|
+
|
|
163
|
+
if result.success:
|
|
164
|
+
self.logger.warning("upfplan solver produced no plan")
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
self.logger.warning(f"upfplan solver failed: {result.stderr}")
|
|
168
|
+
return None
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'''
|
|
2
|
+
utility functions
|
|
3
|
+
'''
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import logging
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
# Logger name that warn() routes through. Defaults to the package logger so the
|
|
12
|
+
# core is usable standalone; a consumer that configures its own named logger (with
|
|
13
|
+
# file/console handlers) can point warn() at it via set_warn_logger_name() so these
|
|
14
|
+
# warnings share the consumer's handlers. Read at call time, so wiring may happen
|
|
15
|
+
# any time before the first warn().
|
|
16
|
+
_warn_logger_name = 'solver_support'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def set_warn_logger_name(name: str) -> None:
|
|
20
|
+
"""Route warn() through the logger called ``name``.
|
|
21
|
+
|
|
22
|
+
Consumers call this once (e.g. at import) to funnel core warnings into their
|
|
23
|
+
own configured logger, preserving pre-extraction behaviour where warnings
|
|
24
|
+
reached the runner's log file/console handlers.
|
|
25
|
+
"""
|
|
26
|
+
global _warn_logger_name
|
|
27
|
+
_warn_logger_name = name
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def warn(s, verbose_only: bool = False):
|
|
31
|
+
"""
|
|
32
|
+
Print a warning message to stderr.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
s: Warning message
|
|
36
|
+
verbose_only: If True, only print when verbose mode is enabled (but still logged to file)
|
|
37
|
+
"""
|
|
38
|
+
# Get the logger for proper structured logging (name is consumer-configurable)
|
|
39
|
+
logger = logging.getLogger(_warn_logger_name)
|
|
40
|
+
|
|
41
|
+
if verbose_only:
|
|
42
|
+
# Log at INFO level: will appear in log file but not console in non-verbose mode
|
|
43
|
+
# (console handler is set to WARNING level in non-verbose mode)
|
|
44
|
+
logger.info(s)
|
|
45
|
+
else:
|
|
46
|
+
# Regular warning: always print to stderr
|
|
47
|
+
logger.warning(s)
|
|
48
|
+
print('Warning: %s, continuing' % s, file=sys.stderr)
|
|
49
|
+
|
|
50
|
+
def arrayify(d):
|
|
51
|
+
'''
|
|
52
|
+
Make an array from a dict with contiguous string-numeric keys.
|
|
53
|
+
'''
|
|
54
|
+
return [ d[i] for i in sorted(d.keys(), key=int) ]
|
|
55
|
+
|
|
56
|
+
def indices(d):
|
|
57
|
+
'''
|
|
58
|
+
Return properly ordered array indices for pseudo-array dict.
|
|
59
|
+
'''
|
|
60
|
+
return [ i for i in sorted(d.keys(), key=int) ]
|
|
61
|
+
|
|
62
|
+
def format_timestamp() -> str:
|
|
63
|
+
'''
|
|
64
|
+
Generate timestamp format YYYYMMDDHHMMSS for use in filenames.
|
|
65
|
+
'''
|
|
66
|
+
return datetime.now().strftime("%Y%m%d%H%M%S")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class BrokenPipeHandler(logging.StreamHandler):
|
|
70
|
+
"""
|
|
71
|
+
Custom logging handler that gracefully handles broken pipe errors.
|
|
72
|
+
|
|
73
|
+
This is needed when piping output to commands like 'head' that may
|
|
74
|
+
close the pipe early, causing BrokenPipeError exceptions.
|
|
75
|
+
"""
|
|
76
|
+
def emit(self, record):
|
|
77
|
+
try:
|
|
78
|
+
super().emit(record)
|
|
79
|
+
except BrokenPipeError:
|
|
80
|
+
# Ignore broken pipe errors (e.g., when piping to head)
|
|
81
|
+
pass
|
|
82
|
+
except Exception:
|
|
83
|
+
# Silently ignore other exceptions to prevent recursion
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def setup_logger(name: str, log_file: Optional[Path] = None, log_level: int = logging.INFO) -> logging.Logger:
|
|
88
|
+
"""
|
|
89
|
+
Set up a logger with consistent formatting and broken pipe handling.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
name: Logger name
|
|
93
|
+
log_file: Optional file path for logging (if None, only console logging)
|
|
94
|
+
log_level: Console logging level (file always captures everything)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Configured logger instance
|
|
98
|
+
"""
|
|
99
|
+
logger = logging.getLogger(name)
|
|
100
|
+
logger.setLevel(logging.DEBUG) # Logger itself should allow all levels
|
|
101
|
+
|
|
102
|
+
# Clear any existing handlers
|
|
103
|
+
logger.handlers.clear()
|
|
104
|
+
|
|
105
|
+
# Shared formatter for both console and file output
|
|
106
|
+
log_format = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y%m%d %H:%M:%S')
|
|
107
|
+
|
|
108
|
+
# Console handler with broken pipe handling - respects user verbosity
|
|
109
|
+
console_handler = BrokenPipeHandler(sys.stdout)
|
|
110
|
+
console_handler.setLevel(log_level) # Console respects user's verbosity setting
|
|
111
|
+
console_handler.setFormatter(log_format)
|
|
112
|
+
logger.addHandler(console_handler)
|
|
113
|
+
|
|
114
|
+
# File handler if log file specified
|
|
115
|
+
if log_file:
|
|
116
|
+
file_handler = logging.FileHandler(log_file, mode='a')
|
|
117
|
+
file_handler.setLevel(logging.DEBUG) # File should capture everything
|
|
118
|
+
file_handler.setFormatter(log_format)
|
|
119
|
+
logger.addHandler(file_handler)
|
|
120
|
+
|
|
121
|
+
return logger
|
|
122
|
+
|
|
123
|
+
def relativize_path(path: str, output_dir: Path) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Convert absolute paths to relative paths based on output directory.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
path: File path (absolute or relative)
|
|
129
|
+
output_dir: Output directory for relativization
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Path relative to output directory, or original path if conversion fails
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
path_obj = Path(path)
|
|
136
|
+
if path_obj.is_absolute():
|
|
137
|
+
# Convert to relative path from output directory
|
|
138
|
+
relative_path = path_obj.relative_to(output_dir)
|
|
139
|
+
return str(relative_path)
|
|
140
|
+
else:
|
|
141
|
+
# Already relative, return as-is
|
|
142
|
+
return path
|
|
143
|
+
except (ValueError, OSError):
|
|
144
|
+
# If relative_to fails (path not under output_dir), return original
|
|
145
|
+
return path
|
|
146
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: solver-support
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Generic Conjure / Savile Row / Fast Downward / SymK / ENHSP / UPF solver pipeline
|
|
5
|
+
Author: András Salamon
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/ott2/solver-support
|
|
8
|
+
Project-URL: Repository, https://github.com/ott2/solver-support
|
|
9
|
+
Project-URL: Issues, https://github.com/ott2/solver-support/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: psutil>=5.8.0
|
|
26
|
+
Requires-Dist: PyYAML>=5.4
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# solver-support
|
|
30
|
+
|
|
31
|
+
A generic, domain-agnostic pipeline for running constraint-modelling and
|
|
32
|
+
planning solvers: **Conjure** / **Savile Row** (+ SAT/CP backends), **Fast
|
|
33
|
+
Downward**, **SymK**, **ENHSP**, and **UPF**-constructed models. It handles the
|
|
34
|
+
mechanics that every such run needs — building solver command lines from a model
|
|
35
|
+
registry, spawning subprocesses with timeouts and clean teardown, naming the
|
|
36
|
+
per-run files, parsing solver output into a uniform timing / cost-layer model,
|
|
37
|
+
and driving the horizon (increasing-difficulty) loop.
|
|
38
|
+
|
|
39
|
+
It carries **no** notion of any particular problem domain or objective. A
|
|
40
|
+
consumer supplies:
|
|
41
|
+
|
|
42
|
+
- its own **model registry** (a `models.yaml`) via a config path — the loader is
|
|
43
|
+
generic, the model content is the consumer's;
|
|
44
|
+
- its **scoring / objective strategy** via injection — the core reports the raw
|
|
45
|
+
objective a solver emits and lets the consumer interpret it (is-this-an-
|
|
46
|
+
optimisation-model, how to read the objective, any reporting multiplier).
|
|
47
|
+
|
|
48
|
+
## Status
|
|
49
|
+
|
|
50
|
+
**Alpha.** The pipeline is in day-to-day use by two consumers and is covered by
|
|
51
|
+
its own test suite, but this is the first public release and the API may still
|
|
52
|
+
shift between 0.x versions.
|
|
53
|
+
|
|
54
|
+
The package ships **unwired**: a bare `import solver_support` gives a pipeline
|
|
55
|
+
that resolves no model registry and interprets no objective, because it has no
|
|
56
|
+
way to guess either. A consumer registers its scoring, registry paths, warning
|
|
57
|
+
logger and UPF CLI once, before first use.
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install solver-support
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Requires Python 3.9 or newer (3.9.6 is the system Python on macOS, and is
|
|
66
|
+
supported deliberately). The only runtime dependencies are `psutil` and `PyYAML`.
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
See [`docs/injection-api.md`](https://github.com/ott2/solver-support/blob/main/docs/injection-api.md)
|
|
71
|
+
for the four injection points a consumer wires — model-registry paths, objective
|
|
72
|
+
extraction, warning logger, UPF CLI — with a worked example.
|
|
73
|
+
|
|
74
|
+
## Docs
|
|
75
|
+
|
|
76
|
+
- [`injection-api.md`](https://github.com/ott2/solver-support/blob/main/docs/injection-api.md)
|
|
77
|
+
— the four seams a consumer wires.
|
|
78
|
+
- [`solver-runner.md`](https://github.com/ott2/solver-support/blob/main/docs/solver-runner.md)
|
|
79
|
+
— running a solver as a subprocess: `SolverResult`, timeout vs duration, phases.
|
|
80
|
+
- [`timing-model.md`](https://github.com/ott2/solver-support/blob/main/docs/timing-model.md)
|
|
81
|
+
— how solver timing is recorded, how the backends map onto one shape, and the
|
|
82
|
+
invariants parsers rely on.
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
Clone and install editable. A consumer under development alongside it is
|
|
87
|
+
normally installed into the same virtualenv, so both are importable and edits to
|
|
88
|
+
either take effect immediately:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
git clone https://github.com/ott2/solver-support
|
|
92
|
+
pip install -e solver-support
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Tests
|
|
96
|
+
|
|
97
|
+
`pytest tests/` — the core's own suite, and the home for solver-level
|
|
98
|
+
development. It runs against `solver_support` alone: no consumer need be
|
|
99
|
+
installed, and none is imported.
|
|
100
|
+
|
|
101
|
+
Since the core ships unwired, a test that needs a seam wired wires a stand-in
|
|
102
|
+
(`tests/conftest.py`):
|
|
103
|
+
|
|
104
|
+
| fixture | supplies |
|
|
105
|
+
|---------|----------|
|
|
106
|
+
| `fixture_registry` | a `ModelRegistry` over `tests/fixtures/models.yaml`, this suite's own synthetic config — never a consumer's |
|
|
107
|
+
| `wired_stdout_score` | a consumer-style `Score: N` extractor, so a score reaches `solver_stats` |
|
|
108
|
+
| `wired_timeout_score` | a consumer-style extractor for a salvaged timeout solution |
|
|
109
|
+
|
|
110
|
+
Wiring a fixture config rather than mocking the registry keeps the real loader
|
|
111
|
+
and accessors in the path. Anything a test needs from the registry goes in
|
|
112
|
+
`tests/fixtures/models.yaml`, keeping its shape faithful to a real consumer
|
|
113
|
+
config — that shape is the contract the loader implements.
|
|
114
|
+
|
|
115
|
+
## Licence
|
|
116
|
+
|
|
117
|
+
BSD-3-Clause — see
|
|
118
|
+
[`LICENSE`](https://github.com/ott2/solver-support/blob/main/LICENSE).
|
|
119
|
+
|
|
120
|
+
By András Salamon, with Claude Opus 4.8 and 5.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
solver_support/__init__.py,sha256=b227_QtUysX4TjTJ3479hJ7TxtPb0XOJJ3MFAawHgh8,3824
|
|
2
|
+
solver_support/command_builders.py,sha256=i9inzQg8O_bQdg67Phx3fSX6mPIDZG8stH6nzqoRLBI,15920
|
|
3
|
+
solver_support/file_management.py,sha256=3rMyl7kSVFDTSbbz3plx_ZLJHxAtt3nDYcJMK8GwF_c,8756
|
|
4
|
+
solver_support/global_state.py,sha256=yf2v6SeXKlSXp6Sr3cTY2SQILJaBH9wiMOTOZbnOQNI,2132
|
|
5
|
+
solver_support/horizon_utils.py,sha256=FvdLQmUmfWOcy07KRgkmdmSusjtEmLcCkMyikKBk2JM,16070
|
|
6
|
+
solver_support/model_registry.py,sha256=DeHofjm5YgLHaNrE_98kx4uj3e0Jw4KUIXjZ81x6ms4,31382
|
|
7
|
+
solver_support/objective.py,sha256=BciFu6AJNhMakXesQ4DXE7k2w1O6xB6ISHGogM5GCVk,4161
|
|
8
|
+
solver_support/run_summary.py,sha256=IV_tY8j49gEsJsTQ8e-S5gaLIRJ5ssUQ0Tk-wYY6pW0,16566
|
|
9
|
+
solver_support/solver_base.py,sha256=-9agNTxOYzSuhedodmZ7-A6j1SibahCFLbGrGsfKam8,4878
|
|
10
|
+
solver_support/solver_enhsp.py,sha256=3EyU0xtkyqpSDdEyNDeqpJMomfBds9QceiR1FLyKEZI,4289
|
|
11
|
+
solver_support/solver_fd.py,sha256=8I0cmCB-p0q63yokX01D527lLIBtVi-_Fo-E2PxXCpk,12325
|
|
12
|
+
solver_support/solver_horizon.py,sha256=-ob2Q6ifhN3aknZWm-SqgquivIa_ZYkK7aPoMEeFCAw,30538
|
|
13
|
+
solver_support/solver_runner.py,sha256=yFw35qZGyvjfBlC6og2Wno7eN8zRkVKv1stSoiCVPeg,30363
|
|
14
|
+
solver_support/solver_stats.py,sha256=kIOeebbicXcVQooJgjMMS0PHtB1LTFTUbJqSn0lDpkU,34910
|
|
15
|
+
solver_support/solver_upfplan.py,sha256=F4y1urb1KHAjNN3OcrqfKfMAXW646rg7EtVROgFvU6M,8250
|
|
16
|
+
solver_support/utilities.py,sha256=Aan8L-OTSzEpNf8-krrP1cfh5FMUmYDGkKn4KRJQfqM,4920
|
|
17
|
+
solver_support-0.1.1.dist-info/licenses/LICENSE,sha256=xaDouhdJlWIrxBMOcomqpCBqJsc4MgHFknB4l9yT6QM,1523
|
|
18
|
+
solver_support-0.1.1.dist-info/METADATA,sha256=_2Ffip1nOU9l_X58tjkl86QrAzayb3Y4strQOTU2DsE,5053
|
|
19
|
+
solver_support-0.1.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
20
|
+
solver_support-0.1.1.dist-info/top_level.txt,sha256=GmfATXT2pguSk0_6_J-3chal_ql5vxBmXGacUWTBe5w,15
|
|
21
|
+
solver_support-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, András Salamon
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
solver_support
|