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.
- solverpilot/__init__.py +250 -0
- solverpilot/_immutability.py +27 -0
- solverpilot/_version.py +3 -0
- solverpilot/applications/__init__.py +3 -0
- solverpilot/applications/routing/__init__.py +19 -0
- solverpilot/applications/routing/compile_milp.py +321 -0
- solverpilot/applications/routing/diagnostics.py +78 -0
- solverpilot/applications/routing/errors.py +14 -0
- solverpilot/applications/routing/heuristics.py +167 -0
- solverpilot/applications/routing/model.py +259 -0
- solverpilot/applications/routing/reference.py +81 -0
- solverpilot/applications/routing/solution.py +89 -0
- solverpilot/applications/routing/timeline.py +79 -0
- solverpilot/applications/routing/validation.py +169 -0
- solverpilot/applications/tsp/__init__.py +41 -0
- solverpilot/applications/tsp/compile_cp.py +121 -0
- solverpilot/applications/tsp/compile_milp.py +139 -0
- solverpilot/applications/tsp/distance.py +60 -0
- solverpilot/applications/tsp/errors.py +17 -0
- solverpilot/applications/tsp/heuristics.py +112 -0
- solverpilot/applications/tsp/model.py +175 -0
- solverpilot/applications/tsp/reference.py +121 -0
- solverpilot/applications/tsp/solution.py +65 -0
- solverpilot/applications/tsp/validation.py +92 -0
- solverpilot/backends/__init__.py +51 -0
- solverpilot/backends/_casadi_highs_worker.py +92 -0
- solverpilot/backends/base.py +28 -0
- solverpilot/backends/bundled_capi.py +910 -0
- solverpilot/backends/casadi_conic.py +303 -0
- solverpilot/backends/health.py +240 -0
- solverpilot/backends/highspy_native.py +217 -0
- solverpilot/backends/nlopt_native.py +237 -0
- solverpilot/backends/osqp_native.py +297 -0
- solverpilot/backends/protocol_v2.py +119 -0
- solverpilot/backends/pyscipopt_native.py +200 -0
- solverpilot/backends/registry.py +38 -0
- solverpilot/backends/scipy_highs.py +138 -0
- solverpilot/backends/scipy_highs_lp.py +195 -0
- solverpilot/backends/scipy_slsqp_qp.py +133 -0
- solverpilot/backends/scipy_vendored_highs_dev.py +363 -0
- solverpilot/benchmark/__init__.py +23 -0
- solverpilot/benchmark/acquire.py +199 -0
- solverpilot/benchmark/baselines.py +70 -0
- solverpilot/benchmark/bundle.py +37 -0
- solverpilot/benchmark/campaign.py +150 -0
- solverpilot/benchmark/environment.py +115 -0
- solverpilot/benchmark/integrity.py +46 -0
- solverpilot/benchmark/model.py +146 -0
- solverpilot/benchmark/policy.py +182 -0
- solverpilot/benchmark/registry.py +38 -0
- solverpilot/benchmark/report.py +44 -0
- solverpilot/benchmark/runner.py +516 -0
- solverpilot/benchmark/slurm.py +55 -0
- solverpilot/benchmark/specs.py +75 -0
- solverpilot/benchmark/splits.py +181 -0
- solverpilot/benchmark/summary.py +384 -0
- solverpilot/benchmark/verify.py +142 -0
- solverpilot/benchmark/worker.py +71 -0
- solverpilot/bridges/__init__.py +19 -0
- solverpilot/bridges/engine.py +44 -0
- solverpilot/bridges/indicator.py +232 -0
- solverpilot/bridges/registry.py +78 -0
- solverpilot/bridges/transformation-tape.schema.json +53 -0
- solverpilot/bridges/types.py +149 -0
- solverpilot/bridges/validation.py +110 -0
- solverpilot/capabilities/__init__.py +32 -0
- solverpilot/capabilities/backend-capability-v2.schema.json +21 -0
- solverpilot/capabilities/conformance.py +151 -0
- solverpilot/capabilities/enums.py +27 -0
- solverpilot/capabilities/manifest.py +19 -0
- solverpilot/capabilities/ontology.py +73 -0
- solverpilot/capabilities/requirements.py +37 -0
- solverpilot/capabilities/resolve.py +30 -0
- solverpilot/capabilities/v2.py +711 -0
- solverpilot/cli/__init__.py +1 -0
- solverpilot/cli/backend_health.py +77 -0
- solverpilot/cli/benchmark.py +331 -0
- solverpilot/cli/benchmark_worker.py +33 -0
- solverpilot/cli/capabilities.py +65 -0
- solverpilot/conic/__init__.py +11 -0
- solverpilot/conic/backend.py +263 -0
- solverpilot/conic/compiler.py +310 -0
- solverpilot/conic/conformance.py +60 -0
- solverpilot/conic/conic-ir-v1.schema.json +24 -0
- solverpilot/conic/ir.py +299 -0
- solverpilot/conic/validation.py +149 -0
- solverpilot/cp/__init__.py +7 -0
- solverpilot/cp/conformance.py +32 -0
- solverpilot/cp/cp-ir-v1.schema.json +13 -0
- solverpilot/cp/ir.py +170 -0
- solverpilot/cp/model.py +143 -0
- solverpilot/cp/ortools_backend.py +423 -0
- solverpilot/cp/ortools_worker.py +75 -0
- solverpilot/cp/reference.py +51 -0
- solverpilot/cp/serialization.py +117 -0
- solverpilot/cp/validate.py +83 -0
- solverpilot/diagnose/__init__.py +33 -0
- solverpilot/diagnose/infeasibility.py +463 -0
- solverpilot/diagnose/model.py +87 -0
- solverpilot/evaluation/__init__.py +53 -0
- solverpilot/evaluation/metrics.py +150 -0
- solverpilot/evaluation/oracle.py +288 -0
- solverpilot/evaluation/proof.py +53 -0
- solverpilot/evaluation/references.py +73 -0
- solverpilot/evaluation/regret.py +281 -0
- solverpilot/exceptions.py +34 -0
- solverpilot/experimental/__init__.py +32 -0
- solverpilot/extensions/__init__.py +56 -0
- solverpilot/extensions/entrypoints.py +34 -0
- solverpilot/extensions/errors.py +35 -0
- solverpilot/extensions/manager.py +651 -0
- solverpilot/extensions/manifest.py +232 -0
- solverpilot/extensions/model.py +101 -0
- solverpilot/extensions/versioning.py +136 -0
- solverpilot/history/__init__.py +22 -0
- solverpilot/history/errors.py +15 -0
- solverpilot/history/model.py +129 -0
- solverpilot/history/sqlite.py +651 -0
- solverpilot/inspect/__init__.py +3 -0
- solverpilot/inspect/fingerprint.py +199 -0
- solverpilot/intelligence/__init__.py +36 -0
- solverpilot/intelligence/errors.py +16 -0
- solverpilot/intelligence/fingerprint.py +108 -0
- solverpilot/intelligence/leakage.py +69 -0
- solverpilot/intelligence/schema.py +314 -0
- solverpilot/intelligence/shift.py +208 -0
- solverpilot/io/__init__.py +41 -0
- solverpilot/io/binary64.py +49 -0
- solverpilot/io/csv.py +230 -0
- solverpilot/io/errors.py +25 -0
- solverpilot/io/hashing.py +111 -0
- solverpilot/io/json.py +90 -0
- solverpilot/io/mapping.py +195 -0
- solverpilot/io/model.py +407 -0
- solverpilot/io/numeric.py +108 -0
- solverpilot/io/provenance.py +114 -0
- solverpilot/io/result.py +45 -0
- solverpilot/io/source.py +104 -0
- solverpilot/io/strict_json.py +133 -0
- solverpilot/minlp/__init__.py +6 -0
- solverpilot/minlp/compiler.py +66 -0
- solverpilot/minlp/conformance.py +34 -0
- solverpilot/minlp/convexity.py +85 -0
- solverpilot/minlp/ir.py +35 -0
- solverpilot/minlp/minlp-ir-v1.schema.json +1 -0
- solverpilot/minlp/orchestrator.py +220 -0
- solverpilot/minlp/validation.py +28 -0
- solverpilot/model/__init__.py +27 -0
- solverpilot/model/compiler.py +1263 -0
- solverpilot/model/errors.py +22 -0
- solverpilot/model/expression.py +373 -0
- solverpilot/model/model.py +716 -0
- solverpilot/model/sets.py +60 -0
- solverpilot/model/types.py +36 -0
- solverpilot/nlp/__init__.py +7 -0
- solverpilot/nlp/ad.py +166 -0
- solverpilot/nlp/backend.py +113 -0
- solverpilot/nlp/compiler.py +116 -0
- solverpilot/nlp/conformance.py +58 -0
- solverpilot/nlp/ir.py +70 -0
- solverpilot/nlp/nlp-ir-v1.schema.json +1 -0
- solverpilot/nlp/validation.py +60 -0
- solverpilot/plan/__init__.py +33 -0
- solverpilot/plan/model.py +79 -0
- solverpilot/plan/planner.py +201 -0
- solverpilot/plan/production.py +627 -0
- solverpilot/plan/selective_lp.py +65 -0
- solverpilot/problem/__init__.py +16 -0
- solverpilot/problem/enums.py +20 -0
- solverpilot/problem/hashing.py +88 -0
- solverpilot/problem/linear.py +196 -0
- solverpilot/problem/mps.py +447 -0
- solverpilot/problem/quadratic.py +192 -0
- solverpilot/reporting/__init__.py +27 -0
- solverpilot/reporting/explain.py +327 -0
- solverpilot/reporting/model.py +264 -0
- solverpilot/reporting/render.py +55 -0
- solverpilot/runtime/__init__.py +19 -0
- solverpilot/runtime/auto.py +193 -0
- solverpilot/runtime/budgeting.py +43 -0
- solverpilot/runtime/executor.py +126 -0
- solverpilot/runtime/portfolio.py +197 -0
- solverpilot/runtime/result.py +72 -0
- solverpilot/session/__init__.py +29 -0
- solverpilot/session/conformance_v2.py +239 -0
- solverpilot/session/mutations.py +115 -0
- solverpilot/session/persistent-session-trace.schema.json +45 -0
- solverpilot/session/persistent.py +491 -0
- solverpilot/session/reuse.py +81 -0
- solverpilot/session/session.py +236 -0
- solverpilot/trace/__init__.py +3 -0
- solverpilot/trace/schema.py +47 -0
- solverpilot/validate/__init__.py +10 -0
- solverpilot/validate/core.py +179 -0
- solverpilot/validate/result.py +55 -0
- solverpilot-0.1.dist-info/METADATA +554 -0
- solverpilot-0.1.dist-info/RECORD +201 -0
- solverpilot-0.1.dist-info/WHEEL +5 -0
- solverpilot-0.1.dist-info/entry_points.txt +4 -0
- solverpilot-0.1.dist-info/licenses/LICENSE +202 -0
- solverpilot-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import math
|
|
8
|
+
import re
|
|
9
|
+
from types import MappingProxyType
|
|
10
|
+
from typing import Any, Mapping
|
|
11
|
+
|
|
12
|
+
from .errors import ExtensionManifestError
|
|
13
|
+
from .versioning import SemanticVersion
|
|
14
|
+
|
|
15
|
+
_ID_RE = re.compile(r"^[a-z][a-z0-9_.-]{2,127}$")
|
|
16
|
+
_TOKEN_RE = re.compile(r"^[a-z][a-z0-9_.-]{0,127}$")
|
|
17
|
+
_ENTRYPOINT_RE = re.compile(
|
|
18
|
+
r"^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*:"
|
|
19
|
+
r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$"
|
|
20
|
+
)
|
|
21
|
+
_DEP_RE = re.compile(r"^([a-z][a-z0-9_.-]{2,127})(.*)$")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ExtensionKind(str, Enum):
|
|
25
|
+
BACKEND = "backend"
|
|
26
|
+
BRIDGE = "bridge"
|
|
27
|
+
DATA_CONNECTOR = "data_connector"
|
|
28
|
+
FEATURE_EXTRACTOR = "feature_extractor"
|
|
29
|
+
EVALUATOR = "evaluator"
|
|
30
|
+
REPORTER = "reporter"
|
|
31
|
+
APPLICATION = "application"
|
|
32
|
+
MIXED = "mixed"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ExtensionLifecycle(str, Enum):
|
|
36
|
+
EXPERIMENTAL = "experimental"
|
|
37
|
+
ACTIVE = "active"
|
|
38
|
+
DEPRECATED = "deprecated"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _kind(value: ExtensionKind | str) -> ExtensionKind:
|
|
42
|
+
if isinstance(value, ExtensionKind):
|
|
43
|
+
return value
|
|
44
|
+
try:
|
|
45
|
+
return ExtensionKind(str(value).strip().lower())
|
|
46
|
+
except ValueError as exc:
|
|
47
|
+
raise ExtensionManifestError(f"unsupported extension kind {value!r}") from exc
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _lifecycle(value: ExtensionLifecycle | str) -> ExtensionLifecycle:
|
|
51
|
+
if isinstance(value, ExtensionLifecycle):
|
|
52
|
+
return value
|
|
53
|
+
try:
|
|
54
|
+
return ExtensionLifecycle(str(value).strip().lower())
|
|
55
|
+
except ValueError as exc:
|
|
56
|
+
raise ExtensionManifestError(f"unsupported extension lifecycle {value!r}") from exc
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _token(value: object, *, field_name: str) -> str:
|
|
60
|
+
item = str(value).strip().lower()
|
|
61
|
+
if not _TOKEN_RE.fullmatch(item):
|
|
62
|
+
raise ExtensionManifestError(f"invalid {field_name} token {value!r}")
|
|
63
|
+
return item
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _freeze_json(
|
|
67
|
+
value: Any,
|
|
68
|
+
*,
|
|
69
|
+
field_name: str,
|
|
70
|
+
_seen: set[int] | None = None,
|
|
71
|
+
_depth: int = 0,
|
|
72
|
+
) -> Any:
|
|
73
|
+
if _depth > 64:
|
|
74
|
+
raise ExtensionManifestError(f"{field_name} nesting exceeds the supported depth")
|
|
75
|
+
if value is None or isinstance(value, (str, bool, int)):
|
|
76
|
+
return value
|
|
77
|
+
if isinstance(value, float):
|
|
78
|
+
if not math.isfinite(value):
|
|
79
|
+
raise ExtensionManifestError(f"{field_name} may not contain NaN or Infinity")
|
|
80
|
+
return value
|
|
81
|
+
if isinstance(value, (Mapping, list, tuple)):
|
|
82
|
+
seen = set() if _seen is None else _seen
|
|
83
|
+
marker = id(value)
|
|
84
|
+
if marker in seen:
|
|
85
|
+
raise ExtensionManifestError(f"{field_name} may not contain cyclic data")
|
|
86
|
+
seen.add(marker)
|
|
87
|
+
try:
|
|
88
|
+
if isinstance(value, Mapping):
|
|
89
|
+
frozen: dict[str, Any] = {}
|
|
90
|
+
for key, item in value.items():
|
|
91
|
+
if not isinstance(key, str):
|
|
92
|
+
raise ExtensionManifestError(f"{field_name} mapping keys must be strings")
|
|
93
|
+
frozen[key] = _freeze_json(
|
|
94
|
+
item, field_name=field_name, _seen=seen, _depth=_depth + 1
|
|
95
|
+
)
|
|
96
|
+
return MappingProxyType(frozen)
|
|
97
|
+
return tuple(
|
|
98
|
+
_freeze_json(item, field_name=field_name, _seen=seen, _depth=_depth + 1)
|
|
99
|
+
for item in value
|
|
100
|
+
)
|
|
101
|
+
finally:
|
|
102
|
+
seen.remove(marker)
|
|
103
|
+
raise ExtensionManifestError(
|
|
104
|
+
f"{field_name} must be finite JSON-compatible data, not {type(value).__name__}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _thaw_json(value: Any) -> Any:
|
|
109
|
+
if isinstance(value, Mapping):
|
|
110
|
+
return {str(key): _thaw_json(item) for key, item in value.items()}
|
|
111
|
+
if isinstance(value, tuple):
|
|
112
|
+
return [_thaw_json(item) for item in value]
|
|
113
|
+
return value
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True, slots=True)
|
|
117
|
+
class ExtensionDependency:
|
|
118
|
+
extension_id: str
|
|
119
|
+
version: str = "*"
|
|
120
|
+
|
|
121
|
+
def __post_init__(self) -> None:
|
|
122
|
+
extension_id = str(self.extension_id).strip().lower()
|
|
123
|
+
if not _ID_RE.fullmatch(extension_id):
|
|
124
|
+
raise ExtensionManifestError(f"invalid dependency extension_id {self.extension_id!r}")
|
|
125
|
+
spec = str(self.version).strip() or "*"
|
|
126
|
+
# Validate every referenced version eagerly.
|
|
127
|
+
if spec != "*":
|
|
128
|
+
from .versioning import version_satisfies
|
|
129
|
+
|
|
130
|
+
# A version that satisfies itself is not required; this call is only syntax validation.
|
|
131
|
+
# Use a harmless concrete version and tolerate False, but propagate malformed syntax.
|
|
132
|
+
version_satisfies("0.0.0", spec)
|
|
133
|
+
object.__setattr__(self, "extension_id", extension_id)
|
|
134
|
+
object.__setattr__(self, "version", spec)
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def parse(cls, value: "ExtensionDependency | str") -> "ExtensionDependency":
|
|
138
|
+
if isinstance(value, cls):
|
|
139
|
+
return value
|
|
140
|
+
raw = str(value).strip()
|
|
141
|
+
match = _DEP_RE.fullmatch(raw)
|
|
142
|
+
if match is None:
|
|
143
|
+
raise ExtensionManifestError(f"invalid dependency declaration {value!r}")
|
|
144
|
+
extension_id = match.group(1)
|
|
145
|
+
spec = match.group(2).strip() or "*"
|
|
146
|
+
return cls(extension_id=extension_id, version=spec)
|
|
147
|
+
|
|
148
|
+
def to_dict(self) -> dict[str, str]:
|
|
149
|
+
return {"extension_id": self.extension_id, "version": self.version}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(frozen=True, slots=True)
|
|
153
|
+
class ExtensionManifest:
|
|
154
|
+
"""Immutable, content-addressed description of a trusted SolverPilot extension."""
|
|
155
|
+
|
|
156
|
+
extension_id: str
|
|
157
|
+
name: str
|
|
158
|
+
version: str
|
|
159
|
+
kind: ExtensionKind | str
|
|
160
|
+
description: str
|
|
161
|
+
capabilities: tuple[str, ...] = ()
|
|
162
|
+
dependencies: tuple[ExtensionDependency | str, ...] = ()
|
|
163
|
+
entrypoint: str | None = None
|
|
164
|
+
lifecycle: ExtensionLifecycle | str = ExtensionLifecycle.EXPERIMENTAL
|
|
165
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
166
|
+
|
|
167
|
+
def __post_init__(self) -> None:
|
|
168
|
+
extension_id = str(self.extension_id).strip().lower()
|
|
169
|
+
if not _ID_RE.fullmatch(extension_id):
|
|
170
|
+
raise ExtensionManifestError(
|
|
171
|
+
"extension_id must start with a lowercase letter, be 3-128 characters, "
|
|
172
|
+
"and contain only lowercase letters, digits, '.', '_' or '-'"
|
|
173
|
+
)
|
|
174
|
+
name = str(self.name).strip()
|
|
175
|
+
description = str(self.description).strip()
|
|
176
|
+
if not name:
|
|
177
|
+
raise ExtensionManifestError("name must be non-empty")
|
|
178
|
+
if not description:
|
|
179
|
+
raise ExtensionManifestError("description must be non-empty")
|
|
180
|
+
version = str(self.version).strip()
|
|
181
|
+
SemanticVersion.parse(version)
|
|
182
|
+
capabilities: list[str] = []
|
|
183
|
+
for value in self.capabilities:
|
|
184
|
+
item = _token(value, field_name="capability")
|
|
185
|
+
if item not in capabilities:
|
|
186
|
+
capabilities.append(item)
|
|
187
|
+
dependencies: list[ExtensionDependency] = []
|
|
188
|
+
seen: set[str] = set()
|
|
189
|
+
for value in self.dependencies:
|
|
190
|
+
dep = ExtensionDependency.parse(value)
|
|
191
|
+
if dep.extension_id == extension_id:
|
|
192
|
+
raise ExtensionManifestError("an extension may not depend on itself")
|
|
193
|
+
if dep.extension_id in seen:
|
|
194
|
+
raise ExtensionManifestError(f"duplicate dependency {dep.extension_id!r}")
|
|
195
|
+
seen.add(dep.extension_id)
|
|
196
|
+
dependencies.append(dep)
|
|
197
|
+
entrypoint = None if self.entrypoint is None else str(self.entrypoint).strip()
|
|
198
|
+
if entrypoint == "":
|
|
199
|
+
entrypoint = None
|
|
200
|
+
if entrypoint is not None and not _ENTRYPOINT_RE.fullmatch(entrypoint):
|
|
201
|
+
raise ExtensionManifestError("entrypoint must use the form 'package.module:Attribute'")
|
|
202
|
+
object.__setattr__(self, "extension_id", extension_id)
|
|
203
|
+
object.__setattr__(self, "name", name)
|
|
204
|
+
object.__setattr__(self, "version", version)
|
|
205
|
+
object.__setattr__(self, "kind", _kind(self.kind))
|
|
206
|
+
object.__setattr__(self, "description", description)
|
|
207
|
+
object.__setattr__(self, "capabilities", tuple(capabilities))
|
|
208
|
+
object.__setattr__(self, "dependencies", tuple(dependencies))
|
|
209
|
+
object.__setattr__(self, "entrypoint", entrypoint)
|
|
210
|
+
object.__setattr__(self, "lifecycle", _lifecycle(self.lifecycle))
|
|
211
|
+
object.__setattr__(self, "metadata", _freeze_json(self.metadata, field_name="metadata"))
|
|
212
|
+
|
|
213
|
+
def to_dict(self) -> dict[str, Any]:
|
|
214
|
+
return {
|
|
215
|
+
"extension_id": self.extension_id,
|
|
216
|
+
"name": self.name,
|
|
217
|
+
"version": self.version,
|
|
218
|
+
"kind": self.kind.value,
|
|
219
|
+
"description": self.description,
|
|
220
|
+
"capabilities": list(self.capabilities),
|
|
221
|
+
"dependencies": [dep.to_dict() for dep in self.dependencies],
|
|
222
|
+
"entrypoint": self.entrypoint,
|
|
223
|
+
"lifecycle": self.lifecycle.value,
|
|
224
|
+
"metadata": _thaw_json(self.metadata),
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def sha256(self) -> str:
|
|
229
|
+
raw = json.dumps(
|
|
230
|
+
self.to_dict(), sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=False
|
|
231
|
+
).encode("utf-8")
|
|
232
|
+
return hashlib.sha256(raw).hexdigest()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
from solverpilot.backends.base import Backend
|
|
9
|
+
from solverpilot.bridges.registry import BridgeSpec
|
|
10
|
+
|
|
11
|
+
from .errors import ExtensionManifestError
|
|
12
|
+
from .manifest import ExtensionManifest
|
|
13
|
+
|
|
14
|
+
_NAME_RE = re.compile(r"^[a-z][a-z0-9_.-]{0,127}$")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class ExtensionSelfCheck:
|
|
19
|
+
ok: bool
|
|
20
|
+
checks: tuple[str, ...] = ()
|
|
21
|
+
warnings: tuple[str, ...] = ()
|
|
22
|
+
|
|
23
|
+
def __post_init__(self) -> None:
|
|
24
|
+
if not isinstance(self.ok, bool):
|
|
25
|
+
raise ExtensionManifestError("self-check ok must be a boolean")
|
|
26
|
+
checks = tuple(str(item).strip() for item in self.checks)
|
|
27
|
+
warnings = tuple(str(item).strip() for item in self.warnings)
|
|
28
|
+
if any(not item for item in checks):
|
|
29
|
+
raise ExtensionManifestError("self-check identifiers must be non-empty")
|
|
30
|
+
if any(not item for item in warnings):
|
|
31
|
+
raise ExtensionManifestError("self-check warnings must be non-empty")
|
|
32
|
+
object.__setattr__(self, "checks", checks)
|
|
33
|
+
object.__setattr__(self, "warnings", warnings)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ExportKind(str, Enum):
|
|
37
|
+
DATA_CONNECTOR = "data_connector"
|
|
38
|
+
FEATURE_EXTRACTOR = "feature_extractor"
|
|
39
|
+
EVALUATOR = "evaluator"
|
|
40
|
+
REPORTER = "reporter"
|
|
41
|
+
APPLICATION = "application"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class NamedExtensionExport:
|
|
46
|
+
kind: ExportKind | str
|
|
47
|
+
name: str
|
|
48
|
+
value: Any
|
|
49
|
+
|
|
50
|
+
def __post_init__(self) -> None:
|
|
51
|
+
try:
|
|
52
|
+
kind = self.kind if isinstance(self.kind, ExportKind) else ExportKind(str(self.kind).strip().lower())
|
|
53
|
+
except ValueError as exc:
|
|
54
|
+
raise ExtensionManifestError(f"unsupported export kind {self.kind!r}") from exc
|
|
55
|
+
name = str(self.name).strip().lower()
|
|
56
|
+
if not _NAME_RE.fullmatch(name):
|
|
57
|
+
raise ExtensionManifestError(f"invalid export name {self.name!r}")
|
|
58
|
+
if self.value is None:
|
|
59
|
+
raise ExtensionManifestError("export value may not be None")
|
|
60
|
+
object.__setattr__(self, "kind", kind)
|
|
61
|
+
object.__setattr__(self, "name", name)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def key(self) -> str:
|
|
65
|
+
return f"{self.kind.value}:{self.name}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True, slots=True)
|
|
69
|
+
class ExtensionContributions:
|
|
70
|
+
backends: tuple[Backend, ...] = ()
|
|
71
|
+
bridges: tuple[BridgeSpec, ...] = ()
|
|
72
|
+
exports: tuple[NamedExtensionExport, ...] = ()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@runtime_checkable
|
|
76
|
+
class Extension(Protocol):
|
|
77
|
+
@property
|
|
78
|
+
def manifest(self) -> ExtensionManifest: ...
|
|
79
|
+
|
|
80
|
+
def self_check(self) -> ExtensionSelfCheck: ...
|
|
81
|
+
|
|
82
|
+
def contributions(self) -> ExtensionContributions: ...
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True, slots=True)
|
|
86
|
+
class StagedExtensionContribution:
|
|
87
|
+
extension_id: str
|
|
88
|
+
extension_version: str
|
|
89
|
+
backends: tuple[Backend, ...] = ()
|
|
90
|
+
bridges: tuple[BridgeSpec, ...] = ()
|
|
91
|
+
exports: tuple[NamedExtensionExport, ...] = ()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True, slots=True)
|
|
95
|
+
class ExtensionActivationPlan:
|
|
96
|
+
generation: int
|
|
97
|
+
extension_order: tuple[str, ...]
|
|
98
|
+
staged: tuple[StagedExtensionContribution, ...]
|
|
99
|
+
target_backend_names: tuple[str, ...]
|
|
100
|
+
target_bridge_keys: tuple[str, ...]
|
|
101
|
+
plan_sha256: str
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from .errors import ExtensionManifestError
|
|
7
|
+
|
|
8
|
+
_SEMVER_RE = re.compile(
|
|
9
|
+
r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"
|
|
10
|
+
r"(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?"
|
|
11
|
+
r"(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$"
|
|
12
|
+
)
|
|
13
|
+
_COMPARATOR_RE = re.compile(r"^(==|!=|>=|<=|>|<|\^|~)?\s*(.+)$")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _identifier_key(identifier: str) -> tuple[int, int | str]:
|
|
17
|
+
if identifier.isdigit():
|
|
18
|
+
return (0, int(identifier))
|
|
19
|
+
return (1, identifier)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class SemanticVersion:
|
|
24
|
+
major: int
|
|
25
|
+
minor: int
|
|
26
|
+
patch: int
|
|
27
|
+
prerelease: tuple[str, ...] = ()
|
|
28
|
+
build: tuple[str, ...] = ()
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def parse(cls, value: str) -> "SemanticVersion":
|
|
32
|
+
raw = str(value).strip()
|
|
33
|
+
match = _SEMVER_RE.fullmatch(raw)
|
|
34
|
+
if match is None:
|
|
35
|
+
raise ExtensionManifestError(
|
|
36
|
+
f"version {value!r} must be valid SemVer MAJOR.MINOR.PATCH with optional prerelease/build suffix"
|
|
37
|
+
)
|
|
38
|
+
pre = tuple(match.group(4).split(".")) if match.group(4) else ()
|
|
39
|
+
build = tuple(match.group(5).split(".")) if match.group(5) else ()
|
|
40
|
+
for ident in pre:
|
|
41
|
+
if ident.isdigit() and len(ident) > 1 and ident.startswith("0"):
|
|
42
|
+
raise ExtensionManifestError(f"invalid numeric prerelease identifier {ident!r}")
|
|
43
|
+
return cls(int(match.group(1)), int(match.group(2)), int(match.group(3)), pre, build)
|
|
44
|
+
|
|
45
|
+
def _precedence(self) -> tuple[object, ...]:
|
|
46
|
+
base: tuple[object, ...] = (self.major, self.minor, self.patch)
|
|
47
|
+
if not self.prerelease:
|
|
48
|
+
return base + ((2, ""),)
|
|
49
|
+
return base + ((1, tuple(_identifier_key(x) for x in self.prerelease)),)
|
|
50
|
+
|
|
51
|
+
def __lt__(self, other: "SemanticVersion") -> bool:
|
|
52
|
+
return self._precedence() < other._precedence()
|
|
53
|
+
|
|
54
|
+
def __le__(self, other: "SemanticVersion") -> bool:
|
|
55
|
+
return self == other or self < other
|
|
56
|
+
|
|
57
|
+
def __gt__(self, other: "SemanticVersion") -> bool:
|
|
58
|
+
return not self <= other
|
|
59
|
+
|
|
60
|
+
def __ge__(self, other: "SemanticVersion") -> bool:
|
|
61
|
+
return not self < other
|
|
62
|
+
|
|
63
|
+
def __eq__(self, other: object) -> bool:
|
|
64
|
+
if not isinstance(other, SemanticVersion):
|
|
65
|
+
return NotImplemented
|
|
66
|
+
return (
|
|
67
|
+
self.major,
|
|
68
|
+
self.minor,
|
|
69
|
+
self.patch,
|
|
70
|
+
self.prerelease,
|
|
71
|
+
) == (
|
|
72
|
+
other.major,
|
|
73
|
+
other.minor,
|
|
74
|
+
other.patch,
|
|
75
|
+
other.prerelease,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def __str__(self) -> str:
|
|
79
|
+
text = f"{self.major}.{self.minor}.{self.patch}"
|
|
80
|
+
if self.prerelease:
|
|
81
|
+
text += "-" + ".".join(self.prerelease)
|
|
82
|
+
if self.build:
|
|
83
|
+
text += "+" + ".".join(self.build)
|
|
84
|
+
return text
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _caret_upper(v: SemanticVersion) -> SemanticVersion:
|
|
88
|
+
if v.major > 0:
|
|
89
|
+
return SemanticVersion(v.major + 1, 0, 0)
|
|
90
|
+
if v.minor > 0:
|
|
91
|
+
return SemanticVersion(0, v.minor + 1, 0)
|
|
92
|
+
return SemanticVersion(0, 0, v.patch + 1)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _tilde_upper(v: SemanticVersion) -> SemanticVersion:
|
|
96
|
+
return SemanticVersion(v.major, v.minor + 1, 0)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def version_satisfies(version: str, specifier: str) -> bool:
|
|
100
|
+
"""Return whether *version* satisfies a small deterministic SemVer constraint language.
|
|
101
|
+
|
|
102
|
+
Supported clauses are comma-separated ``== != >= <= > < ^ ~`` comparisons. An empty
|
|
103
|
+
specifier accepts every valid version. Build metadata does not affect precedence.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
current = SemanticVersion.parse(version)
|
|
107
|
+
raw = str(specifier).strip()
|
|
108
|
+
if not raw or raw == "*":
|
|
109
|
+
return True
|
|
110
|
+
parts = raw.split(",")
|
|
111
|
+
if any(not part.strip() for part in parts):
|
|
112
|
+
raise ExtensionManifestError(f"invalid version constraint {specifier!r}")
|
|
113
|
+
clauses = [part.strip() for part in parts]
|
|
114
|
+
for clause in clauses:
|
|
115
|
+
match = _COMPARATOR_RE.fullmatch(clause)
|
|
116
|
+
if match is None:
|
|
117
|
+
raise ExtensionManifestError(f"invalid version constraint {clause!r}")
|
|
118
|
+
op = match.group(1) or "=="
|
|
119
|
+
target = SemanticVersion.parse(match.group(2))
|
|
120
|
+
if op == "==" and not current == target:
|
|
121
|
+
return False
|
|
122
|
+
if op == "!=" and current == target:
|
|
123
|
+
return False
|
|
124
|
+
if op == ">=" and not current >= target:
|
|
125
|
+
return False
|
|
126
|
+
if op == "<=" and not current <= target:
|
|
127
|
+
return False
|
|
128
|
+
if op == ">" and not current > target:
|
|
129
|
+
return False
|
|
130
|
+
if op == "<" and not current < target:
|
|
131
|
+
return False
|
|
132
|
+
if op == "^" and not (current >= target and current < _caret_upper(target)):
|
|
133
|
+
return False
|
|
134
|
+
if op == "~" and not (current >= target and current < _tilde_upper(target)):
|
|
135
|
+
return False
|
|
136
|
+
return True
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Optional local history storage for reproducible optimization research.
|
|
2
|
+
|
|
3
|
+
Nothing is persisted unless a caller explicitly constructs :class:`HistoryStore`
|
|
4
|
+
and records data. The schema intentionally excludes raw problem arrays and raw
|
|
5
|
+
solution vectors.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .errors import HistoryDataError, HistorySchemaError, HistoryStoreError
|
|
9
|
+
from .model import ProblemHistoryRecord, SolveHistoryRecord
|
|
10
|
+
from .sqlite import HISTORY_SCHEMA_LABEL, HISTORY_SCHEMA_NAME, HISTORY_SCHEMA_VERSION, HistoryStore
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"HISTORY_SCHEMA_LABEL",
|
|
14
|
+
"HISTORY_SCHEMA_NAME",
|
|
15
|
+
"HISTORY_SCHEMA_VERSION",
|
|
16
|
+
"HistoryDataError",
|
|
17
|
+
"HistorySchemaError",
|
|
18
|
+
"HistoryStoreError",
|
|
19
|
+
"HistoryStore",
|
|
20
|
+
"ProblemHistoryRecord",
|
|
21
|
+
"SolveHistoryRecord",
|
|
22
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from solverpilot.exceptions import SolverPilotError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HistoryStoreError(SolverPilotError):
|
|
7
|
+
"""Base error for SolverPilot's optional local history store."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HistorySchemaError(HistoryStoreError):
|
|
11
|
+
"""Raised when a database schema is unsupported or inconsistent."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HistoryDataError(HistoryStoreError):
|
|
15
|
+
"""Raised when a record violates the history-store contract."""
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
|
|
6
|
+
from solverpilot.io.hashing import deep_freeze_json, deep_thaw_json
|
|
7
|
+
|
|
8
|
+
from .errors import HistoryDataError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _text(value: str, field_name: str) -> str:
|
|
12
|
+
if not isinstance(value, str) or not value.strip():
|
|
13
|
+
raise HistoryDataError(f"{field_name} must be a non-empty string")
|
|
14
|
+
return value.strip()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _sha(value: str, field_name: str) -> str:
|
|
18
|
+
text = _text(value, field_name).lower()
|
|
19
|
+
if len(text) != 64 or any(ch not in "0123456789abcdef" for ch in text):
|
|
20
|
+
raise HistoryDataError(f"{field_name} must be a SHA-256 digest")
|
|
21
|
+
return text
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _json_object(value: Mapping[str, Any], field_name: str) -> Mapping[str, Any]:
|
|
25
|
+
if not isinstance(value, Mapping):
|
|
26
|
+
raise HistoryDataError(f"{field_name} must be a mapping")
|
|
27
|
+
try:
|
|
28
|
+
frozen = deep_freeze_json(value)
|
|
29
|
+
except Exception as exc: # normalize lower-layer configuration errors
|
|
30
|
+
raise HistoryDataError(f"{field_name} must contain finite JSON-safe values") from exc
|
|
31
|
+
if not isinstance(frozen, Mapping):
|
|
32
|
+
raise HistoryDataError(f"{field_name} must form a JSON object")
|
|
33
|
+
return frozen
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class ProblemHistoryRecord:
|
|
38
|
+
problem_data_hash: str
|
|
39
|
+
problem_structural_hash: str
|
|
40
|
+
problem_class: str
|
|
41
|
+
n_variables: int
|
|
42
|
+
n_constraints: int
|
|
43
|
+
source_provenance: Mapping[str, Any] = field(default_factory=dict)
|
|
44
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
def __post_init__(self) -> None:
|
|
47
|
+
object.__setattr__(self, "problem_data_hash", _sha(self.problem_data_hash, "problem_data_hash"))
|
|
48
|
+
object.__setattr__(self, "problem_structural_hash", _sha(self.problem_structural_hash, "problem_structural_hash"))
|
|
49
|
+
object.__setattr__(self, "problem_class", _text(self.problem_class, "problem_class"))
|
|
50
|
+
for name in ("n_variables", "n_constraints"):
|
|
51
|
+
value = getattr(self, name)
|
|
52
|
+
if type(value) is not int or value < 0:
|
|
53
|
+
raise HistoryDataError(f"{name} must be a non-negative integer")
|
|
54
|
+
object.__setattr__(self, "source_provenance", _json_object(self.source_provenance, "source_provenance"))
|
|
55
|
+
object.__setattr__(self, "metadata", _json_object(self.metadata, "metadata"))
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict[str, Any]:
|
|
58
|
+
return {
|
|
59
|
+
"problem_data_hash": self.problem_data_hash,
|
|
60
|
+
"problem_structural_hash": self.problem_structural_hash,
|
|
61
|
+
"problem_class": self.problem_class,
|
|
62
|
+
"n_variables": self.n_variables,
|
|
63
|
+
"n_constraints": self.n_constraints,
|
|
64
|
+
"source_provenance": deep_thaw_json(self.source_provenance),
|
|
65
|
+
"metadata": deep_thaw_json(self.metadata),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True, slots=True)
|
|
70
|
+
class SolveHistoryRecord:
|
|
71
|
+
run_id: str
|
|
72
|
+
problem_data_hash: str
|
|
73
|
+
problem_structural_hash: str
|
|
74
|
+
problem_class: str
|
|
75
|
+
backend: str | None
|
|
76
|
+
backend_version: str | None
|
|
77
|
+
public_status: str
|
|
78
|
+
backend_status: str
|
|
79
|
+
objective: float | None
|
|
80
|
+
validation_valid: bool | None
|
|
81
|
+
independently_verified_optimal: bool
|
|
82
|
+
total_s: float | None
|
|
83
|
+
environment_id: str | None = None
|
|
84
|
+
trace_created_at_utc: str | None = None
|
|
85
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
86
|
+
|
|
87
|
+
def __post_init__(self) -> None:
|
|
88
|
+
import math
|
|
89
|
+
|
|
90
|
+
object.__setattr__(self, "run_id", _text(self.run_id, "run_id"))
|
|
91
|
+
object.__setattr__(self, "problem_data_hash", _sha(self.problem_data_hash, "problem_data_hash"))
|
|
92
|
+
object.__setattr__(self, "problem_structural_hash", _sha(self.problem_structural_hash, "problem_structural_hash"))
|
|
93
|
+
object.__setattr__(self, "problem_class", _text(self.problem_class, "problem_class"))
|
|
94
|
+
object.__setattr__(self, "public_status", _text(self.public_status, "public_status"))
|
|
95
|
+
object.__setattr__(self, "backend_status", _text(self.backend_status, "backend_status"))
|
|
96
|
+
for name in ("backend", "backend_version", "environment_id", "trace_created_at_utc"):
|
|
97
|
+
value = getattr(self, name)
|
|
98
|
+
if value is not None:
|
|
99
|
+
object.__setattr__(self, name, _text(value, name))
|
|
100
|
+
if self.objective is not None and (type(self.objective) not in (int, float) or not math.isfinite(float(self.objective))):
|
|
101
|
+
raise HistoryDataError("objective must be finite when provided")
|
|
102
|
+
if self.total_s is not None and (type(self.total_s) not in (int, float) or not math.isfinite(float(self.total_s)) or float(self.total_s) < 0):
|
|
103
|
+
raise HistoryDataError("total_s must be finite and non-negative when provided")
|
|
104
|
+
if self.validation_valid is not None and type(self.validation_valid) is not bool:
|
|
105
|
+
raise HistoryDataError("validation_valid must be boolean or None")
|
|
106
|
+
if type(self.independently_verified_optimal) is not bool:
|
|
107
|
+
raise HistoryDataError("independently_verified_optimal must be boolean")
|
|
108
|
+
object.__setattr__(self, "objective", None if self.objective is None else float(self.objective))
|
|
109
|
+
object.__setattr__(self, "total_s", None if self.total_s is None else float(self.total_s))
|
|
110
|
+
object.__setattr__(self, "metadata", _json_object(self.metadata, "metadata"))
|
|
111
|
+
|
|
112
|
+
def to_dict(self) -> dict[str, Any]:
|
|
113
|
+
return {
|
|
114
|
+
"run_id": self.run_id,
|
|
115
|
+
"problem_data_hash": self.problem_data_hash,
|
|
116
|
+
"problem_structural_hash": self.problem_structural_hash,
|
|
117
|
+
"problem_class": self.problem_class,
|
|
118
|
+
"backend": self.backend,
|
|
119
|
+
"backend_version": self.backend_version,
|
|
120
|
+
"public_status": self.public_status,
|
|
121
|
+
"backend_status": self.backend_status,
|
|
122
|
+
"objective": self.objective,
|
|
123
|
+
"validation_valid": self.validation_valid,
|
|
124
|
+
"independently_verified_optimal": self.independently_verified_optimal,
|
|
125
|
+
"total_s": self.total_s,
|
|
126
|
+
"environment_id": self.environment_id,
|
|
127
|
+
"trace_created_at_utc": self.trace_created_at_utc,
|
|
128
|
+
"metadata": deep_thaw_json(self.metadata),
|
|
129
|
+
}
|