solver-support 0.3.1__tar.gz → 0.3.2__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.
Files changed (45) hide show
  1. {solver_support-0.3.1/solver_support.egg-info → solver_support-0.3.2}/PKG-INFO +1 -1
  2. {solver_support-0.3.1 → solver_support-0.3.2}/docs/timing-model.md +18 -1
  3. {solver_support-0.3.1 → solver_support-0.3.2}/pyproject.toml +1 -1
  4. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support/solver_stats.py +27 -6
  5. {solver_support-0.3.1 → solver_support-0.3.2/solver_support.egg-info}/PKG-INFO +1 -1
  6. {solver_support-0.3.1 → solver_support-0.3.2}/tests/test_solver_stats.py +41 -0
  7. {solver_support-0.3.1 → solver_support-0.3.2}/LICENSE +0 -0
  8. {solver_support-0.3.1 → solver_support-0.3.2}/MANIFEST.in +0 -0
  9. {solver_support-0.3.1 → solver_support-0.3.2}/README.md +0 -0
  10. {solver_support-0.3.1 → solver_support-0.3.2}/docs/injection-api.md +0 -0
  11. {solver_support-0.3.1 → solver_support-0.3.2}/docs/solver-runner.md +0 -0
  12. {solver_support-0.3.1 → solver_support-0.3.2}/setup.cfg +0 -0
  13. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support/__init__.py +0 -0
  14. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support/command_builders.py +0 -0
  15. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support/file_management.py +0 -0
  16. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support/global_state.py +0 -0
  17. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support/horizon_utils.py +0 -0
  18. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support/model_registry.py +0 -0
  19. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support/objective.py +0 -0
  20. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support/run_summary.py +0 -0
  21. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support/solver_base.py +0 -0
  22. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support/solver_enhsp.py +0 -0
  23. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support/solver_fd.py +0 -0
  24. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support/solver_horizon.py +0 -0
  25. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support/solver_runner.py +0 -0
  26. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support/solver_upfplan.py +0 -0
  27. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support/utilities.py +0 -0
  28. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support.egg-info/SOURCES.txt +0 -0
  29. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support.egg-info/dependency_links.txt +0 -0
  30. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support.egg-info/not-zip-safe +0 -0
  31. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support.egg-info/requires.txt +0 -0
  32. {solver_support-0.3.1 → solver_support-0.3.2}/solver_support.egg-info/top_level.txt +0 -0
  33. {solver_support-0.3.1 → solver_support-0.3.2}/tests/conftest.py +0 -0
  34. {solver_support-0.3.1 → solver_support-0.3.2}/tests/fixtures/models.yaml +0 -0
  35. {solver_support-0.3.1 → solver_support-0.3.2}/tests/test_command_builders.py +0 -0
  36. {solver_support-0.3.1 → solver_support-0.3.2}/tests/test_helpers.py +0 -0
  37. {solver_support-0.3.1 → solver_support-0.3.2}/tests/test_horizon_utils.py +0 -0
  38. {solver_support-0.3.1 → solver_support-0.3.2}/tests/test_import.py +0 -0
  39. {solver_support-0.3.1 → solver_support-0.3.2}/tests/test_registry_dump.py +0 -0
  40. {solver_support-0.3.1 → solver_support-0.3.2}/tests/test_run_summary.py +0 -0
  41. {solver_support-0.3.1 → solver_support-0.3.2}/tests/test_run_summary_success.py +0 -0
  42. {solver_support-0.3.1 → solver_support-0.3.2}/tests/test_savilerow_info_recording.py +0 -0
  43. {solver_support-0.3.1 → solver_support-0.3.2}/tests/test_solution_naming.py +0 -0
  44. {solver_support-0.3.1 → solver_support-0.3.2}/tests/test_solver_runner.py +0 -0
  45. {solver_support-0.3.1 → solver_support-0.3.2}/tests/test_solvers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solver-support
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Generic Conjure / Savile Row / Fast Downward / SymK / ENHSP / UPF solver pipeline
5
5
  Author: András Salamon
6
6
  License-Expression: BSD-3-Clause
@@ -193,7 +193,7 @@ establishes nothing. `SolverStatsExtractor.savilerow_verdict` separates them and
193
193
  | verdict | evidence | meaning |
194
194
  |---|---|---|
195
195
  | `sat` / `unsat` | `SolverSatisfiable` present | decided (`proven_optimal` too, on optimisation models) |
196
- | `unknown` | absent, with `verdict_reason` | `solver-limit`, `tailoring-limit`, `memory`, `encoding-limit`, or `unexplained` |
196
+ | `unknown` | absent, with `verdict_reason` | `solver-limit`, `tailoring-limit`, `memory`, `tailoring-memory`, `encoding-limit`, or `unexplained` |
197
197
  | `error` | `SavileRowUncaughtException` | Savile Row itself failed |
198
198
  | `killed` | no `.info` at all | recorded by the caller, from the phase's `timed_out` / `launch_failed` |
199
199
 
@@ -202,6 +202,23 @@ the solver having run: Savile Row can settle an instance during tailoring and
202
202
  never invoke a solver, leaving `SolverTotalTime: 0` beside a perfectly good
203
203
  verdict. Testing "did the solver run?" reports every one of those as undecided.
204
204
 
205
+ **Each limit names the process it bound.** `solver-limit` / `tailoring-limit`
206
+ are the two time limits (see above), and `memory` / `tailoring-memory` are the
207
+ same split for the heap: `SolverMemOut` is the backend, `SavileRowOutOfMemory`
208
+ the JVM running Savile Row. The front-end memory-out is `unknown` and not
209
+ `error` for the same reason its timeout is — a resource cap stopped the run
210
+ before it decided; nothing malfunctioned. It is checked **before** the verdict,
211
+ and before `SavileRowUncaughtException`: one heap exhaustion sets both flags,
212
+ because the `OutOfMemoryError` reaches Savile Row's top-level handler, and the
213
+ cause is a more useful report than the symptom. `error` is then what is left
214
+ when Savile Row has no account of what went wrong.
215
+
216
+ You will mostly see this flag if your limits are **in band**. A harness that
217
+ caps memory externally (`runsolver` and friends) kills the JVM before it reaches
218
+ its own `-Xmx`, so Savile Row never writes the flag and you get a killed process
219
+ with no statistics instead — the same trade the time limits make. Bounding a run
220
+ from the inside buys diagnostics that killing it from outside destroys.
221
+
205
222
  **A horizon's `unsat` is about the bound, not the problem.** It means "not at
206
223
  this horizon", never "not at all" — refuting horizons 1..N says nothing about
207
224
  N+1. Only the registry knows which models decide the problem itself
@@ -20,7 +20,7 @@ name = "solver-support"
20
20
  # escape hatch for a version that genuinely cannot be known without building.
21
21
  # Ours can. Carrying a `.devN` suffix between releases keeps the working tree
22
22
  # from claiming a version that is already tagged and published.
23
- version = "0.3.1"
23
+ version = "0.3.2"
24
24
  description = "Generic Conjure / Savile Row / Fast Downward / SymK / ENHSP / UPF solver pipeline"
25
25
  readme = "README.md"
26
26
  requires-python = ">=3.9"
@@ -101,7 +101,8 @@ SAVILEROW_VERDICTS = ('sat', 'unsat', 'unknown', 'error', 'killed')
101
101
  # Why a run ended without deciding, keyed by the ``.info`` flag that explains it,
102
102
  # in the order they are checked. Savile Row sets at most one of these in practice;
103
103
  # the order records which explanation would win if it ever set two, most specific
104
- # (the solver's own limit) first.
104
+ # (the solver's own limit) first. ``memory`` is the *backend* running out; the
105
+ # front end running out is an abort flag below, not one of these.
105
106
  _SAVILEROW_STOP_FLAGS = (
106
107
  ('SolverTimeOut', 'solver-limit'),
107
108
  ('SavileRowTimeOut', 'tailoring-limit'),
@@ -109,6 +110,23 @@ _SAVILEROW_STOP_FLAGS = (
109
110
  ('SavileRowClauseOut', 'encoding-limit'),
110
111
  )
111
112
 
113
+ # Savile Row itself did not finish. Checked BEFORE the verdict test - a run that
114
+ # died mid-flight cannot be trusted to have written a sound one - and so, unlike
115
+ # the stop flags above, these outrank a ``SolverSatisfiable`` that is present.
116
+ #
117
+ # Ordered cause before symptom: a JVM heap exhaustion reaches Savile Row's
118
+ # top-level handler, so it sets BOTH ``SavileRowOutOfMemory`` and
119
+ # ``SavileRowUncaughtException``, and "ran out of memory" is the more useful of
120
+ # the two reports. Out-of-memory is ``unknown`` rather than ``error`` because
121
+ # nothing malfunctioned: it is the front-end twin of ``SolverMemOut``, a resource
122
+ # cap that stopped the run before it decided, exactly as the two time limits are
123
+ # a matched ``solver-limit`` / ``tailoring-limit`` pair. ``error`` stays for the
124
+ # case where Savile Row has no account of what went wrong.
125
+ _SAVILEROW_ABORT_FLAGS = (
126
+ ('SavileRowOutOfMemory', 'unknown', 'tailoring-memory'),
127
+ ('SavileRowUncaughtException', 'error', 'uncaught-exception'),
128
+ )
129
+
112
130
 
113
131
  class SolverStatsExtractor:
114
132
  """Extract structured statistics from solver output."""
@@ -492,11 +510,14 @@ class SolverStatsExtractor:
492
510
  except (TypeError, ValueError):
493
511
  return False
494
512
 
495
- # Checked before the verdict: a Savile Row that threw cannot be trusted
496
- # to have written a sound one. The two have never been observed together,
497
- # so this ordering only decides a case not yet seen in practice.
498
- if _flagged('SavileRowUncaughtException'):
499
- return {'verdict': 'error', 'verdict_reason': 'uncaught-exception'}
513
+ # Checked before the verdict: a Savile Row that died cannot be trusted to
514
+ # have written a sound one. An abort has never been observed together
515
+ # with a verdict, so that half of the ordering only decides a case not
516
+ # yet seen in practice; the cause-before-symptom half of it decides a
517
+ # real one, since an out-of-memory sets the exception flag as well.
518
+ for key, verdict, reason in _SAVILEROW_ABORT_FLAGS:
519
+ if _flagged(key):
520
+ return {'verdict': verdict, 'verdict_reason': reason}
500
521
 
501
522
  # `SolverSatisfiable` is written if and only if something DECIDED this
502
523
  # run, and is deliberately not keyed on the solver having run: Savile Row
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solver-support
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Generic Conjure / Savile Row / Fast Downward / SymK / ENHSP / UPF solver pipeline
5
5
  Author: András Salamon
6
6
  License-Expression: BSD-3-Clause
@@ -281,6 +281,47 @@ class TestSavileRowVerdict:
281
281
  assert SolverStatsExtractor.savilerow_verdict(info) == {
282
282
  'verdict': 'error', 'verdict_reason': 'uncaught-exception'}
283
283
 
284
+ def test_out_of_memory_is_reported_by_cause_not_symptom(self, tmp_path):
285
+ """Both flags are set by one event, and the cause is the useful one.
286
+
287
+ A JVM heap exhaustion reaches Savile Row's top-level handler, so the
288
+ ``.info`` carries ``SavileRowUncaughtException: 1`` beside the memory
289
+ flag. Reporting the exception says *Savile Row has a bug*; reporting the
290
+ memory says *this model did not fit*, which is what decides whether a
291
+ consumer retries at a larger scale or files a fault. Keys verbatim from
292
+ a real run (Savile Row 1.11.1, ~183 s, exit 1, ``ERROR: Out of Memory``).
293
+ """
294
+ info = self._write_info(
295
+ tmp_path,
296
+ "SavileRowClauseOut:0\nSavileRowTotalTime:182.786\n"
297
+ "SavileRowOutOfMemory:1\nSavileRowUncaughtException:1\n"
298
+ "SavileRowTimeOut:0\n")
299
+ assert SolverStatsExtractor.savilerow_verdict(info) == {
300
+ 'verdict': 'unknown', 'verdict_reason': 'tailoring-memory'}
301
+
302
+ def test_out_of_memory_alone_is_not_unexplained(self, tmp_path):
303
+ """An ``OutOfMemoryError`` caught and reported cleanly sets one flag.
304
+
305
+ Without its own branch this falls past the exception test, finds no stop
306
+ flag set, and lands on ``unexplained`` - which for a consumer that sets
307
+ the backend's limit itself reads as the routine solver timeout, turning
308
+ a fault into a cap.
309
+ """
310
+ info = self._write_info(
311
+ tmp_path,
312
+ "SavileRowTotalTime:120.5\nSavileRowOutOfMemory:1\n"
313
+ "SavileRowTimeOut:0\nSolverTimeOut:0\n")
314
+ assert SolverStatsExtractor.savilerow_verdict(info) == {
315
+ 'verdict': 'unknown', 'verdict_reason': 'tailoring-memory'}
316
+
317
+ def test_backend_memout_stays_distinct_from_tailoring_memory(self, tmp_path):
318
+ """The two memory reasons name different processes' heaps."""
319
+ info = self._write_info(
320
+ tmp_path,
321
+ "SolverTotalTime:88.1\nSolverMemOut:1\nSolverTimeOut:0\n")
322
+ assert SolverStatsExtractor.savilerow_verdict(info) == {
323
+ 'verdict': 'unknown', 'verdict_reason': 'memory'}
324
+
284
325
  def test_proven_optimum_marked_on_sat(self, tmp_path):
285
326
  info = self._write_info(
286
327
  tmp_path,
File without changes
File without changes
File without changes