solver-support 0.1.1__tar.gz
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-0.1.1/LICENSE +29 -0
- solver_support-0.1.1/MANIFEST.in +18 -0
- solver_support-0.1.1/PKG-INFO +120 -0
- solver_support-0.1.1/README.md +92 -0
- solver_support-0.1.1/docs/injection-api.md +137 -0
- solver_support-0.1.1/docs/solver-runner.md +134 -0
- solver_support-0.1.1/docs/timing-model.md +164 -0
- solver_support-0.1.1/pyproject.toml +88 -0
- solver_support-0.1.1/setup.cfg +4 -0
- solver_support-0.1.1/solver_support/__init__.py +132 -0
- solver_support-0.1.1/solver_support/command_builders.py +334 -0
- solver_support-0.1.1/solver_support/file_management.py +243 -0
- solver_support-0.1.1/solver_support/global_state.py +62 -0
- solver_support-0.1.1/solver_support/horizon_utils.py +350 -0
- solver_support-0.1.1/solver_support/model_registry.py +792 -0
- solver_support-0.1.1/solver_support/objective.py +92 -0
- solver_support-0.1.1/solver_support/run_summary.py +338 -0
- solver_support-0.1.1/solver_support/solver_base.py +140 -0
- solver_support-0.1.1/solver_support/solver_enhsp.py +101 -0
- solver_support-0.1.1/solver_support/solver_fd.py +261 -0
- solver_support-0.1.1/solver_support/solver_horizon.py +634 -0
- solver_support-0.1.1/solver_support/solver_runner.py +702 -0
- solver_support-0.1.1/solver_support/solver_stats.py +724 -0
- solver_support-0.1.1/solver_support/solver_upfplan.py +168 -0
- solver_support-0.1.1/solver_support/utilities.py +146 -0
- solver_support-0.1.1/solver_support.egg-info/PKG-INFO +120 -0
- solver_support-0.1.1/solver_support.egg-info/SOURCES.txt +40 -0
- solver_support-0.1.1/solver_support.egg-info/dependency_links.txt +1 -0
- solver_support-0.1.1/solver_support.egg-info/not-zip-safe +1 -0
- solver_support-0.1.1/solver_support.egg-info/requires.txt +2 -0
- solver_support-0.1.1/solver_support.egg-info/top_level.txt +1 -0
- solver_support-0.1.1/tests/conftest.py +109 -0
- solver_support-0.1.1/tests/fixtures/models.yaml +57 -0
- solver_support-0.1.1/tests/test_helpers.py +27 -0
- solver_support-0.1.1/tests/test_horizon_utils.py +360 -0
- solver_support-0.1.1/tests/test_import.py +103 -0
- solver_support-0.1.1/tests/test_run_summary.py +655 -0
- solver_support-0.1.1/tests/test_run_summary_success.py +129 -0
- solver_support-0.1.1/tests/test_solution_naming.py +117 -0
- solver_support-0.1.1/tests/test_solver_runner.py +870 -0
- solver_support-0.1.1/tests/test_solver_stats.py +998 -0
- solver_support-0.1.1/tests/test_solvers.py +548 -0
|
@@ -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,18 @@
|
|
|
1
|
+
# What lands in the SOURCE distribution (`python -m build --sdist`).
|
|
2
|
+
#
|
|
3
|
+
# This does not affect the wheel -- the artefact `pip install solver-support`
|
|
4
|
+
# actually fetches -- which carries only the solver_support package and its
|
|
5
|
+
# metadata, and is already minimal. It governs the source release, which should
|
|
6
|
+
# be self-contained: buildable, and testable by anyone repackaging it (a distro
|
|
7
|
+
# or conda-forge recipe typically runs the suite at build time).
|
|
8
|
+
#
|
|
9
|
+
# setuptools' defaults already pick up README.md, LICENSE, pyproject.toml and
|
|
10
|
+
# tests/test_*.py -- but NOT tests/conftest.py, whose name does not match
|
|
11
|
+
# `test*`, nor the YAML fixture the registry tests load. The sdist therefore
|
|
12
|
+
# shipped a test suite that could not run at all: every test needing a fixture
|
|
13
|
+
# errored on a missing one. These two lines are what make it coherent.
|
|
14
|
+
recursive-include tests *.py *.yaml
|
|
15
|
+
|
|
16
|
+
# The docs the README points at, so the source release does not dangle links to
|
|
17
|
+
# files it does not carry.
|
|
18
|
+
recursive-include docs *.md
|
|
@@ -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,92 @@
|
|
|
1
|
+
# solver-support
|
|
2
|
+
|
|
3
|
+
A generic, domain-agnostic pipeline for running constraint-modelling and
|
|
4
|
+
planning solvers: **Conjure** / **Savile Row** (+ SAT/CP backends), **Fast
|
|
5
|
+
Downward**, **SymK**, **ENHSP**, and **UPF**-constructed models. It handles the
|
|
6
|
+
mechanics that every such run needs — building solver command lines from a model
|
|
7
|
+
registry, spawning subprocesses with timeouts and clean teardown, naming the
|
|
8
|
+
per-run files, parsing solver output into a uniform timing / cost-layer model,
|
|
9
|
+
and driving the horizon (increasing-difficulty) loop.
|
|
10
|
+
|
|
11
|
+
It carries **no** notion of any particular problem domain or objective. A
|
|
12
|
+
consumer supplies:
|
|
13
|
+
|
|
14
|
+
- its own **model registry** (a `models.yaml`) via a config path — the loader is
|
|
15
|
+
generic, the model content is the consumer's;
|
|
16
|
+
- its **scoring / objective strategy** via injection — the core reports the raw
|
|
17
|
+
objective a solver emits and lets the consumer interpret it (is-this-an-
|
|
18
|
+
optimisation-model, how to read the objective, any reporting multiplier).
|
|
19
|
+
|
|
20
|
+
## Status
|
|
21
|
+
|
|
22
|
+
**Alpha.** The pipeline is in day-to-day use by two consumers and is covered by
|
|
23
|
+
its own test suite, but this is the first public release and the API may still
|
|
24
|
+
shift between 0.x versions.
|
|
25
|
+
|
|
26
|
+
The package ships **unwired**: a bare `import solver_support` gives a pipeline
|
|
27
|
+
that resolves no model registry and interprets no objective, because it has no
|
|
28
|
+
way to guess either. A consumer registers its scoring, registry paths, warning
|
|
29
|
+
logger and UPF CLI once, before first use.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install solver-support
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Requires Python 3.9 or newer (3.9.6 is the system Python on macOS, and is
|
|
38
|
+
supported deliberately). The only runtime dependencies are `psutil` and `PyYAML`.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
See [`docs/injection-api.md`](https://github.com/ott2/solver-support/blob/main/docs/injection-api.md)
|
|
43
|
+
for the four injection points a consumer wires — model-registry paths, objective
|
|
44
|
+
extraction, warning logger, UPF CLI — with a worked example.
|
|
45
|
+
|
|
46
|
+
## Docs
|
|
47
|
+
|
|
48
|
+
- [`injection-api.md`](https://github.com/ott2/solver-support/blob/main/docs/injection-api.md)
|
|
49
|
+
— the four seams a consumer wires.
|
|
50
|
+
- [`solver-runner.md`](https://github.com/ott2/solver-support/blob/main/docs/solver-runner.md)
|
|
51
|
+
— running a solver as a subprocess: `SolverResult`, timeout vs duration, phases.
|
|
52
|
+
- [`timing-model.md`](https://github.com/ott2/solver-support/blob/main/docs/timing-model.md)
|
|
53
|
+
— how solver timing is recorded, how the backends map onto one shape, and the
|
|
54
|
+
invariants parsers rely on.
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
Clone and install editable. A consumer under development alongside it is
|
|
59
|
+
normally installed into the same virtualenv, so both are importable and edits to
|
|
60
|
+
either take effect immediately:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
git clone https://github.com/ott2/solver-support
|
|
64
|
+
pip install -e solver-support
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Tests
|
|
68
|
+
|
|
69
|
+
`pytest tests/` — the core's own suite, and the home for solver-level
|
|
70
|
+
development. It runs against `solver_support` alone: no consumer need be
|
|
71
|
+
installed, and none is imported.
|
|
72
|
+
|
|
73
|
+
Since the core ships unwired, a test that needs a seam wired wires a stand-in
|
|
74
|
+
(`tests/conftest.py`):
|
|
75
|
+
|
|
76
|
+
| fixture | supplies |
|
|
77
|
+
|---------|----------|
|
|
78
|
+
| `fixture_registry` | a `ModelRegistry` over `tests/fixtures/models.yaml`, this suite's own synthetic config — never a consumer's |
|
|
79
|
+
| `wired_stdout_score` | a consumer-style `Score: N` extractor, so a score reaches `solver_stats` |
|
|
80
|
+
| `wired_timeout_score` | a consumer-style extractor for a salvaged timeout solution |
|
|
81
|
+
|
|
82
|
+
Wiring a fixture config rather than mocking the registry keeps the real loader
|
|
83
|
+
and accessors in the path. Anything a test needs from the registry goes in
|
|
84
|
+
`tests/fixtures/models.yaml`, keeping its shape faithful to a real consumer
|
|
85
|
+
config — that shape is the contract the loader implements.
|
|
86
|
+
|
|
87
|
+
## Licence
|
|
88
|
+
|
|
89
|
+
BSD-3-Clause — see
|
|
90
|
+
[`LICENSE`](https://github.com/ott2/solver-support/blob/main/LICENSE).
|
|
91
|
+
|
|
92
|
+
By András Salamon, with Claude Opus 4.8 and 5.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# injection API
|
|
2
|
+
|
|
3
|
+
`solver_support` ships **unwired**: a bare `import solver_support` gives a working
|
|
4
|
+
pipeline that knows nothing about any problem domain. It records whatever
|
|
5
|
+
objective a solver emits but cannot interpret it, resolves no model registry, and
|
|
6
|
+
routes warnings to its own package logger. A **consumer** supplies the missing
|
|
7
|
+
domain policy once, at its own import time, through the injection points below.
|
|
8
|
+
|
|
9
|
+
Every seam follows the same shape: a module-level default, a `set_*` setter, and
|
|
10
|
+
a stable wrapper (or constructor argument) that reads the current value **at call
|
|
11
|
+
time**. So wiring may happen any time before first use, and the core never imports
|
|
12
|
+
the consumer — the dependency arrow points one way, consumer → core.
|
|
13
|
+
|
|
14
|
+
## the four seams
|
|
15
|
+
|
|
16
|
+
### 1. model registry paths (`solver_support.model_registry`)
|
|
17
|
+
|
|
18
|
+
The loader is generic; the model content and its packaging are the consumer's.
|
|
19
|
+
|
|
20
|
+
| setter | supplies |
|
|
21
|
+
|--------|----------|
|
|
22
|
+
| `set_default_config_path(path)` | the consumer's `models.yaml` (a `Path` / str) |
|
|
23
|
+
| `set_resource_root(root)` | root that model-file paths in the config resolve against |
|
|
24
|
+
| `set_user_config_finder(finder)` | a **callable** returning an optional per-invocation user-config `Path` |
|
|
25
|
+
|
|
26
|
+
`set_user_config_finder` takes a *finder*, not a fixed path, so cwd-relative
|
|
27
|
+
candidates bind when the registry is constructed rather than at import. Until a
|
|
28
|
+
default config path is registered, constructing `ModelRegistry()` raises
|
|
29
|
+
`ModelRegistryError` rather than guessing a consumer's config.
|
|
30
|
+
|
|
31
|
+
### 2. objective extraction (`solver_support.objective`)
|
|
32
|
+
|
|
33
|
+
The core asks for a solution's objective at a few points but does not know how a
|
|
34
|
+
domain encodes it. Register an extractor for each route the consumer uses; an
|
|
35
|
+
unregistered route yields `None` (the standalone core simply records no score).
|
|
36
|
+
|
|
37
|
+
| setter | extractor signature | used for |
|
|
38
|
+
|--------|--------------------|----------|
|
|
39
|
+
| `set_reported_score_from_solution(fn)` | `fn(solution_file) -> int \| None` | reported score read from a solution file |
|
|
40
|
+
| `set_reported_score_from_stdout(fn)` | `fn(stdout) -> int \| None` | reported score printed on solver stdout |
|
|
41
|
+
| `set_reported_score_from_timeout_solution(fn)` | `fn(solution_file) -> int \| None` | a salvaged, possibly-truncated timeout solution |
|
|
42
|
+
| `set_raw_objective_from_solution(fn)` | `fn(solution_file) -> int \| None` | the **raw** (unscaled) objective the horizon scan compares between horizons |
|
|
43
|
+
|
|
44
|
+
Keep the raw objective in model units — it drives horizon comparison — while the
|
|
45
|
+
reported extractors may apply any display scaling the consumer wants.
|
|
46
|
+
|
|
47
|
+
### 3. warning logger name (`solver_support.utilities`)
|
|
48
|
+
|
|
49
|
+
`warn()` routes through a logger named `_warn_logger_name` (default
|
|
50
|
+
`'solver_support'`). A consumer that configures its own named logger (with file /
|
|
51
|
+
console handlers) points `warn()` at it so these warnings share those handlers:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from solver_support import set_warn_logger_name
|
|
55
|
+
set_warn_logger_name('my_runner') # matches setup_logger('my_runner', ...)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 4. UPF planner CLI (`solver_support.solver_upfplan.UpfPlanSolver`)
|
|
59
|
+
|
|
60
|
+
The generic UPF driver shells out to a CLI that performs the UPF
|
|
61
|
+
PDDLReader/construction and solve. There is **no** built-in default (the CLI is
|
|
62
|
+
consumer-provided). Inject it per instance, or override the class attribute:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
UpfPlanSolver(runner, solver_name='upffd', build_mode='upf',
|
|
66
|
+
upfplan_command='my-upfplan') # per instance (preferred)
|
|
67
|
+
# or, once:
|
|
68
|
+
UpfPlanSolver.UPFPLAN_COMMAND = 'my-upfplan' # class-attribute override
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`solve()` refuses with a clear `ValueError` if no command is configured, rather
|
|
72
|
+
than shelling out to a `None`-prefixed argv.
|
|
73
|
+
|
|
74
|
+
## worked example
|
|
75
|
+
|
|
76
|
+
Wire everything once, at the consumer's own import time — in its
|
|
77
|
+
`__init__.py`, so that every entry point and every test that imports the package
|
|
78
|
+
gets an identically configured core, and nothing has to remember to wire it:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# in mypuzzle_support/__init__.py
|
|
82
|
+
|
|
83
|
+
# 1. scoring: register the objective extractors this domain uses. Grouping them
|
|
84
|
+
# behind one wire_into_core() keeps the registration next to the parsing.
|
|
85
|
+
from . import mypuzzle_scoring
|
|
86
|
+
mypuzzle_scoring.wire_into_core()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
# in mypuzzle_support/mypuzzle_scoring.py
|
|
91
|
+
import re
|
|
92
|
+
from solver_support import (
|
|
93
|
+
set_reported_score_from_stdout, set_raw_objective_from_solution,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def _score_from_stdout(stdout):
|
|
97
|
+
m = re.search(r'^Score:\s*(-?\d+)', stdout, re.MULTILINE)
|
|
98
|
+
return int(m.group(1)) if m else None
|
|
99
|
+
|
|
100
|
+
def wire_into_core():
|
|
101
|
+
set_reported_score_from_stdout(_score_from_stdout)
|
|
102
|
+
set_raw_objective_from_solution(_objective_from_solution) # model units
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
# back in __init__.py
|
|
107
|
+
|
|
108
|
+
# 2. registry packaging: where this consumer's models.yaml and model files live.
|
|
109
|
+
# A finder (not a fixed path) for the user override, so cwd-relative
|
|
110
|
+
# candidates bind when the registry is built rather than at import.
|
|
111
|
+
from pathlib import Path
|
|
112
|
+
from solver_support import (
|
|
113
|
+
set_default_config_path, set_resource_root, set_user_config_finder,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
_here = Path(__file__).parent
|
|
117
|
+
set_default_config_path(_here / 'config' / 'models.yaml')
|
|
118
|
+
set_resource_root(_here)
|
|
119
|
+
set_user_config_finder(lambda: next(
|
|
120
|
+
(p for p in (Path.cwd() / 'models.yaml',) if p.is_file()), None))
|
|
121
|
+
|
|
122
|
+
# 3. warnings: funnel the core's warn() into this consumer's configured logger,
|
|
123
|
+
# so they reach the same file and console handlers as its own messages.
|
|
124
|
+
from solver_support import set_warn_logger_name
|
|
125
|
+
set_warn_logger_name('mypuzzle_runner')
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
and inject the UPF CLI where the solver is constructed:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
UpfPlanSolver(runner, solver_name=solver, build_mode=build_mode,
|
|
132
|
+
upfplan_command='mypuzzle-upfplan')
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Each consumer registers its own scoring, its own `models.yaml`, its own logger
|
|
136
|
+
name and its own UPF CLI. Nothing above is built into the core, and the core
|
|
137
|
+
never imports the consumer.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# solver runner
|
|
2
|
+
|
|
3
|
+
`solver_runner.py` runs a solver as a subprocess and reports what happened. It
|
|
4
|
+
exists because a bare `subprocess.run` cannot answer the question the pipeline
|
|
5
|
+
actually asks — *did this solver fail, or did it run out of time, or did the user
|
|
6
|
+
stop it?* — and because a solver that is killed must not leave a process tree
|
|
7
|
+
behind. It handles:
|
|
8
|
+
|
|
9
|
+
- timeouts, with the whole process tree torn down (via `psutil`);
|
|
10
|
+
- signal handling, so an interrupted run shuts down cleanly rather than orphaning
|
|
11
|
+
a solver;
|
|
12
|
+
- separate stdout/stderr capture (the plan stays clean on stdout while an engine
|
|
13
|
+
logs its search to stderr);
|
|
14
|
+
- structured logging to console and file;
|
|
15
|
+
- phase tracking, feeding the `RunSummary` (see [`timing-model.md`](timing-model.md)).
|
|
16
|
+
|
|
17
|
+
## SolverResult
|
|
18
|
+
|
|
19
|
+
The complete result of one execution — the raw data plus the status properties
|
|
20
|
+
the pipeline branches on.
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
SolverResult(command, returncode, stdout, stderr, duration,
|
|
24
|
+
timed_out=False, interrupted=False)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| field | meaning |
|
|
28
|
+
|-------|---------|
|
|
29
|
+
| `command: List[str]` | the command and arguments that ran |
|
|
30
|
+
| `returncode: int` | process return code (0 for success) |
|
|
31
|
+
| `stdout: str` / `stderr: str` | captured output, kept separate |
|
|
32
|
+
| `duration: float` | **actual** seconds the command ran |
|
|
33
|
+
| `timed_out: bool` | it was killed for exceeding the timeout |
|
|
34
|
+
| `interrupted: bool` | it was stopped by a signal |
|
|
35
|
+
|
|
36
|
+
### timeout vs duration
|
|
37
|
+
|
|
38
|
+
Three easily-confused numbers:
|
|
39
|
+
|
|
40
|
+
- `default_timeout` (a `SolverRunner` parameter) — the *limit*;
|
|
41
|
+
- `duration` (a `SolverResult` field) — the *actual* time it ran, whether it
|
|
42
|
+
finished or was killed;
|
|
43
|
+
- `timed_out` (a `SolverResult` field) — whether that limit was hit.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
runner = SolverRunner(default_timeout=10.0)
|
|
47
|
+
|
|
48
|
+
runner.run(['sleep', '3']) # duration ~3.0, timed_out False, returncode 0
|
|
49
|
+
runner.run(['sleep', '15']) # duration ~10.0, timed_out True, returncode -1
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`duration` is ground truth for the phase, and every finer-grained solver time is
|
|
53
|
+
a component of it — see the invariants in [`timing-model.md`](timing-model.md).
|
|
54
|
+
|
|
55
|
+
### properties
|
|
56
|
+
|
|
57
|
+
- `success: bool` — completed with return code 0, no timeout, no interruption.
|
|
58
|
+
- `failed: bool` — the inverse; useful as a direct `if` on a run.
|
|
59
|
+
|
|
60
|
+
Note that `failed` covers three *different* outcomes, and the pipeline usually
|
|
61
|
+
cares which: check `timed_out` and `interrupted` before treating a run as a
|
|
62
|
+
genuine solver failure. A timed-out anytime solver may still have banked a
|
|
63
|
+
solution worth keeping.
|
|
64
|
+
|
|
65
|
+
The runner applies that rule to its own logging: a timeout or an interrupt is
|
|
66
|
+
recorded at INFO (`Command did not complete`), and only a solver that ran and
|
|
67
|
+
exited non-zero is logged as `Command failed` at ERROR. A launch failure logs its
|
|
68
|
+
own error before returning. So a consumer whose timed-out run *is* the result -
|
|
69
|
+
a benchmark baseline, say - gets no spurious error rows.
|
|
70
|
+
|
|
71
|
+
`run_phase` carries the same distinction into the persisted summary: the phase
|
|
72
|
+
records `returncode`, `timed_out` and `launch_failed` alongside `success`, so the
|
|
73
|
+
outcome can be reconstructed from a saved run without re-reading the log.
|
|
74
|
+
|
|
75
|
+
- `__str__()` renders `SolverResult({command}) -> {STATUS} (rc={returncode},
|
|
76
|
+
t={duration}s)` with `STATUS` one of `SUCCESS` / `FAILED` / `TIMEOUT` /
|
|
77
|
+
`INTERRUPTED`:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
SolverResult(echo hello) -> SUCCESS (rc=0, t=0.01s)
|
|
81
|
+
SolverResult(sleep 10) -> TIMEOUT (rc=-1, t=5.00s)
|
|
82
|
+
SolverResult(ls /nonexistent) -> FAILED (rc=1, t=0.01s)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## usage
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from solver_support import SolverRunner
|
|
89
|
+
|
|
90
|
+
runner = SolverRunner(default_timeout=30.0)
|
|
91
|
+
result = runner.run(['savilerow', 'model.eprime', 'problem.param'])
|
|
92
|
+
|
|
93
|
+
if result.timed_out:
|
|
94
|
+
print(f"solver timed out after {result.duration:.1f}s")
|
|
95
|
+
elif result.interrupted:
|
|
96
|
+
print("solver was interrupted")
|
|
97
|
+
elif result.failed:
|
|
98
|
+
print(f"solver failed with return code {result.returncode}: {result.stderr}")
|
|
99
|
+
else:
|
|
100
|
+
print(f"solved in {result.duration:.2f}s")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Two variants of the same call:
|
|
104
|
+
|
|
105
|
+
- `run_phase(phase_name, command, expected_outputs=..., horizon_number=...)` —
|
|
106
|
+
what the pipeline uses. It runs the command *and* records a `PhaseResult` on
|
|
107
|
+
the `RunSummary`, with the parsed solver stats attached; use it for anything
|
|
108
|
+
whose timing should appear in the summary.
|
|
109
|
+
- `run_simple(*command_parts)` — the terse variant, returning `result.failed`
|
|
110
|
+
(truthy on failure) for call sites that only branch on success and want no
|
|
111
|
+
phase recorded.
|
|
112
|
+
|
|
113
|
+
Two optional controls, both defaulting to the historical behaviour:
|
|
114
|
+
|
|
115
|
+
- `cwd=` on `run` / `run_phase` — the subprocess's working directory. Omitted, it
|
|
116
|
+
inherits the caller's, which is what the `Solver` classes rely on (they are
|
|
117
|
+
documented to run after a chdir, with paths relative to it). A consumer that
|
|
118
|
+
would rather not chdir a whole process can place each subprocess instead.
|
|
119
|
+
- `log_level=` on the `SolverRunner` constructor — console verbosity for this
|
|
120
|
+
runner. `global_state.log_level()` can only ever *raise* verbosity (it floors
|
|
121
|
+
at WARNING), so this is the way to make a runner quieter. The file handler
|
|
122
|
+
captures everything regardless.
|
|
123
|
+
|
|
124
|
+
## tests
|
|
125
|
+
|
|
126
|
+
`tests/test_solver_runner.py` — execution outcomes (success / failure / timeout /
|
|
127
|
+
interruption), process-tree and child-process cleanup, signal handling, logging
|
|
128
|
+
setup, environment and working-directory handling, output edge cases (long
|
|
129
|
+
output, special characters, empty commands), and integration tests that run real
|
|
130
|
+
system commands.
|
|
131
|
+
|
|
132
|
+
Signals are *not* handled here: `SolverRunner` runs a command and reports, while
|
|
133
|
+
installing the handler and deciding what an interrupt means for the run as a
|
|
134
|
+
whole belongs to the consumer's top-level runner.
|