solverpilot 0.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.
Files changed (201) hide show
  1. solverpilot/__init__.py +250 -0
  2. solverpilot/_immutability.py +27 -0
  3. solverpilot/_version.py +3 -0
  4. solverpilot/applications/__init__.py +3 -0
  5. solverpilot/applications/routing/__init__.py +19 -0
  6. solverpilot/applications/routing/compile_milp.py +321 -0
  7. solverpilot/applications/routing/diagnostics.py +78 -0
  8. solverpilot/applications/routing/errors.py +14 -0
  9. solverpilot/applications/routing/heuristics.py +167 -0
  10. solverpilot/applications/routing/model.py +259 -0
  11. solverpilot/applications/routing/reference.py +81 -0
  12. solverpilot/applications/routing/solution.py +89 -0
  13. solverpilot/applications/routing/timeline.py +79 -0
  14. solverpilot/applications/routing/validation.py +169 -0
  15. solverpilot/applications/tsp/__init__.py +41 -0
  16. solverpilot/applications/tsp/compile_cp.py +121 -0
  17. solverpilot/applications/tsp/compile_milp.py +139 -0
  18. solverpilot/applications/tsp/distance.py +60 -0
  19. solverpilot/applications/tsp/errors.py +17 -0
  20. solverpilot/applications/tsp/heuristics.py +112 -0
  21. solverpilot/applications/tsp/model.py +175 -0
  22. solverpilot/applications/tsp/reference.py +121 -0
  23. solverpilot/applications/tsp/solution.py +65 -0
  24. solverpilot/applications/tsp/validation.py +92 -0
  25. solverpilot/backends/__init__.py +51 -0
  26. solverpilot/backends/_casadi_highs_worker.py +92 -0
  27. solverpilot/backends/base.py +28 -0
  28. solverpilot/backends/bundled_capi.py +910 -0
  29. solverpilot/backends/casadi_conic.py +303 -0
  30. solverpilot/backends/health.py +240 -0
  31. solverpilot/backends/highspy_native.py +217 -0
  32. solverpilot/backends/nlopt_native.py +237 -0
  33. solverpilot/backends/osqp_native.py +297 -0
  34. solverpilot/backends/protocol_v2.py +119 -0
  35. solverpilot/backends/pyscipopt_native.py +200 -0
  36. solverpilot/backends/registry.py +38 -0
  37. solverpilot/backends/scipy_highs.py +138 -0
  38. solverpilot/backends/scipy_highs_lp.py +195 -0
  39. solverpilot/backends/scipy_slsqp_qp.py +133 -0
  40. solverpilot/backends/scipy_vendored_highs_dev.py +363 -0
  41. solverpilot/benchmark/__init__.py +23 -0
  42. solverpilot/benchmark/acquire.py +199 -0
  43. solverpilot/benchmark/baselines.py +70 -0
  44. solverpilot/benchmark/bundle.py +37 -0
  45. solverpilot/benchmark/campaign.py +150 -0
  46. solverpilot/benchmark/environment.py +115 -0
  47. solverpilot/benchmark/integrity.py +46 -0
  48. solverpilot/benchmark/model.py +146 -0
  49. solverpilot/benchmark/policy.py +182 -0
  50. solverpilot/benchmark/registry.py +38 -0
  51. solverpilot/benchmark/report.py +44 -0
  52. solverpilot/benchmark/runner.py +516 -0
  53. solverpilot/benchmark/slurm.py +55 -0
  54. solverpilot/benchmark/specs.py +75 -0
  55. solverpilot/benchmark/splits.py +181 -0
  56. solverpilot/benchmark/summary.py +384 -0
  57. solverpilot/benchmark/verify.py +142 -0
  58. solverpilot/benchmark/worker.py +71 -0
  59. solverpilot/bridges/__init__.py +19 -0
  60. solverpilot/bridges/engine.py +44 -0
  61. solverpilot/bridges/indicator.py +232 -0
  62. solverpilot/bridges/registry.py +78 -0
  63. solverpilot/bridges/transformation-tape.schema.json +53 -0
  64. solverpilot/bridges/types.py +149 -0
  65. solverpilot/bridges/validation.py +110 -0
  66. solverpilot/capabilities/__init__.py +32 -0
  67. solverpilot/capabilities/backend-capability-v2.schema.json +21 -0
  68. solverpilot/capabilities/conformance.py +151 -0
  69. solverpilot/capabilities/enums.py +27 -0
  70. solverpilot/capabilities/manifest.py +19 -0
  71. solverpilot/capabilities/ontology.py +73 -0
  72. solverpilot/capabilities/requirements.py +37 -0
  73. solverpilot/capabilities/resolve.py +30 -0
  74. solverpilot/capabilities/v2.py +711 -0
  75. solverpilot/cli/__init__.py +1 -0
  76. solverpilot/cli/backend_health.py +77 -0
  77. solverpilot/cli/benchmark.py +331 -0
  78. solverpilot/cli/benchmark_worker.py +33 -0
  79. solverpilot/cli/capabilities.py +65 -0
  80. solverpilot/conic/__init__.py +11 -0
  81. solverpilot/conic/backend.py +263 -0
  82. solverpilot/conic/compiler.py +310 -0
  83. solverpilot/conic/conformance.py +60 -0
  84. solverpilot/conic/conic-ir-v1.schema.json +24 -0
  85. solverpilot/conic/ir.py +299 -0
  86. solverpilot/conic/validation.py +149 -0
  87. solverpilot/cp/__init__.py +7 -0
  88. solverpilot/cp/conformance.py +32 -0
  89. solverpilot/cp/cp-ir-v1.schema.json +13 -0
  90. solverpilot/cp/ir.py +170 -0
  91. solverpilot/cp/model.py +143 -0
  92. solverpilot/cp/ortools_backend.py +423 -0
  93. solverpilot/cp/ortools_worker.py +75 -0
  94. solverpilot/cp/reference.py +51 -0
  95. solverpilot/cp/serialization.py +117 -0
  96. solverpilot/cp/validate.py +83 -0
  97. solverpilot/diagnose/__init__.py +33 -0
  98. solverpilot/diagnose/infeasibility.py +463 -0
  99. solverpilot/diagnose/model.py +87 -0
  100. solverpilot/evaluation/__init__.py +53 -0
  101. solverpilot/evaluation/metrics.py +150 -0
  102. solverpilot/evaluation/oracle.py +288 -0
  103. solverpilot/evaluation/proof.py +53 -0
  104. solverpilot/evaluation/references.py +73 -0
  105. solverpilot/evaluation/regret.py +281 -0
  106. solverpilot/exceptions.py +34 -0
  107. solverpilot/experimental/__init__.py +32 -0
  108. solverpilot/extensions/__init__.py +56 -0
  109. solverpilot/extensions/entrypoints.py +34 -0
  110. solverpilot/extensions/errors.py +35 -0
  111. solverpilot/extensions/manager.py +651 -0
  112. solverpilot/extensions/manifest.py +232 -0
  113. solverpilot/extensions/model.py +101 -0
  114. solverpilot/extensions/versioning.py +136 -0
  115. solverpilot/history/__init__.py +22 -0
  116. solverpilot/history/errors.py +15 -0
  117. solverpilot/history/model.py +129 -0
  118. solverpilot/history/sqlite.py +651 -0
  119. solverpilot/inspect/__init__.py +3 -0
  120. solverpilot/inspect/fingerprint.py +199 -0
  121. solverpilot/intelligence/__init__.py +36 -0
  122. solverpilot/intelligence/errors.py +16 -0
  123. solverpilot/intelligence/fingerprint.py +108 -0
  124. solverpilot/intelligence/leakage.py +69 -0
  125. solverpilot/intelligence/schema.py +314 -0
  126. solverpilot/intelligence/shift.py +208 -0
  127. solverpilot/io/__init__.py +41 -0
  128. solverpilot/io/binary64.py +49 -0
  129. solverpilot/io/csv.py +230 -0
  130. solverpilot/io/errors.py +25 -0
  131. solverpilot/io/hashing.py +111 -0
  132. solverpilot/io/json.py +90 -0
  133. solverpilot/io/mapping.py +195 -0
  134. solverpilot/io/model.py +407 -0
  135. solverpilot/io/numeric.py +108 -0
  136. solverpilot/io/provenance.py +114 -0
  137. solverpilot/io/result.py +45 -0
  138. solverpilot/io/source.py +104 -0
  139. solverpilot/io/strict_json.py +133 -0
  140. solverpilot/minlp/__init__.py +6 -0
  141. solverpilot/minlp/compiler.py +66 -0
  142. solverpilot/minlp/conformance.py +34 -0
  143. solverpilot/minlp/convexity.py +85 -0
  144. solverpilot/minlp/ir.py +35 -0
  145. solverpilot/minlp/minlp-ir-v1.schema.json +1 -0
  146. solverpilot/minlp/orchestrator.py +220 -0
  147. solverpilot/minlp/validation.py +28 -0
  148. solverpilot/model/__init__.py +27 -0
  149. solverpilot/model/compiler.py +1263 -0
  150. solverpilot/model/errors.py +22 -0
  151. solverpilot/model/expression.py +373 -0
  152. solverpilot/model/model.py +716 -0
  153. solverpilot/model/sets.py +60 -0
  154. solverpilot/model/types.py +36 -0
  155. solverpilot/nlp/__init__.py +7 -0
  156. solverpilot/nlp/ad.py +166 -0
  157. solverpilot/nlp/backend.py +113 -0
  158. solverpilot/nlp/compiler.py +116 -0
  159. solverpilot/nlp/conformance.py +58 -0
  160. solverpilot/nlp/ir.py +70 -0
  161. solverpilot/nlp/nlp-ir-v1.schema.json +1 -0
  162. solverpilot/nlp/validation.py +60 -0
  163. solverpilot/plan/__init__.py +33 -0
  164. solverpilot/plan/model.py +79 -0
  165. solverpilot/plan/planner.py +201 -0
  166. solverpilot/plan/production.py +627 -0
  167. solverpilot/plan/selective_lp.py +65 -0
  168. solverpilot/problem/__init__.py +16 -0
  169. solverpilot/problem/enums.py +20 -0
  170. solverpilot/problem/hashing.py +88 -0
  171. solverpilot/problem/linear.py +196 -0
  172. solverpilot/problem/mps.py +447 -0
  173. solverpilot/problem/quadratic.py +192 -0
  174. solverpilot/reporting/__init__.py +27 -0
  175. solverpilot/reporting/explain.py +327 -0
  176. solverpilot/reporting/model.py +264 -0
  177. solverpilot/reporting/render.py +55 -0
  178. solverpilot/runtime/__init__.py +19 -0
  179. solverpilot/runtime/auto.py +193 -0
  180. solverpilot/runtime/budgeting.py +43 -0
  181. solverpilot/runtime/executor.py +126 -0
  182. solverpilot/runtime/portfolio.py +197 -0
  183. solverpilot/runtime/result.py +72 -0
  184. solverpilot/session/__init__.py +29 -0
  185. solverpilot/session/conformance_v2.py +239 -0
  186. solverpilot/session/mutations.py +115 -0
  187. solverpilot/session/persistent-session-trace.schema.json +45 -0
  188. solverpilot/session/persistent.py +491 -0
  189. solverpilot/session/reuse.py +81 -0
  190. solverpilot/session/session.py +236 -0
  191. solverpilot/trace/__init__.py +3 -0
  192. solverpilot/trace/schema.py +47 -0
  193. solverpilot/validate/__init__.py +10 -0
  194. solverpilot/validate/core.py +179 -0
  195. solverpilot/validate/result.py +55 -0
  196. solverpilot-0.1.dist-info/METADATA +554 -0
  197. solverpilot-0.1.dist-info/RECORD +201 -0
  198. solverpilot-0.1.dist-info/WHEEL +5 -0
  199. solverpilot-0.1.dist-info/entry_points.txt +4 -0
  200. solverpilot-0.1.dist-info/licenses/LICENSE +202 -0
  201. solverpilot-0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,651 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager
4
+ from datetime import datetime, timezone
5
+ import hashlib
6
+ import json
7
+ import math
8
+ from pathlib import Path
9
+ import sqlite3
10
+ import threading
11
+ from typing import Any, Iterable, Iterator, Mapping, Sequence
12
+
13
+ from solverpilot.inspect import inspect_problem
14
+ from solverpilot.intelligence import FeatureRecord
15
+ from solverpilot.io.provenance import SourceProvenance
16
+ from solverpilot.problem import LinearProblem, QuadraticProblem
17
+ from solverpilot.runtime import SolveResult
18
+
19
+ from .errors import HistoryDataError, HistorySchemaError, HistoryStoreError
20
+ from .model import ProblemHistoryRecord, SolveHistoryRecord
21
+
22
+ HISTORY_SCHEMA_NAME = "solverpilot.history"
23
+ HISTORY_SCHEMA_VERSION = 1
24
+ HISTORY_SCHEMA_LABEL = "1.0.0"
25
+
26
+
27
+ def _utc_now() -> str:
28
+ return datetime.now(timezone.utc).isoformat()
29
+
30
+
31
+ def _canonical_json(value: Any) -> str:
32
+ try:
33
+ return json.dumps(
34
+ value,
35
+ sort_keys=True,
36
+ separators=(",", ":"),
37
+ ensure_ascii=False,
38
+ allow_nan=False,
39
+ )
40
+ except (TypeError, ValueError) as exc:
41
+ raise HistoryDataError("history payload must contain JSON-safe finite values") from exc
42
+
43
+
44
+ def _json_load(text: str) -> Any:
45
+ try:
46
+ return json.loads(text)
47
+ except Exception as exc:
48
+ raise HistoryStoreError("history database contains invalid JSON") from exc
49
+
50
+
51
+ def _bool_db(value: bool | None) -> int | None:
52
+ return None if value is None else int(value)
53
+
54
+
55
+ def _sha256_text(text: str) -> str:
56
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
57
+
58
+
59
+ def _run_id_from_result(result: SolveResult) -> str:
60
+ raw = "\0".join(
61
+ [
62
+ result.trace.problem_data_hash,
63
+ str(result.trace.backend or ""),
64
+ str(result.trace.created_at_utc),
65
+ str(result.trace.termination or result.backend_status),
66
+ ]
67
+ ).encode("utf-8")
68
+ return "solve_" + hashlib.sha256(raw).hexdigest()[:32]
69
+
70
+
71
+ def _require_sha(value: str, field_name: str) -> str:
72
+ text = str(value).strip().lower()
73
+ if len(text) != 64 or any(ch not in "0123456789abcdef" for ch in text):
74
+ raise HistoryDataError(f"{field_name} must be a SHA-256 digest")
75
+ return text
76
+
77
+
78
+ class HistoryStore:
79
+ """Explicitly enabled local SQLite history for research and reproducibility.
80
+
81
+ SolverPilot never constructs this store automatically. The durable schema
82
+ excludes raw problem arrays, raw request payloads, raw solution vectors, and
83
+ backend ``raw_statistics``.
84
+ """
85
+
86
+ def __init__(self, path: str | Path, *, timeout_s: float = 5.0) -> None:
87
+ self.path = Path(path)
88
+ if not str(self.path).strip():
89
+ raise HistoryStoreError("history path must be non-empty")
90
+ if self.path.exists() and self.path.is_dir():
91
+ raise HistoryStoreError("history path must point to a SQLite file")
92
+ if self.path.is_symlink():
93
+ raise HistoryStoreError("history database path may not be a symlink")
94
+ if type(timeout_s) not in (int, float) or not math.isfinite(float(timeout_s)) or float(timeout_s) <= 0:
95
+ raise HistoryStoreError("timeout_s must be finite and positive")
96
+ self.path.parent.mkdir(parents=True, exist_ok=True)
97
+ self._lock = threading.RLock()
98
+ try:
99
+ self._connection = sqlite3.connect(
100
+ str(self.path),
101
+ timeout=float(timeout_s),
102
+ isolation_level=None,
103
+ check_same_thread=False,
104
+ )
105
+ self._connection.row_factory = sqlite3.Row
106
+ self._connection.execute("PRAGMA foreign_keys = ON")
107
+ self._connection.execute(f"PRAGMA busy_timeout = {int(float(timeout_s) * 1000)}")
108
+ self._initialize_or_migrate()
109
+ except HistoryStoreError:
110
+ connection = getattr(self, "_connection", None)
111
+ if connection is not None:
112
+ connection.close()
113
+ raise
114
+ except sqlite3.DatabaseError as exc:
115
+ connection = getattr(self, "_connection", None)
116
+ if connection is not None:
117
+ connection.close()
118
+ raise HistoryStoreError("history database is not a valid/compatible SQLite database") from exc
119
+ except Exception:
120
+ connection = getattr(self, "_connection", None)
121
+ if connection is not None:
122
+ connection.close()
123
+ raise
124
+
125
+ def __enter__(self) -> "HistoryStore":
126
+ return self
127
+
128
+ def __exit__(self, exc_type, exc, tb) -> None: # noqa: ANN001
129
+ self.close()
130
+
131
+ def close(self) -> None:
132
+ with self._lock:
133
+ self._connection.close()
134
+
135
+ @contextmanager
136
+ def transaction(self) -> Iterator[sqlite3.Connection]:
137
+ with self._lock:
138
+ try:
139
+ self._connection.execute("BEGIN IMMEDIATE")
140
+ yield self._connection
141
+ except Exception:
142
+ if self._connection.in_transaction:
143
+ self._connection.execute("ROLLBACK")
144
+ raise
145
+ else:
146
+ if not self._connection.in_transaction:
147
+ raise HistoryStoreError("history transaction ended unexpectedly")
148
+ self._connection.execute("COMMIT")
149
+
150
+ def _initialize_or_migrate(self) -> None:
151
+ with self._lock:
152
+ version = int(self._connection.execute("PRAGMA user_version").fetchone()[0])
153
+ if version > HISTORY_SCHEMA_VERSION:
154
+ raise HistorySchemaError(
155
+ f"history database schema {version} is newer than supported {HISTORY_SCHEMA_VERSION}"
156
+ )
157
+ while version < HISTORY_SCHEMA_VERSION:
158
+ migrator = getattr(self, f"_migrate_{version}_to_{version + 1}", None)
159
+ if migrator is None:
160
+ raise HistorySchemaError(f"missing history migration {version}->{version + 1}")
161
+ migrator()
162
+ version += 1
163
+ self._validate_schema_identity()
164
+
165
+ def _migrate_0_to_1(self) -> None:
166
+ now = _utc_now()
167
+ script = f"""
168
+ BEGIN IMMEDIATE;
169
+ CREATE TABLE history_metadata (
170
+ key TEXT PRIMARY KEY,
171
+ value_json TEXT NOT NULL,
172
+ updated_at_utc TEXT NOT NULL
173
+ );
174
+ CREATE TABLE problems (
175
+ problem_data_hash TEXT PRIMARY KEY,
176
+ problem_structural_hash TEXT NOT NULL,
177
+ problem_class TEXT NOT NULL,
178
+ n_variables INTEGER NOT NULL CHECK(n_variables >= 0),
179
+ n_constraints INTEGER NOT NULL CHECK(n_constraints >= 0),
180
+ metadata_json TEXT NOT NULL,
181
+ created_at_utc TEXT NOT NULL
182
+ );
183
+ CREATE TABLE problem_sources (
184
+ source_id TEXT PRIMARY KEY,
185
+ problem_data_hash TEXT NOT NULL,
186
+ provenance_json TEXT NOT NULL,
187
+ created_at_utc TEXT NOT NULL,
188
+ FOREIGN KEY(problem_data_hash) REFERENCES problems(problem_data_hash) ON DELETE CASCADE
189
+ );
190
+ CREATE TABLE feature_records (
191
+ record_id TEXT PRIMARY KEY,
192
+ problem_data_hash TEXT NOT NULL,
193
+ instance_id TEXT NOT NULL,
194
+ feature_schema_id TEXT NOT NULL,
195
+ feature_schema_version TEXT NOT NULL,
196
+ values_json TEXT NOT NULL,
197
+ missingness_reasons_json TEXT NOT NULL,
198
+ source_content_sha256 TEXT NOT NULL,
199
+ extractor_id TEXT NOT NULL,
200
+ extractor_version TEXT NOT NULL,
201
+ created_at_utc TEXT NOT NULL,
202
+ FOREIGN KEY(problem_data_hash) REFERENCES problems(problem_data_hash) ON DELETE CASCADE
203
+ );
204
+ CREATE TABLE solve_runs (
205
+ run_id TEXT PRIMARY KEY,
206
+ problem_data_hash TEXT NOT NULL,
207
+ problem_structural_hash TEXT NOT NULL,
208
+ problem_class TEXT NOT NULL,
209
+ backend TEXT,
210
+ backend_version TEXT,
211
+ public_status TEXT NOT NULL,
212
+ backend_status TEXT NOT NULL,
213
+ objective REAL,
214
+ validation_valid INTEGER,
215
+ independently_verified_optimal INTEGER NOT NULL,
216
+ total_s REAL,
217
+ environment_id TEXT,
218
+ trace_created_at_utc TEXT,
219
+ metadata_json TEXT NOT NULL,
220
+ created_at_utc TEXT NOT NULL,
221
+ FOREIGN KEY(problem_data_hash) REFERENCES problems(problem_data_hash) ON DELETE RESTRICT
222
+ );
223
+ CREATE TABLE benchmark_rows (
224
+ run_id TEXT PRIMARY KEY,
225
+ protocol_id TEXT NOT NULL,
226
+ environment_id TEXT,
227
+ problem_data_hash TEXT,
228
+ instance_name TEXT NOT NULL,
229
+ instance_sha256 TEXT,
230
+ backend TEXT NOT NULL,
231
+ repetition INTEGER NOT NULL CHECK(repetition >= 0),
232
+ state TEXT NOT NULL,
233
+ public_status TEXT,
234
+ validated INTEGER,
235
+ objective REAL,
236
+ wall_s REAL,
237
+ reference_check TEXT,
238
+ row_json TEXT NOT NULL,
239
+ created_at_utc TEXT NOT NULL
240
+ );
241
+ CREATE TABLE artifacts (
242
+ artifact_id TEXT PRIMARY KEY,
243
+ run_id TEXT,
244
+ artifact_type TEXT NOT NULL,
245
+ path TEXT,
246
+ size_bytes INTEGER NOT NULL CHECK(size_bytes >= 0),
247
+ sha256 TEXT,
248
+ metadata_json TEXT NOT NULL,
249
+ created_at_utc TEXT NOT NULL
250
+ );
251
+ CREATE INDEX idx_problem_sources_problem ON problem_sources(problem_data_hash);
252
+ CREATE INDEX idx_features_problem ON feature_records(problem_data_hash);
253
+ CREATE INDEX idx_solve_problem_backend ON solve_runs(problem_data_hash, backend);
254
+ CREATE INDEX idx_solve_backend_status ON solve_runs(backend, public_status);
255
+ CREATE INDEX idx_benchmark_protocol_backend ON benchmark_rows(protocol_id, backend);
256
+ CREATE INDEX idx_benchmark_instance ON benchmark_rows(instance_name, instance_sha256);
257
+ INSERT INTO history_metadata(key,value_json,updated_at_utc)
258
+ VALUES('schema_name','\"solverpilot.history\"','{now}');
259
+ INSERT INTO history_metadata(key,value_json,updated_at_utc)
260
+ VALUES('schema_label','\"1.0.0\"','{now}');
261
+ PRAGMA user_version = 1;
262
+ COMMIT;
263
+ """
264
+ try:
265
+ self._connection.executescript(script)
266
+ except Exception:
267
+ if self._connection.in_transaction:
268
+ self._connection.execute("ROLLBACK")
269
+ raise
270
+
271
+ def _validate_schema_identity(self) -> None:
272
+ expected = {"schema_name": HISTORY_SCHEMA_NAME, "schema_label": HISTORY_SCHEMA_LABEL}
273
+ try:
274
+ rows = {
275
+ row["key"]: _json_load(row["value_json"])
276
+ for row in self._connection.execute(
277
+ "SELECT key,value_json FROM history_metadata WHERE key IN ('schema_name','schema_label')"
278
+ )
279
+ }
280
+ except sqlite3.Error as exc:
281
+ raise HistorySchemaError("history schema metadata is missing or unreadable") from exc
282
+ if rows != expected:
283
+ raise HistorySchemaError("history schema identity does not match SolverPilot")
284
+
285
+ @staticmethod
286
+ def _insert_or_verify(
287
+ con: sqlite3.Connection,
288
+ *,
289
+ table: str,
290
+ key_column: str,
291
+ columns: Sequence[str],
292
+ values: Sequence[Any],
293
+ compare_columns: Sequence[str] | None = None,
294
+ ) -> bool:
295
+ key_index = columns.index(key_column)
296
+ key = values[key_index]
297
+ existing = con.execute(f"SELECT * FROM {table} WHERE {key_column}=?", (key,)).fetchone()
298
+ if existing is None:
299
+ placeholders = ",".join("?" for _ in columns)
300
+ con.execute(
301
+ f"INSERT INTO {table}({','.join(columns)}) VALUES({placeholders})",
302
+ tuple(values),
303
+ )
304
+ return True
305
+ checked = tuple(compare_columns or columns)
306
+ mismatch = [name for name in checked if existing[name] != values[columns.index(name)]]
307
+ if mismatch:
308
+ raise HistoryDataError(
309
+ f"conflicting history record for {table}.{key_column}={key!r}; fields={mismatch}"
310
+ )
311
+ return False
312
+
313
+ def summary(self) -> dict[str, Any]:
314
+ tables = ("problems", "problem_sources", "feature_records", "solve_runs", "benchmark_rows", "artifacts")
315
+ with self._lock:
316
+ counts = {
317
+ name: int(self._connection.execute(f"SELECT COUNT(*) FROM {name}").fetchone()[0])
318
+ for name in tables
319
+ }
320
+ return {
321
+ "path": str(self.path),
322
+ "schema_name": HISTORY_SCHEMA_NAME,
323
+ "schema_version": HISTORY_SCHEMA_VERSION,
324
+ "schema_label": HISTORY_SCHEMA_LABEL,
325
+ "explicit_opt_in": True,
326
+ "raw_problem_payloads_persisted": False,
327
+ "raw_solution_vectors_persisted": False,
328
+ "counts": counts,
329
+ }
330
+
331
+ def integrity_check(self) -> dict[str, Any]:
332
+ with self._lock:
333
+ integrity = [str(row[0]) for row in self._connection.execute("PRAGMA integrity_check").fetchall()]
334
+ foreign = [tuple(row) for row in self._connection.execute("PRAGMA foreign_key_check").fetchall()]
335
+ return {"ok": integrity == ["ok"] and not foreign, "integrity": integrity, "foreign_key_violations": foreign}
336
+
337
+ def record_problem(self, record: ProblemHistoryRecord) -> None:
338
+ if not isinstance(record, ProblemHistoryRecord):
339
+ raise HistoryDataError("record must be ProblemHistoryRecord")
340
+ now = _utc_now()
341
+ record_payload = record.to_dict()
342
+ metadata_json = _canonical_json(record_payload["metadata"])
343
+ with self.transaction() as con:
344
+ self._insert_or_verify(
345
+ con,
346
+ table="problems",
347
+ key_column="problem_data_hash",
348
+ columns=("problem_data_hash", "problem_structural_hash", "problem_class", "n_variables", "n_constraints", "metadata_json", "created_at_utc"),
349
+ values=(record.problem_data_hash, record.problem_structural_hash, record.problem_class, record.n_variables, record.n_constraints, metadata_json, now),
350
+ compare_columns=("problem_data_hash", "problem_structural_hash", "problem_class", "n_variables", "n_constraints", "metadata_json"),
351
+ )
352
+ if record.source_provenance:
353
+ provenance_json = _canonical_json(record_payload["source_provenance"])
354
+ source_id = _sha256_text(provenance_json)
355
+ self._insert_or_verify(
356
+ con,
357
+ table="problem_sources",
358
+ key_column="source_id",
359
+ columns=("source_id", "problem_data_hash", "provenance_json", "created_at_utc"),
360
+ values=(source_id, record.problem_data_hash, provenance_json, now),
361
+ compare_columns=("source_id", "problem_data_hash", "provenance_json"),
362
+ )
363
+
364
+ def record_problem_object(
365
+ self,
366
+ problem: LinearProblem | QuadraticProblem,
367
+ *,
368
+ provenance: SourceProvenance | None = None,
369
+ metadata: Mapping[str, Any] | None = None,
370
+ ) -> ProblemHistoryRecord:
371
+ fingerprint = inspect_problem(problem)
372
+ record = ProblemHistoryRecord(
373
+ problem_data_hash=problem.data_hash,
374
+ problem_structural_hash=problem.structural_hash,
375
+ problem_class=fingerprint.problem_class,
376
+ n_variables=fingerprint.n_variables,
377
+ n_constraints=fingerprint.n_constraints,
378
+ source_provenance={} if provenance is None else provenance.to_dict(),
379
+ metadata=dict(metadata or {}),
380
+ )
381
+ self.record_problem(record)
382
+ return record
383
+
384
+ def record_feature(self, record: FeatureRecord, *, problem_data_hash: str | None = None) -> None:
385
+ if not isinstance(record, FeatureRecord):
386
+ raise HistoryDataError("record must be FeatureRecord")
387
+ problem_hash = _require_sha(
388
+ record.source_content_sha256 if problem_data_hash is None else problem_data_hash,
389
+ "problem_data_hash",
390
+ )
391
+ values_json = _canonical_json(dict(record.values))
392
+ reasons_json = _canonical_json(dict(record.missingness_reasons))
393
+ with self.transaction() as con:
394
+ if con.execute("SELECT 1 FROM problems WHERE problem_data_hash=?", (problem_hash,)).fetchone() is None:
395
+ raise HistoryDataError("feature record references an unknown problem_data_hash")
396
+ self._insert_or_verify(
397
+ con,
398
+ table="feature_records",
399
+ key_column="record_id",
400
+ columns=(
401
+ "record_id", "problem_data_hash", "instance_id", "feature_schema_id",
402
+ "feature_schema_version", "values_json", "missingness_reasons_json",
403
+ "source_content_sha256", "extractor_id", "extractor_version", "created_at_utc",
404
+ ),
405
+ values=(
406
+ record.record_id, problem_hash, record.instance_id, record.feature_schema_id,
407
+ record.feature_schema_version, values_json, reasons_json,
408
+ record.source_content_sha256, record.extractor_id, record.extractor_version, _utc_now(),
409
+ ),
410
+ compare_columns=(
411
+ "record_id", "problem_data_hash", "instance_id", "feature_schema_id",
412
+ "feature_schema_version", "values_json", "missingness_reasons_json",
413
+ "source_content_sha256", "extractor_id", "extractor_version",
414
+ ),
415
+ )
416
+
417
+ def record_solve(self, record: SolveHistoryRecord) -> None:
418
+ if not isinstance(record, SolveHistoryRecord):
419
+ raise HistoryDataError("record must be SolveHistoryRecord")
420
+ metadata_json = _canonical_json(record.to_dict()["metadata"])
421
+ with self.transaction() as con:
422
+ if con.execute("SELECT 1 FROM problems WHERE problem_data_hash=?", (record.problem_data_hash,)).fetchone() is None:
423
+ raise HistoryDataError("solve record references an unknown problem_data_hash")
424
+ self._insert_or_verify(
425
+ con,
426
+ table="solve_runs",
427
+ key_column="run_id",
428
+ columns=(
429
+ "run_id", "problem_data_hash", "problem_structural_hash", "problem_class",
430
+ "backend", "backend_version", "public_status", "backend_status", "objective",
431
+ "validation_valid", "independently_verified_optimal", "total_s", "environment_id",
432
+ "trace_created_at_utc", "metadata_json", "created_at_utc",
433
+ ),
434
+ values=(
435
+ record.run_id, record.problem_data_hash, record.problem_structural_hash, record.problem_class,
436
+ record.backend, record.backend_version, record.public_status, record.backend_status, record.objective,
437
+ _bool_db(record.validation_valid), int(record.independently_verified_optimal), record.total_s,
438
+ record.environment_id, record.trace_created_at_utc, metadata_json, _utc_now(),
439
+ ),
440
+ compare_columns=(
441
+ "run_id", "problem_data_hash", "problem_structural_hash", "problem_class",
442
+ "backend", "backend_version", "public_status", "backend_status", "objective",
443
+ "validation_valid", "independently_verified_optimal", "total_s", "environment_id",
444
+ "trace_created_at_utc", "metadata_json",
445
+ ),
446
+ )
447
+
448
+ def record_solve_result(
449
+ self,
450
+ result: SolveResult,
451
+ *,
452
+ run_id: str | None = None,
453
+ environment_id: str | None = None,
454
+ metadata: Mapping[str, Any] | None = None,
455
+ ) -> SolveHistoryRecord:
456
+ if not isinstance(result, SolveResult):
457
+ raise HistoryDataError("result must be SolveResult")
458
+ trace = result.trace
459
+ record = SolveHistoryRecord(
460
+ run_id=_run_id_from_result(result) if run_id is None else run_id,
461
+ problem_data_hash=trace.problem_data_hash,
462
+ problem_structural_hash=trace.problem_structural_hash,
463
+ problem_class=trace.problem_class,
464
+ backend=trace.backend,
465
+ backend_version=trace.backend_version,
466
+ public_status=result.status.value,
467
+ backend_status=result.backend_status,
468
+ objective=result.objective,
469
+ validation_valid=None if result.validation is None else result.validation.valid,
470
+ independently_verified_optimal=result.optimality_evidence.independently_verified_optimal,
471
+ total_s=trace.timings.total_s,
472
+ environment_id=environment_id,
473
+ trace_created_at_utc=trace.created_at_utc,
474
+ metadata=dict(metadata or {}),
475
+ )
476
+ self.record_solve(record)
477
+ return record
478
+
479
+ def record_benchmark_row(self, row: Mapping[str, Any]) -> None:
480
+ if not isinstance(row, Mapping):
481
+ raise HistoryDataError("benchmark row must be a mapping")
482
+ required = ("run_id", "protocol_id", "instance", "backend", "repetition", "state")
483
+ missing = [name for name in required if name not in row]
484
+ if missing:
485
+ raise HistoryDataError(f"benchmark row missing fields: {missing}")
486
+ identity = {name: str(row[name]).strip() for name in ("run_id", "protocol_id", "instance", "backend", "state")}
487
+ if not all(identity.values()):
488
+ raise HistoryDataError("benchmark identity fields must be non-empty")
489
+ repetition = row["repetition"]
490
+ if type(repetition) is not int or repetition < 0:
491
+ raise HistoryDataError("benchmark repetition must be a non-negative integer")
492
+ for numeric in ("objective", "wall_s", "parse_s", "worker_parse_s", "worker_solve_wall_s"):
493
+ value = row.get(numeric)
494
+ if value is not None and (type(value) not in (int, float) or not math.isfinite(float(value)) or (numeric.endswith("_s") and float(value) < 0)):
495
+ raise HistoryDataError(f"benchmark {numeric} must be finite" + (" and non-negative" if numeric.endswith("_s") else ""))
496
+ validated = row.get("validated")
497
+ if validated is not None and type(validated) is not bool:
498
+ raise HistoryDataError("benchmark validated must be boolean or None")
499
+ allowed_fields = {
500
+ "schema_version", "protocol_id", "environment_id", "thread_policy", "run_id", "complete",
501
+ "instance", "filename", "instance_sha256", "problem_data_hash", "backend", "repetition",
502
+ "parse_s", "state", "wall_s", "public_status", "objective", "validated",
503
+ "reference_status", "reference_objective", "reference_check", "worker_parse_s",
504
+ "worker_solve_wall_s", "error_type", "error",
505
+ }
506
+ compact = {key: row[key] for key in allowed_fields if key in row}
507
+ row_json = _canonical_json(compact)
508
+ values = (
509
+ identity["run_id"], identity["protocol_id"],
510
+ None if row.get("environment_id") is None else str(row.get("environment_id")),
511
+ None if row.get("problem_data_hash") is None else _require_sha(str(row.get("problem_data_hash")), "problem_data_hash"),
512
+ identity["instance"], None if row.get("instance_sha256") is None else _require_sha(str(row.get("instance_sha256")), "instance_sha256"),
513
+ identity["backend"], repetition, identity["state"],
514
+ None if row.get("public_status") is None else str(row.get("public_status")),
515
+ _bool_db(validated), None if row.get("objective") is None else float(row["objective"]),
516
+ None if row.get("wall_s") is None else float(row["wall_s"]),
517
+ None if row.get("reference_check") is None else str(row.get("reference_check")),
518
+ row_json, _utc_now(),
519
+ )
520
+ columns = (
521
+ "run_id", "protocol_id", "environment_id", "problem_data_hash", "instance_name",
522
+ "instance_sha256", "backend", "repetition", "state", "public_status", "validated",
523
+ "objective", "wall_s", "reference_check", "row_json", "created_at_utc",
524
+ )
525
+ with self.transaction() as con:
526
+ self._insert_or_verify(
527
+ con,
528
+ table="benchmark_rows",
529
+ key_column="run_id",
530
+ columns=columns,
531
+ values=values,
532
+ compare_columns=columns[:-1],
533
+ )
534
+
535
+ def record_benchmark_rows(self, rows: Iterable[Mapping[str, Any]]) -> int:
536
+ # Each row is atomic. This intentionally does not make the entire iterable one giant transaction.
537
+ count = 0
538
+ for row in rows:
539
+ self.record_benchmark_row(row)
540
+ count += 1
541
+ return count
542
+
543
+ def record_artifact(
544
+ self,
545
+ *,
546
+ artifact_id: str,
547
+ artifact_type: str,
548
+ run_id: str | None = None,
549
+ path: str | None = None,
550
+ size_bytes: int = 0,
551
+ sha256: str | None = None,
552
+ metadata: Mapping[str, Any] | None = None,
553
+ ) -> None:
554
+ artifact_id = str(artifact_id).strip()
555
+ artifact_type = str(artifact_type).strip()
556
+ if not artifact_id or not artifact_type:
557
+ raise HistoryDataError("artifact_id and artifact_type must be non-empty")
558
+ if type(size_bytes) is not int or size_bytes < 0:
559
+ raise HistoryDataError("size_bytes must be a non-negative integer")
560
+ digest = None if sha256 is None else _require_sha(sha256, "artifact sha256")
561
+ metadata_json = _canonical_json(dict(metadata or {}))
562
+ columns = ("artifact_id", "run_id", "artifact_type", "path", "size_bytes", "sha256", "metadata_json", "created_at_utc")
563
+ values = (artifact_id, None if run_id is None else str(run_id), artifact_type, None if path is None else str(path), size_bytes, digest, metadata_json, _utc_now())
564
+ with self.transaction() as con:
565
+ self._insert_or_verify(
566
+ con,
567
+ table="artifacts",
568
+ key_column="artifact_id",
569
+ columns=columns,
570
+ values=values,
571
+ compare_columns=columns[:-1],
572
+ )
573
+
574
+ def problem(self, problem_data_hash: str) -> dict[str, Any] | None:
575
+ with self._lock:
576
+ row = self._connection.execute("SELECT * FROM problems WHERE problem_data_hash=?", (problem_data_hash,)).fetchone()
577
+ sources = self._connection.execute("SELECT source_id,provenance_json,created_at_utc FROM problem_sources WHERE problem_data_hash=? ORDER BY source_id", (problem_data_hash,)).fetchall()
578
+ if row is None:
579
+ return None
580
+ out = dict(row)
581
+ out["metadata"] = _json_load(out.pop("metadata_json"))
582
+ out["sources"] = [
583
+ {"source_id": item["source_id"], "provenance": _json_load(item["provenance_json"]), "created_at_utc": item["created_at_utc"]}
584
+ for item in sources
585
+ ]
586
+ return out
587
+
588
+ def solve_runs(self, *, problem_data_hash: str | None = None, backend: str | None = None) -> list[dict[str, Any]]:
589
+ clauses: list[str] = []
590
+ params: list[Any] = []
591
+ if problem_data_hash is not None:
592
+ clauses.append("problem_data_hash=?"); params.append(problem_data_hash)
593
+ if backend is not None:
594
+ clauses.append("backend=?"); params.append(backend)
595
+ sql = "SELECT * FROM solve_runs" + (" WHERE " + " AND ".join(clauses) if clauses else "") + " ORDER BY created_at_utc, run_id"
596
+ with self._lock:
597
+ rows = self._connection.execute(sql, tuple(params)).fetchall()
598
+ output = []
599
+ for row in rows:
600
+ item = dict(row)
601
+ item["validation_valid"] = None if item["validation_valid"] is None else bool(item["validation_valid"])
602
+ item["independently_verified_optimal"] = bool(item["independently_verified_optimal"])
603
+ item["metadata"] = _json_load(item.pop("metadata_json"))
604
+ output.append(item)
605
+ return output
606
+
607
+ def benchmark_rows(self, *, protocol_id: str | None = None, backend: str | None = None) -> list[dict[str, Any]]:
608
+ clauses: list[str] = []
609
+ params: list[Any] = []
610
+ if protocol_id is not None:
611
+ clauses.append("protocol_id=?"); params.append(protocol_id)
612
+ if backend is not None:
613
+ clauses.append("backend=?"); params.append(backend)
614
+ sql = "SELECT row_json FROM benchmark_rows" + (" WHERE " + " AND ".join(clauses) if clauses else "") + " ORDER BY run_id"
615
+ with self._lock:
616
+ rows = self._connection.execute(sql, tuple(params)).fetchall()
617
+ return [_json_load(row["row_json"]) for row in rows]
618
+
619
+ def feature_rows(self, *, problem_data_hash: str | None = None) -> list[dict[str, Any]]:
620
+ sql = "SELECT * FROM feature_records"
621
+ params: tuple[Any, ...] = ()
622
+ if problem_data_hash is not None:
623
+ sql += " WHERE problem_data_hash=?"; params = (problem_data_hash,)
624
+ sql += " ORDER BY created_at_utc, record_id"
625
+ with self._lock:
626
+ rows = self._connection.execute(sql, params).fetchall()
627
+ output: list[dict[str, Any]] = []
628
+ for row in rows:
629
+ item = dict(row)
630
+ item["values"] = _json_load(item.pop("values_json"))
631
+ item["missingness_reasons"] = _json_load(item.pop("missingness_reasons_json"))
632
+ output.append(item)
633
+ return output
634
+
635
+ def solver_performance_summary(self, *, backend: str | None = None) -> list[dict[str, Any]]:
636
+ where = " WHERE backend=?" if backend is not None else ""
637
+ params: tuple[Any, ...] = (backend,) if backend is not None else ()
638
+ with self._lock:
639
+ rows = self._connection.execute(
640
+ f"""
641
+ SELECT backend,
642
+ COUNT(*) AS runs,
643
+ SUM(CASE WHEN validation_valid=1 THEN 1 ELSE 0 END) AS validated_runs,
644
+ SUM(CASE WHEN independently_verified_optimal=1 THEN 1 ELSE 0 END) AS independently_verified_optimal_runs,
645
+ AVG(total_s) AS average_total_s
646
+ FROM solve_runs{where}
647
+ GROUP BY backend ORDER BY backend
648
+ """,
649
+ params,
650
+ ).fetchall()
651
+ return [dict(row) for row in rows]
@@ -0,0 +1,3 @@
1
+ from .fingerprint import DistributionStats, ProblemFingerprint, inspect_problem
2
+
3
+ __all__ = ["DistributionStats", "ProblemFingerprint", "inspect_problem"]